From cf7d4d4819e054ac7165944be634452dface5ceb Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 09:26:06 +0000 Subject: [PATCH] Root-cause and document issue #383's Key$-missing fuzzer finding Confirmed the "Key $N does not exists in frame." finding is still live (a wider search across ~2M generated programs reproduces it, contradicting the 2026-07-09 non-reproduction audit), and corrected its root cause: it is not a register/variable-binding bug in any lowering provider (Frame, getResultRegister, bindResult, the cpp backend's getVariable are all fine), but a tracer bug in loop back-edge recording for a provably-zero-trip Kind::Loop whose body contains a Kind::Call argument with a Kind::LoopContinue -- one layer upstream of SSACreationPhase's return-anchored backward walk, which never visits the loop body block because it never reaches the recorded Return. Pins a minimized, directly replayable 23-byte reproducer and updates README.md's "Known findings" and the ReplayMain.cpp tolerance comment with the corrected diagnosis so a future fix attempt starts from accurate information instead of the original (incorrect) lowering-provider theory. --- nautilus/test/fuzz/README.md | 77 ++++++++++++++++-- nautilus/test/fuzz/ReplayMain.cpp | 13 +-- .../finding3-loop-call-continue.bin | Bin 0 -> 23 bytes 3 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 nautilus/test/fuzz/regressions/finding3-loop-call-continue.bin diff --git a/nautilus/test/fuzz/README.md b/nautilus/test/fuzz/README.md index 2b1fb3c30..97b686760 100644 --- a/nautilus/test/fuzz/README.md +++ b/nautilus/test/fuzz/README.md @@ -723,15 +723,74 @@ backend/type bucket) for future investigation. minimization; it is only reliably reproduced through the real fuzz/replay harness's deeper call stack. 3. **Merged-value Frame lookup miss** (`"Key $N does not exists in frame."`): - a `Frame` (`compiler/Frame.hpp`, shared by the cpp/bc/asmjit lowering - providers) lookup miss for an SSA value merged across two `Kind::If` arms, - reachable with a `Kind::If` nested inside a `Kind::Call` argument (e.g. - `if(p0 != 0, sum3(p1, if(...) ^ (...), p0), p2)`). This looks like the same - *class* of merged-value bookkeeping bug as the `zeroTripLoopMergeThenAddConstant` - BC/AsmJit fix above, just a control-flow shape neither `getResultRegister` - nor `bindResult` covers -- worth a maintainer's attention alongside it, but - root-causing and fixing the shared `Frame` lookup path is out of - scope for this Call-coverage expansion. + originally described (and tracked as nebulastream/nautilus#383) as a + `Frame` lookup miss in the cpp/bc/asmjit lowering providers for a + `Kind::If` nested inside a `Kind::Call` argument, e.g. + `if(p0 != 0, sum3(p1, if(...) ^ (...), p0), p2)`, and believed to be the + same *class* of merged-value bookkeeping bug as the + `zeroTripLoopMergeThenAddConstant` BC/AsmJit fix above (`getResultRegister` + / `bindResult`), just a shape neither of those covers. + A 2026-07-09 audit could not reproduce it (`nautilus-fuzz-replay --survey + 10000` came back clean) and marked it latent. + + **Update, same day, after a wider search (~2M generated programs across + bc/tbc/asmjit) turned up a live reproducer** + (`test/fuzz/regressions/finding3-loop-call-continue.bin`, replay with + `nautilus-fuzz-replay test/fuzz/regressions/finding3-loop-call-continue.bin`): + this is **not** a lowering-provider register/variable-binding bug at all -- + `getResultRegister`/`bindResult`/the cpp backend's `getVariable` are all + fine here, and every one of them throws identically for the *same* input, + including the interpreter-free cpp backend which has no register frame to + get out of sync (`Frame` is also instantiated, with a different `V`, + by `TraceToIRConversionPhase::IRConversionContext` -- the plain trace -> + IR conversion that runs once, before any backend sees the IR at all). + Minimized, the crash reduces to nothing resembling the original `Kind::If` + shape: + + ``` + loop(count=((0i8 + 0i8) + 0i8), init=0i8, body=sum3(0i8, cont(cond=0i8, value=0i8), 0i8)) + ``` + + i.e. a `Kind::Loop` whose trip count traces to (but is not a literal) zero, + whose body is a `Kind::Call` one of whose arguments is a + `Kind::LoopContinue` (`cont`) -- no `Kind::If` required. Root cause, traced + with instrumented builds of `SSACreationPhase.cpp`: the block reached when + the loop's `i < trips` guard is taken (the loop body) never gets visited by + `SSACreationPhaseContext::processBlock`'s worklist at all, even though it is + a live successor of a real `CMP` in the recorded trace. That worklist only + walks *backward from the recorded `Return` operation* via `Block::predecessors` + (by design -- it's how the value-propagation in `propagateValue`/ + `processValueRef` decides what needs threading as a block argument, and + blocks that don't reach any `Return` are legitimately dead code). Here, + with the trip count resolved to zero, the *only* recorded `Return` is + reached through the loop's exit edge (`i < trips` false), never through the + body -- meaning the tracer never recorded a back-edge from the body + (executed only via the tracer's own speculative/backtracking exploration of + "what if `i < trips`", needed even though this program's concrete trip + count is zero, because the compiled function must still be correct for + whatever trip count a *future* call computes) closing back to the loop's + own condition check. The body block is therefore real (a genuine `CMP` + successor with real operations, including the `Kind::LoopContinue`'s own + internal branch merge) but orphaned from the backward walk, so nothing + ever threads its free values ($128 in the original repro, $35 in the + minimized one -- the loop index / accumulator snapshot the `Call` + argument's `LoopContinue` merge needs) in as block arguments. + `TraceToIRConversionPhase` visits the orphaned block anyway (it walks every + `CMP`/`JMP` successor unconditionally, not just backward-from-return), finds + the value was never threaded, and `Frame::getValue` throws. + + This is a bug in the tracer's loop/back-edge recording for a + provably-zero-trip loop, one level upstream of `SSACreationPhase`'s own + backward walk (in the same speculative-exploration machinery already + implicated by findings 1 and 2 below -- `ExecutionTrace`/ + `TagRecorder`/the exception-based retracing that explores a branch the + concrete run didn't take), not a register-allocation or variable-naming + bug in any lowering provider. A fix belongs in that tracing layer, not in + `Frame`, `getResultRegister`, `bindResult`, or the cpp backend's + `getVariable` -- out of scope to attempt blind here given how fragile that + machinery already is (see findings 1-2), but the tolerance in + `ReplayMain.cpp` stays (this is confirmed live, not latent) and the pinned + reproducer above makes it directly actionable for whoever picks it up. Adding `val`/`val` as generated domains (see "Bool domain"/"Enum domain" above) immediately surfaced four more latent bugs, all fixed by this diff --git a/nautilus/test/fuzz/ReplayMain.cpp b/nautilus/test/fuzz/ReplayMain.cpp index 4c49124be..da7a25c66 100644 --- a/nautilus/test/fuzz/ReplayMain.cpp +++ b/nautilus/test/fuzz/ReplayMain.cpp @@ -96,12 +96,13 @@ std::vector randomBuffer() { // hardened (see SSACreationPhase.cpp) to throw this catchable exception // instead. The underlying "why is the Return missing" tracing defect is // still open. -// 3. "Key $N does not exists in frame." -- a Frame (compiler/Frame.hpp, -// shared by the cpp/bc/asmjit lowering providers) lookup miss for an -// SSA value merged across two Kind::If arms, reachable with nested -// Kind::If inside a Kind::Call argument -- the same *class* of -// merged-value bookkeeping bug as the already-fixed BC/AsmJit -// instances documented below, just a shape those fixes didn't cover. +// 3. "Key $N does not exists in frame." -- confirmed live (issue #383), +// root-caused to a tracer bug in loop back-edge recording for a +// provably-zero-trip Kind::Loop whose body contains a Kind::Call +// argument with a Kind::LoopContinue, one layer upstream of any +// lowering provider (see README.md "Known findings" #3 for the full +// writeup and test/fuzz/regressions/finding3-loop-call-continue.bin +// for a minimized, directly replayable reproducer). // 4. "std::get: wrong index for variant" -- an internal std::variant // accessed through the wrong alternative somewhere in the cpp backend's // lowering of a voidReturn-shaped kernel (issue #355/#363) whose AST diff --git a/nautilus/test/fuzz/regressions/finding3-loop-call-continue.bin b/nautilus/test/fuzz/regressions/finding3-loop-call-continue.bin new file mode 100644 index 0000000000000000000000000000000000000000..f719bc887bc08bf4f6774fe3f1a881e11e7e16a7 GIT binary patch literal 23 YcmZQz;OaQe@SOn$maVp8V321305qWk`v3p{ literal 0 HcmV?d00001