Skip to content

nvx-rs: compare lock files by their 128-bit file IDs on Windows - #984

Closed
Pedro Henrique Penna (ppenna) wants to merge 3 commits into
devfrom
agents/issue-923-refs-lock-file-ids
Closed

Pedro Henrique Penna (ppenna) wants to merge 3 commits into
devfrom
agents/issue-923-refs-lock-file-ids

Conversation

@ppenna

Copy link
Copy Markdown
Contributor

Fixes #923

On Windows, the checks that tell whether two handles or paths reach the same lock file compared the volume serial number and the 64-bit file index of GetFileInformationByHandle, which is not guaranteed unique on ReFS. They now compare the identity that the file's FileSeal holds, as the store's directory identities already do: the volume serial number and the 128-bit file ID of FILE_ID_INFO, or those of BY_HANDLE_FILE_INFORMATION on a volume without file ID information, and the device and inode on Unix.

  • The lock checks (first commit). The sandbox store's and the template store's take_lock, and the release that removes a holder's lock file, identify the held lock file with store::regular::identity_of, and the file that the path names with the new store::regular::identity_at, which follows links and on Windows opens the file without access rights, as platform::file_identity does. A lock file whose deletion is pending still refuses that open, and counts as replaced.
  • The unbinding's check (second commit). platform::Directory::entry_identity, whose only caller is LockedStore::unbind, returns the entry's FileSeal identity, which the removal compares with the held lock file's.
  • Tests (third commit). lock_files_are_identified_as_their_seals_identify_them checks that the lock's handle, the path, and the directory's entry identify a lock file as its FileSeal does, and tell two files of one directory apart. It also fails on NTFS if one of them returns to the 64-bit identity, whose 32-bit volume serial number differs from FILE_ID_INFO's 64-bit one. Two existing tests now compare the new identities.

The checks compare two files at one moment, so no record and no public signature changes. As the issue scopes it, FileIdentity, which the launch journal records for its claimed log, and the public platform::file_identity keep their 64-bit indexes.

Both sides of each comparison must use the same scheme: with a 128-bit identity on one side and a 64-bit one on the other, every lock file looks replaced, and a lock without a deadline retries forever, which a deliberate mismatch reproduced locally.

How ReFS forms the 64-bit index. A probe on a ReFS 3.14 volume (Windows Server 2025) showed that the 128-bit ID holds the file's directory ID in its high half and the file's ID within that directory in its low half. ReFS packs them into the 64-bit index as the number of hexadecimal digits of the directory ID, then the file's ID, then the directory ID: 0000000000001a92_0000000000000002 becomes 4000000000021a92. Among 10,000 files, one in each of 5,000 directories and 5,000 in one more, no two indexes collided. When the packing does not fit, MS-FSCC §2.1.9 requires 0xFFFFFFFFFFFFFFFF for every file that has no unique 64-bit ID, and those files then collide.

Validation

  • Windows 11, NTFS (local), Rust 1.95.0: cargo fmt --all --check; Clippy with -D warnings for --all-targets --all-features, --lib, and --lib --no-default-features, both natively and with --target x86_64-unknown-linux-gnu; cargo doc --no-deps --all-features with -D warnings; cargo test --all-features, cargo test, and cargo test --lib --no-default-features; cargo +1.89 check --all-targets --all-features; and cargo check --all-targets --all-features --target aarch64-apple-darwin. In tests/runtime-adapter: format, Clippy for the protocol-only and host-only configurations and for the guest-only configuration with --target x86_64-unknown-linux-musl, the host-only tests, and the 1.89 check.
  • Each commit builds, passes Clippy with --all-targets --all-features, and passes the host::store and host::template::store tests on Windows (git rebase -x) and on Linux.
  • ReFS (nvx-whp-test, Windows Server 2025, a temporary ReFS 3.14 VHDX): cargo test --all-features --lib --tests with TMP and TEMP on the ReFS volume, at the base and at the head: 981 and 982 library tests passed, and every integration test too. A run with TMP on a missing drive confirmed that the test directories follow TMP. The head also passes on NTFS on that host.
  • Linux (nvx-kvm-test, Azure Linux 3, kernel 6.6): cargo test (all 829 library tests), cargo test --lib --no-default-features, cargo doc, the runtime adapter's protocol-only and host-only tests, and its musl guest build. In cargo test --all-features, all host:: tests pass. Three guest:: tests fail identically at the base commit on that host, as an unprivileged user on its kernel: guest::entry::tests::runtime_binds_need_a_mount_root_unless_there_are_none with EOPNOTSUPP, and two guest::exec tests that expect ESRCH and get EINVAL.
  • Left to CI: the bundled-release fixture, the guest binaries' musl lint, tests, and build, the MSRV check on Linux, and the hypervisor jobs.

Once it holds a lock, the sandbox store keeps it only if the lock
file's path still names the locked file, as the template store does,
and a holder that leaves a directory without its caller's configuration
record removes the lock file only if the path names the held one. These
checks compared the identities of host::platform: on Windows, the
volume serial number and the 64-bit file index of
GetFileInformationByHandle, which ReFS does not keep unique. ReFS
identifies a file by the 128 bits of FILE_ID_INFO, the ID of its
directory and its ID within that directory, which it packs into the
64-bit index only while they fit; MS-FSCC gives every file without a
unique 64-bit ID the same index, 0xFFFFFFFFFFFFFFFF. Two lock files of
one volume could then pass for one file, so that a transition could
keep the lock of a replaced lock file while another locked its
replacement, and a holder could remove a lock file that another
transition had created.

The checks now compare the identity that a file's FileSeal holds, as
the store's directory identities already do: the device and inode on
Unix, and on Windows the volume serial number and the 128-bit file ID
of FILE_ID_INFO, or those of BY_HANDLE_FILE_INFORMATION on a volume
that has no file ID information. store::regular::identity_of
identifies the held lock file, and the new identity_at the file that
the path names, following links. On Windows, identity_at opens the file
without access rights, as platform::file_identity does, so that the
open succeeds while another process holds the file without sharing it,
and fails for a lock file whose deletion is pending, which the lock
then takes for a replaced one. The checks compare two files at one
moment, so neither a record nor a public signature changes.

The test of a lock file replaced while a waiter waits compares these
identities.

Part of #923.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A removal unbinds a clone's sandbox through one handle of the directory
that the sandbox's path names, and refuses unless that directory's lock
file is the one whose lock the removal holds, so that a link in the
path that changes once the lock is taken cannot redirect the unbinding
to another sandbox, whose clone may still need its binding. The check
compared the identities of host::platform, which on Windows were the
volume serial number and the 64-bit file index that ReFS does not keep
unique, so a collision between the lock files of two sandboxes of one
ReFS volume reopened that race.

platform::Directory::entry_identity now returns the identity that the
entry's FileSeal holds: the device and inode on Linux, and on Windows
the volume serial number and the 128-bit file ID of FILE_ID_INFO, or
those of BY_HANDLE_FILE_INFORMATION on a volume that has no file ID
information. The removal compares it with the identity of the held lock
file that store::regular::identity_of gives. The entry is still opened
through the directory's handle, without following a link or other
reparse point, and the checks for the launch journal and the runtime
record still only look for their entries. Directory::entry_identity
has no other caller, so no record or public signature changes.

The test that a removal makes the binding's absence durable compares
the lock file of the directory that it syncs by identity_at.

Part of #923.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The lock checks compare a lock file's identity through the lock's
handle, which store::regular::identity_of reads, through its path,
which identity_at reads, and through its directory's entry, which
platform::Directory::entry_identity reads. A new test checks that the
three agree with the identity that the file's FileSeal holds, and that
they tell two files of one directory apart: on Windows by the volume
serial number and the 128-bit file ID of FILE_ID_INFO, which tell the
files of ReFS volumes apart where their 64-bit file indexes may not.

The test also fails on NTFS should one of these identities return to
the platform's 64-bit one there: NTFS's 128-bit file ID is its 64-bit
file index, but FILE_ID_INFO holds the 64-bit volume serial number, of
which BY_HANDLE_FILE_INFORMATION holds only the low 32 bits.

Part of #923.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 29, 2026 01:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain for the lock-file identity changes supporting ReFS.

No open issues 🎉

What changed in this PR

This PR addresses #923 by using FileSeal identities for internal lock-file comparisons, allowing NVX’s sandbox and template stores to distinguish files on ReFS using 128-bit Windows file IDs. Public APIs and persisted record formats remain unchanged.

Changes:

  • Use consistent handle and path identities for lock acquisition, release, and sandbox unbinding.
  • Widen directory-entry identities while preserving Unix behavior and the Windows fallback.
  • Add identity-consistency coverage and update existing lock tests.
File Description
nvx-rs/​src/​host/​template/​store.rs Use seal-based identities when validating template locks.
nvx-rs/​src/​host/​store/​tests.rs Update replacement-lock assertions to use the new identities.
nvx-rs/​src/​host/​store/​regular.rs Add a cross-platform path identity helper.
nvx-rs/​src/​host/​store/​mod.rs Use consistent identities when validating sandbox locks.
nvx-rs/​src/​host/​store/​lifecycle/​tests.rs Test identity consistency and distinguish separate lock files.
nvx-rs/​src/​host/​store/​lifecycle.rs Update lock removal and unbinding identity checks.
nvx-rs/​src/​host/​platform/​windows.rs Return seal-based directory-entry identities with 128-bit file IDs.
nvx-rs/​src/​host/​platform/​other.rs Align the unsupported-platform identity signature.
nvx-rs/​src/​host/​platform/​linux.rs Widen the inode return type without changing identity semantics.

Review effort: Balanced

@ppenna

Copy link
Copy Markdown
Contributor Author

Superseded by #949, which landed e9d7531 ("nvx-rs: tell the files of a store apart by their 128-bit IDs on Windows") on dev while this pull request's CI ran. That commit makes the same change through platform::file_id, platform::opened_file_id, and platform::Directory::entry_id, at every check that #923 names, and adds the test that #923 asks for, the_stores_tell_files_apart_by_the_identities_that_their_seals_hold. This pull request now only conflicts with it.

Closing. #923 records the validation of dev on a ReFS volume.

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.

nvx-rs: compare lock files by their 128-bit file IDs on Windows, as ReFS needs

2 participants