Skip to content

Advisory file lock keys on the resolved path string, so a link name and its referent take different locks (follow-up to #1405, head 94dc7c40cf) #2375

Open

Description

@easonLiangWorldedtech

The advisory file lock keys on the resolved path string, so a link name and its referent are different locks

Filed from work on #1405 (branch feat/guarded-write-s4a, head 94dc7c4), where review thread PRRT_...607461 on src/utils/safeWriteJson.ts:225 asks whether a write through a link and a write to the referent serialise with each other. The thread stays open because the answer is a contract decision rather than a unit commit; this issue exists so the gap is owned somewhere other than a resolved thread.

Measured at that head

  • src/utils/fileLock.ts is 100 lines. acquireFileLock(filePath) at :29 computes path.resolve(filePath) at :30 and hands that string to lockfile.lock(absoluteFilePath, ...) at :33, inside an options object that sets realpath: false at :36 with the comment "the file may not exist yet, which is acceptable".
  • resolveLinkPathLockKey occurs zero times anywhere under src or packages at this head (git grep exit 1). There is no normalisation layer between a caller's path spelling and the lock key.
  • So a lock taken on a link name and a lock taken on the referent are two different keys, and the two writes do not serialise with each other. path.resolve removes . and .. and makes the path absolute; it does not follow links.
  • #2350, the tracking issue for the staging-name guard, does not carry this question either: its body is 4,933 characters and contains no occurrence of "lock". Lock keys appear there only in comment 6719060399, the four-generation map, which credits one generation with exporting resolveLinkPathLockKey and another with not exporting it.

What #1405 already pins

,

The unit under review tests the behaviour from three directions, which is why the mechanism is not in dispute — the missing decision is which identity the lock should use:

  • src/utils/__tests__/safeWriteJson.test.ts:960 — a mocked referent,
  • :1111 — a real link, created as a junction on Windows and a symlink elsewhere, with the platform's refusal recorded rather than assumed,
  • :1612 — the mode where link resolution is refused (refuseSymlinkTarget), so no resolution happens at all.

What the owner has to decide

  • Whether the lock identity is the link name, the resolved referent, or the pair. Canonicalising to the referent makes link-mediated and direct writes exclude each other, which is what callers of acquireFileLock say they want in the doc comment at :23-:27 — "serialize with every safeWriteJson write to the same path" — but it requires resolving a path that may not exist yet, which is exactly why realpath: false is there today.
  • If the referent is chosen, how to reach it for a not-yet-existing final component: resolve the nearest existing ancestor and rejoin, which is the shape the canonicalisation fix in src/services/file-safety/safeWriteText.ts already uses for the staging directory, so the repo would then have one idiom rather than two.
  • Whether anything outside this repository depends on today's behaviour, since a lock-key change alters which concurrent processes collide.

No code change is proposed here and nothing is asserted about which identity is correct. Suppression counts are unaffected by this issue.

Activity

  1. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Precision defect found in the same key while working #2368, recorded here because this issue owns what a lock identity is made of: statIdentity built the key from fs.statSync(target), which reports dev and ino as JS numbers, and above 2^53 an inode stops being a distinct number — Number(17451448568457636n) === Number(17451448568457637n) is true (both are 17451448568457636), so two directories one inode apart could be handed the same key and share a lock neither of them asked for. Fixed in #2368 commit 20343f818a7af671c285e77818c7c7c27ff9bd2d: the identity is read with fs.statSync(target, { bigint: true }), with a regression row whose statSync double answers plain stats with rounded numbers and BigInts only when asked, so the row is red before the fix rather than green twice.

    That is the numeric level of this issue's question. The naming level — whether an 8.3 alias of a file name may take a different lock because the artefact is spelled from the caller's path (fileLock.ts lockArtefactPath) — is untouched by that commit and stays open for a maintainer in #2368 review thread 4507583833.

  2. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Related but not a duplicate: #2356 is the same defect class by a different mechanism - there an NTFS 8.3 alias and its long form take different locks, here a link name and its referent. The fix is the same (identity-backed lock key, carried by the split unit planned on #2350), so the two are cross-linked rather than merged.

    Link correction, measured now: the token "thread PRRT_...607461" in this issue's title and body is an abbreviation of a review-thread node id and resolves to nothing as written. All 196 review comments on #1405 were read over two pages and not one names resolveLockKey, so the finding cannot be recovered from that pull request's comment listing by content either. The claim stands on the anchors this body already carries - branch head 94dc7c4 and the file and line cited in it - and the title now cites those instead of the unresolvable token.

  3. changed the title [-]Advisory file lock keys on the resolved path string, so a link name and its referent take different locks (follow-up to #1405 thread 607461)[/-] [+]Advisory file lock keys on the resolved path string, so a link name and its referent take different locks (follow-up to #1405, head 94dc7c40cf)[/+] on Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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