A production GWP-ASan report can give you an allocation stack, a free stack and a failing access — then refuse to recur when you repeat the same user action. That is still useful evidence. The next step is to reconstruct the object's lifetime, not just keep pressing the button.
This is especially useful for Android native-code reports, but the sampling distinction comes from GWP-ASan's allocator design.
Why repeating the trigger is not the whole experiment
LLVM's GWP-ASan documentation describes a small subset of allocations being redirected into guarded slots. An out-of-bounds access is detected when it reaches an inaccessible guard page. Freeing a sampled allocation makes its slot inaccessible too, until that slot is reused.
Two consequences matter:
- Enabled does not mean every allocation is guarded. Android's
android:gwpAsanMode="always"still samples allocations, as the Android GWP-ASan guide explains. - Guarded does not mean every invalid access is caught. Allocations can be left- or right-aligned, and alignment can leave space before a guard page. Use-after-free detection is also transient because freed slots are reused.
Repeatedly accessing the same unsampled live object does not give that object a fresh allocation-sampling opportunity. A fresh eligible heap allocation can give a new sampling opportunity. Restarting the process changes another boundary, including process selection in configurations that sample processes.
So record whether a retry repeats only the access, reconstructs the object, or restarts the process. Those are different experiments, not interchangeable retry counts.
Read the report as a lifetime
For a use-after-free, read all three stacks together:
- Allocation: which code created the storage, and what was meant to own it?
- Deallocation: which path ended that lifetime? Was a callback, view or another thread still holding a reference?
- Access: which later consumer used that reference?
The Android guide's example is a useful trap: a std::string_view refers to the temporary string produced by concatenation. The temporary is destroyed at the end of the statement; a later JNI call reads through the dangling view. The access stack identifies the consumer, while the allocation and free stacks explain why its input is no longer valid.
For a buffer overflow, start with the reported allocation size and access offset instead; do not force every report into a use-after-free story.
Before changing the build, preserve the complete report, app/library version, OS build, ABI, available allocation/free stacks, and the steps that create, release and use the object. A screenshot of the top frame throws away much of the advantage.
Choose the next detector deliberately
Google recommends HWASan when investigating these reports on affected 64-bit Android devices, then focused fuzzing if necessary. That is a testing direction, not a promise that instrumentation will preserve the original heap layout or reproduce the fault on demand. Keep the suspected lifetime transition comparable while checking the testing tool's device and build requirements.
Also preserve the GWP-ASan mode. Reporting and process termination are separate questions: AOSP's documented mode behavior makes manifest always non-recoverable through Android 14 and recoverable from Android 15. Recovery is not a repair of the lifetime bug; verify behavior on the actual OS build.
My starting point would be the lifetime evidence, not a raw count of unsuccessful retries. When a production report will not recur, which evidence has been most useful to you for separating a missing lifetime transition from a missed sampling opportunity?
