Skip to content

fix(store): on Windows the path comparison reads a path's volume through a handle — on Node 22.16.x (libuv 1.49.2) a path through a share of the workspace compared as outside it - #165

Merged
MoebiusX merged 3 commits into
developfrom
codex/paths-volume-through-handle
Oct 15, 2026
Merged

MoebiusX merged 3 commits into
developfrom
codex/paths-volume-through-handle

Conversation

@MoebiusX

Copy link
Copy Markdown
Owner

Problem

libuv 1.49.0–1.50.x (Node 22.16.x ships 1.49.2) declares FILE_STAT_BASIC_INFORMATION with FileId128 before VolumeSerialNumber, which is the wrong order. libuv 1.51.0 fixed it ("win: fix order of FILE_STAT_BASIC_INFORMATION struct fields"). So a stat by name reports st_dev = the upper 64 bits of the 128-bit file id, which is 0 on NTFS. libuv stats by name (GetFileInformationByName, Windows 11 24H2 / Server 2025) for any local path whose last name is not a reparse point. A path through a share, or one ending in a junction or symlink, is opened instead and reports the real 32-bit serial.

server/store/paths.mjs (from #161) read both the file ids used for placement and the volume used for the "through a share on beside's volume" rule with statSync. On 1.49.2 a local path and a share of it then had no file id in common, and a local volume never matched a share's. The share-volume rule could fail open.

Reproduced on Linux through the injected win32 host (before the fix)

libuv 1.51.0: dev of C:\ws by name = 0x5eed000000000043, of \\myhost\ws (opened) = 0x43
  export \\myhost\exports\new beside C:\ws     -> unplaced (refused)
  export \\myhost\ws\exports\new beside C:\ws  -> inside (refused)
  purge  \\myhost\orgs\users.json beside C:\ws\orgs\beta -> "c:\\ws\\orgs\\users.json"
libuv 1.49.2: dev of C:\ws by name = 0x0, of \\myhost\ws (opened) = 0x43
  export \\myhost\exports\new beside C:\ws     -> outside (WRITTEN)
  export \\myhost\ws\exports\new beside C:\ws  -> outside (WRITTEN)
  purge  \\myhost\orgs\users.json beside C:\ws\orgs\beta -> null

On 1.49.2:

  • A directory export to \\myhost\ws\exports\new was written into the workspace.
  • The in-place detection and backup's own-file check missed the workspace and the database's -wal through a share.
  • purge-org refused a users file that a share places outside the root (a false refusal; deleting still failed closed).

Fix

On Windows, comparedPath reads a file id and a volume through a handle: openSync(p, 'r') + fstatSync(fd, { bigint: true }) + closeSync, with the handle closed in finally. This is the same approach #163 (fdb1603) uses in users-file-place.mjs. libuv's fs__open sets FILE_FLAG_BACKUP_SEMANTICS, so directories open, and fs__stat_handle reports the serial on every libuv. A path that doesn't open is passed over, the same way a path that didn't stat was. comparedPath takes openSync, fstatSync and closeSync in place of statSync.

Fixture

server/fixtures/ntfs.mjs gets the same model as #163's mount-fs.mjs, which isn't on develop yet:

  • fastDev is the device a stat by name reports. null (the default) gives all 64 bits of the serial, as 1.51.0 reads it; 0n gives what 1.49.2 reports.
  • A path through a share, or one whose last name is a link or another reparse point, is opened and reports the serial's low 32 bits.
  • The fixture returns openSync, fstatSync, closeSync and opened().
  • The header no longer says a local stat may report all 64 bits on every libuv.

Tests

  • New test in server/test-store-paths.mjs that loops libuv 1.51.0 and 1.49.2. It covers the export through \\myhost\ws and into a workspace named \\myhost\ws, the in-place detection, backup's -wal, purge-org placing under and beside the root, the share-volume rule, another volume, and every handle closed.
  • At the parent commit the new test fails on 1.49.2: a directory export through \\myhost\ws into C:\ws is refused, expected 'inside', got null.
  • Every comparison the suite builds now injects the fixture's handles (hostOf, handlesOf; flipping routes a handle to the volume it was opened on).
  • Tests: 1495 → 1496.

Gates (local, Windows 11, Node 22.16.0)

  • npm run lint: 0 errors. The 153 warnings are unchanged, with 0 in the changed files (develop's versions also had 0).
  • test:golden and test:golden:compile ✓; vendor-manifest:check ✓ (no tools/lib touched); node tools/sync-spec.mjs --check ✓; tools/test-doc-test-totals.mjs ✓.
  • server/test-store-paths.mjs 19/19; tools/test-journey-scope.mjs ✓.
  • Full npm test locally has 5 failures (test-auth-local, test-authz, test-brand-shell, test-smoke, test-studio-bundle). They fail the same way with develop's versions of the changed files on this machine, so they're local-environment failures that already exist. CI is the gate for Node 22 (22.22) and the Windows legs.

Not in this PR

crawlScopeRefusal in tools/lib/journey.mjs (paths.mjs's "twin") still places by statSync's dev & 0xffffffff. On 1.49.2 it fails closed: a local path beside a workspace named through a share is refused, not let through. This is a follow-up because it touches tools/lib, so it needs the vendor-manifest + CHANGELOG export rule.

Merge order with #163: both claim 1495 → 1496, so whichever lands second re-chains its Tests pair.

🤖 Generated with Claude Code

MoebiusX and others added 3 commits October 14, 2026 23:39
…ugh a handle — on Node 22.16.x (libuv 1.49.2) a path through a share of the workspace compared as outside it

server/store/paths.mjs placed a path reached through a share by a file id
it shares with a directory of the path it is checked against, and judged
one nothing places by its volume, both read with statSync. libuv answers a
stat by the path's name (GetFileInformationByName, Windows 11 24H2 and
Server 2025) unless the path is reached through a share or its last name
is a reparse point, which it opens (FILE_FS_VOLUME_INFORMATION: the
serial). libuv 1.49.0 to 1.50.x declared FILE_STAT_BASIC_INFORMATION with
FileId128 before VolumeSerialNumber (fixed in 1.51.0), so a stat by name
reported the upper half of the 128-bit file id, 0 on NTFS. A local path
and its share then had no file id in common and their volumes never
matched: a directory export to \\myhost\ws\exports\new with the workspace
at C:\ws was written into the workspace.

On Windows a file id and a volume are now read through a handle (openSync,
fstatSync, closeSync), which reports the serial on every libuv, as #163
reads a directory's volume in users-file-place.mjs. Every handle is closed
before the comparison goes on.

server/fixtures/ntfs.mjs models both libuvs (fastDev: 0n for 1.49.2, null
for 1.51.0's 64-bit serial by name; a path through a share or ending in a
link or reparse point is opened) and returns openSync, fstatSync,
closeSync and opened(). Its header no longer says a local stat may report
all 64 bits on every libuv. The new test in server/test-store-paths.mjs
loops both models. At the parent commit it fails on 1.49.2 (the export
through \\myhost\ws was written). Tests: 1495 -> 1496.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chain this follow-up's Tests pair from develop's 1519 — the develop merge cd24602 dropped #163's entry (17 lines, now byte-for-byte develop's again, below this entry) and left this entry's pair at 1495 → 1496, which test-doc-test-totals matched against #163's journey pair from 1495; 1519 → 1520 is the measured npm test total

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhPGnqVJ24goSVtbKpdkLJ
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants