#!/usr/bin/env dub
/+ dub.sdl:
name "gc_uaf_blindspot"
platforms "linux"
dflags "-g"
dflags "-fsanitize=address" platform="ldc"
targetPath "build"
+/
/**
* The AddressSanitizer GC blind spot: a use-after-free inside GC-managed
* memory is invisible to ASan, while the identical bug on `malloc`/`free`
* memory is caught — demonstrated as a self-verifying contrast pair.
*
* Two child-process demonstrations (the parent re-execs itself and asserts on
* each child's exit code and stderr report):
*
* 1. `GC.malloc` -> `GC.free` -> read. The D GC allocates its pools with
* `mmap` (`core/internal/gc/os.d`, `os_mem_map`) and recycles memory
* internally, so freed GC memory never passes through ASan's intercepted
* `free` and its shadow is never poisoned: the child reads a garbage
* value and exits 0 — ASan reports nothing. The blind spot.
* 2. `core.stdc.stdlib.malloc` -> `free` -> read. The C allocator *is*
* intercepted (quarantine + shadow poisoning), so the same read dies
* with `heap-use-after-free`, a symbolized `file:line`, and exit 1.
*
* Companion to docs/research/sanitizers/d-toolchain.md
* § "The GC blind spot: ASan cannot see GC pools".
*
* Run with: dub run --single gc-uaf-blindspot.d
*
* Environment recorded: Linux 6.18.26, AMD Ryzen 9 7940HX (Zen 4), LDC 1.41.0
* (LLVM 18.1.8). nixpkgs LDC ships no compiler-rt, so the ASan runtime is GCC
* 15.2.0's `libasan.so.8` via LDC's `linker-gcc.cpp` fallback (`-fsanitize=
* address` handed to the C-compiler link driver); it self-symbolizes without
* `llvm-symbolizer`.
*
* Portability: builds without instrumentation (DMD has no `-fsanitize`; the
* flag is gated `platform="ldc"`), in which case — detected at compile time
* via the `LDC_AddressSanitizer` predefined version — the probe prints a
* `SKIP:` line and exits 0 so CI stays green on any host.
*/
module (module) gc_uaf_blindspotThe AddressSanitizer GC blind spot: a use-after-free inside GC-managed
memory is invisible to ASan, while the identical bug on malloc/free
memory is caught — demonstrated as a self-verifying contrast pair.
Two child-process demonstrations (the parent re-execs itself and asserts on
each child's exit code and stderr report):
GC.malloc -> GC.free -> read. The D GC allocates its pools with
mmap (core/internal/gc/os.d, os_mem_map) and recycles memory
internally, so freed GC memory never passes through ASan's intercepted
free and its shadow is never poisoned: the child reads a garbage
value and exits 0 — ASan reports nothing. The blind spot.
core.stdc.stdlib.malloc -> free -> read. The C allocator is
intercepted (quarantine + shadow poisoning), so the same read dies
with heap-use-after-free, a symbolized file:line, and exit 1.
Companion to docs/research/sanitizers/d-toolchain.md
§ "The GC blind spot: ASan cannot see GC pools".
Run with: dub run --single gc-uaf-blindspot.d
Environment recorded: Linux 6.18.26, AMD Ryzen 9 7940HX (Zen 4), LDC 1.41.0
(LLVM 18.1.8). nixpkgs LDC ships no compiler-rt, so the ASan runtime is GCC
15.2.0's libasan.so.8 via LDC's linker-gcc.cpp fallback (-fsanitize=
address handed to the C-compiler link driver); it self-symbolizes without
llvm-symbolizer.
Portability
builds without instrumentation (DMD has no -fsanitize; the
flag is gated platform="ldc"), in which case — detected at compile time
via the LDC_AddressSanitizer predefined version — the probe prints a
SKIP: line and exits 0 so CI stays green on any host.
gc_uaf_blindspot;
import (package) stdstd.(module) std.stdioCategory Symbols File handles _popen File isFileHandle openNetwork stderr stdin stdout Reading chunks lines readf readfln readln Writing toFile write writef writefln writeln Misc KeepTerminator LockType StdioException
Standard I/O functions that extend core.stdc.stdio. core.stdc.stdio
is publically imported when importing std.stdio.
There are three layers of I/O:
The lowest layer is the operating system layer. The two main schemes are Windows and Posix.
C's stdio.h which unifies the two operating system schemes.
std.stdio, this module, unifies the various stdio.h implementations into
a high level package for D programs.
Source
std/stdio.d
stdio : (alias template) gc_uaf_blindspot.writefln = std.stdio.writefln(alias fmt, A...)(A args) if (isSomeString!(typeof(fmt)))Equivalent to writef(fmt, args, '\n').
writefln, (alias template) gc_uaf_blindspot.writeln = std.stdio.writeln(T...)(T args)Equivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln;
version (LDC_AddressSanitizerLDC_AddressSanitizer)
private enum instrumented = true;
else
private enum (constant) bool gc_uaf_blindspot.instrumented = falseinstrumented = false;
/// Name of the env var that selects the faulty child leg.
private enum (constant) string gc_uaf_blindspot.childVar = "GC_UAF_BLINDSPOT_CHILD"Name of the env var that selects the faulty child leg.
childVar = "GC_UAF_BLINDSPOT_CHILD";
/// Child leg 1: use-after-free entirely inside GC-managed memory.
/// ASan never sees the GC's mmap'd pools, so this runs to completion.
private int int gc_uaf_blindspot.runGcLeg()Child leg 1: use-after-free entirely inside GC-managed memory.
ASan never sees the GC's mmap'd pools, so this runs to completion.
runGcLeg()
{
import (package) corecore.(module) core.memoryThis module provides an interface to the garbage collector used by
applications written in the D programming language. It allows the
garbage collector in the runtime to be swapped without affecting
binary compatibility of applications.
Using this module is not necessary in typical D code. It is mostly
useful when doing low-level memory management.
Notes to users
The GC is a conservative mark-and-sweep collector. It only runs a
collection cycle when an allocation is requested of it, never
otherwise. Hence, if the program is not doing allocations,
there will be no GC collection pauses. The pauses occur because
all threads the GC knows about are halted so the threads' stacks
and registers can be scanned for references to GC allocated data.
The GC does not know about threads that were created by directly calling
the OS/C runtime thread creation APIs and D threads that were detached
from the D runtime after creation.
Such threads will not be paused for a GC collection, and the GC might not detect
references to GC allocated data held by them. This can cause memory corruption.
There are several ways to resolve this issue:
Do not hold references to GC allocated data in such threads.
Register/unregister such data with calls to addRoot/removeRoot and
addRange/removeRange.
Maintain another reference to that same data in another thread that the
GC does know about.
Disable GC collection cycles while that thread is active with disable/enable.
Register the thread with the GC using thread_attachThis/thread_detachThis.
Notes to implementors
On POSIX systems, the signals SIGRTMIN and SIGRTMIN + 1 are reserved
by this module for use in the garbage collector implementation.
Typically, they will be used to stop and resume other threads
when performing a collection, but an implementation may choose
not to use this mechanism (or not stop the world at all, in the
case of concurrent garbage collectors).
Registers, the stack, and any other memory locations added through
the GC.addRange`` function are always scanned conservatively.
This means that even if a variable is e.g. of type float,
it will still be scanned for possible GC pointers. And, if the
word-interpreted representation of the variable matches a GC-managed
memory block's address, that memory block is considered live.
Implementations are free to scan the non-root heap in a precise
manner, so that fields of types like float will not be considered
relevant when scanning the heap. Thus, casting a GC pointer to an
integral type (e.g. size_t) and storing it in a field of that
type inside the GC heap may mean that it will not be recognized
if the memory block was allocated with precise type info or with
the GC.BlkAttr.NO_SCAN`` attribute.
Destructors will always be executed while other threads are
active; that is, an implementation that stops the world must not
execute destructors until the world has been resumed.
A destructor of an object must not access object references
within the object. This means that an implementation is free to
optimize based on this rule.
An implementation is free to perform heap compaction and copying
so long as no valid GC pointers are invalidated in the process.
However, memory allocated with GC.BlkAttr.NO_MOVE`` must
not be moved/copied.
Implementations must support interior pointers. That is, if the
only reference to a GC-managed memory block points into the
middle of the block rather than the beginning (for example), the
GC must consider the memory block live. The exception to this
rule is when a memory block is allocated with the
GC.BlkAttr.NO_INTERIOR`` attribute; it is the user's
responsibility to make sure such memory blocks have a proper pointer
to them when they should be considered live.
It is acceptable for an implementation to store bit flags into
pointer values and GC-managed memory blocks, so long as such a
trick is not visible to the application. In practice, this means
that only a stop-the-world collector can do this.
Implementations are free to assume that GC pointers are only
stored on word boundaries. Unaligned pointers may be ignored
entirely.
Implementations are free to run collections at any point. It is,
however, recommendable to only do so when an allocation attempt
happens and there is insufficient memory available.
Source
core/memory.d
memory : (struct) core.memory.GCThis struct encapsulates all garbage collection functionality for the D
programming language.
GC;
int* (local variable) int* pp = cast(int*) (struct) core.memory.GCThis struct encapsulates all garbage collection functionality for the D
programming language.
GC.void* core.memory.GC.malloc(ulong sz, uint ba = 0u, scope const(object.TypeInfo) ti = null) pure nothrowRequests an aligned block of managed memory from the garbage collector.
This memory may be deleted at will with a call to free, or it may be
discarded and cleaned up automatically during a collection run. If
allocation fails, this function will call onOutOfMemory which is
expected to throw an OutOfMemoryError.
malloc(64);
(local variable) int* pp[0] = 42;
(struct) core.memory.GCThis struct encapsulates all garbage collection functionality for the D
programming language.
GC.void core.memory.GC.free(void* p) pure nothrow @nogcDeallocates the memory referenced by p. If p is null, no action occurs.
If p references memory not originally allocated by this garbage
collector, if p points to the interior of a memory block, or if this
method is called from a finalizer, no action will be taken. The block
will not be finalized regardless of whether the FINALIZE attribute is
set. If finalization is desired, call destroy prior to GC.free``.
free((local variable) int* pp);
// Deliberate read-after-free of GC memory (the value is garbage).
void std.stdio.writefln!(char, int)(in char[] fmt, int __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("gc leg: read-after-GC.free = %s (not flagged)", (local variable) int* pp[0]);
return 0;
}
/// Child leg 2: the identical bug on the intercepted C allocator.
/// ASan poisons freed memory, so this read aborts with a report.
private int int gc_uaf_blindspot.runMallocLeg()Child leg 2: the identical bug on the intercepted C allocator.
ASan poisons freed memory, so this read aborts with a report.
runMallocLeg()
{
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdlibD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html, stdlib.h
Source
core/stdc/stdlib.d
stdlib : (alias) free = void core.stdc.stdlib.free(void* ptr) nothrow @nogcfree, (alias) malloc = void* core.stdc.stdlib.malloc(ulong size) nothrow @nogcmalloc;
int* (local variable) int* pp = cast(int*) void* core.stdc.stdlib.malloc(ulong size) nothrow @nogcmalloc(64);
(local variable) int* pp[0] = 42;
void core.stdc.stdlib.free(void* ptr) nothrow @nogcfree((local variable) int* pp);
void std.stdio.writefln!(char, int)(in char[] fmt, int __param_1) @safeEquivalent to writef(fmt, args, '\n').
writefln("malloc leg: read-after-free = %s (should never print)", (local variable) int* pp[0]);
return 0;
}
/// Re-execs this binary with `childVar=leg`, captures combined output, and
/// returns (exitCode, output).
private auto std.typecons.Tuple!(int, "status", string, "output") gc_uaf_blindspot.runChild(string leg) @safeRe-execs this binary with childVar=leg``, captures combined output, and
returns (exitCode, output).
runChild((alias) object.string = stringstring (parameter) string legleg)
{
import (package) stdstd.(module) std.processFunctions for starting and interacting with other processes, and for
working with the current process' execution environment.
Process handling
`spawnProcess` spawns a new `process`, optionally assigning it an
arbitrary set of standard input, output, and error streams.
The function returns immediately, leaving the child process to execute
in parallel with its parent. All other functions in this module that
spawn processes are built around spawnProcess.
`wait` makes the parent `process` wait for a child `process` to
terminate. In general one should always do this, to avoid
child processes becoming "zombies" when the parent process exits.
Scope guards are perfect for this – see the spawnProcess
documentation for examples. tryWait is similar to wait,
but does not block if the process has not yet terminated.
`pipeProcess` also spawns a child `process` which runs
in parallel with its parent. However, instead of taking
arbitrary streams, it automatically creates a set of
pipes that allow the parent to communicate with the child
through the child's standard input, output, and/or error streams.
This function corresponds roughly to C's popen function.
`execute` starts a new `process` and waits for it
to complete before returning. Additionally, it captures
the process' standard output and error streams and returns
the output of these as a string.
`spawnShell`, `pipeShell` and `executeShell` work like
spawnProcess, pipeProcess and execute, respectively,
except that they take a single command string and run it through
the current user's default command interpreter.
executeShell corresponds roughly to C's system function.
`kill` attempts to terminate a running `process`.
The following table compactly summarises the different process creation
functions and how they relate to each other:
Runs program directly
Runs shell command Low-level process creation spawnProcess spawnShell Automatic input/output redirection using pipes pipeProcess pipeShell Execute and wait for completion, collect output execute executeShell
Other functionality
`pipe` is used to create unidirectional pipes.
`environment` is an interface through which the current `process`'
environment variables can be read and manipulated.
`escapeShellCommand` and `escapeShellFileName` are useful
for constructing shell command lines in a portable way.
Source
std/process.d
Note
Most of the functionality in this module is not available on iOS, tvOS
and watchOS. The only functions available on those platforms are:
environment, thisProcessID and thisThreadID.
process : (class) std.process.environmentManipulates environment variables using an associative-array-like
interface.
This class contains only static methods, and cannot be instantiated.
See below for examples of use.
environment, (alias) escapeShellCommand = string std.process.escapeShellCommand(const(char[])[] args...) pure @safeEscapes an argv-style argument array to be used with $(LREF spawnShell),
$(LREF pipeShell) or $(LREF executeShell).
string url = "http://dlang.org/";
executeShell(escapeShellCommand("wget", url, "-O", "dlang-index.html"));
Concatenate multiple escapeShellCommand and
$(LREF escapeShellFileName) results to use shell redirection or
piping operators.
executeShell(
escapeShellCommand("curl", "http://dlang.org/download.html") ~
"|" ~
escapeShellCommand("grep", "-o", http://\S*\.zip) ~
">" ~
escapeShellFileName("D download links.txt"));
Throws:
$(OBJECTREF Exception) if any part of the command line contains unescapable
characters (NUL on all platforms, as well as CR and LF on Windows).
escapeShellCommand, (alias) executeShell = std.typecons.Tuple!(int, "status", string, "output") std.process.executeShell(scope const(char)[] command, const(string[string]) env = cast(const(string[string]))null, std.process.Config config = Config(Flags.none, null, null), ulong maxOutput = 18446744073709551615LU, scope const(char)[] workDir = null, string shellPath = nativeShell()) @safeexecuteShell;
// detect_leaks=0: keep the asserted exit codes purely about the
// demonstrated defect (LSan's exit-at-process-end would add exit 23).
const (local variable) const(string[string]) envenv = [
(constant) string gc_uaf_blindspot.childVar = "GC_UAF_BLINDSPOT_CHILD"Name of the env var that selects the faulty child leg.
childVar: (parameter) string legleg,
"ASAN_OPTIONS": "detect_leaks=0",
];
import (package) stdstd.(module) std.fileUtilities for manipulating files and scanning directories. Functions
in this module handle files as a unit, e.g., read or write one file
at a time. For opening files and manipulating them via handles refer
to module std.stdio.
Category Functions General exists isDir isFile isSymlink rename thisExePath Directories chdir dirEntries getcwd mkdir mkdirRecurse rmdir rmdirRecurse tempDir Files append copy read readText remove slurp write Symlinks symlink readLink Attributes attrIsDir attrIsFile attrIsSymlink getAttributes getLinkAttributes getSize setAttributes Timestamp getTimes getTimesWin setTimes timeLastModified timeLastAccessed timeStatusChanged Other DirEntry FileException PreserveAttributes SpanMode getAvailableDiskSpace
Source
std/file.d
file : (alias) thisExePath = string std.file.thisExePath() @trustedReturns the full path of the current executable.
Returns:
The path of the executable as a string.
Throws:
$(REF1 Exception, object)
thisExePath;
return std.typecons.Tuple!(int, "status", string, "output") std.process.executeShell(scope const(char)[] command, const(string[string]) env = cast(const(string[string]))null, std.process.Config config = Config(Flags.none, null, null), ulong maxOutput = 18446744073709551615LU, scope const(char)[] workDir = null, string shellPath = nativeShell()) @safeexecuteShell(string std.process.escapeShellCommand(const(char[])[] args...) pure @safeEscapes an argv-style argument array to be used with spawnShell,
pipeShell or executeShell.
string url = "http://dlang.org/";
executeShell(escapeShellCommand("wget", url, "-O", "dlang-index.html"));
Concatenate multiple escapeShellCommand and
escapeShellFileName results to use shell redirection or
piping operators.
executeShell(
escapeShellCommand("curl", "http://dlang.org/download.html") ~
"|" ~
escapeShellCommand("grep", "-o", `http://\S*\.zip`) ~
">" ~
escapeShellFileName("D download links.txt"));
escapeShellCommand(string std.file.thisExePath() @trustedReturns the full path of the current executable.
Examples
import std.path : isAbsolute;
auto path = thisExePath();
assert(path.exists);
assert(path.isAbsolute);
assert(path.isFile);
thisExePath) ~ " 2>&1", (local variable) const(string[string]) envenv);
}
version (linuxlinux) private int int gc_uaf_blindspot.run()run()
{
import (package) stdstd.(package) std.algorithmalgorithm.(module) std.algorithm.searchingThis is a submodule of std.algorithm.
It contains generic searching algorithms.
Function Name Description all all!"a > 0"([1, 2, 3, 4]) returns true because all elements are positive any any!"a > 0"([1, 2, -3, -4]) returns true because at least one element is positive balancedParens balancedParens("((1 + 1) / 2)", '(', ')') returns true because the string has balanced parentheses. boyerMooreFinder find("hello world", boyerMooreFinder("or")) returns "orld" using the Boyer-Moore algorithm. canFind canFind("hello world", "or") returns true. count Counts all elements or elements matching a predicate, specific element or sub-range.
count([1, 2, 1]) returns 3,
count([1, 2, 1], 1) returns 2 and
count!"a < 0"([1, -3, 0]) returns 1. |
| countUntil | countUntil(a, b) returns the number of steps taken in a to reach b; for example, countUntil("hello!", "o") returns 4. |
| commonPrefix | commonPrefix("parakeet", "parachute") returns "para". |
| endsWith | endsWith("rocks", "ks") returns true. |
| extrema | extrema([2, 1, 3, 5, 4]) returns [1, 5]. |
| find | find("hello world", "or") returns "orld" using linear search. (For binary search refer to SortedRange.) |
| findAdjacent | findAdjacent([1, 2, 3, 3, 4]) returns the subrange starting with two equal adjacent elements, i.e. [3, 3, 4]. |
| findAmong | findAmong("abcd", "qcx") returns "cd" because 'c' is among "qcx". |
| findSkip | If a = "abcde", then findSkip(a, "x") returns false and leaves a unchanged, whereas findSkip(a, "c") advances a to "de" and returns true. |
| findSplit | findSplit("abcdefg", "de") returns a tuple of three ranges "abc", "de", and "fg". |
| findSplitAfter | findSplitAfter("abcdefg", "de") returns a tuple of two ranges "abcde" and "fg". |
| findSplitBefore | findSplitBefore("abcdefg", "de") returns a tuple of two ranges "abc" and "defg". |
| minCount | minCount([2, 1, 1, 4, 1]) returns tuple(1, 3). |
| maxCount | maxCount([2, 4, 1, 4, 1]) returns tuple(4, 2). |
| minElement | Selects the minimal element of a range. minElement([3, 4, 1, 2]) returns 1. |
| maxElement | Selects the maximal element of a range. maxElement([3, 4, 1, 2]) returns 4. |
| minIndex | Index of the minimal element of a range. minIndex([3, 4, 1, 2]) returns 2. |
| maxIndex | Index of the maximal element of a range. maxIndex([3, 4, 1, 2]) returns 1. |
| minPos | minPos([2, 3, 1, 3, 4, 1]) returns the subrange [1, 3, 4, 1], i.e., positions the range at the first occurrence of its minimal element. |
| maxPos | maxPos([2, 3, 1, 3, 4, 1]) returns the subrange [4, 1], i.e., positions the range at the first occurrence of its maximal element. |
| skipOver | Assume a = "blah". Then skipOver(a, "bi") leaves a unchanged and returns false, whereas skipOver(a, "bl") advances a to refer to "ah" and returns true. |
| startsWith | startsWith("hello, world", "hello") returns true. |
| until | Lazily iterates a range until a specific value is found. |
Source
std/algorithm/searching.d
searching : (alias template) canFind = std.algorithm.searching.canFind(alias pred = "a == b")Convenience function. Like find, but only returns whether or not the search
was successful.
For more information about pred see $(LREF find).
See_Also:
$(REF among, std,algorithm,comparison) for checking a value against multiple arguments.
canFind;
import (package) stdstd.(module) std.processFunctions for starting and interacting with other processes, and for
working with the current process' execution environment.
Process handling
`spawnProcess` spawns a new `process`, optionally assigning it an
arbitrary set of standard input, output, and error streams.
The function returns immediately, leaving the child process to execute
in parallel with its parent. All other functions in this module that
spawn processes are built around spawnProcess.
`wait` makes the parent `process` wait for a child `process` to
terminate. In general one should always do this, to avoid
child processes becoming "zombies" when the parent process exits.
Scope guards are perfect for this – see the spawnProcess
documentation for examples. tryWait is similar to wait,
but does not block if the process has not yet terminated.
`pipeProcess` also spawns a child `process` which runs
in parallel with its parent. However, instead of taking
arbitrary streams, it automatically creates a set of
pipes that allow the parent to communicate with the child
through the child's standard input, output, and/or error streams.
This function corresponds roughly to C's popen function.
`execute` starts a new `process` and waits for it
to complete before returning. Additionally, it captures
the process' standard output and error streams and returns
the output of these as a string.
`spawnShell`, `pipeShell` and `executeShell` work like
spawnProcess, pipeProcess and execute, respectively,
except that they take a single command string and run it through
the current user's default command interpreter.
executeShell corresponds roughly to C's system function.
`kill` attempts to terminate a running `process`.
The following table compactly summarises the different process creation
functions and how they relate to each other:
Runs program directly
Runs shell command Low-level process creation spawnProcess spawnShell Automatic input/output redirection using pipes pipeProcess pipeShell Execute and wait for completion, collect output execute executeShell
Other functionality
`pipe` is used to create unidirectional pipes.
`environment` is an interface through which the current `process`'
environment variables can be read and manipulated.
`escapeShellCommand` and `escapeShellFileName` are useful
for constructing shell command lines in a portable way.
Source
std/process.d
Note
Most of the functionality in this module is not available on iOS, tvOS
and watchOS. The only functions available on those platforms are:
environment, thisProcessID and thisThreadID.
process : (class) std.process.environmentManipulates environment variables using an associative-array-like
interface.
This class contains only static methods, and cannot be instantiated.
See below for examples of use.
environment;
const (local variable) const(string) legleg = (class) std.process.environmentManipulates environment variables using an associative-array-like
interface.
This class contains only static methods, and cannot be instantiated.
See below for examples of use.
environment.string std.process.environment.get(scope const(char)[] name, string defaultValue = null) @safeRetrieves the value of the environment variable with the given name,
or a default value if the variable doesn't exist.
Unlike environment.opIndex, this function never throws on Posix.
auto sh = environment.get("SHELL", "/bin/sh");
This function is also useful in checking for the existence of an
environment variable.
auto myVar = environment.get("MYVAR");
if (myVar is null)
{
// Environment variable doesn't exist.
// Note that we have to use 'is' for the comparison, since
// myVar == null is also true if the variable exists but is
// empty.
}
get((constant) string gc_uaf_blindspot.childVar = "GC_UAF_BLINDSPOT_CHILD"Name of the env var that selects the faulty child leg.
childVar);
if ((local variable) const(string) legleg == "gc")
return int gc_uaf_blindspot.runGcLeg()Child leg 1: use-after-free entirely inside GC-managed memory.
ASan never sees the GC's mmap'd pools, so this runs to completion.
runGcLeg();
if ((local variable) const(string) legleg == "malloc")
return int gc_uaf_blindspot.runMallocLeg()Child leg 2: the identical bug on the intercepted C allocator.
ASan poisons freed memory, so this read aborts with a report.
runMallocLeg();
static if (!instrumented)
{
void std.stdio.writeln!(string, string)(string __param_0, string __param_1) @safeEquivalent to write(args, '\n'). Calling writeln without
arguments is valid and just prints a newline to the standard
output.
Example
Reads stdin and writes it to stdout with an argument
counter.
import std.stdio;
void main()
{
string line;
for (size_t count = 0; (line = readln) !is null; count++)
{
writeln("Input ", count, ": ", line);
}
}
writeln("SKIP: built without AddressSanitizer instrumentation ",
"(DMD has no -fsanitize; LDC-only dflags are platform-gated)");
return 0;
}
else
{
// Parent: run both legs and verify the contrast.
const gc = runChild("gc");
assert(gc.status == 0,
"expected the GC use-after-free to go UNdetected (the blind spot)");
assert(!gc.output.canFind("AddressSanitizer"),
"expected no ASan report for GC memory");
writeln("proved: use-after-free inside GC pools is invisible to ASan");
const mal = runChild("malloc");
assert(mal.status != 0,
"expected ASan to abort the malloc/free use-after-free child");
assert(mal.output.canFind("heap-use-after-free"),
"expected a heap-use-after-free report; got:\n" ~ mal.output);
writefln("proved: the same bug on malloc/free dies with " ~
"heap-use-after-free (child exit %s)", mal.status);
writeln("OK: ASan sees the C heap but not the GC's mmap'd pools");
return 0;
}
}
int int D main()main()
{
version (linuxlinux)
return int gc_uaf_blindspot.run()run();
else
{
writeln("SKIP: this probe records Linux behavior (platforms \"linux\")");
return 0;
}
}