diff --git a/Cargo.lock b/Cargo.lock index 2449a5d8..8d11b397 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1894,6 +1894,7 @@ dependencies = [ "pango", "pangocairo", "png", + "rustix", "schemars", "serde", "serde_ignored", diff --git a/Cargo.toml b/Cargo.toml index d871ff0d..f6b9622d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,8 @@ serde = { version = "1.0", features = ["derive"] } serde_ignored = "0.1" schemars = { version = "1.2", features = ["derive"], optional = true } libc = "0.2" +# Safe wrappers for the kernel interfaces that the std library does not cover. +rustix = { version = "1", features = ["event", "fs", "net", "pipe", "process", "stdio", "termios", "thread", "time"] } # Kernel random bytes for portal handle tokens and daemon protocol identifiers. getrandom = { version = "0.4", features = ["std"] } # Securely created, self-deleting temporary files for the OCR engine handoff. diff --git a/src/daemon/protocol_v2/action.rs b/src/daemon/protocol_v2/action.rs index ee637e91..bb378674 100644 --- a/src/daemon/protocol_v2/action.rs +++ b/src/daemon/protocol_v2/action.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use std::fs::{self, File, OpenOptions}; use std::io::{self, ErrorKind}; -use std::os::fd::AsRawFd; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; #[cfg(test)] @@ -11,6 +10,7 @@ use std::sync::LazyLock; use std::sync::Mutex; use anyhow::{Context, Result, anyhow, bail}; +use rustix::fs::{self as rustix_fs, FlockOperation}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -224,10 +224,13 @@ fn try_open_journal_lock(root: &Path, nonblocking: bool) -> Result> if !file.metadata()?.is_file() { bail!("action journal lock is not a regular file"); } - let operation = libc::LOCK_EX | if nonblocking { libc::LOCK_NB } else { 0 }; - // SAFETY: file owns the descriptor; flock retains no pointer. - if unsafe { libc::flock(file.as_raw_fd(), operation) } != 0 { - let error = io::Error::last_os_error(); + let operation = if nonblocking { + FlockOperation::NonBlockingLockExclusive + } else { + FlockOperation::LockExclusive + }; + if let Err(errno) = rustix_fs::flock(&file, operation) { + let error = io::Error::from(errno); if nonblocking && error.kind() == ErrorKind::WouldBlock { return Ok(None); } @@ -237,12 +240,9 @@ fn try_open_journal_lock(root: &Path, nonblocking: bool) -> Result> } fn unlock(file: &File) -> Result<()> { - // SAFETY: file owns the descriptor; flock retains no pointer. - if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) } == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()).context("failed to unlock action journal") - } + rustix_fs::flock(file, FlockOperation::Unlock) + .map_err(io::Error::from) + .context("failed to unlock action journal") } fn write_record(path: &Path, value: &T) -> Result<()> { diff --git a/src/daemon/protocol_v2/action/tests.rs b/src/daemon/protocol_v2/action/tests.rs index 8ba46acb..181ebeb3 100644 --- a/src/daemon/protocol_v2/action/tests.rs +++ b/src/daemon/protocol_v2/action/tests.rs @@ -1,5 +1,7 @@ use std::{env, thread}; +use rustix::fs::{self as rustix_fs, FlockOperation}; + use crate::{test_env, test_temp}; use super::*; @@ -179,21 +181,11 @@ fn event_loop_claim_and_finish_defer_instead_of_waiting_for_locks() { .write(true) .open(decision_path) .unwrap(); - assert_eq!( - // SAFETY: `held_decision` keeps the descriptor open for the call, - // and flock retains no pointer. - unsafe { libc::flock(held_decision.as_raw_fd(), libc::LOCK_EX) }, - 0 - ); + rustix_fs::flock(&held_decision, FlockOperation::LockExclusive).unwrap(); let ActionFinishOutcome::Deferred(action) = action.try_finish(true, None).unwrap() else { panic!("contended command finish should defer"); }; - assert_eq!( - // SAFETY: `held_decision` keeps the descriptor open for the call, - // and flock retains no pointer. - unsafe { libc::flock(held_decision.as_raw_fd(), libc::LOCK_UN) }, - 0 - ); + rustix_fs::flock(&held_decision, FlockOperation::Unlock).unwrap(); assert!(matches!( action.try_finish(true, None).unwrap(), ActionFinishOutcome::Complete diff --git a/src/daemon/protocol_v2/command/action_link.rs b/src/daemon/protocol_v2/command/action_link.rs index f91e706f..e474792e 100644 --- a/src/daemon/protocol_v2/command/action_link.rs +++ b/src/daemon/protocol_v2/command/action_link.rs @@ -2,6 +2,7 @@ use std::io::ErrorKind; use std::time::Duration; use anyhow::{Context, Result, bail}; +use rustix::fs::FlockOperation; use super::super::BootClock; use super::super::action::PreparedAction; @@ -37,14 +38,14 @@ fn claim_command_action_inner( let path = control_path(&command_root(), command_identity); let decision = open_lock(&path.join("decision.lock"), false)?; if nonblocking { - match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) { + match flock(&decision, FlockOperation::NonBlockingLockExclusive) { Ok(()) => {} Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(None), Err(error) => return Err(error).context("failed to try command-action decision lock"), } } else { let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; - lock_until(&decision, libc::LOCK_EX, deadline)?; + lock_until(&decision, FlockOperation::LockExclusive, deadline)?; } let mut control = read_control(&path)?; let exact = |status: &ActionStatus| match status { @@ -193,14 +194,14 @@ fn finish_command_action_inner( let path = control_path(&command_root(), command_identity); let decision = open_lock(&path.join("decision.lock"), false)?; if nonblocking { - match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) { + match flock(&decision, FlockOperation::NonBlockingLockExclusive) { Ok(()) => {} Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(false), Err(error) => return Err(error).context("failed to try command-action decision lock"), } } else { let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; - lock_until(&decision, libc::LOCK_EX, deadline)?; + lock_until(&decision, FlockOperation::LockExclusive, deadline)?; } let mut control = read_control(&path)?; let applied = result == CommandActionResult::Applied; diff --git a/src/daemon/protocol_v2/command/claimed.rs b/src/daemon/protocol_v2/command/claimed.rs index 1c386649..3fb2bfd5 100644 --- a/src/daemon/protocol_v2/command/claimed.rs +++ b/src/daemon/protocol_v2/command/claimed.rs @@ -1,6 +1,7 @@ use std::time::Duration; use anyhow::{Result, anyhow, bail}; +use rustix::fs::FlockOperation; #[cfg(test)] use super::super::wire::CommandControl; @@ -61,7 +62,8 @@ impl ClaimedCommand { journal.prepare_command(&expected.identity, &expected.target_daemon_token, action)?; let deadline = BootDeadline::from_nanos(expected.submission_clock.authorization_deadline_boottime_ns); - if let Err(error) = lock_until(&self.decision_lock, libc::LOCK_EX, deadline) { + if let Err(error) = lock_until(&self.decision_lock, FlockOperation::LockExclusive, deadline) + { let _ = journal.abandon(&prepared, "command decision lock was not reacquired"); return Err(error); } @@ -71,7 +73,11 @@ impl ClaimedCommand { let reason = "command was canceled during action preparation"; journal.abandon(&prepared, reason)?; let recovery_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; - lock_until(&self.decision_lock, libc::LOCK_EX, recovery_deadline)?; + lock_until( + &self.decision_lock, + FlockOperation::LockExclusive, + recovery_deadline, + )?; let mut latest = read_control(&self.control_path)?; if latest != current || !matches!( @@ -221,6 +227,6 @@ impl ClaimedCommand { impl Drop for ClaimedCommand { fn drop(&mut self) { - let _ = flock(&self.decision_lock, libc::LOCK_UN); + let _ = flock(&self.decision_lock, FlockOperation::Unlock); } } diff --git a/src/daemon/protocol_v2/command/client.rs b/src/daemon/protocol_v2/command/client.rs index 08ed3847..21c03c2d 100644 --- a/src/daemon/protocol_v2/command/client.rs +++ b/src/daemon/protocol_v2/command/client.rs @@ -3,6 +3,7 @@ use std::time::Duration; use std::{fs, process, thread}; use anyhow::{Context, Result, bail}; +use rustix::fs::FlockOperation; #[cfg(test)] use anyhow::anyhow; @@ -62,8 +63,16 @@ impl ClientCommand { create_private_directory(&stage)?; let decision_lock = open_lock(&stage.join("decision.lock"), true)?; let caller_lease = open_lock(&stage.join("caller.lease"), true)?; - lock_until(&caller_lease, libc::LOCK_SH, authorization_deadline)?; - lock_until(&decision_lock, libc::LOCK_EX, authorization_deadline)?; + lock_until( + &caller_lease, + FlockOperation::LockShared, + authorization_deadline, + )?; + lock_until( + &decision_lock, + FlockOperation::LockExclusive, + authorization_deadline, + )?; let mut control = CommandControl { protocol_version: DAEMON_COMMAND_PROTOCOL_VERSION, @@ -154,7 +163,7 @@ impl ClientCommand { let slice = now .checked_add(Duration::from_millis(20))? .min(self.response_deadline); - if !try_lock_until(&self.decision_lock, libc::LOCK_EX, slice)? { + if !try_lock_until(&self.decision_lock, FlockOperation::LockExclusive, slice)? { if slice < self.response_deadline { continue; } @@ -201,7 +210,11 @@ impl ClientCommand { let _ = unlock(&self.caller_lease); return Ok(TerminalCommandResult::AdmittedIndeterminate(reason)); } - lock_until(&self.decision_lock, libc::LOCK_EX, self.response_deadline)?; + lock_until( + &self.decision_lock, + FlockOperation::LockExclusive, + self.response_deadline, + )?; let mut control = read_control(&self.control_path)?; if cancel_open(&mut control)? { write_control(&self.control_path, &control)?; diff --git a/src/daemon/protocol_v2/command/layout.rs b/src/daemon/protocol_v2/command/layout.rs index 00e956c2..67efc6f9 100644 --- a/src/daemon/protocol_v2/command/layout.rs +++ b/src/daemon/protocol_v2/command/layout.rs @@ -1,13 +1,13 @@ use std::collections::BTreeSet; use std::fs::{self, File, OpenOptions}; use std::io::{self, ErrorKind}; -use std::os::fd::AsRawFd; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::path::{Path, PathBuf}; use std::thread; use std::time::Duration; use anyhow::{Context, Result, anyhow, bail}; +use rustix::fs::{self as rustix_fs, FlockOperation}; use serde::de::DeserializeOwned; use crate::durable_io::AtomicWriteOptions; @@ -279,22 +279,25 @@ pub(super) fn open_lock(path: &Path, create_new: bool) -> Result { Ok(file) } -pub(super) fn flock(file: &File, operation: libc::c_int) -> io::Result<()> { - // SAFETY: file owns a valid descriptor and flock does not retain it. - if unsafe { libc::flock(file.as_raw_fd(), operation) } == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) +pub(super) fn flock(file: &File, operation: FlockOperation) -> io::Result<()> { + rustix_fs::flock(file, operation).map_err(io::Error::from) +} + +fn nonblocking(operation: FlockOperation) -> FlockOperation { + match operation { + FlockOperation::LockShared => FlockOperation::NonBlockingLockShared, + FlockOperation::LockExclusive => FlockOperation::NonBlockingLockExclusive, + other => other, } } pub(super) fn unlock(file: &File) -> Result<()> { - flock(file, libc::LOCK_UN).context("failed to unlock protocol descriptor") + flock(file, FlockOperation::Unlock).context("failed to unlock protocol descriptor") } pub(super) fn lock_until( file: &File, - operation: libc::c_int, + operation: FlockOperation, deadline: BootDeadline, ) -> Result<()> { if try_lock_until(file, operation, deadline)? { @@ -306,11 +309,11 @@ pub(super) fn lock_until( pub(super) fn try_lock_until( file: &File, - operation: libc::c_int, + operation: FlockOperation, deadline: BootDeadline, ) -> Result { loop { - match flock(file, operation | libc::LOCK_NB) { + match flock(file, nonblocking(operation)) { Ok(()) => return Ok(true), Err(error) if error.kind() == ErrorKind::WouldBlock => { if BootClock::now()? >= deadline { @@ -341,7 +344,7 @@ pub(super) fn try_admission_lock(root: &Path, deadline: BootDeadline) -> Result< } Err(error) => return Err(error), }; - if try_lock_until(&file, libc::LOCK_EX, deadline)? { + if try_lock_until(&file, FlockOperation::LockExclusive, deadline)? { Ok(Some(file)) } else { Ok(None) diff --git a/src/daemon/protocol_v2/command/owner.rs b/src/daemon/protocol_v2/command/owner.rs index 62b0771a..07e7d001 100644 --- a/src/daemon/protocol_v2/command/owner.rs +++ b/src/daemon/protocol_v2/command/owner.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; use std::time::Duration; use anyhow::{Context, Result, bail}; +use rustix::fs::FlockOperation; use super::super::wire::{ ActionStatus, CallerDisposition, ChildReportStatus, CommandDecision, CommandResponse, @@ -191,7 +192,11 @@ impl CommandOwner { Err(error) => return Err(error), }; let retry_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; - if !try_lock_until(&decision_lock, libc::LOCK_EX, retry_deadline)? { + if !try_lock_until( + &decision_lock, + FlockOperation::LockExclusive, + retry_deadline, + )? { return Ok(None); } let Ok(mut control) = read_control(&path) else { @@ -316,7 +321,7 @@ impl CommandOwner { Err(error) => return Err(error), }; let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; - lock_until(&decision, libc::LOCK_EX, deadline)?; + lock_until(&decision, FlockOperation::LockExclusive, deadline)?; let mut control = read_control(&path)?; if control.identity == reference.command_identity && control.queue_order == reference.queue_order @@ -341,13 +346,13 @@ impl CommandOwner { observed: &super::super::wire::CommandControl, ) -> Result<()> { let caller_lease = open_lock(&path.join("caller.lease"), false)?; - match flock(&caller_lease, libc::LOCK_EX | libc::LOCK_NB) { + match flock(&caller_lease, FlockOperation::NonBlockingLockExclusive) { Ok(()) => {} Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(()), Err(error) => return Err(error).context("failed to inspect unqueued caller lease"), } let decision = open_lock(&path.join("decision.lock"), false)?; - match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) { + match flock(&decision, FlockOperation::NonBlockingLockExclusive) { Ok(()) => {} Err(error) if error.kind() == ErrorKind::WouldBlock => { unlock(&caller_lease)?; @@ -390,7 +395,7 @@ impl CommandOwner { } let decision_lock = open_lock(&path.join("decision.lock"), false)?; let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; - if !try_lock_until(&decision_lock, libc::LOCK_EX, deadline)? { + if !try_lock_until(&decision_lock, FlockOperation::LockExclusive, deadline)? { return Ok(None); } let mut control = read_control(&path)?; @@ -440,11 +445,11 @@ impl CommandOwner { for entry in read_dir_bounded(&controls_dir(&self.root), MAX_DIRECTORY_ENTRIES_PER_SCAN)? { let path = entry.path(); let lease = open_lock(&path.join("caller.lease"), false)?; - if flock(&lease, libc::LOCK_EX | libc::LOCK_NB).is_err() { + if flock(&lease, FlockOperation::NonBlockingLockExclusive).is_err() { continue; } let decision = open_lock(&path.join("decision.lock"), false)?; - if flock(&decision, libc::LOCK_EX | libc::LOCK_NB).is_err() { + if flock(&decision, FlockOperation::NonBlockingLockExclusive).is_err() { unlock(&lease)?; continue; } diff --git a/src/daemon/protocol_v2/command/recovery.rs b/src/daemon/protocol_v2/command/recovery.rs index d5639540..4a6f8524 100644 --- a/src/daemon/protocol_v2/command/recovery.rs +++ b/src/daemon/protocol_v2/command/recovery.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::time::Duration; use anyhow::{Result, anyhow, bail}; +use rustix::fs::FlockOperation; use super::super::BootClock; use super::super::wire::{ @@ -32,7 +33,7 @@ pub(super) fn recover_previous_generation(root: &Path, daemon_token: &str) -> Re continue; }; let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; - lock_until(&decision_lock, libc::LOCK_EX, deadline)?; + lock_until(&decision_lock, FlockOperation::LockExclusive, deadline)?; let Ok(mut control) = read_control(&path) else { unlock(&decision_lock)?; quarantine_entry(root, &path, QuarantineKind::Control)?; diff --git a/src/daemon/protocol_v2/command/staging.rs b/src/daemon/protocol_v2/command/staging.rs index 5b6cba77..48449536 100644 --- a/src/daemon/protocol_v2/command/staging.rs +++ b/src/daemon/protocol_v2/command/staging.rs @@ -3,6 +3,7 @@ use std::path::Path; use std::time::Duration; use anyhow::Result; +use rustix::fs::FlockOperation; use super::super::{BootClock, BootIdentity}; use super::MAX_COMMAND_STAGING_DIRECTORIES; @@ -24,7 +25,7 @@ pub(super) fn recover_staging(root: &Path) -> Result<()> { let _ = fs::remove_dir_all(path); continue; }; - if flock(&lease, libc::LOCK_EX | libc::LOCK_NB).is_ok() { + if flock(&lease, FlockOperation::NonBlockingLockExclusive).is_ok() { fs::remove_dir_all(path)?; } } diff --git a/src/daemon/protocol_v2/command/tests.rs b/src/daemon/protocol_v2/command/tests.rs index fdf5d264..4418c3bb 100644 --- a/src/daemon/protocol_v2/command/tests.rs +++ b/src/daemon/protocol_v2/command/tests.rs @@ -1,6 +1,8 @@ use std::env; use std::time::Duration; +use rustix::fs::FlockOperation; + use crate::{test_env, test_temp}; use super::super::BootClock; @@ -208,7 +210,7 @@ fn contended_published_command_claim_is_deferred_and_retried() { &token, ) .unwrap(); - flock(&client.decision_lock, libc::LOCK_EX).unwrap(); + flock(&client.decision_lock, FlockOperation::LockExclusive).unwrap(); assert!(owner.claim_next().unwrap().is_none()); let now = BootClock::now().unwrap(); @@ -252,7 +254,7 @@ fn contended_ref_less_recovery_is_deferred_and_retried() { &control.identity, )) .unwrap(); - flock(&client.decision_lock, libc::LOCK_EX).unwrap(); + flock(&client.decision_lock, FlockOperation::LockExclusive).unwrap(); assert!(owner.claim_next().unwrap().is_none()); unlock(&client.decision_lock).unwrap(); @@ -287,7 +289,7 @@ fn terminal_collection_defers_while_admission_is_busy() { drop(client); let admission = open_lock(&owner.root.join("admission.lock"), false).unwrap(); - flock(&admission, libc::LOCK_EX).unwrap(); + flock(&admission, FlockOperation::LockExclusive).unwrap(); assert_eq!(owner.collect_terminal().unwrap(), 0); let now = BootClock::now().unwrap(); let retry = owner @@ -715,20 +717,20 @@ fn lock_deadline_is_reported_without_parsing_error_text() { let path = root.join("structured-timeout.lock"); let held = open_lock(&path, true).unwrap(); let contender = open_lock(&path, false).unwrap(); - flock(&held, libc::LOCK_EX).unwrap(); + flock(&held, FlockOperation::LockExclusive).unwrap(); let deadline = BootClock::now() .unwrap() .checked_add(Duration::from_millis(5)) .unwrap(); - assert!(!try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap()); + assert!(!try_lock_until(&contender, FlockOperation::LockExclusive, deadline).unwrap()); unlock(&held).unwrap(); let deadline = BootClock::now() .unwrap() .checked_add(Duration::from_millis(5)) .unwrap(); - assert!(try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap()); + assert!(try_lock_until(&contender, FlockOperation::LockExclusive, deadline).unwrap()); unlock(&contender).unwrap(); }); } diff --git a/src/durable_io/lock.rs b/src/durable_io/lock.rs index f8dab3f7..a236e4fd 100644 --- a/src/durable_io/lock.rs +++ b/src/durable_io/lock.rs @@ -4,19 +4,11 @@ use std::fs::File; use std::io; #[cfg(unix)] -use std::os::unix::io::AsRawFd; +use rustix::fs::{self as rustix_fs, FlockOperation}; #[cfg(unix)] -fn flock(file: &File, op: libc::c_int) -> io::Result<()> { - let fd = file.as_raw_fd(); - // SAFETY: `fd` stays open for the borrowed `file`, and flock takes no - // pointers or ownership of the descriptor. - let result = unsafe { libc::flock(fd, op) }; - if result == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } +fn flock(file: &File, operation: FlockOperation) -> io::Result<()> { + rustix_fs::flock(file, operation).map_err(io::Error::from) } #[cfg(not(unix))] @@ -30,7 +22,7 @@ fn flock(_file: &File, _op: i32) -> io::Result<()> { pub fn lock_shared(file: &File) -> io::Result<()> { #[cfg(unix)] { - flock(file, libc::LOCK_SH) + flock(file, FlockOperation::LockShared) } #[cfg(not(unix))] { @@ -41,7 +33,7 @@ pub fn lock_shared(file: &File) -> io::Result<()> { pub fn lock_exclusive(file: &File) -> io::Result<()> { #[cfg(unix)] { - flock(file, libc::LOCK_EX) + flock(file, FlockOperation::LockExclusive) } #[cfg(not(unix))] { @@ -52,7 +44,7 @@ pub fn lock_exclusive(file: &File) -> io::Result<()> { pub fn try_lock_exclusive(file: &File) -> io::Result<()> { #[cfg(unix)] { - flock(file, libc::LOCK_EX | libc::LOCK_NB) + flock(file, FlockOperation::NonBlockingLockExclusive) } #[cfg(not(unix))] { @@ -63,7 +55,7 @@ pub fn try_lock_exclusive(file: &File) -> io::Result<()> { pub fn unlock(file: &File) -> io::Result<()> { #[cfg(unix)] { - flock(file, libc::LOCK_UN) + flock(file, FlockOperation::Unlock) } #[cfg(not(unix))] { diff --git a/tools/csharp/Commands/ChecksCommand.cs b/tools/csharp/Commands/ChecksCommand.cs index f897b69b..e979081f 100644 --- a/tools/csharp/Commands/ChecksCommand.cs +++ b/tools/csharp/Commands/ChecksCommand.cs @@ -340,6 +340,7 @@ internal static class ChecksCommand ["pango"] = ["pango"], ["pangocairo"] = ["pango", "cairo"], ["png"] = [], + ["rustix"] = [], ["schemars"] = [], ["serde"] = [], ["serde_ignored"] = [],