What a clean sanitizer run doesn't prove
3 min read

Updated

What a clean sanitizer run doesn't prove

Two kernel race investigations showed why lifetime rules, test reachability, and exploitability need separate evidence. Silence from a sanitizer is only meaningful within what it can observe.

A race test can finish without a sanitizer report even when the code has a lifetime defect. It can also finish cleanly because the test never reached the failing interleaving.

Those explanations require different next steps. Treating either as “the code is safe” ends the investigation too early.

We encountered that distinction while studying two already patched Linux races: an eventpoll file-lifetime issue and a Unix-domain socket garbage-collection issue.

A reference has to outlive its use

The eventpoll investigation centered on __ep_remove(). The relevant path obtained a file pointer and used it during removal while another path could close the file.

The upstream fix, a6dc643c6931, addresses the lifetime problem. For a test, the question is more specific: which accesses occur after the object has ceased to be safely usable, and what can the instrumentation observe at that point?

A pointer being non-null doesn’t answer that question. Neither does reaching the function.

Logical removal and memory reclamation differ

Read-copy-update, or RCU, separates removing an object from reclaiming its memory. Existing readers can continue under the applicable lifetime rules while reclamation waits. The kernel’s RCU documentation explains why this separation is essential.

That creates an important distinction for debugging. An access can violate a reference or identity rule without looking like a simple access to poisoned, already reclaimed memory.

Caches using SLAB_TYPESAFE_BY_RCU add another constraint: preventing a slab page from disappearing is not the same as preserving the identity of a particular object in that page. A reader still has to follow the required reference and revalidation rules.

A gap in a circular structure representing object-lifetime boundaries

Sanitizer behavior depends on the allocator, kernel configuration, instrumentation mode, and timing. It is too broad to say that all RCU use-after-free bugs are invisible to KASAN, or that one configuration option makes every such test decisive.

The test needs an observation tied to the failure being investigated: a lifetime violation, an incorrect object identity, or a concrete change in behavior.

Inspect the operations after the race

Even after identifying an unsafe lifetime, we still had to examine the resulting writes.

The eventpoll analysis initially treated several stores as promising corruption primitives. Closer inspection narrowed that interpretation. One store participated in the condition permitting cleanup; another had constrained operands. Neither could simply be counted as an attacker-controlled write to an arbitrary location.

For each operation, ask what the attacker controls: the destination, the value, the timing, and the object that occupies the memory. A fixed-value store with a narrow timing window is a different primitive from a controlled arbitrary write.

A patch fixing a race doesn’t establish that our test produced a usable exploit.

A second test that didn’t establish a trigger

The Unix-domain socket case involved garbage collection and MSG_PEEK, addressed by upstream commit 591f1ac21742.

Our attempts didn’t establish a reproduced use-after-free. Entering the relevant operations was insufficient: the test needed the required socket graph and a particular interleaving during collection.

The correct result was therefore limited. We had a published fix to study and an unsuccessful attempt to reproduce the failure. We couldn’t infer that the bug was harmless, and we couldn’t claim a demonstrated primitive.

Write down which question the test answers

A useful result distinguishes source evidence, path reachability, observed failure, and exploitability. A large iteration count doesn’t merge those stages.

Before a long run, establish a positive control for the observation you expect. After it, report what was actually measured. “No KASAN report in this configuration” is precise. “No bug” requires more.