diff --git a/nvx-rs/src/host/platform/linux.rs b/nvx-rs/src/host/platform/linux.rs index c26a28516..8b9b94d01 100644 --- a/nvx-rs/src/host/platform/linux.rs +++ b/nvx-rs/src/host/platform/linux.rs @@ -655,14 +655,14 @@ impl Directory { .map(Self) } - /// Returns the device and inode of its entry `name`, not following a link, as - /// [`opened_file_identity`] gives them, or `None` if it has no such entry. - pub(crate) fn entry_identity(&self, name: &str) -> io::Result> { + /// Returns the device and inode of its entry `name`, not following a link, as the entry's + /// [`FileSeal`](crate::host::seal::FileSeal) holds them, or `None` if it has no such entry. + pub(crate) fn entry_identity(&self, name: &str) -> io::Result> { use nix::fcntl::AtFlags; use nix::sys::stat::fstatat; match fstatat(&self.0, name, AtFlags::AT_SYMLINK_NOFOLLOW) { - Ok(status) => Ok(Some((status.st_dev, status.st_ino))), + Ok(status) => Ok(Some((status.st_dev, u128::from(status.st_ino)))), Err(Errno::ENOENT) => Ok(None), Err(errno) => Err(errno.into()), } diff --git a/nvx-rs/src/host/platform/other.rs b/nvx-rs/src/host/platform/other.rs index 57af35f57..761abbcca 100644 --- a/nvx-rs/src/host/platform/other.rs +++ b/nvx-rs/src/host/platform/other.rs @@ -108,7 +108,7 @@ impl Directory { } /// Never called: no directory is open. - pub(crate) fn entry_identity(&self, _name: &str) -> io::Result> { + pub(crate) fn entry_identity(&self, _name: &str) -> io::Result> { match *self {} } diff --git a/nvx-rs/src/host/platform/windows.rs b/nvx-rs/src/host/platform/windows.rs index 287f4a566..15fa10b51 100644 --- a/nvx-rs/src/host/platform/windows.rs +++ b/nvx-rs/src/host/platform/windows.rs @@ -69,6 +69,7 @@ use windows_sys::Win32::System::WindowsProgramming::QueryInterruptTimePrecise; use super::Transport; use super::inherit::Inherited; +use crate::host::seal::FileSeal; use crate::hypervisor::Hypervisor; /// Whether this host can run NVX microVMs. @@ -1407,10 +1408,12 @@ impl Directory { .map(Self) } - /// Returns the volume serial number and file index of its entry `name`, not following a link - /// or other reparse point, as [`opened_file_identity`] gives them, or `None` if it has no such - /// entry. - pub(crate) fn entry_identity(&self, name: &str) -> io::Result> { + /// Returns the identity of its entry `name`, not following a link or other reparse point, as + /// the entry's [`FileSeal`] holds it, or `None` if it has no such entry: 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, or those of `BY_HANDLE_FILE_INFORMATION` on + /// a volume that has no file ID information. + pub(crate) fn entry_identity(&self, name: &str) -> io::Result> { use windows_sys::Wdk::Storage::FileSystem::{ FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, }; @@ -1422,7 +1425,10 @@ impl Directory { FILE_READ_ATTRIBUTES | SYNCHRONIZE, FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT, ) { - Ok(entry) => handle_identity(raw(&entry)).map(Some), + Ok(entry) => { + let seal = FileSeal::of(&fs::File::from(entry))?; + Ok(Some((seal.device(), seal.file_id()))) + } Err(STATUS_OBJECT_NAME_NOT_FOUND) => Ok(None), Err(status) => Err(status_error(status)), } diff --git a/nvx-rs/src/host/store/lifecycle.rs b/nvx-rs/src/host/store/lifecycle.rs index 0310337b3..bbb976d31 100644 --- a/nvx-rs/src/host/store/lifecycle.rs +++ b/nvx-rs/src/host/store/lifecycle.rs @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf}; use std::time::Instant; use super::caller::{is_report_staging, is_store_temporary}; +use super::regular; use super::{ CLONE_BINDING_NAME, CallerFiles, FILE_NAMES, JOURNAL_NAME, LOCK_NAME, LockedStore, RUNTIME_NAME, Races, SandboxStore, Taken, absolute, pause, transition_in_progress, @@ -246,8 +247,8 @@ impl LockedStore { return; } let path = self.path(LOCK_NAME); - let held = platform::opened_file_identity(&self.lock); - if matches!((held, platform::file_identity(&path)), (Ok(held), Ok(current)) if held == current) + let held = regular::identity_of(&self.lock); + if matches!((held, regular::identity_at(&path)), (Ok(held), Ok(current)) if held == current) { let _ = fs::remove_file(&path); } @@ -380,7 +381,7 @@ impl LockedStore { move |error: io::Error| RemovalError::Io { path, error } }; let directory = platform::Directory::open(dir).map_err(failed)?; - let held = platform::opened_file_identity(&self.lock).map_err(entry_failed(LOCK_NAME))?; + let held = regular::identity_of(&self.lock).map_err(entry_failed(LOCK_NAME))?; if directory .entry_identity(LOCK_NAME) .map_err(entry_failed(LOCK_NAME))? diff --git a/nvx-rs/src/host/store/lifecycle/tests.rs b/nvx-rs/src/host/store/lifecycle/tests.rs index cdafeaaf7..532ea2f4c 100644 --- a/nvx-rs/src/host/store/lifecycle/tests.rs +++ b/nvx-rs/src/host/store/lifecycle/tests.rs @@ -430,7 +430,7 @@ fn a_removal_makes_the_absence_of_the_clone_binding_durable_before_it_removes_an assert_eq!( synced.into_inner(), [( - Some(platform::file_identity(&dir.join(LOCK_NAME)).unwrap()), + Some(regular::identity_at(&dir.join(LOCK_NAME)).unwrap()), sorted(&[BOOT, CONFIG, LOCK_NAME, LOG_NAME]) )] ); @@ -728,6 +728,35 @@ fn directories_are_identified_as_their_seals_identify_them() { assert_eq!(hold_directory(directory.path()).unwrap().0, expected); } +#[cfg(any(target_os = "linux", windows))] +#[test] +fn lock_files_are_identified_as_their_seals_identify_them() { + use crate::host::seal::{FileSeal, open_metadata_only}; + + // The lock, the path, and the directory's entry identify a lock file alike, and two files of + // one directory apart: on Windows, by the 128-bit file ID of `FILE_ID_INFO`, which tells the + // files of ReFS volumes apart where their 64-bit file indexes may not. + let (_directory, dir) = sandbox(); + let other = "other.lock"; + fs::write(dir.join(other), b"").unwrap(); + let locked = SandboxStore::lock_existing(&dir, &files(), None).unwrap(); + let directory = platform::Directory::open(&dir).unwrap(); + let identities = [LOCK_NAME, other].map(|name| { + let path = dir.join(name); + let seal = FileSeal::of(&open_metadata_only(&path).unwrap()).unwrap(); + let expected = (seal.device(), seal.file_id()); + assert_eq!(regular::identity_at(&path).unwrap(), expected, "{name}"); + assert_eq!( + directory.entry_identity(name).unwrap(), + Some(expected), + "{name}" + ); + expected + }); + assert_ne!(identities[0], identities[1]); + assert_eq!(regular::identity_of(&locked.lock).unwrap(), identities[0]); +} + #[test] fn a_removal_needs_the_configuration_record() { let (_directory, dir) = sandbox(); diff --git a/nvx-rs/src/host/store/mod.rs b/nvx-rs/src/host/store/mod.rs index 0bef346a1..4d97664fd 100644 --- a/nvx-rs/src/host/store/mod.rs +++ b/nvx-rs/src/host/store/mod.rs @@ -250,6 +250,7 @@ pub use self::lifecycle::{Lingering, RemovalError}; pub use self::record::{ LaunchId, LaunchJournal, LaunchState, LogClaim, MAX_ENDPOINT_LEN, RuntimeRecord, }; +use self::regular::{identity_at, identity_of}; use super::filesystem::FileIdentity; use super::platform; use super::process::{BootId, Process, ProcessRef}; @@ -595,8 +596,8 @@ impl SandboxStore { } }, } - let locked = platform::opened_file_identity(&file).map_err(failed)?; - match platform::file_identity(&path) { + let locked = identity_of(&file).map_err(failed)?; + match identity_at(&path) { Ok(current) if current == locked => Ok(Taken::Locked(file)), Ok(_) => Ok(Taken::Replaced), Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Taken::Replaced), diff --git a/nvx-rs/src/host/store/regular.rs b/nvx-rs/src/host/store/regular.rs index 771c18ab0..e4bbd96c0 100644 --- a/nvx-rs/src/host/store/regular.rs +++ b/nvx-rs/src/host/store/regular.rs @@ -1,6 +1,7 @@ //! The files that the template modules read, and that a sandbox's store reads as its clone -//! binding: regular files that are not links, which they read within a bound, and the identity of -//! a file that a template module holds open. +//! binding: regular files that are not links, which they read within a bound, and the identities +//! by which the template modules and the stores tell files apart: of a file that one holds open, +//! and of the file that a path names. use std::fs::{File, OpenOptions}; use std::io::{self, Read}; @@ -90,3 +91,36 @@ pub(crate) fn identity_of(file: &File) -> io::Result<(u64, u128)> { let seal = FileSeal::of(file)?; Ok((seal.device(), seal.file_id())) } + +/// Returns the identity of the file or directory that `path` names, following links, as +/// [`identity_of`] gives it for the object open. +/// +/// On Windows, the object is opened without access rights, as [`platform::file_identity`] opens +/// it, which succeeds even while another process holds it open without sharing it. A file whose +/// deletion is pending, which a file system without POSIX deletion keeps under its name until its +/// last handle closes, refuses even that open, with a denied access. +pub(crate) fn identity_at(path: &Path) -> io::Result<(u64, u128)> { + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + + let metadata = std::fs::metadata(path)?; + Ok((metadata.dev(), u128::from(metadata.ino()))) + } + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + + /// `FILE_FLAG_BACKUP_SEMANTICS`, which opens a directory too. + const BACKUP_SEMANTICS: u32 = 0x0200_0000; + let file = OpenOptions::new() + .access_mode(0) + .custom_flags(BACKUP_SEMANTICS) + .open(path)?; + identity_of(&file) + } + #[cfg(not(any(unix, windows)))] + { + platform::file_identity(path).map(|(device, index)| (device, u128::from(index))) + } +} diff --git a/nvx-rs/src/host/store/tests.rs b/nvx-rs/src/host/store/tests.rs index 44300b42d..f3f9c0a1e 100644 --- a/nvx-rs/src/host/store/tests.rs +++ b/nvx-rs/src/host/store/tests.rs @@ -322,9 +322,10 @@ fn a_lock_file_replaced_while_waiting_locks_nothing() { let store = store.clone(); move || { let locked = store.lock(None).unwrap(); - // The waiter holds the lock of the file that the path names now. - let current = platform::file_identity(&locked.path(LOCK_NAME)).unwrap(); - let held = platform::opened_file_identity(&locked.lock).unwrap(); + // The waiter holds the lock of the file that the path names now, by the identities + // that the lock compares. + let current = identity_at(&locked.path(LOCK_NAME)).unwrap(); + let held = identity_of(&locked.lock).unwrap(); assert_eq!(current, held); } }); diff --git a/nvx-rs/src/host/template/store.rs b/nvx-rs/src/host/template/store.rs index 8dd9cf4dd..af8aa6302 100644 --- a/nvx-rs/src/host/template/store.rs +++ b/nvx-rs/src/host/template/store.rs @@ -25,6 +25,7 @@ use crate::host::platform::{self, parse_hex_id}; use crate::host::process::BootId; use crate::host::store::files::{open_lock, write_private}; use crate::host::store::lease::{ID_LEN, hex, read_private, read_record, store_error}; +use crate::host::store::regular::{identity_at, identity_of}; pub use self::capture::{CAPTURE_RECORD_NAME, CAPTURES_NAME, Capture, Created, TRANSACTION_NAME}; pub use self::leases::{Acquired, LEASES_NAME, Retirement, TOMBSTONES_NAME}; @@ -878,8 +879,8 @@ fn take_lock(dir: &Path, deadline: Option) -> Result { } }, } - let locked = platform::opened_file_identity(&file).map_err(failed)?; - match platform::file_identity(&path) { + let locked = identity_of(&file).map_err(failed)?; + match identity_at(&path) { Ok(current) if current == locked => return Ok(file), Ok(_) => {} Err(error) if error.kind() == io::ErrorKind::NotFound => {}