AddressSanitizer (ASan) is a compiler technology that detects addressability-related memory errors with some additional checks. It consists of two components: compiler instrumentation and a runtime library. To put it simply,
- The compiler instruments global variables, stack frames, and heap allocations to monitor shadow memory.
- The compiler also instruments memory access instructions to verify shadow memory.
- In case of an error, the inserted code invokes a callback (implemented in the runtime library) to report the error along with a stack trace. Typically, the program will terminate after displaying the error message.
This article describes global variable instrumentation.
Global variable instrumentation
AddressSanitizer instruments certain defined global variables of LLVM external or internal linkage. To be instrumented, the variable must satisfy a bunch of conditions.
- It is not thread-local.
- It has a smaller alignment.
- It is not synthesized by LLVM.
- It does not have the
no_sanitize_address
attribute in LLVM IR. Variables receive this attribute when annotated as__attribute__((no_sanitize("address")))
or__attribute__((disable_sanitizer_instrumentation))
in C/C++.
1 | int g0; |
Each instrumented global variable is padded with a right redzone to detect out-of-bounds accesses.
1 | @g0 = dso_local global { i32, [28 x i8] } zeroinitializer, comdat, align 32 |
On ELF platforms, by default (since Clang 17.0) each instrumented
global variable receives an associated __asan_global_$name
variable, which is located within the asan_globals
section.
Additionally, there are several related variables, including some
unnamed ones (@0
and @1
), as well as
__odr_asan_gen_g0
and __odr_asan_gen_g1
, along
with metadata nodes (!0
and !1
), which we will
discuss in more detail later."
1 | @___asan_gen_.1 = private unnamed_addr constant [3 x i8] c"g0\00", align 1 |
The module constructor asan.module_ctor
processes
garbage-collectable asan_globals
input sections. This
constructor invokes a runtime callback to register the instrumented
global variables, which involves poisoning the redzone and conducting
ODR violation checks. I will discuss ODR violation checking later.
1 | define internal void @asan.module_ctor() #0 comdat { |
The runtime poisons the redzone of each instrumented global variable.
1 | void __asan_register_elf_globals(uptr *flag, void *start, void *stop) { |
Every full granule in the shadow of the redzone is filled with 0xf9
(kAsanGlobalRedzoneMagic
) while a partial granule is filled
in a manner similar to partially-addressable stack memory.
1 | ALWAYS_INLINE void PoisonRedZones(const Global &g) { |
global-buffer-overflow example
If an access occurs within a redzone byte poisoned by 0xf9 or within
a partial redzone preceding 0xf9, the runtime will report a
global-buffer-overflow
error. Here is an example:
1 | cat > a.c <<e |
1 | % ./a 1 # a[argc * 5] == a[10] is out-of-bounds |
ODR violation checker
The global variable poisoning mechanism offers a straightforward means to detect differences in variable definitions between two components, such as between the main executable and a shared object, or between two shared objects. This can be considered a category of ODR violations.
1 | echo 'int var; int main() { return var; }' > a.cc |
1 | % ./a |
The default mode, detect_odr_violation=2
, also prohibits
symbol interposition on variables. If you change long
to
int
in b.cc
, you will still encounter an
odr-violation
error. In contrast, with
detect_odr_violation=1
, errors are suppressed if the
registered variables are of the same size.
1 | % ASAN_OPTIONS=detect_odr_violation=1 ./a |
For a variable named $var
, a one-byte variable,
__odr_asan_gen_$var
, is created with the original linkage
(essentially must be external
).
If $var
is defined in two instrumented modules, their
__odr_asan_gen_$var
symbols reference to the same copy due
to symbol interposition. When registering $var
, the runtime
checks whether __odr_asan_gen_$var
is already 1, and if
yes, the program has an ODR violation; otherwise
__odr_asan_gen_$var
is set to 1.
1 | @__odr_asan_gen_g0 = global i8 0, align 1 |
The private aliases @0 and @1 were due to http://reviews.llvm.org/D15642.
ODR indicator
The previous example uses
-fsanitize-address-use-odr-indicator
.
Prior to Clang 16,
-fno-sanitize-address-use-odr-indicator
was the default for
non-Windows platforms. The runtime checks checks whether a variable has
been registered by verifying whether its redzone has been poisoned, and
reports an ODR violation when the redzone has been poisoned.
1 | @___asan_gen_.1 = private unnamed_addr constant [3 x i8] c"g0\00", align 1 |
This mode eliminates the need for an additional variable like
__odr_asan_gen_$var
, but it can lead to interaction issues
when mixing instrumented and uninstrumented components. In the case of a
shared object, if the reference to $var
in
__asan_global_$var
is interposed with an uninstrumented
variable due to symbol interposition, it may result in a spurious error
stating, "The following global variable is not properly aligned."
For Clang 16, I introduced the use of
-fsanitize-address-use-odr-indicator
by default for
non-Windows targets (see https://reviews.llvm.org/D137227).
(Additionally, https://reviews.llvm.org/D127911 changed the ODR
indicator symbol name to __odr_asan_gen_$demangled
.)
Copy relocations
Private aliases have an interest interaction with copy relocations. This issue is reported at https://gcc.gnu.org/PR68016.
The default -fsanitize-address-use-odr-indicator
in
Clang 16 and later cannot detect the global-buffer-overflow
error below:
1 | echo 'int f[5] = {1};' > foo.cc |
The definition of f
in foo.cc
is
instrumented, resulting in the creation of __asan_global_f
.
However, the executable actually accesses the copy created by the linker
due to copy relocation.
When -asan-use-private-alias=1
is in effect (the default
since Clang 16), the __asan_global_f
variable references
the unused copy inside the shared object. The executable accesses the
copy-relocated variable, whose redzone is not poisoned, resulting in no
error.
Conversely, when -asan-use-private-alias=0
is in effect,
the __asan_global_f
variable references the copy-relocated
variable and poisons the redzone within the executable. Consequently,
accessing f[5]
leads to the expected error.
Garbage collection
Since Clang 17, asan.module_ctor
is, by default, placed
in a COMDAT group. When multiple instrumented relocatable object files
are linked together, only one asan.module_ctor
is
retained.
__asan_global_g0
is positioned in a section that links
to the section defining g0
using the
SHF_LINK_ORDER
flag. During linking, if the linker discards
the section defining g0
, the asan_globals
section containing __asan_global_g0
will also be discarded.
For more detail on SHF_LINK_ORDER
, you can refer to Metadata
sections, COMDAT and SHF_LINK_ORDER.
Before Clang 17, the default behavior was to use
-fno-sanitize-address-globals-dead-stripping
. In this mode,
the instrumentation places pointers to instrumented global variables in
a metadata array and calls __asan_register_globals
.
__asan_register_globals
then iterates over the array and
registers each global variable.
1 | @g0 = dso_local global { i32, [28 x i8] } zeroinitializer, align 32 |
asan.module_ctor
references the metadata array
@0
, which, in turn, references @1
and
@2
. @1
and @2
reference the
global variables g0
and g1
, respectively. This
unfortunately indicates that g0
and g1
cannot
be discarded by section-based garbage collection.
It's important to note that this version of
asan.module_ctor
is not placed within a COMDAT group. In
another compile unit, a separate asan.module_ctor
references a different metadata array. As a result, these
asan.module_ctor
functions cannot share the same
implementation.
In a linked component, both __asan_init
and
__asan_version_mismatch_check_v8
will be called multiple
times, incurring a small overhead.
Regrettably, the default setting of
-fsanitize-address-globals-dead-stripping
in Clang 17 had a
bug. Specifically, when there are no global variables, and the unique
module ID is non-empty, a COMDAT asan.module_ctor
is
created without any __asan_register_elf_globals
calls. If
this COMDAT is selected as the prevailing copy by the linker, the
linkage unit will lack a __asan_register_elf_globals
call,
resulting in an unpoisoned redzone and a non-functional ODR violation
checker.
I have fixed this in the main branch (#67745) but LLVM 17.0.2 does not contain the fix.
Global variable metadata
Before Clang 15, Clang's instrumentation included
llvm.asan.globals
, and the AddressSanitizer runtime
required its object file feature for symbolization.
https://reviews.llvm.org/D127552 enabled debug
information for symbolization and https://reviews.llvm.org/D127911 deleted the metadata
node llvm.asan.globals
.