Lessons from using sanitizers as an oracle for LLM vulnerability triage Security researcher Joshua Rogers published a post detailing lessons from building deterministic sanitizer-based oracles — using ASan, MSan, UBSan, TSan and LSan — to verify whether bugs found by LLMs are real vulnerabilities, arguing that a second LLM agent cannot serve as a reliable verifier because "agents cheat, they flip-flop regardless of facts, and are too happy to make you happy." Rogers, who presented related material at the Offensive AI Conference 2026 (OAIC), said each sanitizer only detects one class of bug and that a sanitizer report such as "ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 8" is more trustworthy than a 10,000-word LLM-written report. The approach matters because LLM vulnerability scanners produce many findings that are unreachable or not actual vulnerabilities, and deterministic gates can triage them more accurately than adding another model to the loop. Using LLMs to find bugs and vulnerabilities in software is no longer new and cool, and my 2025 post https://joshua.hu/llm-engineer-review-sast-security-ai-tools-pentesters about using specialized tools to look for vulnerabilities in open-source codebases seems like a distant fever dream. The new cool thing is creating static or deterministic checks gates, oracles, whatever you want to call them to verify that an issue discovered by an LLM is really a vulnerability or not. This post outlines all of the lessons I’ve learnt while attempting to create these deterministic checks, specifically for vulnerabilities which can be discovered using memory sanitizers - ASan, MSan, UBSan, TSan, and LSan https://fuzze.rs/blog/sanitizer-stack-selection . The overall concept is that LLMs happily find bugs, but many of those bugs are either not real, or not reachable – or just not a vulnerability . Asking a secondary agent to prove whether the issue is real or not doesn’t work: agents cheat, they flip-flop regardless of facts, and are too happy to make you happy by saying something is a “proved” vulnerability – even if their proof proves nothing. By adding deterministic gates which must be passed for an issue to be deemed a vulnerability, an operator can better more accurately triage findings than just adding another LLM to the mix; or as somebody I heard recently said with a straight face, “manually verify the findings with Cursor”. This post also vaguely follows a talk https://joshua.hu/files/sanitizers/oaic-2026-slides.pdf I gave at Offensive AI Conference 2026 OAIC , which wasn’t really my work. A friend applied to talk about … something but how did it get accepted.. wtf , they couldn’t make it, so I and another person who also had nothing related to the original intended talk topic had to re-make the talk into something at least… presentable. I decided the best attempt at salvaging it would be to turn it into an informational session, where we would condense all of the problems we’ve independently identified while creating such a triage system, and explain “these are the problems we encountered doing this, if you plan to do something similar, make sure you consider these problems.” Amusingly, it’s most often managers who steal their subordinates’ work; but in this case, it’s the opposite. But anyways, the talk was really, really bad; I know. I’m so sorry. I beg your forgiveness. It wasn’t my fault https://www.youtube.com/watch?v=4U9Yl5CXvcQ . Why a sanitizer is the oracle Sanitizers are a great way to detect bugs and vulnerabilities, and they are certainly more valuable to a developer than a slopped up 10,000-word report that Claude has worked xhard on. Without a sanitizer, an out-of-bounds write just lands in memory that nobody is watching, and if the program even crashes at all, it crashes later, somewhere else, in a way that looks completely unrelated. With a sanitizer, the memory around each object is “poisoned” tracked in the sanitizer’s own shadow memory , and the instant something touches it, the program crashes there with a report of exactly what happened and where. Compare “ the attacker can control len here, which flows into the memcpy at line 412, resulting in a heap overflow of up to 64 bytes ” with ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 8 . The first is a cool story bro , while the second is the program just telling you exactly what happened and where. So, the obvious thing to do is to use them during triage and proof-of-concept generation. For context, consider a simple system: an agent gets a VM with the source code, a compiler with sanitizers, a debugger, and a goal – trigger the bug under a sanitizer. If only it were so easy. Picking a sanitizer The first problem is that each sanitizer only looks for one kind of bug: | | Overflow | UAF / double-free | Uninit read | UB shifts, casts, … | Leak | Data race | |---|---|---|---|---|---|---| | ASan | Yes | Yes | No | No | Yes | No | | MSan | No | No | Yes | No | No | No | | UBSan | Partly | No | No | Yes | No | No | | LSan | No | No | No | No | Yes | No | | TSan | No | No | No | No | No | Yes | Most sanitizers are mutually exclusive. You can’t build with both ASan and MSan, and most of the other sanitizers have some caveats too. See the below table for a support matrix note: LSan is integrated into ASan on supported platforms. | | ASan | MSan | UBSan | TSan | LSan | |---|---|---|---|---|---| | ASan | - | No | Yes | No | Yes | | MSan | No | - | Yes | No | No | | UBSan | Yes | Yes | - | Yes | Yes | | TSan | No | No | Yes | - | No | | LSan | Yes | No | Yes | No | - | So, you need to either decide in advance which sanitizer a specific bug must be proven with, or you need to have sets of them - for example, most memory bugs can be picked up by ASan; but not all of them uninitialized reads are invisible to it ; so you either need to use MSan, or both ASan and MSan in parallel built into two binaries . It would be easy to think, “well, let’s just always use MSan because it will detect everything ASan already does but more”, but that would be an unfortunate premature celebration. First of all, as the table above shows, MSan doesn’t detect overflows or use-after-frees at all ; it only knows about uninitialized memory. Second, MSan only really works when everything is instrumented with it - MSan will scream about all sorts of problems which don’t exist, mostly due to memory initialized by uninstrumented code looking uninitialized to it, but also some other things like incorrect origin tracking, totally bogus state when memory crosses instrumented and uninstrumented code, and some other things like that. In addition to that, some programs simply don’t work in normal operating conditions while instrumented with MSan at all; so we can’t use MSan at all , unless changes are made to the codebase which goes beyond the scope of what we’re doing. So we don’t win a free lunch by just using MSan all the time. Is a sanitizer report proof? So we build an instrumented program or proof-of-concept that’s built with an LLM, but as we’ve stated, agents love to cheat the oracle. A sanitizer report is not proof of a vulnerability or even a bug A vulnerability is really a chain of four claims: 1. the crash reproduces under the test conditions; 2. the same input reaches that code in a default build; 3. someone outside the system an attacker can supply that input; 4. the consequence is worse than the process dying. A sanitizer report only ever proves the first one. The rest of this post is about the other three breaking, because the agent cheats the oracle the A’s , and then about the first one not being as trustworthy as it looks either, because the oracle is blind the B’s . I’ve labelled each problem below A1, B1, and so on , so the checklist at the end can point back at them. A: The agent cheats the oracle A1: Was it already crashing? The first and foremost question that needs to be asked is: is a control in the sense of scientific control build of the application or library already tripping a sanitizer report? If it trips on its own test suite, or on perfectly benign input, everything after that is contaminated. A2: The harness manufactured it Then the next question is whether the proof-of-concept is actually demonstrating anything in the codebase itself – or has it manufactured a crash of its own? I can do something like: include