[Research] Address Sanitizer: Part 1 (EN)

Hello! I am millet, a new member who recently joined Hackyboiz.
While solving wargame challenges in the past, I came across something called ASan. At the time, I only glanced over it briefly, but using this research article as an excuse, I decided to properly study it and chose it as my topic.
In this article, I will take a look at what Address Sanitizer is and how it is implemented.
1. About Address Sanitizer
Address Sanitizer, often abbreviated as ASan, is a runtime error detection tool designed to dynamically detect memory errors that occur during program execution.
You can think of it as a tool that automatically inserts code to catch memory-related bugs when writing programs in C or C++. It was initially developed and maintained by Google, but is now maintained as part of the LLVM project.
ASan is provided through multiple implementations. Since I primarily work in Linux environments, this research article will focus on the Sanitizer provided by GCC.
In most Linux distributions, when GCC is installed, the corresponding runtime package is installed together (typically the libasan package).
$ (dpkg -l | grep libasan) && (gcc --version | head -1)
ii libasan6:amd64 11.4.0-1ubuntu1~22.04.2 amd64 AddressSanitizer -- a fast memory error detector
gcc (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0
The ASan implementation provided by GCC uses the same memory model and detection mechanisms as LLVM ASan, so conceptually there is no difference.

ASan is currently supported on the following platforms.
| OS | x86 | x86_64 | ARM | ARM64 | MIPS | MIPS64 | PowerPC | PowerPC64 |
|---|---|---|---|---|---|---|---|---|
| Linux | O | O | O | O | O | O | ||
| OS X | O | O | ||||||
| iOS Simulator | O | O | ||||||
| FreeBSD | O | O | ||||||
| Android | O | O | O | O |
1.1 Detectable memory errors
As its name suggests, ASan can detect most common memory errors. The detection scope and behavior can be configured using compile-time options and runtime flags. Some representative examples are listed below.
Use After Free (Dangling Pointer Dereference)
Heap Buffer Overflow
Stack Buffer Overflow
Global Buffer Overflow
-fsanitize=address
Use After Return
ASAN_OPTIONS=detect_stack_use_after_return=1
- This option incurs significant performance overhead because stack frames are moved to a separate virtual heap.
Use After Scope
ASAN_OPTIONS=detect_stack_use_after_scope=1
Initialization Order Bugs
ASAN_OPTIONS=check_initialization_order=1
- Applicable only to C++, where global object initialization order matters.
Memory Leaks
ASAN_OPTIONS=detect_leaks=1
- This functionality is actually handled by LeakSanitizer, not ASAN itself.
There are many more runtime flags available, but in this article, the focus will be on the core functionality enabled by -fsanitize=address.
Additionally, ASan’s fast unwinder relies on frame pointer chains, so it is recommended to compile with the following option to ensure that the RBP register is used as a frame pointer.
-fno-omit-frame-pointer
2. ASAN Monitoring Mechanisms
As seen above, simply using -fsanitize=address enables a wide range of detections. How does ASan detect these memory errors?
ASan primarily relies on three monitoring mechanisms:
Shadow Memory-based Access Monitoring
Poisoning / Unpoisoning for Lifetime Tracking
Redzones for Boundary Detection
These mechanisms are inserted into the program during compile-time instrumentation.
Checks are inserted before every memory read, write, allocation, and deallocation, and when a policy violation is detected, an error message is printed and the program is terminated by default.
Instrumentation is mainly inserted at the following points:
- Before memory read/write instructions
- At dynamic memory allocation
- At dynamic memory deallocation
- At function entry (stack frame setup)
- At function return (stack frame teardown)
- At global object initialization and destruction
byte *shadow_address = MemToShadow(address);
byte shadow_value = *shadow_address;
if (shadow_value) {
if (SlowPathCheck(shadow_value, address, kAccessSize)) {
ReportError(address, kAccessSize, kIsWrite);
}
}
Let us examine each mechanism in detail.
2.1 Shadow Memory
ASan maintains a separate memory region called shadow memory, which stores metadata for each byte of the program’s memory.
The address of the shadow memory corresponding to a given memory address is calculated as follows.
Shadow = (Mem >> 3) + SHADOW_OFFSET
Internally, this is implemented using the following macros.
llvm-project/compiler-rt/lib/asan/asan_mapping.h
# define MEM_TO_SHADOW(mem) \
((STRIP_MTE_TAG(mem) >> ASAN_SHADOW_SCALE) + (ASAN_SHADOW_OFFSET))
# define SHADOW_TO_MEM(mem) \
(((mem) - (ASAN_SHADOW_OFFSET)) << (ASAN_SHADOW_SCALE))
As shown above, 8 bytes of application memory correspond to 1 byte of shadow memory.
The value of SHADOW_OFFSET is platform- and architecture-dependent. On x86-64, it is defined as 0x7fff8000.
llvm-project/compiler-rt/lib/asan/asan_mapping.h
// Typical shadow mapping on Linux/x86_64 with SHADOW_OFFSET == 0x00007fff8000:
// || `[0x10007fff8000, 0x7fffffffffff]` || HighMem ||
// || `[0x02008fff7000, 0x10007fff7fff]` || HighShadow ||
// || `[0x00008fff7000, 0x02008fff6fff]` || ShadowGap ||
// || `[0x00007fff8000, 0x00008fff6fff]` || LowShadow ||
// || `[0x000000000000, 0x00007fff7fff]` || LowMem ||
...
# if ASAN_SHADOW_SCALE != 3
# error "Value below is based on shadow scale = 3."
# error "Original formula was: 0x7FFFFFFF & (~0xFFFULL << SHADOW_SCALE)."
# endif
# define ASAN_SHADOW_OFFSET_CONST 0x000000007fff8000
# endif
#endif
...
AS documented in the source code, shadow memory and application memory are logically separated in the virtual address space. On x86-64, the layout is as follows.
| Category | Mapped Region | Description |
|---|---|---|
| HighMem | [0x10007fff8000, 0x7fffffffffff] |
Virtual memory represented by 5 bytes or more |
| HighShadow | [0x02008fff7000, 0x10007fff7fff] |
Shadow Memory corresponding to HighMem |
| ShadowGap | [0x00008fff7000, 0x02008fff6fff] |
Gap region between Shadow Memory areas |
| LowShadow | [0x00007fff8000, 0x00008fff6fff] |
Shadow Memory corresponding to LowMem |
| LowMem | [0x000000000000, 0x00007fff7fff] |
Virtual memory represented by 4 bytes or less |

- The ShadowGap region exists solely to prevent interference between shadow regions and has no read or write permissions.

Because shadow memory is mapped at an 8:1 ratio, certain unaligned memory accesses may not be detected. Consider the following example.
// g++ oob.cpp -o oob -fsanitize=address
int main(void)
{
int *x = new int[2]; // [0, 7]
int *y = (int *)((char *)x + 6); // [6, 9]
*y = 1; // OOB
delete[] x;
return 0;
}
Although this is clearly an out-of-bounds access, ASAN does not detect it due to the 8:1 mapping granularity.
To address such cases, it is recommended to enable additional sanitizers such as -fsanitize=alignment, which is part of Undefined Behavior Sanitizer (UBSan).

Because the shadow memory is mapped at an 8:1 ratio, the result of the expression
(v4 + 6) >> 3 eventually becomes identical to v4 >> 3.
As a result, the same shadow memory byte is checked, which prevents ASan from detecting the out-of-bounds access.
Due to this structural limitation, memory accesses that cross misaligned boundaries may not be properly detected by ASan.
Reducing the mapping scale would allow stricter checks, but doing so would introduce inefficiencies in vectorized instructions and deviate from ASAN’s original design assumptions. For these reasons, the recommended approach is to apply an additional sanitizer that explicitly checks alignment violations, such as -fsanitize=alignment. This option is part of Undefined Behavior Sanitizer (UBSan).
When rebuilding the program with the -fsanitize=alignment option enabled, the out-of-bounds access is correctly detected.
$ g++ oob.cpp -o oob_aligned -fsanitize=address -fsanitize=alignment
$ ./oob_aligned
oob.cpp:7:8: runtime error: store to misaligned address 0x502000000016 for type 'int', which requires 4 byte alignment
0x502000000016: note: pointer points here
be be be be be be 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
^
Examining the resulting binary shows that UBSan has inserted additional type-based checking functions. This topic will be covered in more detail in a future article.

2.2 Poisoning / Unpoisoning
Shadow memory stores one byte of metadata per 8 bytes of application memory. ASan interprets this byte using several predefined values to represent memory accessibility.
Accessible memory is referred to as unpoisoned, while inaccessible memory is poisoned. Read and write accesses are not distinguished at this level.
| Shadow Byte | Meaning | State |
|---|---|---|
0 |
All 8 bytes accessible | unpoison |
< 0 |
All 8 bytes inaccessible | poison |
k (1~7) |
First k bytes accessible |
부분 unpoison |
At process startup, shadow memory is initialized as poisoned. Known memory regions are unpoisoned before entering main.
It can be summarized in a table as follows.
| Region | Initialized before main thread execution | Modified during process execution |
|---|---|---|
| Global / static variables | O (unpoison) | X |
| Stack frame (main) | O (partially unpoison) | Updated on function call and return |
| Heap | X (poison) | Managed by dedicated interceptor functions |
| TLS | O (unpoison) | X |
Dynamic heap memory is managed via interceptor functions inserted by ASan, such as wrapped versions of malloc and free.
llvm-project/compiler-rt/lib/interception/interception.h
#define INTERCEPTOR(ret_type, func, ...) \
DEFINE_REAL(ret_type, func, __VA_ARGS__) \
DECLARE_WRAPPER(ret_type, func, __VA_ARGS__) \
extern "C" INTERCEPTOR_ATTRIBUTE ret_type WRAP(func)(__VA_ARGS__)
llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp
...
INTERCEPTOR(void, free, void *ptr) {
if (DlsymAlloc::PointerIsMine(ptr))
return DlsymAlloc::Free(ptr);
GET_STACK_TRACE_FREE;
asan_free(ptr, &stack);
}
...
INTERCEPTOR(void*, malloc, uptr size) {
if (DlsymAlloc::Use())
return DlsymAlloc::Allocate(size);
GET_STACK_TRACE_MALLOC;
return asan_malloc(size, &stack);
}
...
ASan also provides explicit APIs to poison or unpoison memory regions.
llvm-project/compiler-rt/include/sanitizer/asan_interface.h
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
#define ASAN_POISON_MEMORY_REGION(addr, size) \
__asan_poison_memory_region((addr), (size))
#define ASAN_UNPOISON_MEMORY_REGION(addr, size) \
__asan_unpoison_memory_region((addr), (size))
#else
#define ASAN_POISON_MEMORY_REGION(addr, size) \
((void)(addr), (void)(size))
#define ASAN_UNPOISON_MEMORY_REGION(addr, size) \
((void)(addr), (void)(size))
#endif
Explicit poisoning can be enabled or disabled through runtime flags.
ASAN_OPTIONS=allow_user_poisoning=0
These can be useful for enforcing custom memory lifetimes. Let us verify this behavior using an example program.
// g++ unpoison.cpp -o unpoison -fsanitize=address
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sanitizer/asan_interface.h>
void *cus_alloc(size_t size)
{
void *p = malloc(size);
ASAN_POISON_MEMORY_REGION(p, size);
return p;
}
void activate_p(void *p, size_t size)
{
ASAN_UNPOISON_MEMORY_REGION(p, size);
}
int main(void)
{
char *p = (char *)cus_alloc(8);
activate_p(p, 6);
strcpy(p, "millet");
puts(p);
free(p);
return 0;
}
In ASan, the redefined malloc() function manages newly allocated memory as unpoisoned by default. In this example, the memory region is first marked as inaccessible using ASAN_POISON_MEMORY_REGION, and then explicitly unpoisoned only when it is actually used. This approach provides an additional layer of safety in memory management.
If the memory is not explicitly unpoisoned, the program terminates with an error as shown below.
./unpoison
=================================================================
==1381294==ERROR: AddressSanitizer: use-after-poison on address 0x502000000010 at pc 0x7f0804f052c3 bp 0x7ffcc58dca60 sp 0x7ffcc58dc208
WRITE of size 7 at 0x502000000010 thread T0
#0 0x7f0804f052c2 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827
#1 0x55577d55a300 in main (/root/asan/unpoison+0x1300)
#2 0x7f0804998d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#3 0x7f0804998e3f in __libc_start_main_impl ../csu/libc-start.c:392
#4 0x55577d55a1a4 in _start (/root/asan/unpoison+0x11a4)
0x502000000010 is located 0 bytes inside of 8-byte region [0x502000000010,0x502000000018)
allocated by thread T0 here:
#0 0x7f0804f7f887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x55577d55a284 in cus_alloc(unsigned long) (/root/asan/unpoison+0x1284)
#2 0x55577d55a2e1 in main (/root/asan/unpoison+0x12e1)
#3 0x7f0804998d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: use-after-poison ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827 in __interceptor_memcpy
Shadow bytes around the buggy address:
0x0a047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0a047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0a047fff8000: fa fa[f7]fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0a047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==1381294==ABORTING
It clearly reports the issue as a use-after-poison error.
2.3 Redzone
While poisoning tracks memory lifetime, redzones protect boundaries around memory objects. Redzones are regions of memory placed around the actual usable area of a memory object where access is explicitly forbidden.

Regions designated as redzones are always kept in a poisoned state. As a result, any read or write that crosses a boundary, or any logically invalid access, ends up touching a redzone, allowing ASan to detect heap buffer overflows or stack buffer overflows.
Redzones are primarily placed around heap, stack, and global buffers, and all of them are represented as poisoned regions in shadow memory.
| Shadow Byte | Meaning | State |
|---|---|---|
0xfa |
Heap left redzone | poison |
0xf1 |
Stack left redzone | poison |
0xf2 |
Stack mid redzone | poison |
0xf3 |
Stack right redzone | poison |
0xf5 |
Stack after return | poison |
0xf9 |
Global redzone | poison |
0xca |
Left alloca redzone | poison |
0xcb |
Right alloca redzone | poison |
Let us examine an example to observe the redzones inserted around heap allocations.
// g++ redzone.cpp -o redzone -fsanitize=address
#include <stdlib.h>
int main(void)
{
char *p = (char *)malloc(6);
p[6] = 'm';
free(p);
return 0;
}
In an x86-64 environment, ptmalloc2 allocates memory in 16-byte–aligned chunks.
When malloc(6) is called, ptmalloc2 actually allocates a total of 32 bytes.
Except for the 6 bytes of user data, the remaining space must not be accessed.
...
0x5555555551d1 <main+0008> sub rsp, 0x10
0x5555555551d5 <main+000c> mov edi, 0x6
0x5555555551da <main+0011> call 0x5555555550b0 <malloc@plt>
→ 0x5555555551df <main+0016> mov QWORD PTR [rbp-0x8], rax
...
gef➤ x/4bx (($rax >> 3) + 0x7fff8000) - 2
0xa047fff8000: 0xfa 0xfa 0x06 0xfa
After the memory allocation, the state appears as shown above. Only the shadow byte corresponding to the actually usable region is set to 0x06, while the remaining bytes are set to 0xfa, which represents the heap left redzone.
Therefore, the following conditional check detects the invalid memory access.

This time, let us examine a stack buffer overflow using an example.
// g++ redzone.cpp -o redzone -fsanitize=address
#include <stdlib.h>
int main(void)
{
char sp[16] = {0, };
sp[17] = 'i';
return 0;
}
After compilation, the main() function rewritten by ASan appears as follows.
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int64 v3; // rbx
__int64 v4; // rax
_BYTE *v5; // rdx
__int64 v6; // rax
char v7; // si
_BYTE v9[88]; // [rsp+0h] [rbp-70h] BYREF
unsigned __int64 cnry; // [rsp+58h] [rbp-18h]
v3 = (unsigned __int64)v9;
if ( _TMC_END__ )
{
v4 = __asan_stack_malloc_0(64, argv, envp);
if ( v4 )
v3 = v4;
}
v5 = (_BYTE *)(v3 + 0x60);
*(_QWORD *)v3 = 0x41B58AB3;
*(_QWORD *)(v3 + 8) = "1 32 16 4 sp:5";
*(_QWORD *)(v3 + 16) = main;
v6 = v3 >> 3;
*(_DWORD *)(v6 + 0x7FFF8000) = 0xF1F1F1F1;
*(_DWORD *)(v6 + 0x7FFF8004) = 0xF3F30000;
cnry = __readfsqword(0x28u);
if ( *(_WORD *)(((v3 + 32) >> 3) + 0x7FFF8000) )
v6 = __asan_report_store16(v3 + 32);
*((_QWORD *)v5 - 8) = 0;
*((_QWORD *)v5 - 7) = 0;
v7 = *(_BYTE *)(((unsigned __int64)(v5 - 47) >> 3) + 0x7FFF8000);
if ( v7 != 0 && (((unsigned __int8)v5 - 64 + 17) & 7) >= v7 )
v6 = __asan_report_store1(v5 - 47);
*(v5 - 47) = 'i';
if ( v9 == (_BYTE *)v3 )
{
*(_QWORD *)(v6 + 0x7FFF8000) = 0;
}
else
{
*(_QWORD *)v3 = 0x45E0360E;
*(_QWORD *)(v6 + 0x7FFF8000) = 0xF5F5F5F5F5F5F5F5LL;
**(_BYTE **)(v3 + 0x38) = 0;
}
return 0;
}
Wow… quite a lot of code has been inserted.
It may look complex at first glance, but if we apply the concepts discussed earlier, it is not particularly difficult to understand. Let us break it down step by step.
ASan Stack Frame Setup
v9 represents the top of the stack frame buffer set up by the compiler. To allow ASan to manage this stack frame, its value is moved into the variable v3.
v3 = (unsigned __int64)v9;
if ( _TMC_END__ )
{
v4 = __asan_stack_malloc_0(64, argv, envp);
if ( v4 )
v3 = v4;
}
- There is logic that checks the value
_TMC_END__and reallocates the stack. This behavior is controlled by theASAN_OPTIONS=detect_stack_use_after_returnoption, which is used to detect stack reuse after a function has returned.

Stack Frame Metadata Recording
This code records separate metadata for the stack frame. Here, it stores information such as the current state of the stack frame and the intended layout of objects within the frame.
v5 = (_BYTE *)(v3 + 0x60);
*(_QWORD *)v3 = 0x41B58AB3;
*(_QWORD *)(v3 + 8) = "1 32 16 4 sp:5";
*(_QWORD *)(v3 + 16) = main;
0x41B58AB3: A magic number used to identify the stack frame. In this context, it indicates that the stack frame is valid.llvm-project/compiler-rt/lib/asan/asan_internal.h
static const uptr kCurrentStackFrameMagic = 0x41B58AB3; static const uptr kRetiredStackFrameMagic = 0x45E0360E;"1 32 16 4 sp:5": one stack object, offset 32, size 16, internal name sp:5main: records which function this stack frame belongs to
Stack Redzone Setup
After unpoisoning the two shadow bytes corresponding to the 16 bytes that will actually be used, the regions on both sides are set as the stack left redzone and the stack right redzone, respectively.
v6 = v3 >> 3;
*(_DWORD *)(v6 + 0x7FFF8000) = 0xF1F1F1F1;
*(_DWORD *)(v6 + 0x7FFF8004) = 0xF3F30000;
Pre-access Check → Access Logic
This part follows the same shadow memory check mechanism discussed earlier.
Here, the check begins at the +32 offset, which matches the information recorded in the stack frame metadata.
if ( *(_WORD *)(((v3 + 32) >> 3) + 0x7FFF8000) )
v6 = __asan_report_store16(v3 + 32);
*((_QWORD *)v5 - 8) = 0;
*((_QWORD *)v5 - 7) = 0;
v7 = *(_BYTE *)(((unsigned __int64)(v5 - 47) >> 3) + 0x7FFF8000);
if ( v7 != 0 && (((unsigned __int8)v5 - 64 + 17) & 7) >= v7 )
v6 = __asan_report_store1(v5 - 47);
*(v5 - 47) = 'i';
Although the operations may look complex, it ultimately becomes clear that the access targets the +32 offset where the array is located.
(The __asan_report_storeX() functions will be examined in the next section.)
Cleanup Before Function Return
If v9 and v3 point to the same address, this indicates that the frame resides on the real stack, and the corresponding region is unpoisoned to allow subsequent function calls.
Otherwise, the frame was allocated on ASan’s fake stack. In this case, the magic number is changed to 0x45E0360E, which corresponds to kRetiredStackFrameMagic, and the region is poisoned with 0xf5, representing stack after return.
With the underlying concepts in mind, the code becomes much easier to understand.
if ( v9 == (_BYTE *)v3 )
{
*(_QWORD *)(v6 + 0x7FFF8000) = 0;
}
else
{
*(_QWORD *)v3 = 0x45E0360E;
*(_QWORD *)(v6 + 0x7FFF8000) = 0xF5F5F5F5F5F5F5F5LL;
**(_BYTE **)(v3 + 0x38) = 0;
}
return 0;
}
3. Error Reporting
3.1 Error Reporting Functions
When a check in the instrumentation logic fails, the following reporting functions are invoked.
llvm-project/compiler-rt/lib/asan_abi_asan_abi_shim.cpp
// Functions concerning memory load and store reporting
void __asan_report_load1(uptr addr) {
__asan_abi_report_load_n((void *)addr, 1, true);
}
void __asan_report_load2(uptr addr) {
__asan_abi_report_load_n((void *)addr, 2, true);
}
void __asan_report_load4(uptr addr) {
__asan_abi_report_load_n((void *)addr, 4, true);
}
void __asan_report_load8(uptr addr) {
__asan_abi_report_load_n((void *)addr, 8, true);
}
void __asan_report_load16(uptr addr) {
__asan_abi_report_load_n((void *)addr, 16, true);
}
void __asan_report_load_n(uptr addr, uptr size) {
__asan_abi_report_load_n((void *)addr, size, true);
}
void __asan_report_store1(uptr addr) {
__asan_abi_report_store_n((void *)addr, 1, true);
}
void __asan_report_store2(uptr addr) {
__asan_abi_report_store_n((void *)addr, 2, true);
}
void __asan_report_store4(uptr addr) {
__asan_abi_report_store_n((void *)addr, 4, true);
}
void __asan_report_store8(uptr addr) {
__asan_abi_report_store_n((void *)addr, 8, true);
}
void __asan_report_store16(uptr addr) {
__asan_abi_report_store_n((void *)addr, 16, true);
}
void __asan_report_store_n(uptr addr, uptr size) {
__asan_abi_report_store_n((void *)addr, size, true);
}
As the names suggest, reads are labeled as load, writes as store, and the suffix indicates the number of bytes accessed.
The wrapped functions __asan_abi_report_XXX_n ultimately invoke the ReportGenericError() function.
llvm-project/compiler-rt/lib/asan/asan_report.cpp
void __asan_report_error(uptr pc, uptr bp, uptr sp, uptr addr, int is_write,
uptr access_size, u32 exp) {
ENABLE_FRAME_POINTER;
bool fatal = flags()->halt_on_error;
ReportGenericError(pc, bp, sp, addr, is_write, access_size, exp, fatal);
}
void ReportGenericError(uptr pc, uptr bp, uptr sp, uptr addr, bool is_write,
uptr access_size, u32 exp, bool fatal) {
if (__asan_test_only_reported_buggy_pointer) {
*__asan_test_only_reported_buggy_pointer = addr;
return;
}
if (!fatal && SuppressErrorReport(pc)) return;
ENABLE_FRAME_POINTER;
// Optimization experiments.
// The experiments can be used to evaluate potential optimizations that remove
// instrumentation (assess false negatives). Instead of completely removing
// some instrumentation, compiler can emit special calls into runtime
// (e.g. __asan_report_exp_load1 instead of __asan_report_load1) and pass
// mask of experiments (exp).
// The reaction to a non-zero value of exp is to be defined.
(void)exp;
ScopedInErrorReport in_report(fatal);
ErrorGeneric error(GetCurrentTidOrInvalid(), pc, bp, sp, addr, is_write,
access_size);
in_report.ReportError(error);
}
The fatal value determines whether the program terminates after reporting an error.
This behavior is also controlled by compile-time options and runtime flags.
# Activate ASAN Recovery Mode
-fsanitize-recover=address
# Runtime Flag
ASAN_OPTIONS=halt_on_error=0
If we add a getchar() call to the example code used in the redzone section and run it again, we can observe that the program does not terminate even after the error occurs.
$ g++ redzone.cpp -o redzone -fsanitize=address -fsanitize-recover=address && ASAN_OPTIONS=halt_on_error=0 ./redzone
=================================================================
==1481048==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff00408791 at pc 0x5593d35b02fa bp 0x7fff00408750 sp 0x7fff00408740
WRITE of size 1 at 0x7fff00408791 thread T0
...
$ echo $?
0
3.2 Interpreting Erros
An ASan error report is generally composed of a summary, a call stack, memory information, and the state of the shadow bytes.
Using the error message produced by running the previous example, let us examine each of these components in detail.
$ ./redzone
=================================================================
==1670382==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffaec08e11 at pc 0x5591367762fa bp 0x7fffaec08dd0 sp 0x7fffaec08dc0
WRITE of size 1 at 0x7fffaec08e11 thread T0
#0 0x5591367762f9 in main (/root/asan/redzone+0x12f9)
#1 0x7fe1e1807d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#2 0x7fe1e1807e3f in __libc_start_main_impl ../csu/libc-start.c:392
#3 0x559136776144 in _start (/root/asan/redzone+0x1144)
Address 0x7fffaec08e11 is located in stack of thread T0 at offset 49 in frame
#0 0x559136776218 in main (/root/asan/redzone+0x1218)
This frame has 1 object(s):
[32, 48) 'sp' (line 6) <== Memory access at offset 49 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/root/asan/redzone+0x12f9) in main
Shadow bytes around the buggy address:
0x100075d79170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79190: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791b0: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
=>0x100075d791c0: 00 00[f3]f3 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79210: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==1670382==ABORTING
Error Overview
The type of error is a stack-buffer-overflow, and it occurred on thread T0 during a 1-byte write operation.
Below this, the call stack is displayed, allowing us to trace the function call path that led to the error.
- Faulting location:
0x5591367762fa
==1670382==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffaec08e11 at pc 0x5591367762fa bp 0x7fffaec08dd0 sp 0x7fffaec08dc0
WRITE of size 1 at 0x7fffaec08e11 thread T0
#0 0x5591367762f9 in main (/root/asan/redzone+0x12f9)
#1 0x7fe1e1807d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#2 0x7fe1e1807e3f in __libc_start_main_impl ../csu/libc-start.c:392
#3 0x559136776144 in _start (/root/asan/redzone+0x1144)
Address 0x7fffaec08e11 is located in stack of thread T0 at offset 49 in frame
#0 0x559136776218 in main (/root/asan/redzone+0x1218)
Memory Frame Information
The buffer named sp is valid only within the range [32, 48)—that is, 16 bytes—within the stack frame, but the reported access occurs at offset 49.
From this information, we can determine that an out-of-bounds write has occurred.
This frame has 1 object(s):
[32, 48) 'sp' (line 6) <== Memory access at offset 49 overflows this variable
Shadow Bytes State
The actual accessible range is from 0x100075d791c0 to 0x100075d791c1, but the location marked with => indicates an access to a poisoned region labeled 0xf3, which represents the stack right redzone, as highlighted by the brackets [ ].
From this, we can clearly observe the overall memory access permission state.
Shadow bytes around the buggy address:
0x100075d79170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79190: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791b0: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1 f1
=>0x100075d791c0: 00 00[f3]f3 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d791f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100075d79210: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
These components clearly indicate the location, cause, and memory state of the error, making debugging significantly easier.
Conclusion
In this article, we briefly explored Address Sanitizer. While ASan is extremely effective at detecting memory errors, it introduces significant memory and performance overhead due to shadow memory checks, redzones, and frequent runtime instrumentation. For this reason, it is typically used during development, testing, fuzzing, and vulnerability research rather than in production environments.
Although other sanitizers such as HWASan, UBSan, TSan, and LSan now exist, ASan remains a fundamental and essential tool for memory safety verification.
In the next article, I plan to explore additional sanitizers in more detail. Thank you for reading! 🫂
Reference
- https://learn.microsoft.com/ko-kr/cpp/sanitizers/asan?view=msvc-170
- https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn
- https://clang.llvm.org/docs/AddressSanitizer.html
- https://github.com/llvm/llvm-project/tree/main
- https://github.com/google/sanitizers/issues/100
- https://research.google/pubs/addresssanitizer-a-fast-address-sanity-checker/
- https://arxiv.org/pdf/2506.05022v2
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.