Why HWAddressSanitizer Misses Heap Over-Reads Detected by AddressSanitizer
When fuzzing or hardening C/C++ applications on ARM64 platforms like Android, switching from traditional AddressSanitizer (ASan) to Hardware-Assisted AddressSanitizer (HWASan) is often recommended for its drastically reduced memory overhead and better performance. However, you might encounter situations where ASan reliably reports a heap-buffer-overflow read, but HWASan remains completely silent.
The Core Problem: Granularity Mismatch
Your hypothesis is spot-on: the discrepancy comes down to alignment, tag granularity, and how the two sanitizers track valid memory regions.
Consider an allocation of n = 5 bytes:
unsigned char *p = malloc(5);
// 4-byte read starting at byte 4 (indices 4, 5, 6, 7):
volatile int r = memcmp(p + 4, "FILE", 4);In this snippet, bytes 5, 6, and 7 are past the end of the 5-byte heap buffer. Yet, under HWASan, this read might pass without an error. Here is why.
1. How Classic ASan Detects It
Classic ASan maps every 8 bytes of application memory to 1 byte of shadow memory. Because each byte in the shadow can encode values from 0 (all 8 bytes valid) through 1..7 (only the first k bytes valid) up to negative values (poisoned/redzones), ASan has byte-level precision.
Furthermore, ASan surrounds every allocation with poisoned redzones. When memcmp or an inlined memory access crosses from byte 4 into byte 5, ASan immediately detects that the shadow byte permits only up to index 4, or that subsequent bytes reside in a poisoned redzone, raising a heap-buffer-overflow READ.
2. How HWASan Handles Memory Granules
HWASan is built around 16-byte memory granules to align with ARM's hardware memory tagging (such as ARMv8.5-A MTE) and Top-Byte Ignore (TBI). Instead of byte-level shadow tracking, HWASan assigns a single tag (an 8-bit or 4-bit integer) to each aligned 16-byte chunk of memory.
- When you allocate 5 bytes, the memory allocator returns an address aligned to a 16-byte boundary and tags both the pointer and the corresponding 16-byte physical memory granule with the same tag (e.g.,
0x42). - The bytes from offset 5 to 15 are inside the exact same 16-byte granule.
- When an access occurs at offset
4..7, the pointer tag (0x42) matches the memory tag (0x42). Because the tags match, hardware or software tag checks pass with zero intervention.
Is 16-Byte Granularity Configurable on Android?
No, the 16-byte granule size is not configurable. It is hardwired into both the LLVM HWASan instrumentation passes and the AArch64 hardware specifications (ARM MTE mandates a 16-byte tag granule). Any attempt to change the granule size in software would break compatibility with native hardware tagging instructions.
Can HWASan Catch Intra-Granule Over-Reads?
LLVM introduced a mitigation for this known as Short Granules. In implementations supporting short granules:
- If an allocation ends inside a 16-byte granule, the granule's shadow tag is replaced with a special value representing the allocation size (e.g.,
5), while the pointer tag stores the real color in the top byte. - Every load/store sequence checks if the target byte offset exceeds the short granule size.
However, intra-granule reads still frequently escape detection due to several factors:
- Library Interceptors: Functions like
memcmp,memcpy, orstrlenmay use optimized assembly routines (like NEON vector instructions) that read memory in 8-byte or 16-byte chunks. Sanitizer runtimes often permit intra-granule tail reads in interceptors to avoid breaking vector-optimized standard library calls. - Performance Defaults: Depending on the NDK version and target API level, strict short-granule checks may be disabled or limited to writes to reduce code size and performance penalties.
- Over-Read vs. Over-Write: Intra-granule writes are much more strictly policed than intra-granule reads because over-writing adjacent padding risks memory corruption, while reading within the same granule rarely leads to exploitable cross-object leakage.
Is HWASan Always Strictly Better than ASan?
While HWASan is often advertised as modern and vastly superior, this is true primarily in terms of resource efficiency:
| Feature | AddressSanitizer (ASan) | HWAddressSanitizer (HWASan) |
|---|---|---|
| Overhead | ~2x CPU slowdown, ~2-3x memory overhead | ~10-20% CPU, <15% memory overhead |
| Redzones | Explicit, large poisoned redzones | No redzones (relies on randomized tags) |
| Granularity | Byte-accurate (within 8 bytes) | 16-byte granules |
| Use Case | Unit tests, localized fuzzing, small targets | System-wide Android fuzzing, production dogfooding |
Best Practices for Fuzzing and Debugging
- Use ASan for Focused Unit Fuzzing: If your target is an isolated library (like an image, JSON, or protocol parser) that fits comfortably in host RAM, ASan's byte-level precision makes it superior for uncovering subtle one-byte boundary overflows.
- Use HWASan for Full-System and Integration Fuzzing: If you are fuzzing entire Android OS builds, system services, or long-running daemons where ASan’s memory footprint causes out-of-memory (OOM) crashes, HWASan is the tool of choice.
- Run Both When Possible: The sanitizers complement each other. ASan will catch sub-16-byte tail over-reads, while HWASan's ability to randomize pointer tags across allocations can catch large out-of-bounds jumps that might accidentally land on a valid adjacent ASan object past the redzone.