From 5de1c96d8b0e916a9f8851c356c5543f0f54464e Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 19:18:53 -0800 Subject: [PATCH] host: write private files through the platform layer finish_capture, the CLI's sandbox store and the CLI's snapshots write and sync their files with write_private, sync_dir and create_new from host::store::files, and read through host::store::regular's aliases of open_regular and read_at_most, so code that stays depends on the sandbox store, which SL6.1 deletes. Move write_private, sync_dir and create_new, with private_options and PRIVATE_FILE_MODE, which they share with the store's lock files, to host::platform, which already holds the host's file helpers and the strict reads that SL6.0a moved there. The store re-exports create_new and write_private for its own files until SL6.1 deletes it, and its open_lock takes the moved options and mode; nothing in it syncs a directory through sync_dir since the template store and the old clone path left. finish_capture, the CLI's sandbox store, snapshot store and capture, and their tests use host::platform directly, also for open_regular and read_at_most. Nothing else changes: a private file is still created with mode 0600 on Unix without following a link, replaced through a temporary file that a failure removes, and its directory synced on Unix alone, with the same messages. New platform tests pin the moved helpers: a private write replaces its file whole, with mode 0600 on Unix, and a failed one leaves no temporary file; a new private file never takes the place of an existing entry, nor follows a link on Unix; and a directory sync fails for a missing directory on Unix alone. The snapshot and CLI snapshot tests change only their imports. Part of #1120 (SL6.0d). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/acceptance-case-matrix.md | 2 +- nvx-rs/src/host/cli/snapshots.rs | 8 +- nvx-rs/src/host/cli/snapshots/capture.rs | 2 +- .../src/host/cli/snapshots/capture/tests.rs | 12 +-- nvx-rs/src/host/cli/snapshots/tests.rs | 2 +- nvx-rs/src/host/cli/store/files.rs | 11 +-- nvx-rs/src/host/platform/mod.rs | 95 ++++++++++++++++++- nvx-rs/src/host/platform/tests.rs | 65 +++++++++++++ nvx-rs/src/host/snapshot/capture.rs | 4 +- nvx-rs/src/host/snapshot/key/tests.rs | 2 +- nvx-rs/src/host/snapshot/tests.rs | 22 ++--- nvx-rs/src/host/store/files.rs | 95 ++----------------- 12 files changed, 193 insertions(+), 127 deletions(-) diff --git a/doc/acceptance-case-matrix.md b/doc/acceptance-case-matrix.md index 612d74bc6..0b6b87153 100644 --- a/doc/acceptance-case-matrix.md +++ b/doc/acceptance-case-matrix.md @@ -506,7 +506,7 @@ this matrix. The README's own code is no doctest. | `nvx-rs/src/host/transport.rs - host::transport (line 22)` | kept | | | `nvx-rs/src/lib.rs - (line 58)` | kept | | | `nvx-rs/src/protocol/nvxs/mod.rs - protocol::nvxs (line 68)` | kept | | -| `nvx-rs/src/host/snapshot/capture.rs - host::snapshot::capture::finish_capture (line 50)` | kept | | +| `nvx-rs/src/host/snapshot/capture.rs - host::snapshot::capture::finish_capture (line 48)` | kept | | | `nvx-rs/src/host/store/mod.rs - host::store (line 104)` | kept | The sandbox store leaves (#1121), whose PR retires it | | `nvx-rs/README.md`, its Example: an instance through `LaunchSpec` and the sandbox store | rewritten | Nothing compiles it. `nvx-rs/examples/host-workflows.rs` compiles the documented host workflows, and the library's guide (SL6.10a) replaces the example | | `nvx-rs/examples/external-launch.rs` | kept | Compiled with the crate's targets, and run by hand on a host | diff --git a/nvx-rs/src/host/cli/snapshots.rs b/nvx-rs/src/host/cli/snapshots.rs index 65d3d785a..8cb46544d 100644 --- a/nvx-rs/src/host/cli/snapshots.rs +++ b/nvx-rs/src/host/cli/snapshots.rs @@ -30,7 +30,7 @@ use crate::host::config::NicConfig; use crate::host::sandbox::options::SandboxOperation; use crate::host::snapshot::{OPENVMM_DIR, SNAPSHOT_ARTIFACTS, Snapshot}; use crate::host::vm::VmDir; -use crate::host::{filesystem, paths, platform, store}; +use crate::host::{filesystem, paths, platform}; use crate::{Error, ErrorCode}; /// The parser of the snapshot command. @@ -411,11 +411,11 @@ impl LockedSnapshot<'_> { )); } Snapshot::open(staged.dir())?; - store::files::sync_dir(staged.dir())?; + platform::sync_dir(staged.dir())?; platform::rename_no_replace(staged.dir(), &self.dir) .map_err(|error| io_error(ErrorCode::Io, &error, &self.dir))?; - store::files::sync_dir(self.store.entries.dir())?; - store::files::sync_dir(self.store.staging.dir())?; + platform::sync_dir(self.store.entries.dir())?; + platform::sync_dir(self.store.staging.dir())?; self.open() } } diff --git a/nvx-rs/src/host/cli/snapshots/capture.rs b/nvx-rs/src/host/cli/snapshots/capture.rs index 2bb9e58a2..9a8376b55 100644 --- a/nvx-rs/src/host/cli/snapshots/capture.rs +++ b/nvx-rs/src/host/cli/snapshots/capture.rs @@ -333,7 +333,7 @@ fn capture( /// Reads the geometry through the existing block-device helper on this VM's filesystem. fn disk_geometry(dir: &VmDir) -> Result { let path = dir.path().join("geometry.raw"); - let file = crate::host::store::files::create_new(&path) + let file = crate::host::platform::create_new(&path) .map_err(|error| io_error(ErrorCode::Io, &error, &path))?; file.set_len(512) .map_err(|error| io_error(ErrorCode::Io, &error, &path))?; diff --git a/nvx-rs/src/host/cli/snapshots/capture/tests.rs b/nvx-rs/src/host/cli/snapshots/capture/tests.rs index 9baf978b7..f2ba1838d 100644 --- a/nvx-rs/src/host/cli/snapshots/capture/tests.rs +++ b/nvx-rs/src/host/cli/snapshots/capture/tests.rs @@ -739,23 +739,19 @@ fn capture_helper() { })).unwrap()).unwrap(); vm::create_private_dir(Path::new(OPENVMM_DIR)).unwrap(); let manifest: Vec = serde_json::from_value(recipe["manifest"].clone()).unwrap(); - crate::host::store::files::write_private( - &Path::new(OPENVMM_DIR).join(MANIFEST_NAME), - &manifest, - ) - .unwrap(); + crate::host::platform::write_private(&Path::new(OPENVMM_DIR).join(MANIFEST_NAME), &manifest) + .unwrap(); if refused { thread::sleep(Duration::from_secs(120)); return; } for (entry, field) in [("memory.bin", "memory"), ("state.bin", "state")] { - let file = - crate::host::store::files::create_new(&Path::new(OPENVMM_DIR).join(entry)).unwrap(); + let file = crate::host::platform::create_new(&Path::new(OPENVMM_DIR).join(entry)).unwrap(); platform::make_sparse(&file).unwrap(); file.set_len(recipe[field].as_u64().unwrap()).unwrap(); } let failed = recipe["behavior"] == "fail"; - crate::host::store::files::write_private(Path::new("outcome.json"), &serde_json::to_vec(&json!({ + crate::host::platform::write_private(Path::new("outcome.json"), &serde_json::to_vec(&json!({ "outcome": { "operation": "capture", "category": if failed { "failure" } else { "success" }, "status_code": if failed { 1 } else { 0 } }, })).unwrap()).unwrap(); diff --git a/nvx-rs/src/host/cli/snapshots/tests.rs b/nvx-rs/src/host/cli/snapshots/tests.rs index ea980aa71..44651379a 100644 --- a/nvx-rs/src/host/cli/snapshots/tests.rs +++ b/nvx-rs/src/host/cli/snapshots/tests.rs @@ -130,7 +130,7 @@ fn invalid_or_linked_directories_never_publish_or_open() { let store = Store::create(&root.path().join("store")).unwrap(); let held = store.lock(&name("first"), Deadline::after(0.0)).unwrap(); let pending = staged(&held); - crate::host::store::files::write_private(&pending.dir().join(KEY_FILE), b"bad key").unwrap(); + crate::host::platform::write_private(&pending.dir().join(KEY_FILE), b"bad key").unwrap(); assert!(held.publish(pending.clone()).is_err()); assert!(!held.dir.exists()); let valid = staged(&held); diff --git a/nvx-rs/src/host/cli/store/files.rs b/nvx-rs/src/host/cli/store/files.rs index 9050cf281..674488e55 100644 --- a/nvx-rs/src/host/cli/store/files.rs +++ b/nvx-rs/src/host/cli/store/files.rs @@ -6,7 +6,6 @@ use std::io; use super::StateFile; use super::locked::LockedState; use crate::host::platform; -use crate::host::store::{files, regular}; use crate::{Error, Result}; /// Bounds a fixed record's paths and identities without reading an unbounded corrupt file. @@ -15,7 +14,7 @@ const MAX_RECORD_BYTES: u64 = 1024 * 1024; pub(super) fn read(state: &LockedState, name: StateFile) -> Result>> { let path = state.path(name); let failed = |error| Error::io(format!("cannot read {}", path.display())).with_source(error); - let file = match regular::open_regular(&path) { + let file = match platform::open_regular(&path) { Ok(file) => file, Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None), Err(error) => return Err(failed(error)), @@ -26,7 +25,7 @@ pub(super) fn read(state: &LockedState, name: StateFile) -> Result Res } // Refuse an existing unsafe entry rather than replacing it or following its target. read(state, name)?; - files::write_private(&state.path(name), bytes)?; - files::sync_dir(state.dir()) + platform::write_private(&state.path(name), bytes)?; + platform::sync_dir(state.dir()) } pub(super) fn remove(state: &LockedState, name: StateFile) -> Result<()> { @@ -47,5 +46,5 @@ pub(super) fn remove(state: &LockedState, name: StateFile) -> Result<()> { fs::remove_file(&path).map_err(|error| { Error::io(format!("cannot remove {}", path.display())).with_source(error) })?; - files::sync_dir(state.dir()) + platform::sync_dir(state.dir()) } diff --git a/nvx-rs/src/host/platform/mod.rs b/nvx-rs/src/host/platform/mod.rs index 5a3b0797b..cbf9a585b 100644 --- a/nvx-rs/src/host/platform/mod.rs +++ b/nvx-rs/src/host/platform/mod.rs @@ -11,16 +11,17 @@ //! Its crate-private reads of regular files open a file without following a link, or on Windows //! any other reparse point, refuse one that is not a regular file before reading anything from //! it, and read it within a bound. +//! +//! Its crate-private writes create files that only the current user may read, replace a file +//! atomically, and make the changes to a directory's entries durable. use std::fmt; use std::fs::{self, File, OpenOptions}; -use std::io::{self, Read}; +use std::io::{self, Read, Write}; use std::path::Path; use std::time::Duration; -#[cfg(windows)] -use crate::error::Error; -use crate::error::Result; +use crate::error::{Error, Result}; pub(crate) mod inherit; #[cfg(target_os = "linux")] @@ -137,6 +138,92 @@ pub(crate) fn read_at_most(file: File, limit: u64, what: &str) -> io::Result OpenOptions { + let mut options = OpenOptions::new(); + options.write(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + + options + .mode(PRIVATE_FILE_MODE) + .custom_flags(nix::fcntl::OFlag::O_NOFOLLOW.bits()); + } + options +} + +/// Creates the file at `path`, which must not exist, readable only by the current user. +/// +/// On Unix, the umask may clear bits of the mode that the file is created with, even the +/// current user's permission to read it, so the mode is set again through the new file. +pub(crate) fn create_new(path: &Path) -> io::Result { + let file = private_options().create_new(true).open(path)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + file.set_permissions(fs::Permissions::from_mode(PRIVATE_FILE_MODE))?; + } + Ok(file) +} + +/// Atomically replaces `path` with `contents`, readable only by the current user. +/// +/// The contents go to a fresh temporary file beside `path`, which is flushed to disk and then +/// renamed over it, so a reader sees the old contents or the new ones, and a crash leaves at most +/// the temporary file. +pub(crate) fn write_private(path: &Path, contents: &[u8]) -> Result<()> { + let mut suffix = [0u8; 8]; + getrandom::fill(&mut suffix) + .map_err(|error| Error::other("cannot name a temporary file").with_source(error))?; + let name = path + .file_name() + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_default(); + let temporary = path.with_file_name(format!(".{name}.{}.tmp", u64::from_le_bytes(suffix))); + let written = (|| -> io::Result<()> { + let mut file = create_new(&temporary)?; + file.write_all(contents)?; + file.sync_all()?; + drop(file); + fs::rename(&temporary, path) + })(); + if written.is_err() { + let _ = fs::remove_file(&temporary); + } + written + .map_err(|error| Error::io(format!("cannot write {}", path.display())).with_source(error)) +} + +/// Makes the changes to the entries of the directory `dir` so far durable, such as files that +/// were renamed into it or removed from it: a crash that keeps a later change keeps them too, even +/// one on another filesystem. +/// +/// On Unix, it syncs the directory. Windows has no portable way to flush a directory, so there a +/// rename or a removal is as durable as the filesystem makes it, as OpenVMM's publication of a +/// snapshot assumes. +pub(crate) fn sync_dir(dir: &Path) -> Result<()> { + #[cfg(unix)] + { + File::open(dir) + .and_then(|directory| directory.sync_all()) + .map_err(|error| { + Error::io(format!("cannot sync the directory {}", dir.display())).with_source(error) + }) + } + #[cfg(not(unix))] + { + let _ = dir; + Ok(()) + } +} + /// Refuses a VMM working directory that exceeds Windows' process-creation limit, without /// accessing the filesystem. Other hosts have no such limit. pub(crate) fn check_working_dir(path: &Path) -> Result<()> { diff --git a/nvx-rs/src/host/platform/tests.rs b/nvx-rs/src/host/platform/tests.rs index 0d8aa0abc..f39608e1d 100644 --- a/nvx-rs/src/host/platform/tests.rs +++ b/nvx-rs/src/host/platform/tests.rs @@ -49,6 +49,71 @@ fn an_open_of_a_regular_file_refuses_a_link_and_a_directory() { } } +#[test] +fn a_private_write_replaces_its_file_whole_and_a_failed_one_leaves_no_temporary_file() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("record.json"); + std::fs::write(&path, b"old").unwrap(); + write_private(&path, b"new").unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), b"new"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let mode = std::fs::metadata(&path).unwrap().permissions().mode(); + assert_eq!(mode & 0o7777, PRIVATE_FILE_MODE); + } + // A directory takes the place of the file, so the rename fails. + let occupied = directory.path().join("occupied"); + std::fs::create_dir(&occupied).unwrap(); + let error = write_private(&occupied, b"new").unwrap_err(); + assert_eq!( + error.to_string(), + format!("cannot write {}", occupied.display()) + ); + let mut names: Vec<_> = std::fs::read_dir(directory.path()) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + names.sort(); + assert_eq!(names, ["occupied", "record.json"]); +} + +#[test] +fn a_new_private_file_never_takes_the_place_of_an_existing_entry() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("record.json"); + std::fs::write(&path, b"old").unwrap(); + let error = create_new(&path).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::AlreadyExists, "{error}"); + assert_eq!(std::fs::read(&path).unwrap(), b"old"); + // Nor does it follow a link to a file that does not exist yet. + #[cfg(unix)] + { + let target = directory.path().join("target"); + let link = directory.path().join("link"); + std::os::unix::fs::symlink(&target, &link).unwrap(); + create_new(&link).unwrap_err(); + assert!(!target.exists()); + } +} + +#[test] +fn a_directory_sync_fails_for_a_missing_directory_only_on_unix() { + let directory = tempfile::tempdir().unwrap(); + sync_dir(directory.path()).unwrap(); + let missing = directory.path().join("missing"); + let synced = sync_dir(&missing); + #[cfg(unix)] + assert_eq!( + synced.unwrap_err().to_string(), + format!("cannot sync the directory {}", missing.display()) + ); + // Windows has no portable way to flush a directory. + #[cfg(not(unix))] + synced.unwrap(); +} + #[cfg(any(target_os = "linux", windows))] #[test] fn a_transport_without_a_native_handle_needs_no_clone_implementation() { diff --git a/nvx-rs/src/host/snapshot/capture.rs b/nvx-rs/src/host/snapshot/capture.rs index fc1e51733..0294c27c8 100644 --- a/nvx-rs/src/host/snapshot/capture.rs +++ b/nvx-rs/src/host/snapshot/capture.rs @@ -11,10 +11,8 @@ use super::{ CaptureContext, DESCRIPTION_FILE, KEY_FILE, MANIFEST_NAME, OPENVMM_DIR, SNAPSHOT_ARTIFACTS, SnapshotKey, SnapshotManifest, check_openvmm, }; -use crate::host::platform; +use crate::host::platform::{self, open_regular, read_at_most, sync_dir, write_private}; use crate::host::process::Exited; -use crate::host::store::files::{sync_dir, write_private}; -use crate::host::store::regular::{open_regular, read_at_most}; use crate::host::vm_directory::{OUTCOME, VmDir}; use crate::{Error, ErrorKind, Result}; diff --git a/nvx-rs/src/host/snapshot/key/tests.rs b/nvx-rs/src/host/snapshot/key/tests.rs index 76b9cb199..ed735b3a9 100644 --- a/nvx-rs/src/host/snapshot/key/tests.rs +++ b/nvx-rs/src/host/snapshot/key/tests.rs @@ -748,8 +748,8 @@ fn key_refusal(bytes: &[u8], code: ErrorCode) -> String { #[cfg(any(target_os = "linux", windows))] #[test] fn snapshot_open_refuses_other_key_contracts_and_filesystem_tags() { + use crate::host::platform::write_private; use crate::host::snapshot::{KEY_FILE, Snapshot}; - use crate::host::store::files::write_private; let changes: [fn(&mut SnapshotKey); 2] = [ |key| key.contracts.snapshot += 1, diff --git a/nvx-rs/src/host/snapshot/tests.rs b/nvx-rs/src/host/snapshot/tests.rs index a07bc18e8..ba6520887 100644 --- a/nvx-rs/src/host/snapshot/tests.rs +++ b/nvx-rs/src/host/snapshot/tests.rs @@ -1011,7 +1011,7 @@ pub(in crate::host) struct SnapshotFixture { impl SnapshotFixture { pub(in crate::host) fn new() -> Self { use crate::host::platform; - use crate::host::store::files::{create_new, write_private}; + use crate::host::platform::{create_new, write_private}; let root = tempfile::tempdir().unwrap(); let dir = root.path().join("snapshot"); @@ -1097,7 +1097,7 @@ fn snapshot_open_refuses_every_missing_entry() { #[test] fn snapshot_open_refuses_extra_entries_and_alternate_names() { - use crate::host::store::files::write_private; + use crate::host::platform::write_private; for nested in [false, true] { let fixture = SnapshotFixture::new(); @@ -1127,7 +1127,7 @@ fn snapshot_open_refuses_every_directory_in_place_of_a_regular_file() { #[test] fn snapshot_open_refuses_files_in_place_of_its_directories() { - use crate::host::store::files::write_private; + use crate::host::platform::write_private; let fixture = SnapshotFixture::new(); let file = fixture.root.path().join("file"); @@ -1159,7 +1159,7 @@ fn snapshot_open_refuses_both_artifact_length_mismatches() { #[test] fn snapshot_open_uses_the_strict_manifest_and_bounded_key_decoders() { - use crate::host::store::files::write_private; + use crate::host::platform::write_private; for bytes in [vec![0x80], vec![0; super::MAX_MANIFEST_LEN as usize + 1]] { let fixture = SnapshotFixture::new(); @@ -1184,7 +1184,7 @@ fn snapshot_open_uses_the_strict_manifest_and_bounded_key_decoders() { #[test] fn snapshot_open_refuses_a_key_from_another_manifest_machine() { - use crate::host::store::files::write_private; + use crate::host::platform::write_private; let mutations: [fn(&mut ManifestSpec); 4] = [ |spec| spec.cpu().digest[0] ^= 1, @@ -1205,7 +1205,7 @@ fn snapshot_open_refuses_a_key_from_another_manifest_machine() { #[test] fn snapshot_description_contents_do_not_affect_opening() { - use crate::host::store::files::write_private; + use crate::host::platform::write_private; let fixture = SnapshotFixture::new(); for contents in [b"".as_slice(), b"{broken JSON", &[0xff, 0, 0x80]] { @@ -1216,7 +1216,7 @@ fn snapshot_description_contents_do_not_affect_opening() { #[test] fn snapshot_open_refuses_a_compatible_key_with_another_capture_time() { - use crate::host::store::files::write_private; + use crate::host::platform::write_private; let mutations: &[fn(&mut ManifestSpec)] = &[ |spec| spec.time().capture_tsc += 1, @@ -1486,7 +1486,7 @@ impl RestoreFixture { let image = snapshot.root.path().join("image.raw"); let scratch = snapshot.root.path().join("scratch.raw"); for (index, path, length) in [(0, &image, MIB), (1, &scratch, 2 * MIB)] { - let file = crate::host::store::files::create_new(path).unwrap(); + let file = crate::host::platform::create_new(path).unwrap(); crate::host::platform::make_sparse(&file).unwrap(); file.set_len(length).unwrap(); let geometry = crate::host::sandbox::block_geometry(&file, path, "fixture").unwrap(); @@ -1577,12 +1577,12 @@ impl RestoreFixture { .unwrap() }); self.snapshot.key = SnapshotKey::new(&class, &manifest).unwrap(); - crate::host::store::files::write_private( + crate::host::platform::write_private( &self.snapshot.dir.join(KEY_FILE), &self.snapshot.key.to_bytes(), ) .unwrap(); - crate::host::store::files::write_private( + crate::host::platform::write_private( &self.snapshot.dir.join(OPENVMM_DIR).join(MANIFEST_NAME), &self.snapshot.spec.encode(), ) @@ -1877,7 +1877,7 @@ fn snapshot_nic_returns_only_the_captured_presence_and_address_without_rereading #[test] fn snapshot_open_refuses_captured_network_gateway_and_broadcast_addresses_as_io() { - use crate::host::store::files::write_private; + use crate::host::platform::write_private; for address in [[10, 42, 0, 0], [10, 42, 0, 1], [10, 42, 0, 255]] { let mut fixture = RestoreFixture::new(true); diff --git a/nvx-rs/src/host/store/files.rs b/nvx-rs/src/host/store/files.rs index 9807e59ac..8937e4957 100644 --- a/nvx-rs/src/host/store/files.rs +++ b/nvx-rs/src/host/store/files.rs @@ -1,16 +1,23 @@ //! The private files of the store: their creation, atomic replacement, reading, and removal, and //! the syncs of their directories. //! +//! The creation of a private file and its atomic replacement are [`platform`]'s: this module +//! re-exports its `create_new` and `write_private` for its callers until #1121 deletes them. +//! //! The sandbox's store opens, creates, replaces, and reads its lock, its records, its capability, //! and its log only as [`StoreFile`]s, each of which holds a file's path with what messages call //! it, so that no read or lock of one of them skips the checks that refuse a file that others may //! open. use std::fs::{self, File, OpenOptions}; -use std::io::{self, Read, Write}; +use std::io::{self, Read}; use std::path::{Path, PathBuf}; use crate::error::{Error, Result}; +#[cfg(unix)] +use crate::host::platform::PRIVATE_FILE_MODE; +use crate::host::platform::private_options; +pub(crate) use crate::host::platform::{create_new, write_private}; use crate::host::{filesystem, platform}; /// Builds an error mapper that keeps an I/O source under `context`. @@ -23,41 +30,6 @@ fn not_regular() -> io::Error { io::Error::other("is not a regular file") } -/// Mode of the store's files on Unix: readable and writable by the current user only. -#[cfg(unix)] -const PRIVATE_FILE_MODE: u32 = 0o600; - -/// Returns options that create a file readable only by the current user (mode 0600 on Unix) -/// and that open no link that its path names on Unix. -fn private_options() -> OpenOptions { - let mut options = OpenOptions::new(); - options.write(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - - options - .mode(PRIVATE_FILE_MODE) - .custom_flags(nix::fcntl::OFlag::O_NOFOLLOW.bits()); - } - options -} - -/// Creates the file at `path`, which must not exist, readable only by the current user. -/// -/// On Unix, the umask may clear bits of the mode that the file is created with, even the -/// current user's permission to read it, so the mode is set again through the new file. -pub(crate) fn create_new(path: &Path) -> io::Result { - let file = private_options().create_new(true).open(path)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - - file.set_permissions(fs::Permissions::from_mode(PRIVATE_FILE_MODE))?; - } - Ok(file) -} - /// Opens the lock file at `path`, creating it readable only by the current user if it does not /// exist. /// @@ -137,57 +109,6 @@ pub(crate) fn open_lock(path: &Path) -> io::Result { Ok(file) } -/// Atomically replaces `path` with `contents`, readable only by the current user. -/// -/// The contents go to a fresh temporary file beside `path`, which is flushed to disk and then -/// renamed over it, so a reader sees the old contents or the new ones, and a crash leaves at most -/// the temporary file. -pub(crate) fn write_private(path: &Path, contents: &[u8]) -> Result<()> { - let mut suffix = [0u8; 8]; - getrandom::fill(&mut suffix) - .map_err(|error| Error::other("cannot name a temporary file").with_source(error))?; - let name = path - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_default(); - let temporary = path.with_file_name(format!(".{name}.{}.tmp", u64::from_le_bytes(suffix))); - let written = (|| -> io::Result<()> { - let mut file = create_new(&temporary)?; - file.write_all(contents)?; - file.sync_all()?; - drop(file); - fs::rename(&temporary, path) - })(); - if written.is_err() { - let _ = fs::remove_file(&temporary); - } - written.map_err(io_error(format!("cannot write {}", path.display()))) -} - -/// Makes the changes to the entries of the directory `dir` so far durable, such as files that -/// were renamed into it or removed from it: a crash that keeps a later change keeps them too, even -/// one on another filesystem. -/// -/// On Unix, it syncs the directory. Windows has no portable way to flush a directory, so there a -/// rename or a removal is as durable as the filesystem makes it, as OpenVMM's publication of a -/// snapshot assumes. -pub(crate) fn sync_dir(dir: &Path) -> Result<()> { - #[cfg(unix)] - { - File::open(dir) - .and_then(|directory| directory.sync_all()) - .map_err(io_error(format!( - "cannot sync the directory {}", - dir.display() - ))) - } - #[cfg(not(unix))] - { - let _ = dir; - Ok(()) - } -} - /// Removes the file at `path` if it exists. pub(crate) fn remove_if_present(path: &Path) -> Result<()> { match fs::remove_file(path) {