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