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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions