fix(miner): close the clone-lock TOCTOU on a still-empty lockfile (#9681) - #9915
Conversation
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-29 22:29:09 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
…ONbored#9681) acquireRepoCloneLock creates the lockfile EMPTY (open 'wx' -> 0-byte) and writes the owner record several statements later. isRepoCloneLockStale treated that intermediate state as immediately reclaimable: a concurrent process P2 that hit EEXIST while P1 was between openSync and writeSync read "", JSON.parse threw, P2 declared the lock stale, unlinked it, and its own open 'wx' then succeeded -- so P1's later write landed in an unlinked inode and both processes ran git fetch/checkout/reset on the same .git dir, the exact index/HEAD/refs corruption the lock exists to prevent. The per-acquire token doesn't help: it only stops release() from deleting a peer's lock, not double-holding. - Export DEFAULT_LOCK_INCOMPLETE_GRACE_MS = 5_000. - isRepoCloneLockStale's unparseable-JSON and non-object arms now consult the lockfile's own mtimeMs (via a new injectable statLock seam, defaulting to statSync, threaded from acquireRepoCloneLock like the existing openLock/ writeLock seams) and reclaim ONLY when nowMs - mtimeMs exceeds the grace window; within it the lock is treated as a live mid-acquire holder. A statSync that throws (the file vanished) returns stale. The same-host-live- pid and age-backstop branches are unchanged. - Docstring updated so an unparseable lock is no longer described as unconditionally stale. Tests: unparseable/non-object locks within grace are NOT stale but ARE past it; a throwing stat is stale; acquireRepoCloneLock POLLS a fresh empty peer lock instead of reclaiming it; and it reclaims an over-grace empty lock through the real default statSync path. The reclaim assertions fail against the current code. Closes JSONbored#9681 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9915 +/- ##
===========================================
- Coverage 91.86% 79.56% -12.30%
===========================================
Files 921 282 -639
Lines 113256 58715 -54541
Branches 27300 8683 -18617
===========================================
- Hits 104039 46719 -57320
- Misses 7929 11706 +3777
+ Partials 1288 290 -998
Flags with carried forward coverage won't be shown. Click here to find out more.
|
What
acquireRepoCloneLockcreates the lockfile empty (openSync(.., 'wx')→ 0-byte) and writes the owner record several statements later.isRepoCloneLockStaletreated that intermediate state as immediately reclaimable — itscatch(unparseable JSON) and!metaarms returnedtrueunconditionally. So a genuinely concurrent process P2 that hitsEEXISTwhile P1 is betweenopenSyncandwriteSyncreads"",JSON.parsethrows, P2 declares the lock stale,unlinkSyncs it, and its ownopen 'wx'now succeeds. P1's laterwriteSynclands in an already-unlinked inode, and both processes rungit fetch/checkout/reset --hardon the same.gitdir — precisely the index/HEAD/refs corruption the lock exists to prevent. The per-acquiretokendoesn't help: it only stopsrelease()from deleting a peer's lock, not double-holding.How
DEFAULT_LOCK_INCOMPLETE_GRACE_MS = 5_000.isRepoCloneLockStale's unparseable-JSON and non-object arms now consult the lockfile's ownmtimeMs(via a new injectablestatLockseam defaulting tostatSync, threaded fromacquireRepoCloneLockexactly like the existingopenLock/writeLockseams) and reclaim only whennowMs - mtimeMsexceeds the grace window; within it the lock is treated as a live mid-acquire holder. AstatSyncthat throws (the file vanished) returns stale.Tests
acquireRepoCloneLockpolls a fresh empty peer lock (within grace) instead of reclaiming it (the double-hold path is closed).statSyncpath.The reclaim/poll assertions fail against the current code. All existing
isRepoCloneLockStale/acquireRepoCloneLocktests still pass.Closes #9681