From 7e78026e1afbb704d5b50959ed32f45ab990989d Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 15 Jun 2026 16:09:58 +0000 Subject: [PATCH] fix(cfg): gate out-of-range binding indices to the dense fallback (#2201 review) Tri-review (adversarial lane, reproduced) found the SSA path less tolerant than the dense oracle it replaced: an out-of-range binding index in defs/uses/mayDefs (a corrupted/stale durable store) crashed the nBindings-sized arrays (defBlocks[v]/stacks[u]), where dense tolerated it as a Map key. The throw escaped the unguarded taint/harvest call sites and lost a whole file's taint layer. Add a malformed-input gate that falls back to the dense solver (which handles any index), preserving byte-identity AND the graceful per-function degradation. Add an OOB canonical CFG to the differential fuzz + a production- entry no-throw unit test (the generator only ever emitted in-range indices, so this divergent input was structurally invisible). --- .../src/core/ingestion/cfg/reaching-defs.ts | 22 +++++++++++++++- .../cfg/reaching-defs-equivalence.test.ts | 19 ++++++++++++++ gitnexus/test/unit/cfg/reaching-defs.test.ts | 25 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/ingestion/cfg/reaching-defs.ts b/gitnexus/src/core/ingestion/cfg/reaching-defs.ts index 7cbba52ce..cb60db228 100644 --- a/gitnexus/src/core/ingestion/cfg/reaching-defs.ts +++ b/gitnexus/src/core/ingestion/cfg/reaching-defs.ts @@ -526,8 +526,28 @@ function computeInSetsSparse( const { preds, succs, throwSuccs } = adj; const entry = cfg.entryIndex; - // Gate to the dense oracle for the two shapes the SSA path does not model. + // Gate to the dense oracle for the shapes the SSA path does not model. for (const list of throwSuccs) if (list.length) return computeInSetsDense(cfg, n, h, adj, limits); + // Malformed-input guard: an out-of-range binding index (negative or + // ≥ nBindings — a corrupted/stale durable parsedfile store) would crash the + // SSA path's nBindings-sized arrays (defBlocks[v]/stacks[u]). The dense solver + // tolerates any index (its lattice is a Map), so fall back — keeping the two + // byte-identical AND preserving the graceful per-function degradation the + // dense path gave (a throw here would escape the unguarded taint/harvest call + // sites and lose the whole file's taint layer). See hasEmitSafeFacts (emit.ts). + for (const b of cfg.blocks) { + const stmts = b.statements; + if (!stmts) continue; + for (const s of stmts) { + for (const d of s.defs) + if (d < 0 || d >= nBindings) return computeInSetsDense(cfg, n, h, adj, limits); + for (const u of s.uses) + if (u < 0 || u >= nBindings) return computeInSetsDense(cfg, n, h, adj, limits); + if (s.mayDefs) + for (const d of s.mayDefs) + if (d < 0 || d >= nBindings) return computeInSetsDense(cfg, n, h, adj, limits); + } + } const reachable = new Array(n).fill(false); { const q = [entry]; diff --git a/gitnexus/test/unit/cfg/reaching-defs-equivalence.test.ts b/gitnexus/test/unit/cfg/reaching-defs-equivalence.test.ts index 25c50a59f..e56077cba 100644 --- a/gitnexus/test/unit/cfg/reaching-defs-equivalence.test.ts +++ b/gitnexus/test/unit/cfg/reaching-defs-equivalence.test.ts @@ -317,6 +317,25 @@ function canonicalHardCfgs(): FunctionCfg[] { ), ); + // (7) Malformed input: an OUT-OF-RANGE binding index (≥ nBindings, e.g. from a + // corrupted/stale durable store) in a looping CFG. The dense solver tolerates + // it (its lattice is a Map keyed by index); the SSA path must fall back to + // dense rather than crash its nBindings-sized arrays. Asserting byte-identity + // here pins that gate — without it, the SSA path throws and the differential + // comparison can never reach this divergent input (the generator only ever + // emits in-range indices). + out.push( + mk( + [blk(0, [st(1, [0], [])]), blk(1, [st(2, [3], [3])]), blk(2, [st(3, [], [0])])], + [ + { from: 0, to: 1, kind: 'seq' }, + { from: 1, to: 1, kind: 'loop-back' }, + { from: 1, to: 2, kind: 'cond-false' }, + ], + [bind('x', 1)], // nBindings = 1, so binding index 3 in block 1 is out of range + ), + ); + return out; } diff --git a/gitnexus/test/unit/cfg/reaching-defs.test.ts b/gitnexus/test/unit/cfg/reaching-defs.test.ts index 994fe301e..fb29eed2f 100644 --- a/gitnexus/test/unit/cfg/reaching-defs.test.ts +++ b/gitnexus/test/unit/cfg/reaching-defs.test.ts @@ -400,6 +400,31 @@ describe('computeReachingDefs — determinism and convergence', () => { expect(sparse.status).toBe('computed'); // SSA ignores the ceiling — it never fires expect(render(sparse.facts)).toEqual(render(denseFull.facts)); // and the facts match }); + + it('#2201: an out-of-range binding index in a ≥16-block loop does NOT crash the SSA path', () => { + // A corrupted/stale store can carry a binding index ≥ nBindings. The dense + // solver tolerates it (Map-keyed lattice); the SSA path's nBindings-sized + // arrays would throw. The production dispatcher routes ≥16-block looping + // functions to SSA, so without the malformed-input gate the throw would + // escape the (unguarded) taint/harvest callers and lose a whole file's taint + // layer. The gate falls back to dense — no throw, byte-identical to dense. + const blocks: BlockSpec[] = [{ stmts: [stmt(1, [0], [])] }]; + const edges: [number, number][] = []; + for (let i = 1; i <= 18; i++) { + blocks.push({ stmts: [stmt(i + 1, i === 1 ? [5] : [0], [i === 1 ? 5 : 0])] }); // block 1 uses/defs OOB index 5 + edges.push([i - 1, i]); + } + edges.push([18, 1]); // back-edge → loop; 19 blocks total, ≥16 → SSA dispatch + const cfg = mkCfg(blocks, edges, ['x']); // nBindings = 1; index 5 is out of range + expect(cfg.blocks.length).toBeGreaterThanOrEqual(16); + let prod: ReturnType | undefined; + expect(() => { + prod = computeReachingDefs(cfg); // must NOT throw (gate → dense fallback) + }).not.toThrow(); + const dense = computeReachingDefsDense(cfg); + expect(prod!.status).toBe(dense.status); + expect(render(prod!.facts)).toEqual(render(dense.facts)); // byte-identical to the tolerant dense path + }); }); describe('computeReachingDefs — parser-direct acceptance (with U1/U2)', () => {