Conversation
Under contention, acquireLock can hand one lock to two live processes (Tencent#760). exclusiveCreate wrote the payload directly with the wx flag, so the lock file existed empty/partial between the open and the write. A concurrent staleness check reading that window treats the live lock as unparseable (reclaimable), and the reclaimer's rename-into-place then overwrites the live holder — both sides believe they hold the lock. Publish creation with a unique sibling temp file + hard-link instead: link(2) is atomic and fails with EEXIST when the target already exists, so every on-disk observation of the lock is a complete payload. Stale detection, the reclaim sentinel, and owner-verified release are unchanged. Adds a real-fs regression test that concurrent create/read can never observe a partially written lock, and realigns the fs-extra-mocked update tests with the temp-write + link mechanism. Fixes Tencent#760
|
Findings
No earlier findings were provided to mark as resolved. |
|
Findings
Resolved Earlier Findings
|
|
Findings
Resolved Earlier Findings
|
|
Findings
Resolved Earlier Findings
|
Collaborator
|
Thanks for this. The temp-write + Same harness as #760, 16 processes × 2000 attempts, 3 runs:
#761 also treats an |
Contributor
Author
|
Findings
Resolved Earlier Findings
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes #760 — under contention,
acquireLockcan hand one lock to two live processes.Root cause
exclusiveCreatewrote the payload directly withwriteFile(..., { flag: 'wx' }). The lock file exists empty/partial between the open and the write. A concurrentisLockStalereading that window treats the live lock as unparseable (i.e. reclaimable); the reclaimer then renames over the live holder, and both sides believe they hold the lock. The issue's 16×2000 stress showed ~1% of acquisitions overlapping another holder.Fix
Publish creation with a unique sibling temp file + hard-link:
link(2)is atomic and fails withEEXISTwhen the target already exists, so every on-disk observation of the lock is a complete, parseable payload. Stale detection, the reclaim sentinel, and owner-verified release are unchanged — this only changes how a new lock file comes into existence.Verification
Regression test (red → green): new
lock-atomic.test.tscase — concurrent create/read must never observe a partially written lock.SyntaxError: Unexpected end of JSON input(the partial write is observed).Real-filesystem A/B stress (multi-process): 8 child processes × 30 acquire/release rounds on one lock path, with a detector in the reclaim branch that flags any rename whose target was created during the reclaim (another live creator's lock being overwritten). To make the microsecond-scale window observable, the unfixed build's publish window was artificially widened (the holder's create sleeps 50 ms before writing, mirroring the issue's interleaving):
Unit & contract:
update.test.ts/lock-atomic.test.ts/update-policy.test.ts80/80 pass — the fs-extra mock gainedlink, and the assertions that pinned the oldwx-write implementation were realigned to the temp-write + link mechanism (behavior contract unchanged).Full suite:
npx vitest run— 254 files / 4,202 tests passed; 37 files / 94 tests fail, the identical failure set as unmodifiedmainon this Windows machine (pre-existing symlink/permission/environment failures, none related to the lock path).Build & types:
npm run build(tsup) andnpx tsc --noEmitclean;git diff --checkclean.Note: the change touches only
exclusiveCreate; CLI user-facing output is untouched, so no docs/skill-data wording is affected.Follow-up validation (commit 2c3502b)
Addressed the review findings:
fs-extra.linktype mock.wxexclusive create when hard links are unavailable, so updates do not fail outright on FAT/exFAT or network filesystems.Additional verification on the updated head:
npx tsc --noEmitnpx vitest run src/__tests__/update.test.ts src/__tests__/lock-atomic.test.ts— 69 passednpm run buildnode dist/index.js --helpnode dist/index.js update --helpgit diff --checkThe focused ESLint command was not available in this checkout because the repository does not provide an ESLint config/package;
npxresolved ESLint 10, which reported noeslint.config.*. No lint result is claimed.Follow-up validation (commit ad65c88)
Addressed the remaining race concern in the unsupported-filesystem fallback:
wxwrite cannot be reclaimed while its creator may still be writing.link(tmp, target)returnEEXIST, matching the actual target-lock race rather than an implausible temporary-name collision.The updated head again passes
npx tsc --noEmit, the focused 69-test suite,npm run build, real CLI help smoke checks, andgit diff --check.Follow-up validation (commit d5a19f6)
Reworked the unsupported-filesystem path to remove the remaining partial-write race:
.ownerpayload file; no lock target file is ever partially written.The updated head passes
npx tsc --noEmit, the focused 69-test suite,npm run build, real CLI help smoke checks, andgit diff --check.Follow-up validation (commit 32b9032)
Closed the remaining publication-window concern:
.ownerpayload is written to a sibling temp file and atomically renamed into place.wxrace.The updated head passes
npx tsc --noEmit, the focused 69-test suite,npm run build, real CLI help smoke checks, andgit diff --check.