Skip to content

Cached edge count leaks from an existing file into the next new file #906

Description

@MicroMilo

Summary

FileSyncIdStrategy stores cachedIdCount when it encounters a known file, but the cached-file commit branch never clears cachedIdCount, firstEdgeId, or the release callback. A later new file is therefore treated as cached, checked against the previous file's count, and its newly acquired lock is not released or its allocation recorded.

Impact: After known.js with one cached edge, instrumenting new.js with two edges throws "new.js has 2 edges, but 1 edges reserved in ID sync file". new.js is not appended and only the known-file lock release occurs. Equal counts would avoid the exception but still leave the new-file lock held and unrecorded.

Code path

Checked against current main at commit 4c14e1bf63d82475ea057d6208e6f2eac87fa680.

  • packages/instrumentor/edgeIdStrategy.ts:79-91
  • packages/instrumentor/edgeIdStrategy.ts:150-157
  • packages/instrumentor/edgeIdStrategy.ts:185-217
  • packages/instrumentor/instrument.ts:139-161

Steps to reproduce

Validation level: current HEAD supplemental dynamic witness reproduced.

NODE_NO_WARNINGS=1 node --input-type=module -e 'import fsNative from "node:fs"; import vm from "node:vm"; import { stripTypeScriptTypes } from "node:module"; const source=fsNative.readFileSync("packages/instrumentor/edgeIdStrategy.ts","utf8"); const start=source.indexOf("export abstract class IncrementingEdgeIdStrategy"); const end=source.indexOf("export class ZeroEdgeIdStrategy"); const snippet=stripTypeScriptTypes(source.slice(start,end).replaceAll("export ",""),{mode:"transform"})+"\nglobalThis.FileSyncIdStrategy=FileSyncIdStrategy;"; let contents="known.js,0,1\n"; let releases=0; const appends=[]; const context={fuzzer:{coverageTracker:{enlargeCountersBufferIfNeeded(){}}},fs:{readFileSync(){return contents},appendFileSync(_file,line){contents+=line;appends.push(line.trim())}},os:{EOL:"\n"},lock:{checkSync(){return false},lockSync(){return ()=>{releases++}},unlockSync(){}},process:{exit(code){throw new Error(`exit ${code}`)}},console,Atomics,Int32Array,SharedArrayBuffer,Math,Error}; vm.runInNewContext(snippet,context); const strategy=new context.FileSyncIdStrategy("ids"); strategy.startForSourceFile("known.js"); strategy.nextEdgeId(); strategy.commitIdCount("known.js"); strategy.startForSourceFile("new.js"); strategy.nextEdgeId(); strategy.nextEdgeId(); let error=null; try { strategy.commitIdCount("new.js"); } catch (caught) { error=caught.message; } console.log(JSON.stringify({error,appends,releases}));'

Observed output:

{"error":"new.js has 2 edges, but 1 edges reserved in ID sync file","appends":[],"releases":1}

Neighboring control:

NODE_NO_WARNINGS=1 node --input-type=module -e 'import fsNative from "node:fs"; import vm from "node:vm"; import { stripTypeScriptTypes } from "node:module"; const source=fsNative.readFileSync("packages/instrumentor/edgeIdStrategy.ts","utf8"); const start=source.indexOf("export abstract class IncrementingEdgeIdStrategy"); const end=source.indexOf("export class ZeroEdgeIdStrategy"); const snippet=stripTypeScriptTypes(source.slice(start,end).replaceAll("export ",""),{mode:"transform"})+"\nglobalThis.FileSyncIdStrategy=FileSyncIdStrategy;"; let contents=""; let releases=0; const appends=[]; const context={fuzzer:{coverageTracker:{enlargeCountersBufferIfNeeded(){}}},fs:{readFileSync(){return contents},appendFileSync(_file,line){contents+=line;appends.push(line.trim())}},os:{EOL:"\n"},lock:{checkSync(){return false},lockSync(){return ()=>{releases++}},unlockSync(){}},process:{exit(code){throw new Error(`exit ${code}`)}},console,Atomics,Int32Array,SharedArrayBuffer,Math,Error}; vm.runInNewContext(snippet,context); const strategy=new context.FileSyncIdStrategy("ids"); strategy.startForSourceFile("new.js"); strategy.nextEdgeId(); strategy.nextEdgeId(); let error=null; try { strategy.commitIdCount("new.js"); } catch (caught) { error=caught.message; } console.log(JSON.stringify({error,appends,releases}));'

Control output:

{"error":null,"appends":["new.js,0,2"],"releases":1}

Expected behavior

The documented/source-grounded contract should hold without the drift described above.

Actual behavior

Cached edge-ID state leaks into the next unseen file, causing false count checks, omitted records, and a leaked synchronization lock.

Existing coverage

I checked the current issue and PR lists for overlapping titles/root-cause keywords before filing this. I did not find an item covering this same root cause.

Suggested fix

Clear all per-file cached state after the cached-file commit path in FileSyncIdStrategy so the next transaction is classified independently.

Suggested tests

  • Add a regression test for the reproduction above.
  • Add a neighboring control assertion so the intended non-bug path remains covered.

Submitted with Codex.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions