Repository navigation
nvx-rs: wrap the file operations of the sandbox store in a type - #1012
Merged
Merged
Conversation
The sandbox store reads its runtime record, launch journal, and capability, and takes its lock, through free functions of store/files.rs, to which each caller passes the file's path and its description again (#645). The tests check that a record or the capability fails to read when it is not a regular file, is malformed, or is not private, but mostly through fragments of the messages, such as "is malformed" or "allows other users", and no test checks the messages of a missing or an invalid capability. A change that gathers those paths and descriptions in one type could then change what a message calls a file, or which file it names, without a test noticing. The new test pins, on every platform, the messages with which the store refuses a missing capability, one of the wrong length or all zero, a runtime record or a journal that does not parse, and a directory in the place of either record or of the capability, which Unix opens and refuses as no regular file, and Windows refuses to open. The Windows test of the store's privacy now pins the whole message with which the store refuses a journal, a capability, and a lock whose access control list allows other users, rather than its end. The tests pass with the store as it is. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
store/files.rs offered the sandbox store's file operations as free functions: create_new, open_lock, write_private, read_private, and remove_if_present. Every caller passed a file's path again, with the description that messages give the file, and had to remember to check the privacy of what it opened: read_private refused a record or the capability that others may open, but take_lock checked its lock file with platform::private_file_problem itself, and read_capability built the message of a missing capability from a description of its own. A new record or caller could skip a check, or name a file differently (#645). A StoreFile now holds one file of the store, its path with what messages call it, and gives it the operations that the store uses: create_new, open_lock, replace, read, and remove_if_present. SandboxStore builds one for each file that it creates and reads, its lock, launch journal, runtime record, capability, and log, and opens, creates, replaces, and reads those files only through it: - StoreFile::read is read_private, which is gone, so the store reads a file only as a StoreFile, which refuses it if it is not a regular file or not private. - StoreFile::open_lock opens the lock and refuses it if others may open it, the check that take_lock made, and maps the failures with the caller's mapper, so that "cannot lock" still names them all. - record::read and record::write take the StoreFile of the record, whose description is the record's Record::DESCRIPTION, and name it in their messages. - The messages of the capability and of the log, and the refusal of a lock, name the file by its description: "sandbox control capability", "log", and "sandbox lock", as they did. private_options, which only files.rs uses, is private to it now. The free functions that the template store uses, create_new, open_lock, write_private, remove_if_present, and sync_dir, stay pub(crate). The store's lifecycle still removes its lock file, and reaches the entries of its directory, by name: it removes the lock file only while the lock's handle still names it, and reports a RemovalError, and it removes the other entries through a handle of the directory, neither of which a StoreFile models. The on-disk layout, the record format, the error kinds, and the messages are unchanged. The previous commit's test, which pins the messages with which the store refuses its records and its capability on every platform, and on Windows those that refuse a journal, a capability, or a lock that others may open, passes unchanged, as do the FIFO and link refusals on Unix, the rest of the store's unit tests, which now write records through their StoreFiles, and tests/store.rs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The StoreFile refactor has no active review issues.
No open issues 🎉
What changed in this PR
Addresses #645 by introducing a store-private StoreFile type that keeps each sandbox file’s path, description, and privacy checks together, while preserving existing formats, error messages, and atomic replacement behavior.
Changes:
- Wraps sandbox file creation, reading, locking, replacement, and removal in
StoreFile. - Routes sandbox lifecycle and record operations through the new type, leaving template-store helpers unchanged.
- Adds regression tests for read errors and strengthens Windows privacy-error assertions.
| File | Description |
|---|---|
nvx-rs/src/host/store/tests.rs |
Pins error messages and adapts record-writing tests to StoreFile. |
nvx-rs/src/host/store/record.rs |
Updates record reading and writing to accept StoreFile. |
nvx-rs/src/host/store/mod.rs |
Adds file builders and routes sandbox operations through them. |
nvx-rs/src/host/store/files.rs |
Introduces StoreFile with centralized read and lock privacy checks. |
Review effort: Balanced
Pedro Henrique Penna (ppenna)
deleted the
nvx-issue-fix/645-store-file
branch
October 29, 2026 19:39
6 tasks
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.
Fixes #645.
The sandbox store reached its private files through free functions of
nvx-rs/src/host/store/files.rs, to which every caller passed a file's path and its description again, and every caller had to remember to check the privacy of what it opened. This addsStoreFile, a type private to the store module that holds one file of the store, its path with what messages call it, and gives it the operations that the store uses.SandboxStorenow opens, creates, replaces, and reads its lock, launch journal, runtime record, capability, and log only through it.Based on
devatb8b82a606.devhas since moved to8f3a840f6: #1013, documentation; #960 and #977, which changenvx-rs's clone activation and sandbox options; #1015, which moves the init's command-line readers ontoprotocol::cmdline; and baseline and diagram updates. None touches these files or calls the store functions that this changes, and the branch merges cleanly: at8f3a840f6, the merge passescargo fmt --all --check, Clippy with-D warningsand--all-targets --all-featuresnatively on Windows and forx86_64-unknown-linux-musl,cargo test --locked --all-featureson Windows, and the Markdown link check.Problem
store/files.rsofferedcreate_new,open_lock,write_private,read_private, andremove_if_presentas free functions (introduced by #628, whose review asked for a type that wraps them). Each caller passed a path and a description again, and the privacy checks lived with the callers:read_privaterefused a record or the capability that is not a regular file, or that others may open.take_lockopened its lock withopen_lockand then checked it withplatform::private_file_problemitself.read_capabilitybuilt the message of a missing capability from a description of its own.A new record, or a new caller, could skip a check or name a file differently.
Change
b0fbf2f7anvx-rsstore/tests.rsb59f94f27nvx-rsstore/files.rs,store/mod.rs,store/record.rs,store/tests.rsStoreFilewithcreate_new,open_lock,replace,read, andremove_if_present.SandboxStorebuilds one for each of its lock, journal, runtime record, capability, and log;record::readandrecord::writetake one.How the change meets the issue:
StoreFileholds the path and the description (sandbox lock,sandbox launch journal,sandbox runtime record,sandbox control capability,log; the records' come fromRecord::DESCRIPTION), and the messages of its operations name the file through them.replaceis the atomic replacement through a flushed temporary file (write_private, unchanged);readisread_private, which is gone, so the store reads a file only as aStoreFile, which refuses one that is not a regular file or not private;create_newcreates it exclusively;remove_if_presentremoves it.StoreFile::open_lockopens the lock and refuses it if others may open it, the check thattake_lockmade itself, and maps failures with the caller's mapper, so thatcannot lock <path>still names them all.StoreFileispub(super), and so areSandboxStore's builders.private_options, which onlyfiles.rsuses, is now private to it.What stays as it was, deliberately:
template::Store, its captures and leases, and the clone records) keeps using the free functionscreate_new,open_lock,write_private,remove_if_present, andsync_dir, which staypub(crate): the issue concerns the sandbox store's files. See the follow-ups.RemovalError, and it removes the other entries through a handle of the directory. AStoreFilemodels neither, and changing them would change error types.SandboxStore::log_pathandcapability_path, which callers use, still return the paths.Validation
BASE:
devatb8b82a606e2e501be0ad9ecf630d9c99e31c086a. Both commits are signed.Local, on Windows, the applicable commands of
check-nvx-rs:git rebase -x:cargo fmt --all --check, andcargo clippy --locked --all-targets --all-features -- -D warningsboth natively and with--target x86_64-unknown-linux-gnu. All clean.b0fbf2f7aat BASE: the new test andthe_store_is_private_to_the_current_userpass (22host::store::testspassed).--liband--lib --no-default-features, natively and for Linux;cargo test --locked --all-features(1,090 library tests and every integration test passed),cargo test --locked(948),cargo buildandcargo test --lib --no-default-features(266);cargo doc --no-deps --all-featureswith-D warnings; the minimum Rust version, 1.89, withcargo check --all-targets --all-features; the macOS check foraarch64-apple-darwin; and the runtime adapter's fmt, protocol-only and host-only Clippy (also for Linux), build, tests, documentation, and minimum Rust version. All clean.On a Linux host, from a bundle of these commits:
nvx-kvm-testvirtual-machinecargo test --all-features --lib host::storeand--test storenvx-kvm-testvirtual-machinecargo test --locked --all-features --no-fail-fastnvx-kvm-testvirtual-machinecargo test --locked(1,010),--lib --no-default-features(266),cargo docwith-D warnings, the runtime adapter's host-only build, tests (107), and documentationThe 3 failures are guest tests that this change does not touch, and they fail identically at BASE on this host, so they are pre-existing there:
guest::entry::tests::runtime_binds_need_a_mount_root_unless_there_are_none(Operation not supported, atsrc/guest/entry/tests.rs:542), andguest::exec::tests::a_start_after_another_collector_took_the_status_ends_nothing_by_idandguest::exec::tests::adopting_a_program_whose_status_another_collector_took_ends_nothing_by_id(EINVAL, notESRCH, atsrc/guest/exec/tests.rs:766and:1333).The host has no rustfmt, Clippy, or Rust 1.89, so formatting, Clippy, and the minimum Rust version ran on Windows, Clippy also for the Linux target.
Left to CI, which ran them: the Linux runs of Clippy and of the minimum Rust version, the bundled-artifacts test, and the guest binaries and the runtime adapter's guest-only configuration, which do not compile
host::store. No hypervisor was used locally, since the change touches no microVM behavior; CI's microVM jobs, which changes tonvx-rsselect, cover the sandbox commands that use the store on every backend.CI at head
b59f94f27: run 40914352621, attempt 2, succeeded,Required status checkincluded: 31 checks passed and 12 were skipped. Its attempt 1 failed only in NVX microVM tests / Linux / MSHV / Shard 1 of 2, at itsValidate runnerstep, before any test ran: check H4 ofnvx.py doctoron the runnerazure-mshv-1found its backend's TSC rate 110.467 ppm from the rate measured against chronyd's frequency estimate, over the limit of 100 ppm. That qualifies the runner's clock, which this change cannot affect, so the failed jobs were rerun once, and passed.Follow-ups
template/store.rsopens its lock withopen_lockand checksprivate_file_problemitself,template/store/capture.rsandtemplate/clone/records.rscheck the privacy of what they open themselves, andstore::lease::read_privatereads records with the same checks asStoreFile::readplus a size limit. A type that also takes a size limit could serve both stores.