# 0sec: full public research context > Generated during the site build from the public research collection. Canonical HTML pages remain the citation targets. ## Research index - [From a kernel finding to an upstream fix](https://0.security/blog/fixing-the-linux-kernel/): Two accepted patches show different research paths: a missing lifetime reference in TIPC and an authorization check in VXLAN. [Markdown](https://0.security/blog/fixing-the-linux-kernel.md) - [Finding the function wasn't enough](https://0.security/blog/does-it-discover-or-recall/): In a four-task binary-analysis experiment, the engine located suspicious code but produced no confirmed triggers. What that result says about discovery, and what it cannot tell us about recall. [Markdown](https://0.security/blog/does-it-discover-or-recall.md) - [A real bug. An incorrect claim of novelty.](https://0.security/blog/finding-the-bug-is-the-easy-part/): A driver reproduced a known vulnerability, but our pipeline called it new. The mistake was treating an unfamiliar file hash as evidence of an unfamiliar bug. [Markdown](https://0.security/blog/finding-the-bug-is-the-easy-part.md) - [Testing a Windows driver without its source](https://0.security/blog/the-drivers-nobody-fuzzed/): A stripped driver still exposes imports and dispatch structure. We used those signals to investigate known memory-access bugs, then checked the behavior under a kernel debugger. [Markdown](https://0.security/blog/the-drivers-nobody-fuzzed.md) - [Finding a bug in a stripped binary](https://0.security/blog/the-bug-with-no-source/): A known color-profile bug exposed two problems in our analysis: function ranking and incomplete ground truth. Fixing them let the engine generate a differential reproducer. [Markdown](https://0.security/blog/the-bug-with-no-source.md) - [What a clean sanitizer run doesn't prove](https://0.security/blog/when-the-sanitizer-is-the-wrong-oracle/): 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. [Markdown](https://0.security/blog/when-the-sanitizer-is-the-wrong-oracle.md) - [What maintainer review adds to AI research](https://0.security/blog/ai-agents-are-entering-the-kernel/): The useful output of a kernel investigation is a clear report and a small, correct patch. Public review exposes the details an agent can overlook. [Markdown](https://0.security/blog/ai-agents-are-entering-the-kernel.md) - [Your validator checked one value. Your app used another.](https://0.security/blog/validation-read-stability/): JavaScript properties can change between reads. Validation needs to account for the value the application actually uses, especially when inputs contain getters or proxies. [Markdown](https://0.security/blog/validation-read-stability.md) - [What counts as proof depends on the target](https://0.security/blog/one-research-loop-evidence-matched-to-the-target/): A protocol response, a sanitizer report, and a kernel crash answer different questions. The research record should make those differences visible. [Markdown](https://0.security/blog/one-research-loop-evidence-matched-to-the-target.md) - [A length field the NFC parser trusted](https://0.security/blog/nfc-llcp-15-year-kernel-bug/): Linux's NFC service-discovery parser needed checks against the received packet and against each message type's minimum length. The defect illustrates why declared lengths are not buffer bounds. [Markdown](https://0.security/blog/nfc-llcp-15-year-kernel-bug.md) - [When the test needs an authorization rule](https://0.security/blog/the-bugs-fuzzers-cant-see/): A missing permission check may leave memory perfectly valid. Detecting it requires a test that knows which caller should be allowed to act. [Markdown](https://0.security/blog/the-bugs-fuzzers-cant-see.md) - [Can a researcher find your security contact?](https://0.security/blog/swiss-securitytxt-survey/): Our June survey found security.txt on about 1 in 10 sampled .ch domains. That measures a public contact file, not whether a company can receive reports. [Markdown](https://0.security/blog/swiss-securitytxt-survey.md) - [What we lost when we filtered the findings](https://0.security/blog/triage-moat-and-multi-benchmark-validation/): Our triage experiment reduced report volume, but some configurations also lost flags. Both sides of that tradeoff belong in the result. [Markdown](https://0.security/blog/triage-moat-and-multi-benchmark-validation.md) - [Before comparing scores, check what ran](https://0.security/blog/xbow-benchmark-methodology-and-verification/): Broken builds, retries, and an overly generous flag checker changed the meaning of our benchmark results. Here is how to keep those effects visible. [Markdown](https://0.security/blog/xbow-benchmark-methodology-and-verification.md) - [Why we gave the agent a shell](https://0.security/blog/agentic-pentesting-on-xbow/): An early web-testing experiment showed how tool interfaces can consume an investigation. A cookie jar made the problem concrete. [Markdown](https://0.security/blog/agentic-pentesting-on-xbow.md) - [Keep the investigation outside the context window](https://0.security/blog/2026-05-07-orchestration-not-frontier/): Durable evidence, focused agent sessions, and explicit workflow stages make long research tasks easier to resume and verify. [Markdown](https://0.security/blog/2026-05-07-orchestration-not-frontier.md) - [Replacing a native SQLite dependency](https://0.security/blog/2026-04-07-deleting-better-sqlite3/): A Node.js ABI mismatch led us to a WebAssembly SQLite build. The dependency became easier to install, but the adapter still needed careful testing. [Markdown](https://0.security/blog/2026-04-07-deleting-better-sqlite3.md) - [Introducing 0cloud](https://0.security/blog/2026-04-06-the-marketing-rewrite/): Managed security testing starts with a clear scope, a real target, and evidence an engineer can check. [Markdown](https://0.security/blog/2026-04-06-the-marketing-rewrite.md) - [Test what an AI application can actually do](https://0.security/blog/attack-surface-xbow-doesnt-test/): AI security tests need to follow data access and tool permissions through the application, with a clear definition of success. [Markdown](https://0.security/blog/attack-surface-xbow-doesnt-test.md) - [Give the verifier a fresh start](https://0.security/blog/blind-verification/): A separate verification pass can challenge a finding without inheriting its narrative. Its verdict still needs evidence. [Markdown](https://0.security/blog/blind-verification.md) - [What our npm audits taught us](https://0.security/blog/how-ai-agents-found-7-cves/): Certificate validation, configuration parsing, and template loading exposed different trust-boundary mistakes. The useful part was turning each lead into a precise report. [Markdown](https://0.security/blog/how-ai-agents-found-7-cves.md) - [Security review has to follow the change](https://0.security/blog/the-age-of-agentic-security/): AI-assisted development makes it easier to produce changes. Security testing still needs context, priorities, and a way to check its conclusions. [Markdown](https://0.security/blog/the-age-of-agentic-security.md) ## Full research articles # From a kernel finding to an upstream fix > Two accepted patches show different research paths: a missing lifetime reference in TIPC and an authorization check in VXLAN. Published: 2026-07-24 Updated: 2026-09-10 Canonical article: https://0.security/blog/fixing-the-linux-kernel/ A useful kernel patch explains the failure closely enough that a maintainer can assess both the bug and the proposed repair. That often means reconstructing an object's lifetime or identifying the exact namespace whose permissions matter. Two of our upstream contributions illustrate those different paths. ## TIPC: the decrypt path needed a reference The TIPC crypto issue involved asynchronous work during network-namespace teardown. The encryption path already protected the relevant lifetime. Its decryption counterpart needed the corresponding reference handling. That asymmetry provided a lead: examine a path next to an earlier fix and ask whether it depends on the same invariant. The answer required tracing the callback and teardown behavior, rather than simply matching the shape of two functions. The [accepted TIPC patch](https://github.com/torvalds/linux/commit/bda3348872a2ef0d19f2df6aa8cb5025adce2f20) records the fix and carries 3 `Reviewed-by` tags. Its small diff is backed by the lifetime explanation in the commit message. The merged patch establishes an accepted correction. It doesn't, on its own, establish a working privilege-escalation exploit. ## VXLAN: check the namespace being affected The VXLAN case concerned authorization. A change could involve an underlay in another network namespace, so the permission check needed to account for that namespace. The [upstream VXLAN commit](https://github.com/torvalds/linux/commit/3a61bd9637f3) adds the missing capability check. Memory corruption isn't necessary for this kind of failure: the operation can complete normally while crossing a boundary the caller shouldn't cross. That makes the test different from a sanitizer-driven investigation. It needs to establish the caller's privileges, ownership of the affected object, and the action that should be denied. See [our authorization-testing note](/blog/the-bugs-fuzzers-cant-see) for that method. ## Make the reviewer's job manageable ![Investigation narrowed into a reviewable patch](/blog/pipeline-verification-funnel-minimal.webp) Before sending a patch, check the current tree and the relevant mailing-list queue. A correct fix can already be under review elsewhere. Keep the change focused. Explain the trigger, affected path, and invariant being restored. Record the checks actually performed, including build configuration and any reproducer result. Don't describe a source argument as an executed test. Review can improve more than the code. A corrected `Fixes:` tag helps identify the history of the regression. A clearer explanation helps maintainers decide whether the change belongs in stable trees. A smaller patch makes backporting easier to assess. ## Follow the patch through its actual destination A submitted patch, a maintainer-tree commit, a mainline merge, and a distribution update are separate events. For a system operator, the relevant question is whether the running kernel contains the fix. This article links selected upstream commits rather than maintaining a moving total or implying that every downstream kernel has received them. The durable evidence is the patch, its review, and the behavior it corrects. --- # Finding the function wasn't enough > In a four-task binary-analysis experiment, the engine located suspicious code but produced no confirmed triggers. What that result says about discovery, and what it cannot tell us about recall. Published: 2026-07-22 Updated: 2026-09-10 Canonical article: https://0.security/blog/does-it-discover-or-recall/ Our engine completed a four-task binary-analysis experiment without confirming a single vulnerability. In each case, it reached the relevant code. It couldn't construct an input that demonstrated the bug. That distinction matters. Locating a vulnerable function and proving that an input reaches it are different tasks. A benchmark that awards credit for the first can hide how much work remains. ## What we tested We selected four published vulnerabilities from projects patched in 2026: an audio codec, an XML parser, a RAW image loader, and an EXR image decoder. Each task paired a vulnerable build with a fixed build. We withheld source, debug information, hints about the vulnerable function, and the known reproducer from the agent. The acceptance rule was narrow: an agent-produced input had to crash the vulnerable build and run cleanly on the fixed build. A plausible explanation or a crash in both versions didn't count. We chose fixes dated after the model's stated training cutoff to reduce the chance of straightforward recall. That is a useful precaution, but it doesn't establish a contamination-free experiment. A bug can exist long before its fix, related code can appear in training, and publication dates don't reveal everything a model has seen. The result measures performance under these restrictions. It cannot prove what the model did or didn't remember. ## Four different ways to miss the trigger ![Four tiles representing the comparison between test conditions](/blog/discovery-vs-recall-matrix-minimal.webp) The recorded result was **0 confirmed triggers out of 4 tasks**. The failures exposed different input constraints. The audio task required an older decoder path. Ordinary files exercised the modern container, while the vulnerable path expected a different layout. Generating more ordinary files didn't move the run toward that path. The XML task required a particular nesting of a document-type declaration. Inputs could be valid enough to enter the parser without reaching the depth and structure needed to demonstrate the overflow. The RAW task depended on a relationship between header fields. A generic file could pass basic parsing and still fail the calculated condition that selected the vulnerable code. The EXR task required dimensions that wrapped a size calculation without merely exhausting memory. An input that killed both builds through allocation failure provided no evidence that the patch made a difference. These were observed limits of the runs, not proof that fuzzing or language models can never solve the tasks. ## The benchmark needed checking too Some candidate reproducers were unsuitable as reference inputs. They reached nearby code without causing the expected failure, or failed for the same reason on both builds. Before judging the agent, we therefore had to establish that each vulnerable/fixed pair distinguished the intended defect. Otherwise a failed run could mean the agent missed the bug, the build was wrong, or the test never exercised the relevant behavior. A differential test is useful only after its positive control works. ## What we changed our attention to This experiment directed attention toward the constraints between an input and a suspicious operation: format selection, nested grammar, field relationships, and size calculations. Those constraints are the next thing an analysis system has to recover. A decompiler can expose a dangerous operation while leaving the path to it unresolved. Four tasks are too few to establish a general success rate. They are enough to show why a report should distinguish **located**, **triggered**, and **confirmed against a fixed build**. In this experiment, only the first stage succeeded. For a related example where a generated input did reach the bug, see [our stripped-binary case study](/blog/the-bug-with-no-source). That target was already public, so it demonstrates a different capability. --- # A real bug. An incorrect claim of novelty. > A driver reproduced a known vulnerability, but our pipeline called it new. The mistake was treating an unfamiliar file hash as evidence of an unfamiliar bug. Published: 2026-07-21 Updated: 2026-09-10 Canonical article: https://0.security/blog/finding-the-bug-is-the-easy-part/ Our pipeline reproduced a vulnerability in a Windows driver and labelled it new. The reproduction was useful. The novelty claim was wrong. The issue was already recorded as [CVE-2024-33228](https://nvd.nist.gov/vuln/detail/CVE-2024-33228). What looked like a discovery was a rediscovery. The failure came from asking a file-hash lookup to answer a question about vulnerability history. ## What the machine had established The investigation began with a stripped driver binary. The analysis located a device-control handler and traced a path to a physical-memory mapping operation. In the instrumented test, the pipeline inspected the operation's operands and compared returned data with the corresponding memory. That evidence supported the reported memory-access behavior under the test conditions. It didn't establish that the behavior was unknown. It also wasn't, by itself, proof of an end-to-end privilege-escalation exploit. Those are separate claims requiring separate evidence. ## Where the novelty check failed The driver wasn't present in the known-vulnerable-driver catalogue under the hash we supplied. The pipeline treated that absence as evidence of a new vulnerability. A hash identifies one file. Rebuilding a vulnerable driver can change its hash without changing the vulnerable code. Signing, packaging, or other binary changes can produce the same mismatch. The lookup had answered, “Have we catalogued these exact bytes?” We had interpreted the answer as, “Has anyone reported this defect?” That is why a reproduction and a novelty search have to remain separate parts of a research workflow. ## A better way to search the prior record ![Three gates representing separate checks before a novelty claim](/blog/novelty-gate-architecture-minimal.webp) The useful search terms describe the defect: the product, affected version, handler or function, operation, and weakness class. They can connect two reports whose files have different hashes. Public advisories and vendor notices provide the first comparison. Technical descriptions help when naming differs between reports. A broader search can fill gaps for drivers and firmware whose records don't map neatly onto package names. An internal comparison matters too. The same sink or root cause may have appeared in an earlier investigation under a different target name. Matching technical characteristics is more useful than comparing the wording of two titles. These checks produce evidence for a decision. They don't make the public record complete. ## What a clean search means “No matching report found” is a bounded result. It should carry the sources searched and the date of the search. It cannot establish that no report exists, that a vendor hasn't fixed the issue privately, or that another researcher hasn't submitted it. Likewise, a possible match should remain inspectable. Similar descriptions can refer to different bugs; different descriptions can refer to the same one. Automatically deleting candidates on a loose match would exchange one error for another. Our correction was to treat novelty as a question still requiring review, even when reproduction succeeds. The evidence record needs to distinguish what ran on the machine from what we learned about prior work. The [driver-analysis article](/blog/the-drivers-nobody-fuzzed) explains the reproduction process. This case explains its limit: proving the behavior never established that we were first. --- # Testing a Windows driver without its source > A stripped driver still exposes imports and dispatch structure. We used those signals to investigate known memory-access bugs, then checked the behavior under a kernel debugger. Published: 2026-07-20 Updated: 2026-09-10 Canonical article: https://0.security/blog/the-drivers-nobody-fuzzed/ A signed Windows driver can expose dangerous operations through an ordinary device-control interface. The signature establishes who signed the binary. It doesn't establish that every operation checks its caller correctly. We used already known driver vulnerabilities to test a narrower research question: could our pipeline locate a suspicious operation in a stripped binary and verify its behavior in an instrumented Windows VM? The result was a validation of the analysis workflow, not a claim of new vulnerabilities. ## Find the device-control path A driver receives I/O control requests through a dispatch handler. That handler interprets command codes and input buffers, then calls the implementation for the selected operation. Even when descriptive function names are gone, useful structure remains. The code that installs a dispatch handler writes a function pointer into a known driver structure. Imported kernel functions can also retain names required by the loader. Those signals let the analysis work from two ends: the request handler and a potentially dangerous operation such as physical-memory mapping. The task is to recover the path between them and determine where its arguments originate. A call to `MmMapIoSpace` is not enough to call something a vulnerability. Mapping a fixed hardware region can be legitimate driver behavior. Whether the caller controls the address, which process can open the device, and what the driver returns all affect the security conclusion. ## Observe the operation ![A closed path representing the driver confirmation loop](/blog/byovd-confirmation-loop-minimal.webp) The confirmation environment loaded the target driver in a Windows VM with a kernel debugger attached. For the selected request, the test inspected whether the mapping operation ran, whether its operands matched the values supplied to the request, and whether data returned to user space corresponded to the mapped memory. Negative controls checked how the path behaved with different inputs. These observations tie a report to an executable path. They are stronger than a decompiler showing a dangerous import. They also have a limited meaning. A demonstrated memory-access primitive isn't automatically a working privilege-escalation exploit. An exploit still needs the relevant access conditions, a usable target, and a complete chain under the system's mitigations. ## Keep the threat model attached Bring-your-own-vulnerable-driver attacks often involve an attacker arranging for a vulnerable driver to be loaded. Loading it and opening its device are distinct access questions. A report should state both. It should not describe a result obtained with a privileged setup step as though any unprivileged user could reach it on an ordinary installation. The binary itself also doesn't establish that no one has fuzzed the driver. Binary instrumentation, emulation, and hardware-assisted approaches can test software without source. Our workflow addresses one way to investigate that surface. ## A known bug is still a useful control The pipeline rediscovered published vulnerabilities during these tests. One run initially labelled a result new because the file's hash was absent from a catalogue. That was an error, described in [our novelty-check case study](/blog/finding-the-bug-is-the-easy-part). Known vulnerable drivers provide positive controls for the investigation process. Benign uses of the same kernel functions provide negative controls. Both are necessary: a system that recognizes a dangerous import but cannot distinguish its safe uses will produce convincing false positives. The useful output is a record of the request, access conditions, observed operands, returned behavior, and limitations. That record lets another researcher assess what the test actually established. --- # Finding a bug in a stripped binary > A known color-profile bug exposed two problems in our analysis: function ranking and incomplete ground truth. Fixing them let the engine generate a differential reproducer. Published: 2026-07-19 Updated: 2026-09-10 Canonical article: https://0.security/blog/the-bug-with-no-source/ The vulnerable function was ranked 4,839th in a queue of 5,494 functions. Our analysis budget ended long before it got there. The binary had been stripped of its debug information and most useful function names. A ranking system that relied on those names had lost its strongest signals. This case study used a previously reported bug in a color-management library. The question was whether the engine could investigate the binary without source or a supplied reproducer. It wasn't a test of novel discovery. ## Start with the information that remains Stripping removes information useful to a researcher, but it doesn't erase everything required to run a program. Imports, instructions, data, and references between them still offer starting points. Our first experiment used a stripped, sanitizer-instrumented build. We ranked functions by their references to copy operations and sanitizer-related entry points, rather than relying on descriptive names. The target function moved to rank 180, within the decompilation budget. That solved a scheduling problem: useful code reached the model instead of remaining outside its view. The sanitizer build and the ordinary optimized build provided different signals. The final confirmation used a stripped optimized build; we shouldn't describe the earlier ranking result as though it had been obtained under identical conditions. ## We also had the wrong function The library contained two functions named `WriteCLUT`. We initially investigated the wrong one. Its callers appeared to be missing, which suggested a problem recovering indirect dispatch. Re-reading the patch showed that the reported crash belonged to the other function, reached through a direct call chain. That changed the diagnosis. Part of the apparent call-graph problem was a decompilation-budget problem, and part was our own incorrect mapping of the report to the code. A test can have a precise crash address and still be measuring the wrong thing if its source-to-binary mapping is wrong. ## Recover the path to the input ![Connected tiles representing recovered relationships between functions](/blog/stripped-callgraph-reconstruction-minimal.webp) Two changes followed. We used execution coverage to help choose functions for decompilation, and we improved recovery of call relationships, including indirect edges where they mattered. Coverage answered a practical question: which code did an ordinary seed already reach? That helped prioritize the parser and its callers. Recovering additional relationships gave the analysis more context for tracing input fields toward suspicious operations. The next run produced a roughly 3.7 KB ICC color profile. It demonstrated an out-of-bounds read at the intended site in the vulnerable build and ran cleanly in the patched build. That was the acceptance condition. An explanation of the defect alone would not have passed. ## What this result supports On this target, the engine generated a working input without being given the known reproducer. The result shows why function selection, ground-truth checking, and reachability information belong in the evaluation. It doesn't establish that the model had never encountered the public bug. Withholding an input during a run doesn't reveal what was in training. Nor does one successful target establish a general result for firmware or closed-source drivers. Binary-only software can be fuzzed and analyzed with suitable tooling. The constraint is the information and execution access available to a particular workflow, not the absence of source alone. Our [four-task follow-up](/blog/does-it-discover-or-recall) reached a different result: relevant code was located, but no input satisfied the differential confirmation rule. Together, the experiments show why locating a function and reproducing its defect need separate measurements. --- # 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. Published: 2026-07-17 Updated: 2026-09-10 Canonical article: https://0.security/blog/when-the-sanitizer-is-the-wrong-oracle/ 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`](https://git.kernel.org/linus/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](https://docs.kernel.org/RCU/whatisRCU.html) 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](/blog/rcu-reclaim-gap-minimal.webp) 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`](https://git.kernel.org/linus/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. --- # What maintainer review adds to AI research > The useful output of a kernel investigation is a clear report and a small, correct patch. Public review exposes the details an agent can overlook. Published: 2026-07-16 Updated: 2026-09-10 Canonical article: https://0.security/blog/ai-agents-are-entering-the-kernel/ A maintainer reply can change the most important part of a security report in a few lines. The diagnosis may be right, but the fix may belong elsewhere. The patch may work, but its `Fixes:` tag may point to the wrong change. Our kernel work has made those distinctions concrete. Generating a candidate is only the start of producing something the subsystem can use. ## Read the review, not just the merge The public [MCTP serial patch thread](https://lore.kernel.org/all/20260715082021.46315-1-doruk@0sec.ai/) shows an investigation moving through review. The [maintainer response](https://lore.kernel.org/all/1a69cae2f078e24726bfdef5ee2e6575759c8dda.camel@codeconstruct.com.au/) accepted the analysis and suggested a cleaner shape for the fix. The [VXLAN and GENEVE series](https://lore.kernel.org/all/20260715055648.33060-1-doruk@0sec.ai/) shows another part of the process: explaining a permission boundary and getting the patch metadata right. Those details are easy to omit from a headline about AI finding a bug. A reviewer brings knowledge of the subsystem's intended behavior, history, and maintenance constraints. An agent's confidence score doesn't replace that knowledge. ## Prepare the evidence before the patch A report should identify who can reach the path and under which configuration. It should explain the failure without requiring the reviewer to reconstruct a long agent conversation. If a test ran, include enough detail to repeat it. If the conclusion rests on source analysis, say so. A patch accepted on a sound lifetime argument is useful, but it should not be presented as evidence that a full exploit was demonstrated. Keep the fix narrow enough to review. Unrelated cleanup creates more work and makes it harder to see whether the change addresses the reported root cause. ## Check whether someone already got there We also encountered concurrent work: an io_uring patch addressed an issue already fixed in other postings. Correct analysis didn't make the submission new. Before sending, search current commits and the relevant mailing-list threads. Repeat that check when an investigation has been open for a while. A local checkout can lag behind a maintainer's queue. Deduplication is part of respecting the person who has to read the report. It also keeps the research record from crediting a rediscovery as an original finding. ## Acceptance answers a particular question ![From candidate through review and integration](/blog/kernel-benchmark-flow-minimal.webp) An upstream merge shows that a patch passed the project's integration process. It doesn't measure how many candidates the research system missed, how many false alarms it generated, or whether the bug supports a particular exploit chain. Those questions need their own evaluations. For the engineering contribution, the public trail is still valuable: the report, revisions, reviewer feedback, accepted patch, and subsequent backports can be inspected separately. The next improvement in an AI research system may be a better reproducer, a more accurate history check, or a clearer report. Those changes make the work easier for a maintainer to act on. --- # Your validator checked one value. Your app used another. > JavaScript properties can change between reads. Validation needs to account for the value the application actually uses, especially when inputs contain getters or proxies. Published: 2026-07-14 Updated: 2026-09-10 Canonical article: https://0.security/blog/validation-read-stability/ A JavaScript property doesn't have to return the same value twice. If a validator reads it once and the application reads it again, the two can see different data. That becomes a security problem when the first read passes an allowlist and the second controls an action. The key question is simple: does the application use the value that was checked? ## A small example This example demonstrates the language behavior without depending on a particular validation library: ```js let reads = 0; const input = { get role() { return ++reads === 1 ? "readonly" : "admin"; }, }; const allowed = new Set(["readonly", "guest"]); const passes = allowed.has(input.role); console.log(passes); // true console.log(input.role); // "admin" ``` The allowlist is correct. The problem is that the property was read twice. An accessor can execute code on a read. A proxy can intercept a read. A getter can also derive its answer from state that changes between validation and use. Returning the original object leaves those behaviors attached to it. ![Two branches representing validation and later use of a value](/blog/validation-flow-split-minimal.webp) ## Check what the API returns Some validation APIs inspect an object and report errors. Others return parsed data for the caller to use. Those contracts are different. When an API validates the original object in place, callers need to understand whether its properties remain stable. When an API returns a new value, callers must actually use that value instead of continuing to read the original input. A snapshot can close the gap if the application validates and uses the same captured data. For the scalar example: ```js const checkedRole = input.role; if (!allowed.has(checkedRole)) { throw new Error("Role is not allowed"); } // Use checkedRole from here onward. ``` For nested structures, a shallow copy isn't enough. Nested objects can still contain accessors or mutable state. The parser needs an explicit policy for the values and types it accepts, and for how it materializes them. ## Where the issue is reachable An ordinary JSON request body doesn't carry executable getters. Parsing JSON produces data properties. Turning this observation into an application vulnerability therefore requires another path: an accessor-bearing class instance, a proxy, or an object derived from mutable or attacker-influenced state. That condition belongs in the report. A JavaScript demonstration alone doesn't establish remote exploitability in every application using a validation package. It also doesn't establish that one library is universally safe and another universally unsafe. Versions, schema options, transformations, and the caller's use of the result all matter. ## What we reported, and a correction We raised public hardening reports for [class-validator](https://github.com/typestack/class-validator/issues/2687) and [superstruct](https://github.com/ianstormtaylor/superstruct/issues/1301). They describe the gap between checking a live object and using its properties later. A public report is not the same as maintainer confirmation or a released fix. The earlier version of this article included a class-validator example that copied a getter with `Object.assign`. That was incorrect: assignment invokes the source getter and copies its returned value; it doesn't preserve the accessor. The example above isolates the actual read-instability behavior instead of presenting that snippet as a working library reproducer. When testing this class of issue, record each property access, the value observed during validation, and the value used at the sensitive operation. The evidence needs all three. --- # What counts as proof depends on the target > A protocol response, a sanitizer report, and a kernel crash answer different questions. The research record should make those differences visible. Published: 2026-07-11 Updated: 2026-09-10 Canonical article: https://0.security/blog/one-research-loop-evidence-matched-to-the-target/ An HTTP response can show that a server accepted an invalid request. A sanitizer report can show that a parser accessed memory outside an allocation. Neither observation, by itself, establishes the same security impact. Research becomes easier to review when the record says exactly what each test demonstrated. The tools can differ across targets while the discipline stays consistent. ## Give each target an appropriate test For a protocol implementation, compare a concrete request and response with the rule the server should enforce. Preserve the bytes: a model's summary may omit the detail that makes the behavior significant. For a userspace library, keep the input, build configuration, and sanitizer output together. If the reproducer depends on an optional feature, record that dependency. A clean run without the feature doesn't test the same path. For a kernel, capture the source revision, configuration, reproducer, and complete relevant log. Repeating the test on fresh boots helps distinguish the intended failure from a previous run's state or an unrelated warning. Static mobile analysis has another boundary. An endpoint or risky configuration can be a lead, but its presence doesn't establish an exploitable backend vulnerability. Dynamic testing needs its own authorization and evidence. ## Keep progress and uncertainty visible A useful investigation moves through questions: ```text What looks wrong? Can the relevant caller reach it? Can we exercise it in a controlled environment? What behavior did we observe? Can we reproduce that behavior? What security impact did we demonstrate? ``` These questions aren't interchangeable. Finding a path in source doesn't mean a test reached it. A working harness doesn't mean the candidate triggered. A reproduced crash doesn't automatically establish a privilege escalation. If a build fails or a required device isn't available, record the limitation at that stage. It should remain visible when someone reads the eventual report. ## Check novelty separately A well-reproduced bug can already be known. Search the relevant commits, advisories, issue trackers, and patch queues before treating a candidate as new. The search record matters too. If a source couldn't be queried, an empty result says little. A different filename, driver hash, or package release can still contain a previously reported flaw. Our [driver investigation](/blog/finding-the-bug-is-the-easy-part) is one example of why proof and novelty need separate checks. ## Use the patch as a comparison When a fix is available, run the same input against vulnerable and fixed revisions. Keep the environment as similar as possible and check that both tests reach the relevant code. A failure on one revision and its absence on the other is useful evidence. A build failure on one side is an incomplete experiment. If the proposed detector still reports the fixed code, understand why before using it to search for variants. The resulting report should let another engineer reconstruct the investigation: what ran, what changed, what was observed, and what remains unknown. That record is more useful than a single confidence score. --- # A length field the NFC parser trusted > Linux's NFC service-discovery parser needed checks against the received packet and against each message type's minimum length. The defect illustrates why declared lengths are not buffer bounds. Published: 2026-07-09 Updated: 2026-09-12 Canonical article: https://0.security/blog/nfc-llcp-15-year-kernel-bug/ The NFC service-discovery parser read lengths from an incoming message without consistently checking them against the packet that contained it. A length field describes what the sender claims to have supplied. The buffer length describes what actually arrived. A parser has to compare the two before it reads. In Linux's LLCP service-discovery path, that comparison was incomplete. A separate subtraction also assumed that a request contained at least one byte. ## Follow one entry through the parser LLCP is a link-layer protocol used for NFC peer communication. Its Service Name Lookup messages contain type-length-value entries, usually called TLVs. Each entry starts with a type and a declared value length. The parser then interprets the value according to the type. The function `nfc_llcp_recv_snl()` needed to establish several conditions before advancing: - enough packet data remains for the TLV header; - the declared value fits within the remaining packet; - the value is long enough for the selected message type. Checking only one of those conditions leaves the others open. For a service-discovery request, the value contains a transaction identifier followed by the service name. Calculating the name length as `length - 1` assumes the identifier exists. A zero-length request violates that assumption before the parser reaches the name. ![Packet entries with one extending beyond the valid boundary](/blog/nfc-oob-unbounded-minimal.webp) ## Bound the walk before interpreting the value The submitted fix bounds parsing against the end of the socket buffer and checks the type-specific lengths before using them. A service-discovery request must include its transaction identifier; a service-discovery response has a fixed expected length. That order matters. First establish that the bytes exist. Then interpret what they mean. The patch reached Linus's mainline tree as [`f4c7f37f`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f4c7f37f0ab990952539dc68d931d65c3657600a) — rebased on merge from the maintainer-tree commit `ed85d4cbbfaa`, following review by NFC maintainer David Heidelberg and Simon Horman. On 2026-09-12 it was queued to the 6.6, 6.12, 6.18 and 7.2 stable trees. A queued stable patch is not a released kernel: presence in a tree should not be read as proof that a particular distribution has shipped the fix. This SNL parsing issue is also distinct from the other NFC length-check fixes in our [kernel contribution roundup](/blog/fixing-the-linux-kernel). Similar subsystem names don't make them one finding. ## Keep exposure separate from source presence The code's presence in a kernel tree doesn't establish that every device exposes it. A practical attack depends on the kernel configuration, NFC hardware and driver support, and whether the relevant peer-protocol path is active. Short-range NFC communication is also different from an internet-reachable service. The original article's claims about every kernel and anyone standing next to a phone were broader than the evidence supported. The source-level defect is the unchecked parsing boundary. A claim about a crash, a leak to the sender, or a particular device needs evidence for that additional consequence. ## Why this is useful to investigate statically A source review can ask where a loop ends and how each read is bounded without first building a complete radio test environment. It can identify an assumption that deserves a patch or a targeted reproducer. Execution still matters when describing impact. Static analysis, maintainer acceptance, and a device-level reproduction answer different questions. This parser provides a compact review rule: every length read from a message must be checked against both the available bytes and the format's own minimum requirements. --- # When the test needs an authorization rule > A missing permission check may leave memory perfectly valid. Detecting it requires a test that knows which caller should be allowed to act. Published: 2026-07-05 Updated: 2026-09-10 Canonical article: https://0.security/blog/the-bugs-fuzzers-cant-see/ A kernel operation can succeed, leave every allocation intact, and still be a security bug. The wrong caller may have been allowed to change an object it doesn't own. A memory sanitizer has no reason to complain about that outcome. To detect it, the test needs an expectation about authorization. ## Input generation and bug detection are separate choices Fuzzing generates and varies inputs. An oracle decides whether the resulting behavior is wrong. Crashes and sanitizer reports are common oracles, but fuzzing isn't limited to them. Assertions, differential checks, and application-specific rules can detect failures that never crash. The practical limitation is the test that's been built. If its only success signal is a memory-safety report, an unauthorized operation may pass unnoticed. If its harness can't initialize a device or describe an interface, that surface may receive little useful exercise. Neither limitation means a whole subsystem has been exhausted or that a fuzzer could never reach it. ## Look for the check that differs ![Different tests reveal different failures](/blog/fuzzer-vs-reasoning-coverage-minimal.webp) Source review can help decide where to build a more specific test. One useful starting point is an operation whose neighboring implementations enforce a rule that it omits. Consider network devices that can reference an underlay in another namespace. Checking privileges in the caller's namespace may not establish permission over that underlay. The investigation needs to identify which object is affected and which security context owns it. A [VXLAN fix accepted upstream](https://github.com/torvalds/linux/commit/3a61bd9637f3) added a check for the underlay namespace. The patch is a concrete authorization example. It doesn't establish that every similar-looking omission elsewhere is exploitable. ## Turn the rule into an experiment Write down the actors before constructing the reproducer: who owns the target, which privileges the caller has, and which action should be denied. Then compare the same operation on vulnerable and fixed builds. A useful test shows the relevant action succeeding before the check and being rejected afterward. It should also show that a legitimately authorized caller can still perform the operation. That last control matters. A change that breaks the feature for everyone can stop the test without enforcing the intended rule correctly. ## A patch is a lead for finding variants A security fix can reveal a missing invariant in sibling code. Search for the same operation, then inspect how each path protects it. Differences in caller permissions, object lifetime, or earlier checks may make a superficial match safe. Before sending a report, check existing commits and maintainer queues. Variant hunting often reaches code other researchers are examining at the same time. ## Use the methods together Source reasoning helps formulate the condition to test. Fuzzing can explore inputs and sequences around it. A targeted oracle can distinguish permitted behavior from a boundary crossing. Maintainer review tests whether the explanation and fix fit the subsystem. The opportunity is to improve those connections. A plausible missing check becomes useful research when the test demonstrates which caller can do what, and the patch restores the intended boundary. --- # Can a researcher find your security contact? > Our June survey found security.txt on about 1 in 10 sampled .ch domains. That measures a public contact file, not whether a company can receive reports. Published: 2026-06-03 Updated: 2026-09-10 Canonical article: https://0.security/blog/swiss-securitytxt-survey/ A researcher finds a vulnerability and needs to reach someone who can fix it. A support form might work. A general inbox might forward the message. A published security contact removes some of that guesswork. On June 3, 2026, we checked for `security.txt` across `.ch` domains in two public ranking lists. About 1 in 10 of the sampled domains returned a file containing a `Contact:` field. ## What we measured | Sample | Domains checked | File found with a Contact field | |---|---:|---:| | Majestic Million, full .ch subset | 4,188 | 9.4% | | Tranco, highest-ranked 3,000 .ch domains | 3,000 | 10.7% | The samples came from the [Majestic Million](https://majestic.com/reports/majestic-million) and [Tranco](https://tranco-list.eu/). These are ranked-domain samples, not a census of Swiss organizations. They may overlap, so their sizes shouldn't be added into a count of unique sites. We fetched `/.well-known/security.txt`, followed redirects, and looked for a `Contact:` field. This was a file-discovery check. It didn't test whether someone monitored the contact, whether the file met every requirement, or whether a vulnerability disclosure program worked. The percentages describe that June snapshot. They aren't a fresh measurement of today's sites. ## Absence doesn't mean unreachable Our earlier wording described sites without the file as having no way to receive a report. That went beyond the data. A company can publish a security page, operate a reporting program elsewhere, or handle reports through another contact. The narrower finding is still useful: the standard discovery location was missing from most domains we sampled. Researchers visiting those domains needed another route. Nor did the survey measure how long vulnerabilities remained unreported or which companies were more secure. Those questions require different evidence. ## Redirect handling changed an early result An initial check on a separate large-company set reported 8% coverage. Following redirects raised that result to 48%. This correction concerned that separate set, not the 4,188-domain sample above. It exposed an avoidable measurement error. A collector that treats a redirect as an absent file can underestimate adoption substantially. Timeouts and unexpected responses also need their own categories when interpreting a snapshot. ## Publish a contact someone maintains [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116) describes the file format. For websites, serve it over HTTPS at `/.well-known/security.txt`. Include a reporting contact and an expiry date, and update the file before it expires. This illustrative file needs a real address and a maintained expiry date before use: ```text Contact: mailto:security@example.com Expires: 2027-03-01T00:00:00Z Preferred-Languages: en ``` [securitytxt.org](https://securitytxt.org/) provides a generator. Assign someone to monitor the reporting route and keep the file current. Publishing the file helps a researcher find you; responding to the report is the work that follows. --- # What we lost when we filtered the findings > Our triage experiment reduced report volume, but some configurations also lost flags. Both sides of that tradeoff belong in the result. Published: 2026-05-20 Updated: 2026-09-10 Canonical article: https://0.security/blog/triage-moat-and-multi-benchmark-validation/ A filter can make a security report shorter while making the tool miss more bugs. Counting the reports it removes won't reveal the difference. We encountered that tradeoff while evaluating our triage layers. An early hard-target slice looked bad enough to suggest disabling them. A broader run gave a more complicated answer. ## Preserve the profile names and the denominators The tables below reproduce the historical experiment's profile labels. `none` used no optional features; `no-triage` disabled triage within the broader configuration. The two `moat` profiles enabled the additional review layers, alone or alongside the other features. White-box runs had source access and used 50 challenges: | Profile | Flags / 50 | Reports | Recorded spend | |---|---:|---:|---:| | none | 43 | 67 | $14 | | no-triage | 44 | 67 | $17 | | moat-only | 41 | 25 | $27 | | moat | 41 | 25 | $22 | Black-box runs used 25 challenges: | Profile | Flags / 25 | Reports | Recorded spend | |---|---:|---:|---:| | none | 18 | 27 | $14 | | no-triage | 19 | 34 | $10 | | moat-only | 18 | 13 | $11 | | moat | 19 | 14 | $10 | These are recorded experimental costs, rounded as in the original report. They aren't current product prices. ## Fewer reports isn't a false-positive rate ![Findings passing through separate review stages](/blog/triage-moat-architecture-minimal.webp) In the white-box run, report volume fell from 67 to 25, a reduction of about 63%. Flag recovery also fell: 41 with triage, compared with 43 or 44 in the other profiles. That is a tradeoff. Without adjudicating each report, we can't describe the 42 removed reports as 42 false positives. A target can also generate several reports while yielding one flag, so those columns measure different things. The black-box run was more encouraging: the `moat` profile recovered the same 19 flags as `no-triage`, with 14 reports instead of 34. That result applies to this run and subset. ## Repetition changed the interpretation A second white-box batch produced 44, 43, 42, and 42 flags for `none`, `no-triage`, `moat-only`, and `moat`, respectively. The ordering shifted, while the triage profiles remained below the best result. Two batches don't establish the size or significance of the difference. They do show why choosing a default from one run would be premature. A separate 14-target experiment tested individual additions: | Addition to default | Flags / 14 | |---|---:| | None | 2 | | Proof-of-value | 4 | | Reachability | 5 | | Multimodal | 3 | | Debate | 5 | | Memories | 4 | | EGATS | 1 | | Consensus | 3 | EGATS performed poorly in that run, at a recorded $15.93 per flag versus $3.62 for the default. The result justified further investigation. It didn't establish that the feature would always reduce performance or that its interaction with every other layer was understood. ## Evaluate what the filter accepts and rejects A useful triage evaluation needs known vulnerable cases, clean controls, and reviewable decisions. Track real findings lost, incorrect reports retained, unresolved cases, and review time. Different benchmarks can test different capabilities. Adding an integration for a suite doesn't mean completing its evaluation, and a score on web flags doesn't establish package-audit accuracy. The practical outcome of this experiment was a better question for the next run: which decisions did the filter improve, and which correct discoveries did it discard? --- # Before comparing scores, check what ran > Broken builds, retries, and an overly generous flag checker changed the meaning of our benchmark results. Here is how to keep those effects visible. Published: 2026-05-17 Updated: 2026-09-10 Canonical article: https://0.security/blog/xbow-benchmark-methodology-and-verification/ Our April 7 cold run of a 104-challenge web benchmark encountered 39 targets that wouldn't build or start in that environment. A single success percentage hid that fact. The problems included archived Debian package sources, a Composer dependency issue, a Java startup failure, and a port collision. They were failures in preparing the experiment, not evidence that the agent couldn't exploit a running target. ## State both denominators The recorded run recovered flags on 45 challenges. That gives two useful views: | View of the historical run | Result | |---|---:| | All benchmark challenges | 45 / 104, or 43.3% | | Challenges that built and started | 45 / 65, or 69.2% | | Challenges unavailable in that environment | 39 / 104 | The second percentage describes performance conditional on availability. It shouldn't replace the full-suite result without explanation. The unavailable targets may also differ in difficulty from those that ran. Nor does this establish that the upstream benchmark remains broken today. It describes a particular cold run and its environment. ## Identify the exact target revision The [upstream benchmark](https://github.com/xbow-engineering/validation-benchmarks) and a [community-patched fork](https://github.com/0ca/xbow-validation-benchmarks-patched) are distinct inputs to an experiment. A dependency or container repair can make a challenge runnable. It can also change behavior relevant to exploitation. To compare runs, preserve the revisions and inspect the diff, including application dependencies, hints, source files, and build instructions. Calling a fork patched doesn't establish that only infrastructure changed. That claim needs an actual comparison of the named revisions. ## Separate one attempt from accumulated success A single-attempt score asks how many targets were solved once under a fixed budget. An aggregate score may count a target if any of several attempts or configurations solved it. Both can be useful. They answer different questions and incur different costs. State the attempt count, stopping rule, source access, model, budget, and whether the result combines configurations. Earlier versions of our posts repeated a 103/104 aggregate headline without enough of that context beside it. This revision removes it as a general performance claim. It shouldn't be compared directly with a single-attempt score. ## Repeat a promising result before explaining it One XBEN-061 attempt recovered a flag in 8 turns at a recorded cost of about $0.20. A follow-up attempt took 10 turns, cost about $0.28, and didn't recover the flag. The first success was worth examining. The failed repetition meant we hadn't yet established a reliable improvement. A plausible explanation for the first run wasn't enough to make it a general architecture lesson. Retain both traces. Compare what the agent observed, which branch it explored, and where the attempts diverged. ## Check the checker Another failure was in our own success handling. On XBEN-079, a flag-shaped dummy value was accepted as success even though it wasn't the challenge's actual flag. A checker that trusts the model's claim or a broad string pattern can inflate the score. Match the real challenge condition and preserve the evidence supporting that match. A reported vulnerability and a recovered flag should remain separate outcomes. ## What a reusable result needs Before relying on a benchmark number, look for: 1. Exact target and harness revisions. 2. Environment changes and the diff that introduced them. 3. Source access, hints, and tools available to the agent. 4. Attempt counts, budgets, and stopping rules. 5. Build and runtime failures separated from completed attacks. 6. A success checker tied to the actual objective. 7. Retained traces, costs, and unsuccessful attempts. These details make a result reproducible and a comparison interpretable. They also make debugging easier: a broken container, a lost conversation, and a failed attack need different fixes. --- # Why we gave the agent a shell > An early web-testing experiment showed how tool interfaces can consume an investigation. A cookie jar made the problem concrete. Published: 2026-05-14 Updated: 2026-09-10 Canonical article: https://0.security/blog/agentic-pentesting-on-xbow/ The agent knew it needed to log in and request another user's profile. It spent its budget trying to pass the session cookie between two tools. Our original web-testing interface exposed separate tools for crawling, form submission, and HTTP requests. Each was straightforward in isolation. Together, their different representations of state made a simple authenticated sequence difficult to complete. We tried a smaller interface built around shell execution. ## Let the HTTP client manage its own state The shell gave the agent a familiar way to preserve cookies between requests. This simplified illustration shows the pattern, rather than a literal benchmark transcript: ```bash # In an authorized local test environment: curl -c cookies.txt -d 'username=tester&password=test-password' http://target/login cat cookies.txt curl -b cookies.txt http://target/api/users/2 ``` The cookie jar keeps the session in one representation. The agent can inspect it and reuse it without translating a login tool's output into another tool's input schema. A structured tool could also support this well. Our result exposed a weakness in our interface, not an inherent inability of typed tools to handle authentication. ## What the early run returned The historical ten-challenge slice used one attempt per challenge on a patched version of the public web benchmark. It wasn't a full-suite score. | Challenge type | Turns | Outcome | |---|---:|---| | IDOR | 10 | Flag recovered | | Template injection | 5 | Flag recovered | | Authentication / privilege escalation | 9 | Flag recovered | | File upload | 12 | Flag recovered | | Markdown injection | 10 | Flag recovered | | Deserialization | 4 | Flag recovered | | Blind SQL injection | 20 | Flag recovered | | Bobby Payroll SQL injection | 24 | No flag | | Melodic Mayhem | N/A | Azure timeout | | GraphQL | N/A | Azure timeout | That is 7 successful challenges out of 10 attempted. The 2 provider timeouts remain in the denominator, but they should be distinguishable from completed investigations that failed to find a flag. A separate blind-SQL-injection attempt exhausted a 15-turn limit. With a 25-turn limit, the recorded successful attempt took 20 turns. It was a reason to investigate budget sensitivity, not enough data to estimate the general benefit of longer runs. ## Check the conversation loop before interpreting failure During this work, our historical Responses API adapter serialized assistant text using the wrong content type. Requests failed as conversation history accumulated. Those runs measured a broken integration. They couldn't support conclusions about the model's ability to solve the target. Preserving provider errors separately from unsuccessful attacks made the distinction visible. ## A shell moves responsibility into the harness General tools let the agent combine an HTTP client, a parser, and a short script without adding a new wrapper for each task. They also produce large outputs and expose broad capabilities. The surrounding system must control credentials, filesystem access, networking, command lifetime, and output retention. The correct limits depend on the engagement and must be enforced outside the model's instructions. For our early workflow, shell access simplified state handling. The durable lesson was to inspect where the agent spent its turns. An investigation stalled on cookie formatting needs a different fix from one stalled on the target's security logic. --- # Keep the investigation outside the context window > Durable evidence, focused agent sessions, and explicit workflow stages make long research tasks easier to resume and verify. Published: 2026-05-07 Updated: 2026-09-10 Canonical article: https://0.security/blog/2026-05-07-orchestration-not-frontier/ A long investigation produces more than a conclusion. It leaves build commands, failed inputs, source references, test results, and hypotheses that have already been ruled out. When that record lives only in a model's conversation, summarization can lose the detail needed to continue. A new session may repeat a failed approach or mistake an untested idea for an observed result. Niels Provos's [IronCurtain](https://github.com/provos/ironcurtain) work prompted us to examine this part of our own research workflow. This is an architectural reflection on that work, not a claim that changing orchestration makes every model equally capable. ## Separate durable evidence from working memory An investigation journal should preserve what actually happened: the command, target revision, output location, and outcome. Keep a hypothesis distinguishable from a completed test. A specialist session can then load the part it needs. A verifier may need the reproducer and build instructions. A source reviewer may need the suspected path and surrounding invariants. Neither necessarily needs the entire conversation that produced the lead. The journal still requires care. An append-only record can faithfully preserve a mistaken conclusion. Link conclusions to evidence, and make later corrections visible rather than silently replacing the earlier account. ## Make transitions explicit A workflow can describe when an investigation is ready to move from source review to harness construction, execution, verification, and reporting. The representation might be configuration or code; the important property is that each transition has a checkable condition. A generated reproducer shouldn't advance to verified merely because the agent saved a file. The next stage needs to run it and inspect the result. A build error should remain a build error throughout the record. Clear stage boundaries also help with recovery. After a worker stops, the next worker can see the last completed step and the artifacts it produced. ## Increase the realism of the harness deliberately A small function-level test can quickly check a suspected arithmetic condition. A component harness can include parsing and object setup. A full application or VM can test reachability under realistic configuration. Each level answers a different question. A direct function call may demonstrate a defect while bypassing the checks that protect a public entry point. Moving to the larger harness tests whether those checks change the result. This makes partial progress useful without overstating it. Preserve the smaller result and name what the next level still needs to establish. ## Compare the whole investigation cost Token spend is only one part of the cost. Repeated builds, failed setup, duplicate work, and human review also matter. Track them alongside the outcome and the evidence produced. A model comparison needs the same targets, budgets, access, and success rules. One successful demonstration doesn't isolate the effect of a journal, a workflow format, or a particular model. The practical design aim is an investigation another worker can resume and another engineer can audit. That is a concrete improvement even before a benchmark score changes. --- # Replacing a native SQLite dependency > A Node.js ABI mismatch led us to a WebAssembly SQLite build. The dependency became easier to install, but the adapter still needed careful testing. Published: 2026-04-08 Updated: 2026-09-10 Canonical article: https://0.security/blog/2026-04-07-deleting-better-sqlite3/ The engine installed successfully, then failed when it opened its database: ```text The module was compiled against a different Node.js version. This version of Node.js requires a different NODE_MODULE_VERSION. ``` The native SQLite module didn't match the running Node.js ABI. The failure happened before the user could do any useful work. This is an engineering note about the migration we made in April 2026. It explains the tradeoff behind that change, rather than promising support for every runtime a WebAssembly engine can run in. ## The first fixes kept the same dependency We first made initialization failures visible. A swallowed exception had turned a database problem into an unexplained failure later in the program. Throwing an actionable error was a worthwhile improvement on its own. We also added installation logic to resolve the native binary for the current runtime. That addressed the immediate mismatch, but left us maintaining another installation path. The engine used SQLite for scan history and findings; it didn't need a native database binding for a demanding throughput workload. We replaced `better-sqlite3` with `node-sqlite3-wasm`. ## The query layer still expected the old driver Changing the package name wasn't enough. Our Drizzle integration expected synchronous prepared statements with methods such as `run()`, `get()`, and `all()`. The replacement exposed a different interface. We wrote an adapter for the subset the engine used. It translated statement calls and bound parameters, and connected the replacement database to the existing query layer. We also avoided an import path that loaded the native driver as a side effect. That last detail mattered: an adapter doesn't remove a native dependency if importing the adapter still loads it. The compatibility work needed to cover more than successful queries. Empty results, parameter binding, transaction rollback, migrations, and errors all belong to the database contract. A wrapper that gets ordinary inserts right can still fail on one of those boundaries. ## What changed operationally The WebAssembly build removed this database component's dependence on a Node native-addon ABI. It also let us remove the workaround that selected a replacement native binary during installation. There was a tradeoff. The replacement's filesystem implementation didn't support the WAL configuration we had used, so we removed that setting. That was acceptable for our workload at the time. It wouldn't establish suitability for a service with concurrent writers or different durability requirements. We also needed fresh install-and-run checks on supported runtimes. WebAssembly portability doesn't guarantee that a package's filesystem access, module loading, or surrounding JavaScript will work everywhere. ## A smaller installation problem The migration moved complexity from platform-specific installation into a small adapter we could test directly. For this engine, that was a useful exchange. It didn't eliminate every startup failure or prove that future Node releases would work unchanged. It removed a particular failure mode that had reached users before they could run their first scan. --- # Introducing 0cloud > Managed security testing starts with a clear scope, a real target, and evidence an engineer can check. Published: 2026-04-06 Updated: 2026-09-10 Canonical article: https://0.security/blog/2026-04-06-the-marketing-rewrite/ A product keeps changing after its penetration test ends. New endpoints appear, permissions change, and an integration gives an old feature access to new data. The report still describes the version that was tested. 0cloud is our managed service for testing that changing attack surface. It uses the public 0sec engine, with the scope and engagement agreed directly with our team. ## Start with the question the test should answer A useful engagement starts with a concrete concern. Can one customer read another customer's records? Can an integration perform an action its owner wasn't allowed to authorize? Does a file upload reach a parser with more privileges than it needs? Those questions determine the accounts, environments, source access, and boundaries the test requires. They also define what a successful demonstration would look like. A generic list of suspicious endpoints isn't an answer. The service is available by arrangement. We agree on the target, permitted actions, timing, and evidence needed before testing begins. For systems with sensitive data or availability constraints, those details belong in the scope from the start. ## A finding should be usable An engineer receiving a report needs to know where the behavior occurs, what an attacker must already control, and how to reproduce it. The report should distinguish an observed failure from a plausible consequence that hasn't been demonstrated. A crash, for example, establishes different things from a successful cross-account read. A source review can identify a missing check without establishing that an unauthenticated user can reach it. Keeping those distinctions visible makes remediation easier to prioritize. Verification is part of our research workflow. Its strength depends on what ran and what it observed. A second agent agreeing with the first is useful review, but execution evidence and a clear security boundary carry the claim. ## The engine and the managed service The public [0sec engine](https://github.com/0sec-labs/0sec) provides the research tooling. 0cloud adds the managed engagement around it: agreeing on scope, operating the work, and discussing the results with the team responsible for the target. A benchmark can help us evaluate the engine. It doesn't establish the coverage of a particular customer application, and it doesn't replace a scoped test of that application. If there's a part of your product you want tested, [tell us what it is](/contact). A useful first message includes the system, the concern, and the environment available for testing. We can use that to decide what a meaningful engagement would require. *Updated September 2026 to reflect the current product structure and remove service guarantees that this introduction did not substantiate.* --- # Test what an AI application can actually do > AI security tests need to follow data access and tool permissions through the application, with a clear definition of success. Published: 2026-04-04 Updated: 2026-09-10 Canonical article: https://0.security/blog/attack-surface-xbow-doesnt-test/ An assistant can answer a question correctly and still use the wrong customer's data to do it. It can follow a document's instructions and send information somewhere the user never approved. Those failures require tests that include the application's data and tools. A web challenge suite built around SQL injection or template injection doesn't automatically measure them. ## Start with an unauthorized outcome A prompt that changes the tone of an answer isn't the same as a prompt that causes a cross-account read. Before testing, define the protected resource and the boundary an attacker would need to cross. For a support assistant, that might mean retrieving another tenant's ticket. For a coding agent, it might mean treating repository content as permission to execute an unrelated action. For an email assistant, it might mean sending a message because instructions appeared inside an email it was supposed to summarize. Each case needs a controlled environment and an observable success condition. A model saying that it performed the action doesn't establish that it did. ## Follow the data into the tools Indirect prompt injection places instructions in material the application processes: a retrieved document, webpage, message, or tool response. The security question is what happens when the assistant treats that material as authority. Record the full path. What did the attacker control? How did the application ingest it? Which tool call followed? Did the tool enforce the user's permissions independently? Tool access alone doesn't establish a vulnerability. A failed request to a correctly protected API can show that a boundary held, even when the assistant attempted the wrong action. ## Keep different outcomes separate System-prompt disclosure can reveal implementation details. It isn't automatically a secret leak; assess what the disclosed material actually contains. Credentials shouldn't depend on a model keeping them hidden in its instructions. A jailbreak evaluation measures whether a model follows a behavior policy under adversarial prompting. An application-security evaluation may instead measure unauthorized access or actions. Both can be useful, but their success rates answer different questions. Cross-user leakage also needs an actual shared-data path. Asking a chatbot about a previous user doesn't prove that another user's conversation was accessible. Generated names and invented records mustn't count as retrieved data. ## Combine repeatable cases with adaptive testing Fixed regression tests make it possible to compare builds and catch known failures. Adaptive agents can explore variations, carry context across turns, and follow responses into new paths. Human review can resolve ambiguous outcomes and check whether the test represents a realistic threat. For each approach, preserve the attempts and apply the same success rule. Count the clean controls too: a collection containing only exploitable targets cannot establish a false-positive rate. Our early internal AI challenges helped us exercise this workflow. Their size and scope didn't justify a broad claim about application coverage. The next useful test is the one that reflects a real permission or data boundary in the system being shipped. --- # Give the verifier a fresh start > A separate verification pass can challenge a finding without inheriting its narrative. Its verdict still needs evidence. Published: 2026-03-30 Updated: 2026-09-10 Canonical article: https://0.security/blog/blind-verification/ A plausible explanation can survive longer than the bug it describes. Once an agent has decided that input reaches a dangerous operation, its next steps may keep reinforcing that account. We encountered this while building the research pipeline. Asking the discovering agent to write a proof of concept helped, but the script could inherit the same assumptions as the report. It might exercise a different path, supply privileges the supposed attacker didn't have, or assert something that was true before the attack began. We added a separate verification pass with a narrower brief. ## Hand over the test, not the conclusion ![A separate path for verification](/blog/blind-verification-oracle-minimal.webp) The verifier starts with the reproducer and enough target information to investigate it. Withholding the discovering agent's narrative gives it room to reconstruct the data flow and question the assumptions. That separation is useful, but it isn't statistical independence. Two agents can share a model, tools, or the same mistaken assumption in the reproducer. Calling the pass blind describes the information handoff; it doesn't guarantee a correct verdict. The useful questions are concrete: - Does the input reach the claimed operation in this build? - Which permissions or configuration does it require? - Does the test observe the claimed effect? - Would the same assertion pass without exploiting the candidate? - Does a fixed build or a harmless input change the result? ## Make the output explain the decision A verifier should return its evidence alongside its verdict. If it rejects a candidate, the record should identify the protecting check or the failed assumption. If it reproduces the issue, it should preserve the command, input, environment, and observed result. There also needs to be an inconclusive outcome. A missing dependency, unavailable target, or timed-out run doesn't establish that the candidate is safe. It establishes that this verification attempt didn't resolve it. Code review and execution should remain distinguishable. Tracing a source path can strengthen or refute a hypothesis. It cannot truthfully be recorded as a successful exploit run when nothing was executed. ## What our self-audit showed In an early self-audit, the research stage produced 6 candidates. The follow-up review rejected all 6 after examining the relevant mitigations and data paths. That was a useful local result: reports that would have reached an engineer were challenged first. It wasn't a measurement of a general false-positive rate, and it didn't prove that the engine had no vulnerabilities. We hadn't established that the discovery stage found every real issue. ## Test the verifier too A verifier that rejects everything can look excellent if the evaluation contains only false alarms. Include known vulnerable cases, fixed counterparts, and examples where the environment prevents a decision. Measure confirmed bugs lost as well as noise removed. Preserve disagreements so they can be inspected later. For more on that tradeoff, see our [triage experiment](/blog/triage-moat-and-multi-benchmark-validation). The verification pass earns its place when it makes findings easier to reproduce and decisions easier to inspect. Agreement between agents is supporting information. The behavior of the target remains the evidence. --- # What our npm audits taught us > Certificate validation, configuration parsing, and template loading exposed different trust-boundary mistakes. The useful part was turning each lead into a precise report. Published: 2026-03-27 Updated: 2026-09-10 Canonical article: https://0.security/blog/how-ai-agents-found-7-cves/ In March 2026, we used AI-assisted source review to investigate popular npm packages. We looked at code that handled untrusted input, configuration, certificates, and templates, then tried to turn suspicious paths into reproducible cases. The work produced public advisories and fixes. It also taught us that a package name and a severity label leave out the most useful part of a finding: the conditions under which it works. ## Start with the input that changes authority ![Source review followed by verification](/blog/multi-agent-search-graph-minimal.webp) For each target, we identified an input and followed it toward an operation with security consequences. Could a certificate gain authority it shouldn't have? Could a connection string replace a trusted option? Could template resolution read outside the intended location? The agent helped inspect those paths and develop tests. Reviewing the result meant checking callers, defaults, configuration, and existing mitigations. A suspicious function wasn't enough to establish a vulnerability in a real application. ## Certificate validation needed both missing extensions In node-forge, a certificate-chain verification bypass depended on an intermediate certificate lacking both `basicConstraints` and `keyUsage`. Describing it as a bypass with any certificate would be incorrect. The [maintainer advisory](https://github.com/digitalbazaar/forge/security/advisories/GHSA-2328-f5f3-gj25) records the affected conditions and the fix in version 1.4.0. That detail matters to anyone assessing exposure or building a regression test: supplying a certificate with different extensions tests a different case. ## Keep related template issues separate The Uptime Kuma advisory for [CVE-2026-33130](https://github.com/louislam/uptime-kuma/security/advisories/GHSA-v832-4r73-wx5j) describes authenticated arbitrary file read through notification templates, fixed in 2.2.1. The related LiquidJS issue has its own identifier, [CVE-2026-30952](https://github.com/advisories/GHSA-wmfp-5q7x-987x), and its own affected component and fix. Combining them into one broad template-injection claim obscures both the prerequisites and the remediation. When a report crosses a dependency boundary, name each component's role. The application may expose the feature while the library supplies the unsafe resolution behavior. Updating one component and restricting the application's feature are different actions. ## Credit the contribution accurately Research can contribute an original report, a duplicate discovery, a reproducer, or review of a proposed fix. These are all useful, but they aren't the same credit. For the jsPDF work, another researcher reported the issues first. Our contribution was remediation review and defense-in-depth feedback. The [detailed writeup](https://doruk.ch/blog/jspdf-pdf-injection-xss) explains that work; it shouldn't be counted as an original 0sec discovery. ## Count outcomes, not just candidates The original campaign note recorded 73 findings. That is a campaign-output count, not 73 independently confirmed, newly disclosed vulnerabilities. Download totals also measure package activity, not affected installations. For a research record, the useful unit is the individual case: conditions, reproduction evidence, disclosure status, public advisory when available, and fix. That structure lets another engineer check what happened without relying on an aggregate headline. --- # Security review has to follow the change > AI-assisted development makes it easier to produce changes. Security testing still needs context, priorities, and a way to check its conclusions. Published: 2026-03-24 Updated: 2026-09-10 Canonical article: https://0.security/blog/the-age-of-agentic-security/ A small pull request can change who is allowed to read a record. It can give a background job a new credential or connect a file parser to an upload endpoint. The size of the diff doesn't tell you the size of the security decision. AI-assisted development makes producing those changes easier. The useful question for security teams is how to review the consequences as the application evolves. ## Follow the boundary beyond the diff A changed authorization helper deserves a look at its callers. A new endpoint needs to be understood alongside the middleware that runs before it. An apparently harmless dependency update can alter how an application parses untrusted input. Static analysis, dependency checks, targeted tests, and human review each contribute information. An agent can help connect that information, inspect related code, and construct a test for a specific hypothesis. It still needs the right context. A model looking only at one function may miss the upstream check that makes the path safe. A model given the whole repository may spend its budget on irrelevant files. Selecting and preserving context is part of the engineering problem. ## Ask for a demonstration The most useful output is a finding another engineer can evaluate. State the entry point, the attacker's starting permissions, the behavior observed, and the boundary crossed. For an access-control candidate, use accounts with different roles or owners. Show what the restricted account can access and what should have been denied. For a parser candidate, preserve the input and the build that produced the failure. A script alone isn't sufficient. Its assertions must measure the claimed effect, and its setup must match the threat model. A test that begins with administrator privileges cannot establish that an ordinary user became an administrator. ## Keep review in the workflow Automation can perform repeatable checks and challenge a candidate in a fresh context. Engineers still need to assess impact, intended behavior, and whether the proposed fix fits the system. Our [blind verification work](/blog/blind-verification) grew out of that need. Separating discovery from verification gives a candidate another chance to fail before it becomes someone else's task. The same discipline applies to dependency research. Public advisories are useful evidence of specific issues and fixes. A download count doesn't tell us how many deployments use an affected path, and a collection of findings doesn't prove comprehensive coverage. ## Start where mistakes are expensive A practical rollout can begin with changes to authorization, tenant isolation, credential handling, parsers, and privileged integrations. Use the existing review process to decide which findings were actionable and which tests missed relevant context. Track the time engineers spend reproducing reports, the confirmed bugs the workflow catches, and the known cases it misses. Those measures say more about usefulness than the number of comments an agent leaves. Security review keeps pace by making each change easier to understand and test. Generating more assessments helps only when the team can act on them. ## Authoritative public records - [Research index](https://0.security/research/) - [Security contact](https://0.security/.well-known/security.txt) - [XML sitemap](https://0.security/sitemap-index.xml) - [Research RSS feed](https://0.security/rss.xml)