XERJ indexed its own engine — 197 Rust files, ~201k lines — as a queryable AST and call graph, then hunted for security bugs on top of it. A call-graph query that grep cannot express surfaced an unauthenticated POST /_sql stack-overflow that aborts the whole process. Reaching it cost 10,533 tokens instead of the 1,805,277 it takes to read the source once. The bug is fixed and the fix is proven by crashing — then not crashing — a live server.
READ-ALL = EVERY .rs FILE UNDER engine/ TOKENIZED (tiktoken cl100k_base, an LLM-token proxy) · XERJ-ASSISTED = cycle-finder output + the 4 parser functions actually read + the route lookup · NOT A LATENCY BENCHMARK · SOURCES IN docs/case-studies/xerj-self-audit →
Before trusting a substrate on unknown bugs, test it on known ones. The day before, a PR fixed six real network-reachable vulnerabilities in this exact engine — a labelled test set. We indexed the pre-fix commit and, for each bug, ran the query an auditor would write for that bug class (never its name or line), against the baseline they would otherwise use: grep.
A GREP HIT IS A FILE; AN AST HIT IS A FUNCTION · RECALL 6/6 IS IN-SAMPLE — SEE THE CAVEAT BELOW · FULL PER-BUG TABLE IN DETECTION-QUALITY.md →
grep max_actions_per_bulk on the pre-fix tree returns 0 lines. "Ingest function that never consults a configured limit" is an AST property, not a string.a.len() * b.len() at the first paren, losing the multiplication; taint provenance did not follow one hop into locals; validator detection was presence-only, scoring decorative validation that runs after the delete as "guarded". Fixing those reached 6/6 — so 6/6 is in-sample.guard_after_destructive_op is true for pre-fix restore_snapshot and false after. A detector that fires on patched code is noise.The genuinely out-of-sample result is the finding below: not in the test set, found on current main, and proven by aborting a live server.
A stack overflow in a recursive-descent parser is rarely direct self-recursion — it is a cycle: parse_or_expr → parse_and_expr → parse_condition → parse_or_expr, one turn per (. No single file, function, or regex shows it. The free-function call graph does.
$ python3 rust_ast_index.py ./engine --out ./ast-out # 5,095 fns, 87,137 edges, 477 routes $ python3 ingest.py ./ast-out --url $XERJ # sent == indexed, asserted
$ python3 find_recursion_cycles.py ./ast-out # strongly-connected components of the call graph, # flagged where NO member establishes a depth bound. # ranks the /_sql WHERE-clause parser cycle as UNGUARDED.
$ curl -s "$XERJ/rustroutes/_search" -H 'Content-Type: application/json' \
-d '{"query":{"wildcard":{"path":"*sql*"}},"_source":["method","path","handler"]}'
# POST /_sql -> sql_query (crates/xerj-api/src/router.rs:637) — unauthenticated
A finding is a claim until you trigger it. One POST /_sql with ~50k nested parens in the WHERE clause aborts the entire server — every tenant, every index — with a stack overflow that no catch_unwind can save.
$ curl -s $XERJ/_sql -d '{"query":"SELECT * FROM t WHERE ((((...50k...))))"}'
# thread 'xerj-rt' has overflowed its stack
# fatal runtime error: stack overflow, aborting
# ... Aborted (core dumped) — exit 134 (SIGABRT)
The fix mirrors the query_string parser: a bounded recursion depth. After it, the identical payload returns a clean error in ~2 ms and the server stays up.
$ curl -s $XERJ/_sql -d '{"query":"SELECT * FROM t WHERE ((((...50k...))))"}'
# {"error":{"reason":"SQL parse error: WHERE clause nesting exceeds max depth of 64"},"status":400}
# server still answering 200 afterwards.
A deeper pass (24 verified agents) replaced the category checklist with a single test — a function is dangerous when instruction and data share one channel with no separator, and a filter is only real if it strips the instruction power from the data channel. Five stages: flag the dangerous sinks, trace each to user input, map the privilege model, validate every filter for bypasses, and check the state machines for races. Full playbook: METHODOLOGY.md.
The two sharpest findings are corollary failures, not missing filters. INJ-01: a search-template param was spliced into the query JSON unescaped and re-parsed, so a value could drop the template's own ACL filter — live-proven leaking a private document, now fixed. F-PATH-02: the three filters guarding a snapshot's name are all real and tested; the hole is the un-validated sibling field, the repo location — a snapshot wrote index data outside data_dir, now bounded by a path.repo-style allowlist. Both, plus a session-revocation lost-update, ship fixed with regression tests. Every finding, severity-ranked with its instruction/data analysis: FINDINGS-V2.md.
.len() by name collapses the graph into one false component. Cycles through trait dispatch or macros are out of reach; the recursion result covers free functions.The extractor, the ingest script, the recursion-cycle finder, the query cookbook with live hit counts, the ground-truth recall test, and the coverage accounting are all published. Every number reproduces from scratch.
The same extractor + queries index your crate into XERJ and hunt the same bug shapes — recursion cycles, unbounded allocation, unsafe soundness, path traversal. Want a walkthrough with your team? Leave an email.