Repository navigation
nvx-rs: give user IDs and file modes distinct types in Linux checks - #1011
Conversation
store_dir_problem, owner_only_problem, and foreign_writer in the Linux platform module took a file's owner, its mode, and the current user as three u32s. A caller that passed a mode as a user ID, or a user ID as a mode, still compiled, and the privacy check then compared the wrong values (#643): at dev, store_dir_problem(0o40700, 1000, 1000), which passes a mode as the owner and a user ID as the mode, compiles. The checks now take nix::unistd::Uid for the owner and the current user, and nix::sys::stat::Mode for the mode. nix, a dependency of the host feature, already provides both, so the crate needs no newtype of its own. The new owner_and_mode converts at the boundary where std::os::unix::fs::MetadataExt returns raw integers, with Uid::from_raw and Mode::from_bits_truncate. The latter keeps exactly the permission bits that the checks masked with PERMISSION_BITS, 0o7777, and drops the bits of the file's type, so the mask, and PERMISSION_BITS, go away. The current user is Uid::effective(), the geteuid() that sys::effective_ids() returned. The other places in platform/linux.rs that passed modes as bare integers take Mode too: create_dir_of_mode, restore_private_file_mode, whose only caller, the store's open_lock, converts its PRIVATE_FILE_MODE, and the constants PRIVATE_DIR_MODE, STORE_DIR_MODE, and GROUP_AND_OTHER_BITS. Mode::bits() hands the raw mode to the standard library's DirBuilderExt::mode and PermissionsExt::from_mode. The module-wide import of Mode replaces the local ones of Directory::open_file and Directory::open_dir_with. Nothing else changes. Uid displays as its decimal integer and Mode formats in octal as its bits, so the messages are the same. The comparisons test the same bits: foreign_writer's 0o022 is now S_IWGRP | S_IWOTH, and its comparison with 0 is Uid::is_root. Passing an integer, a Mode where a Uid belongs, or a Uid where a Mode belongs now fails to compile with E0308. The unit tests of store_dir_problem and owner_only_problem keep their cases and assertions, converting the raw st_mode values that they pass, with the bits of a directory's type, as the boundary does. The store's privacy tests, which reach the checks through real directories and files, and the restore-ready socket's test, which now compares the permission bits as a Mode, pass unchanged in what they assert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are reported for the user-ID and file-mode type-safety changes.
No open issues 🎉
What changed in this PR
This PR addresses #643 by using distinct Uid and Mode types in NVX’s Linux host privacy checks, preventing accidental mixing of user IDs and file modes while preserving existing behavior and diagnostics.
Changes:
- Converts raw metadata values at the boundary and uses typed ownership checks, permission checks, and mode constants.
- Updates lock-file permission restoration to pass a typed mode.
- Adapts existing privacy and socket-permission tests without changing their assertions.
| File | Description |
|---|---|
nvx-rs/src/host/template/store/tests.rs |
Adapts directory privacy tests to typed inputs. |
nvx-rs/src/host/store/tests.rs |
Adapts store ownership and mode tests to typed inputs. |
nvx-rs/src/host/store/files.rs |
Converts the lock-file mode for permission restoration. |
nvx-rs/src/host/platform/linux.rs |
Introduces typed ownership and permission checks and metadata conversion. |
Review effort: Balanced
Rahul (rahul05ranjan)
left a comment
There was a problem hiding this comment.
The core claim here — that nix::sys::stat::Mode gives compile-time separation of user IDs from modes while preserving the old masking semantics — checks out against the dependency you're pinned to:
- I pulled the nix 0.31.3 source (from your Cargo.lock):
Modeis defined as exactly the 0o7777 bits (S_IRWXU…S_IXOTH plus S_ISUID/S_ISGID/S_ISVTX), with the file-type bits (S_IFMT etc.) living in the separateSFlagtype. SoMode::from_bits_truncate(metadata.mode())is precisely the oldmode & PERMISSION_BITS— the removal of the internal mask instore_dir_problem/owner_only_problem/foreign_writeris safe because truncation now happens once at construction inowner_and_mode(linux.rs:459), and callers can't smuggle type bits in sinceModecan't represent them. sys::effective_ids()isn't orphaned by the switch toUid::effective()— it's still used byhost/launch.rsand the template clone tests, so no dead-code fallout.- The behavioral assertions are unchanged where it matters:
0o42700still reports "has mode 2700" (setgid survives truncation and display via bitflags' Octal impl),!uid.is_root()matches the olduid != 0, andmode.intersects(GROUP_AND_OTHER_BITS)matchesmode & 0o077 != 0. - The test rewrites in
store/tests.rsandtemplate/store/tests.rsroute through the samefrom_bits_truncateconstruction the production code uses, so they keep testing the truncation seam rather than bypassing it.
One small thing, take it or leave it: in store/files.rs:100 you convert at the call site with Mode::from_bits_truncate(PRIVATE_FILE_MODE). Since PRIVATE_FILE_MODE stays a raw u32 (0o600) for the other uses (DirBuilder, Permissions, and the & PRIVATE_FILE_MODE != PRIVATE_FILE_MODE check at line 131), that's reasonable, but making it a Mode const and calling .bits() at the u32 sites would put the whole file on the typed side of the line too.
|
Rahul (@rahul05ranjan) Thanks for checking the truncation against nix 0.31.3. I'm keeping |
Fixes #643.
The Linux privacy checks of the host's platform module,
store_dir_problem,owner_only_problem, andforeign_writerinnvx-rs/src/host/platform/linux.rs, took a file's owner, its mode, and the current user as threeu32s. A caller that passed a mode as a user ID, or a user ID as a mode, still compiled, and the check then compared the wrong values: atdev,store_dir_problem(0o40700, 1000, 1000), which passes a mode as the owner and a user ID as the mode, compiles. The checks now take distinct types, so such a call fails to compile, and nothing else changes.Based on
devate4ca7acaa.devhas since moved to8f3a840f6: #974, an OpenVMM pin; #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, 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.Change
0e79fce4anvx-rsnvx-rs/src/host/platform/linux.rs,nvx-rs/src/host/store/files.rs,nvx-rs/src/host/store/tests.rs,nvx-rs/src/host/template/store/tests.rsnix::unistd::Uidfor the owner and the current user, andnix::sys::stat::Modefor the mode; the directory and file helpers and the mode constants of the module takeModetoonix, already a dependency of thehostfeature, provides both, so the crate adds no newtype.owner_and_modeconverts wherestd::os::unix::fs::MetadataExtreturns raw integers, withUid::from_rawandMode::from_bits_truncate. The latter keeps exactly the permission bits,0o7777, that the checks masked withPERMISSION_BITS, and drops the bits of the file's type, so the mask andPERMISSION_BITSgo away. The current user isUid::effective(), thegeteuid()thatsys::effective_ids()returned.platform/linux.rs.create_dir_of_mode,restore_private_file_mode, whose only caller, the store'sopen_lock, converts itsPRIVATE_FILE_MODE, and the constantsPRIVATE_DIR_MODE,STORE_DIR_MODE, andGROUP_AND_OTHER_BITStakeMode;Mode::bits()hands the raw mode to the standard library'sDirBuilderExt::modeandPermissionsExt::from_mode.Uiddisplays as its decimal integer, andModeformats in octal as its bits, so the messages are the same. The comparisons test the same bits:foreign_writer's0o022isS_IWGRP | S_IWOTH, and its comparison with 0 isUid::is_root.The issue's acceptance criteria:
Modewhere aUidbelongs, and aUidwhere aModebelongs each fail withE0308.store_dir_problemandowner_only_problemkeep their cases and assertions, converting the rawst_modevalues that they pass, with the bits of a directory's type, as the boundary does. The store's privacy tests, which reach the checks through real directories and files, and the restore-ready socket's test, which now compares the permission bits as aMode, assert the same.Validation
BASE is
devate4ca7acaa87f2b0f3b909b2e390c1824f2de1fae; the head is0e79fce4ac0ad3708b763937710695af24a968dd. The commit is signed.On Windows, in
nvx-rswithNVX_BUNDLE=skip:store_dir_problem(0o40700, 1000, 1000)passescargo check --lib --target x86_64-unknown-linux-gnu; at the head, it and the two other scratch calls fail withE0308.cargo fmt --all --check.-D warningsin check-nvx-rs's three configurations (--all-targets --all-features,--lib, and--lib --no-default-features) forx86_64-unknown-linux-gnu, the only target that compilesplatform/linux.rs, and natively with--all-targets --all-features.-D warningsand--all-featuresforx86_64-unknown-linux-gnu.cargo +1.89 check --all-targets --all-features --target x86_64-unknown-linux-gnu, and the macOS check,cargo check --all-targets --all-features --target aarch64-apple-darwin.cargo fmt --all --check; Clippy with-D warningsfor its protocol-only and host-only configurations, natively and forx86_64-unknown-linux-gnu, and for its guest-only configuration forx86_64-unknown-linux-musl; andcargo +1.89 check --all-targetsof each configuration for its Linux target.On a Linux host, natively; no VM was started, since the change has no hypervisor behavior:
nvx-kvm-testnvx-rs:cargo test --locked --all-features --no-fail-fastnvx-kvm-testnvx-rs:cargo test --locked --no-fail-fastnvx-kvm-testnvx-rs:cargo build --lib --no-default-featuresandcargo test --lib --no-default-featuresnvx-kvm-testnvx-rs:RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-featuresnvx-kvm-testhost::store::tests::{the_store_is_private_to_the_current_user, stores_belong_to_the_current_user_with_mode_700},host::template::store::tests::{the_store_is_private_to_the_current_user, private_directories_belong_to_the_current_user_and_grant_others_nothing},host::platform::linux::tests::a_restore_ready_socket_lets_its_owner_connect_whatever_the_umask, andtests/store.rs, witha_restrictive_umask_leaves_the_store_usablenvx-kvm-test-D warningsPre-existing failures on that host, in guest code that this change doesn't touch:
guest::entry::tests::runtime_binds_need_a_mount_root_unless_there_are_nonegetsEOPNOTSUPPfrom a mount, 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_idgetEINVALwhere they expectESRCH. They fail the same way on a rerun, and at BASE on the same host (Azure Linux 3, kernel 6.6).Left to CI, which ran them: run 40902235482, attempt 1, passed on head
0e79fce4a, withRequired status checkand the other 38 checks passing and 7 skipped:cargo teston Windows, which doesn't compile it either.nvx-rschange, although the checks run on the host before any VM starts: the NVX microVM suites on Linux/KVM, Linux/MSHV, and Windows/WHP, the KVM debug-kernel suite, the OpenVMM unit and VMM tests, and the platform checks, all passed.Follow-ups
check_private(owner: u32, mode: u32, user: u32)innvx-rs/src/guest/root/share.rs, a guest-side check, and theu32modes ofnvx-rs/src/host/cli/sandbox/lifecycle/directory.rshave the same shape, outside the Linux platform module that this issue covers.