Merge pull request #427 from devmobasa/chore/dependency-hardening

Harden dependencies and move kernel calls to rustix
This commit is contained in:
devmobasa authored and GitHub committed 2026-10-31 23:39:34 +01:00
commit 4e35f55a16
68 files changed
+1299 -1806

No files matched your search

+2
View File
@@ -16,6 +16,8 @@
binary points at that page. Renaming or removing an id silently drops the binary points at that page. Renaming or removing an id silently drops the
prefill for binaries that can never be updated retroactively. prefill for binaries that can never be updated retroactively.
- Keep Linux system dependencies aligned with real build needs for Wayland, Cairo, Pango, GTK4/libadwaita, D-Bus, packaging, and Nix checks. - Keep Linux system dependencies aligned with real build needs for Wayland, Cairo, Pango, GTK4/libadwaita, D-Bus, packaging, and Nix checks.
- `deny.toml` is the dependency policy and the `dependency-policy` job enforces it. Advisories report without blocking, so a new advisory cannot fail an unrelated pull request; bans, licenses, and sources block.
- `dependabot.yml` proposes weekly Cargo and GitHub Actions updates. Its zbus major-version ignore is coupled to the advisory ignores in `deny.toml`.
- Do not weaken release checks or skip package layout/version validation without a documented reason. - Do not weaken release checks or skip package layout/version validation without a documented reason.
## Coupled Changes ## Coupled Changes
+27
View File
@@ -0,0 +1,27 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
compatible:
update-types:
- minor
- patch
ignore:
# ksni, which the tray feature needs, still requires zbus 5. Remove this
# with the matching advisory ignores in deny.toml.
- dependency-name: zbus
update-types:
- version-update:semver-major
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns:
- "*"
+23
View File
@@ -144,3 +144,26 @@ jobs:
- name: Check flake package and development shell instantiation - name: Check flake package and development shell instantiation
run: dotnet run tools/wayscriber.cs --no-build -- check nix-instantiation run: dotnet run tools/wayscriber.cs --no-build -- check nix-instantiation
dependency-policy:
name: Dependency policy (${{ matrix.checks }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
checks:
- advisories
- bans licenses sources
# A newly published advisory must not block unrelated pull requests.
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check dependency policy
uses: EmbarkStudios/cargo-deny-action@v2
with:
arguments: --workspace --locked
command: check ${{ matrix.checks }}
rust-version: 1.99.1
+3
View File
@@ -40,6 +40,7 @@
- Never reach inside a poisoned lock. Use `expect` when the critical section cannot panic, or a fallback that leaves the data alone. Tests that serialize on process-wide state use `test_env::lock()` or a `test_env::Lease`. - Never reach inside a poisoned lock. Use `expect` when the critical section cannot panic, or a fallback that leaves the data alone. Tests that serialize on process-wide state use `test_env::lock()` or a `test_env::Lease`.
- `catch_unwind` is reserved for the reviewed worker-isolation sites that carry an `#[expect]` reason. Tests observe panics through a joined scoped thread. - `catch_unwind` is reserved for the reviewed worker-isolation sites that carry an `#[expect]` reason. Tests observe panics through a joined scoped thread.
- Getters are `foo()`/`foo_mut()`, a fallible constructor is `try_new`, and type lifetimes have descriptive names. Every `unsafe` block has a `// SAFETY:` comment that says why it is sound. - Getters are `foo()`/`foo_mut()`, a fallible constructor is `try_new`, and type lifetimes have descriptive names. Every `unsafe` block has a `// SAFETY:` comment that says why it is sound.
- Make kernel calls through `std` or `rustix`, which need no `unsafe`, and rename on import when a module name clashes with `std` (`use rustix::fs as rustix_fs`). Use raw `libc` calls only where neither fits: signal handlers, the post-`clone` broker stub, `fail_stop`, the zero-signal pidfd check, validating the broker's inherited descriptors, the inotify header parser, `dup2` onto a fixed descriptor number, and `localtime_r`. `libc` constants stay fine.
- Convert numbers losslessly with `From` (`f64::from(width)`) wherever it can be called; `const` initializers keep `as`. Take the middle of two values with `midpoint` (`f64::midpoint(a, b)`), which cannot overflow. - Convert numbers losslessly with `From` (`f64::from(width)`) wherever it can be called; `const` initializers keep `as`. Take the middle of two values with `midpoint` (`f64::midpoint(a, b)`), which cannot overflow.
- Inline named format arguments: `format!("{name}: {err}")`. - Inline named format arguments: `format!("{name}: {err}")`.
- Use `map_or`, `map_or_else`, `is_some_and`, or `is_ok_and` instead of `map(..).unwrap_or(..)`. Use `let ... else` for a `match` that only unwraps or leaves, and `if let ... else` for a `match` with one pattern and a fallback. - Use `map_or`, `map_or_else`, `is_some_and`, or `is_ok_and` instead of `map(..).unwrap_or(..)`. Use `let ... else` for a `match` that only unwraps or leaves, and `if let ... else` for a `match` with one pattern and a fallback.
@@ -51,8 +52,10 @@
- Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests. - Config changes often require `config.example.toml`, `docs/CONFIG.md`, configurator models/views, schema behavior, and tests.
- Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests. - Tool/action/keybinding changes often require config defaults, action metadata, help/command UI, toolbar UI, configurator labels/search, docs, and tests.
- Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`. - Daemon, service, shortcut, path, and packaging changes often need updates across `src/daemon/`, `src/paths/`, `src/systemd_user_service.rs`, `src/shortcut_hint.rs`, `configurator/src/app/daemon_setup/`, and `packaging/`.
- Dependency changes must satisfy `deny.toml`. A new license or duplicate version needs an entry there with its reason.
## Validation ## Validation
- For a fast full-suite run while iterating, use `cargo nextest run --workspace --all-features`. Each test gets its own process, so it runs in parallel despite the native-font race that keeps the gate serial. It skips doctests and the isolated render tests, so it does not replace the gate.
- Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, use statements, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles. - Full local CI is `./tools/lint-and-test.sh`. It needs the .NET SDK selected by `global.json`. Without it, `cargo test` still runs the Rust source guards in `tests/repository_guards` (process sites, config writers, shared dependencies, use statements, no Python), but not the C# checks, including the source-coverage check that finds `.rs` files Cargo never compiles.
- That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests. - That script runs the C# repository checks (assets, version, nixpkgs recipe, source coverage, legacy tools), the C# tool tests (which also run the packaging shell contracts), `cargo fmt --all -- --check`, clippy with all targets/features, all-feature tests, and no-default-feature tests.
- Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well. - Run `git diff --check` before handoff. When relevant files are untracked and the index must stay unchanged, check those files directly as well.
+17
View File
@@ -125,6 +125,16 @@ cargo test session::
cargo test -p wayscriber-configurator cargo test -p wayscriber-configurator
``` ```
For a fast run of the whole suite, use [cargo-nextest](https://nexte.st/), which the Nix shell
includes. It gives each test its own process, so it can run in parallel despite the native font
race that keeps the local CI entry point serial:
```bash
cargo nextest run --workspace --all-features
```
It skips doctests and the isolated render tests, so it does not replace that entry point.
Before submitting a broad or cross-package change, run the local CI entry point: Before submitting a broad or cross-package change, run the local CI entry point:
```bash ```bash
@@ -139,6 +149,13 @@ Rust tests include the repository guards in `tests/repository_guards`. The sourc
gate uses current rustc dep-info and rejects tracked or unignored `.rs` files that are outside gate uses current rustc dep-info and rejects tracked or unignored `.rs` files that are outside
the supported Cargo target/feature matrix. the supported Cargo target/feature matrix.
Dependency changes must also satisfy the policy in `deny.toml`, which CI enforces in its own
job. The Nix shell includes `cargo-deny`; elsewhere, install it and run:
```bash
cargo deny --workspace --locked check
```
The all-feature portal transport tests require `dbus-daemon`. Each fixture owns a private The all-feature portal transport tests require `dbus-daemon`. Each fixture owns a private
session bus and connects through its explicit address, leaving the desktop session bus alone. session bus and connects through its explicit address, leaving the desktop session bus alone.
Generated
+12 -37
View File
@@ -294,7 +294,7 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
dependencies = [ dependencies = [
"getrandom 0.4.3", "getrandom",
] ]
[[package]] [[package]]
@@ -513,18 +513,6 @@ dependencies = [
"system-deps 9.0.1", "system-deps 9.0.1",
] ]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"wasip2",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.4.3" version = "0.4.3"
@@ -534,7 +522,7 @@ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
"libc", "libc",
"r-efi 6.0.0", "r-efi",
"wasm-bindgen", "wasm-bindgen",
] ]
@@ -882,6 +870,12 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "lexopt"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413"
[[package]] [[package]]
name = "libadwaita" name = "libadwaita"
version = "0.9.2" version = "0.9.2"
@@ -1155,12 +1149,6 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "r-efi" name = "r-efi"
version = "6.0.0" version = "6.0.0"
@@ -1528,7 +1516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.3", "getrandom",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -1724,15 +1712,6 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen",
]
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.129" version = "0.2.129"
@@ -1903,17 +1882,19 @@ dependencies = [
"anyhow", "anyhow",
"cairo-rs", "cairo-rs",
"flate2", "flate2",
"getrandom 0.3.4", "getrandom",
"glib", "glib",
"gtk4", "gtk4",
"gtk4-layer-shell", "gtk4-layer-shell",
"input", "input",
"ksni", "ksni",
"lexopt",
"libc", "libc",
"log", "log",
"pango", "pango",
"pangocairo", "pangocairo",
"png", "png",
"rustix",
"schemars", "schemars",
"serde", "serde",
"serde_ignored", "serde_ignored",
@@ -2035,12 +2016,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "xcursor" name = "xcursor"
version = "0.3.11" version = "0.3.11"
+8 -2
View File
@@ -3,6 +3,7 @@ name = "wayscriber"
version = "0.9.28" version = "0.9.28"
edition = "2024" edition = "2024"
rust-version = "1.99.1" rust-version = "1.99.1"
license = "MIT"
description = "Screen annotation tool for Wayland compositors" description = "Screen annotation tool for Wayland compositors"
homepage = "https://wayscriber.com" homepage = "https://wayscriber.com"
repository = "https://github.com/devmobasa/wayscriber" repository = "https://github.com/devmobasa/wayscriber"
@@ -92,10 +93,16 @@ serde = { version = "1.0", features = ["derive"] }
serde_ignored = "0.1" serde_ignored = "0.1"
schemars = { version = "1.2", features = ["derive"], optional = true } schemars = { version = "1.2", features = ["derive"], optional = true }
libc = "0.2" 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", "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. # Securely created, self-deleting temporary files for the OCR engine handoff.
tempfile = "3" tempfile = "3"
flate2 = "1.0" flate2 = "1.0"
zune-jpeg = "0.5" zune-jpeg = "0.5"
# Command-line tokenizing without a parser framework.
lexopt = "0.3"
# Grapheme/word segmentation for the text-editor caret and selection. # Grapheme/word segmentation for the text-editor caret and selection.
unicode-segmentation = "1" unicode-segmentation = "1"
@@ -105,7 +112,6 @@ tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "time", "syn
# Screenshot capture # Screenshot capture
zbus = { version = "5.0", optional = true, default-features = false, features = ["tokio"] } zbus = { version = "5.0", optional = true, default-features = false, features = ["tokio"] }
getrandom = { version = "0.3", optional = true }
serde_json = "1.0" serde_json = "1.0"
png = "0.18" png = "0.18"
@@ -125,7 +131,7 @@ xkbcommon = { version = "0.8", optional = true }
tablet-input = [] tablet-input = []
# D-Bus dependent functionality (portal capture, notifications, tray) # D-Bus dependent functionality (portal capture, notifications, tray)
dbus = ["zbus"] dbus = ["zbus"]
portal = ["dbus", "dep:getrandom"] portal = ["dbus"]
tray = ["dbus", "ksni"] tray = ["dbus", "ksni"]
config-schema = ["dep:schemars"] config-schema = ["dep:schemars"]
# GTK4-rendered toolbars on layer-shell compositors; the built-in Cairo # GTK4-rendered toolbars on layer-shell compositors; the built-in Cairo
+2
View File
@@ -3,6 +3,8 @@ name = "wayscriber-configurator"
version = "0.9.28" version = "0.9.28"
edition = "2024" edition = "2024"
rust-version = "1.99.1" rust-version = "1.99.1"
license = "MIT"
publish = false
[lints] [lints]
workspace = true workspace = true
+42
View File
@@ -0,0 +1,42 @@
# Dependency policy for the workspace. CI runs it in the `dependency-policy` job;
# `cargo deny --workspace --locked check` gives the same result locally.
[graph]
all-features = true
targets = [
{ triple = "x86_64-unknown-linux-gnu" },
{ triple = "aarch64-unknown-linux-gnu" },
]
[advisories]
# zbus 6 absorbed these crates. ksni, which the `tray` feature needs, still
# requires zbus 5, so remove these entries once it moves.
ignore = [
{ id = "RUSTSEC-2026-0370", reason = "zvariant: blocked on ksni supporting zbus 6" },
{ id = "RUSTSEC-2026-0371", reason = "zbus_names: blocked on ksni supporting zbus 6" },
{ id = "RUSTSEC-2026-0372", reason = "zvariant_derive: blocked on ksni supporting zbus 6" },
]
[licenses]
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"MIT",
"Unicode-3.0",
"Unlicense",
]
[bans]
multiple-versions = "deny"
wildcards = "deny"
allow-wildcard-paths = true
skip = [
{ crate = "miniz_oxide@0.8", reason = "png needs 0.8 while flate2 needs 0.9" },
{ crate = "syn@2", reason = "proc macros have not all moved to syn 3" },
{ crate = "system-deps@7", reason = "build-only: the GTK sys crates span three majors" },
{ crate = "system-deps@8", reason = "build-only: the GTK sys crates span three majors" },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
+2
View File
@@ -32,6 +32,8 @@
servicePath = pkgs.lib.makeBinPath [ pkgs.grim pkgs.slurp pkgs.wl-clipboard ]; servicePath = pkgs.lib.makeBinPath [ pkgs.grim pkgs.slurp pkgs.wl-clipboard ];
developmentInputs = with pkgs; [ developmentInputs = with pkgs; [
rustToolchain rustToolchain
cargo-deny
cargo-nextest
util-linux util-linux
pkg-config pkg-config
cairo cairo
+15 -9
View File
@@ -22,10 +22,14 @@ use crate::{
}; };
use anyhow::Context; use anyhow::Context;
use env::env_flag_enabled; use env::env_flag_enabled;
#[cfg(unix)]
use rustix::process as rustix_process;
use session::run_session_cli_commands; use session::run_session_cli_commands;
use std::ffi::OsString; use std::ffi::OsString;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::ErrorKind; use std::io::ErrorKind;
#[cfg(unix)]
use std::io::IsTerminal;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use usage::{log_overlay_controls, print_usage}; use usage::{log_overlay_controls, print_usage};
@@ -170,16 +174,17 @@ fn run_update_check() -> anyhow::Result<()> {
#[cfg(unix)] #[cfg(unix)]
fn detach_from_tty() { fn detach_from_tty() {
// Start a new session to drop the controlling terminal (prevents stuck shells). // Start a new session to drop the controlling terminal (prevents stuck shells).
// SAFETY: setsid takes no arguments; a failure only leaves the session as is. // A failure only leaves the session as is.
unsafe { let _ = rustix_process::setsid();
let _ = libc::setsid();
}
// Point stdio that still refers to a TTY at /dev/null. Closing it instead // Point stdio that still refers to a TTY at /dev/null. Closing it instead
// frees the descriptor number for the next open, and the logger's stderr // frees the descriptor number for the next open, and the logger's stderr
// writes would then land in whatever file that open returned. // writes would then land in whatever file that open returned.
for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] { let stdio = [
// SAFETY: isatty only inspects the descriptor number it is given. (libc::STDIN_FILENO, io::stdin().is_terminal()),
let is_tty = unsafe { libc::isatty(fd) } == 1; (libc::STDOUT_FILENO, io::stdout().is_terminal()),
(libc::STDERR_FILENO, io::stderr().is_terminal()),
];
for (fd, is_tty) in stdio {
if is_tty && let Err(error) = redirect_to_dev_null(fd) { if is_tty && let Err(error) = redirect_to_dev_null(fd) {
log::warn!("Failed to detach descriptor {fd} from the terminal: {error}"); log::warn!("Failed to detach descriptor {fd} from the terminal: {error}");
} }
@@ -414,6 +419,8 @@ mod tests {
use std::io::Write; use std::io::Write;
use std::os::fd::AsRawFd; use std::os::fd::AsRawFd;
use rustix::io as rustix_io;
let temp = test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let path = temp.path().join("was-a-terminal"); let path = temp.path().join("was-a-terminal");
let mut file = File::create(&path).unwrap(); let mut file = File::create(&path).unwrap();
@@ -422,8 +429,7 @@ mod tests {
redirect_to_dev_null(file.as_raw_fd()).unwrap(); redirect_to_dev_null(file.as_raw_fd()).unwrap();
file.write_all(b"after").unwrap(); file.write_all(b"after").unwrap();
// SAFETY: F_GETFD only inspects the descriptor the file still owns. rustix_io::fcntl_getfd(&file).unwrap();
assert!(unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFD) } >= 0);
assert_eq!(fs::read(&path).unwrap(), b"before "); assert_eq!(fs::read(&path).unwrap(), b"before ");
} }
@@ -64,11 +64,8 @@ pub(super) fn dispatch_events(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io;
use std::io::ErrorKind;
use std::os::fd::AsRawFd;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Instant; use std::time::{Duration, Instant};
use super::*; use super::*;
use crate::backend::wayland::session::{ use crate::backend::wayland::session::{
@@ -126,27 +123,12 @@ mod tests {
} }
fn wait_for_runtime_wake(runtime_wake: &RuntimeWakeSource) { fn wait_for_runtime_wake(runtime_wake: &RuntimeWakeSource) {
let mut pollfd = libc::pollfd { assert!(
fd: runtime_wake.poll_fd().as_raw_fd(), runtime_wake
events: libc::POLLIN, .is_readable_within(Duration::from_secs(1))
revents: 0, .unwrap(),
}; "persistence worker did not wake the production route"
loop { );
// SAFETY: pollfd and the runtime wake descriptor stay valid for this
// bounded test wait.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
if ready > 0 {
assert_ne!(pollfd.revents & libc::POLLIN, 0);
return;
}
if ready == 0 {
panic!("persistence worker did not wake the production route");
}
let err = io::Error::last_os_error();
if err.kind() != ErrorKind::Interrupted {
panic!("runtime wake poll failed: {err}");
}
}
} }
#[test] #[test]
+50 -107
View File
@@ -3,8 +3,9 @@ use std::io::ErrorKind;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::warn; use log::warn;
use rustix::event::{PollFd, PollFlags};
use std::env; use std::env;
use std::os::fd::{AsRawFd, RawFd}; use std::os::fd::BorrowedFd;
use std::time::Duration; use std::time::Duration;
use wayland_client::EventQueue; use wayland_client::EventQueue;
use wayland_client::backend::{ReadEventsGuard, WaylandError}; use wayland_client::backend::{ReadEventsGuard, WaylandError};
@@ -13,7 +14,8 @@ use super::super::state::WaylandState;
#[cfg(test)] #[cfg(test)]
use super::runtime_wake::timeout_to_poll_ms; use super::runtime_wake::timeout_to_poll_ms;
use super::runtime_wake::{ use super::runtime_wake::{
RuntimeWakeSource, TerminalReadinessPolicy, poll_with_retry, validate_poll_readiness, RuntimeWakeSource, TerminalReadinessPolicy, poll_for_ms, poll_with_retry,
validate_poll_readiness,
}; };
use crate::capture::CaptureFailureKind; use crate::capture::CaptureFailureKind;
use crate::i18n::tr; use crate::i18n::tr;
@@ -73,13 +75,13 @@ struct RuntimeReadOutcome {
} }
trait PreparedWaylandRead { trait PreparedWaylandRead {
fn connection_raw_fd(&self) -> RawFd; fn connection_fd(&self) -> BorrowedFd<'_>;
fn read(self) -> Result<usize, WaylandError>; fn read(self) -> Result<usize, WaylandError>;
} }
impl PreparedWaylandRead for ReadEventsGuard { impl PreparedWaylandRead for ReadEventsGuard {
fn connection_raw_fd(&self) -> RawFd { fn connection_fd(&self) -> BorrowedFd<'_> {
self.connection_fd().as_raw_fd() ReadEventsGuard::connection_fd(self)
} }
fn read(self) -> Result<usize, WaylandError> { fn read(self) -> Result<usize, WaylandError> {
@@ -87,52 +89,28 @@ impl PreparedWaylandRead for ReadEventsGuard {
} }
} }
/// Applies the retry and readiness rules to `poll_once`, which reports the
/// Wayland and runtime wake readiness in that order. Both empty means timeout.
fn poll_runtime_fds_with( fn poll_runtime_fds_with(
wayland_fd: RawFd,
wake_fd: RawFd,
timeout: Option<Duration>, timeout: Option<Duration>,
mut poll_once: impl FnMut(&mut [libc::pollfd], i32) -> io::Result<i32>, mut poll_once: impl FnMut(i32) -> io::Result<[PollFlags; 2]>,
) -> io::Result<RuntimePollReadiness> { ) -> io::Result<RuntimePollReadiness> {
poll_with_retry(timeout, |timeout_ms| { poll_with_retry(timeout, |timeout_ms| {
let mut pollfds = [ let [wayland, wake] = poll_once(timeout_ms)?;
libc::pollfd { if wayland.is_empty() && wake.is_empty() {
fd: wayland_fd, return Ok(None);
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: wake_fd,
events: libc::POLLIN,
revents: 0,
},
];
match poll_once(&mut pollfds, timeout_ms) {
Ok(0) => Ok(None),
Ok(_) => {
let readiness = RuntimePollReadiness {
// A Wayland socket can report final buffered protocol data
// together with HUP/ERR. Read and dispatch it first so the
// compositor's actual disconnect error is preserved.
wayland: validate_poll_readiness(
&pollfds[0],
"Wayland",
TerminalReadinessPolicy::ReadBuffered,
)?,
wake: validate_poll_readiness(
&pollfds[1],
"runtime wake",
TerminalReadinessPolicy::Reject,
)?,
};
if !readiness.wayland && !readiness.wake {
return Err(io::Error::other(
"runtime poll reported readiness without a readable descriptor",
));
}
Ok(Some(readiness))
}
Err(err) => Err(err),
} }
Ok(Some(RuntimePollReadiness {
// A Wayland socket can report final buffered protocol data
// together with HUP/ERR. Read and dispatch it first so the
// compositor's actual disconnect error is preserved.
wayland: validate_poll_readiness(
wayland,
"Wayland",
TerminalReadinessPolicy::ReadBuffered,
)?,
wake: validate_poll_readiness(wake, "runtime wake", TerminalReadinessPolicy::Reject)?,
}))
}) })
.map(|readiness| { .map(|readiness| {
readiness.unwrap_or(RuntimePollReadiness { readiness.unwrap_or(RuntimePollReadiness {
@@ -143,25 +121,17 @@ fn poll_runtime_fds_with(
} }
fn poll_runtime_fds( fn poll_runtime_fds(
wayland_fd: RawFd, wayland_fd: BorrowedFd<'_>,
wake_fd: RawFd, wake_fd: BorrowedFd<'_>,
timeout: Option<Duration>, timeout: Option<Duration>,
) -> io::Result<RuntimePollReadiness> { ) -> io::Result<RuntimePollReadiness> {
poll_runtime_fds_with(wayland_fd, wake_fd, timeout, |pollfds, timeout_ms| { poll_runtime_fds_with(timeout, |timeout_ms| {
// SAFETY: pollfds is a live mutable slice for the duration of this call, let mut pollfds = [
// and both descriptors remain borrowed by their runtime owners. PollFd::from_borrowed_fd(wayland_fd, PollFlags::IN),
let ready = unsafe { PollFd::from_borrowed_fd(wake_fd, PollFlags::IN),
libc::poll( ];
pollfds.as_mut_ptr(), poll_for_ms(&mut pollfds, timeout_ms)?;
pollfds.len() as libc::nfds_t, Ok([pollfds[0].revents(), pollfds[1].revents()])
timeout_ms,
)
};
if ready < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ready)
}
}) })
} }
@@ -170,12 +140,8 @@ fn read_events_with_runtime_wake(
runtime_wake: &RuntimeWakeSource, runtime_wake: &RuntimeWakeSource,
timeout: Option<Duration>, timeout: Option<Duration>,
) -> Result<RuntimeReadOutcome> { ) -> Result<RuntimeReadOutcome> {
let readiness = poll_runtime_fds( let readiness = poll_runtime_fds(guard.connection_fd(), runtime_wake.poll_fd(), timeout)
guard.connection_raw_fd(), .context("runtime readiness poll failed")?;
runtime_wake.poll_fd().as_raw_fd(),
timeout,
)
.context("runtime readiness poll failed")?;
let wayland_read = if readiness.wayland { let wayland_read = if readiness.wayland {
let _ = let _ =
@@ -331,6 +297,7 @@ mod tests {
use crate::set_runtime_session_override; use crate::set_runtime_session_override;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::io::Write; use std::io::Write;
use std::os::fd::AsFd;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
@@ -372,12 +339,7 @@ mod tests {
wake.handle().wake().unwrap(); wake.handle().wake().unwrap();
assert_eq!( assert_eq!(
poll_runtime_fds( poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
wayland_read.as_raw_fd(),
wake.poll_fd().as_raw_fd(),
Some(Duration::ZERO),
)
.unwrap(),
RuntimePollReadiness { RuntimePollReadiness {
wayland: false, wayland: false,
wake: true, wake: true,
@@ -392,12 +354,7 @@ mod tests {
wayland_write.write_all(&[1]).unwrap(); wayland_write.write_all(&[1]).unwrap();
assert_eq!( assert_eq!(
poll_runtime_fds( poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
wayland_read.as_raw_fd(),
wake.poll_fd().as_raw_fd(),
Some(Duration::ZERO),
)
.unwrap(),
RuntimePollReadiness { RuntimePollReadiness {
wayland: true, wayland: true,
wake: false, wake: false,
@@ -413,12 +370,7 @@ mod tests {
drop(wayland_write); drop(wayland_write);
assert_eq!( assert_eq!(
poll_runtime_fds( poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
wayland_read.as_raw_fd(),
wake.poll_fd().as_raw_fd(),
Some(Duration::ZERO),
)
.unwrap(),
RuntimePollReadiness { RuntimePollReadiness {
wayland: true, wayland: true,
wake: false, wake: false,
@@ -434,8 +386,8 @@ mod tests {
assert_eq!( assert_eq!(
poll_runtime_fds( poll_runtime_fds(
wayland_read.as_raw_fd(), wayland_read.as_fd(),
wake.poll_fd().as_raw_fd(), wake.poll_fd(),
Some(Duration::from_secs(30)), Some(Duration::from_secs(30)),
) )
.unwrap(), .unwrap(),
@@ -454,12 +406,7 @@ mod tests {
wake.handle().wake().unwrap(); wake.handle().wake().unwrap();
assert_eq!( assert_eq!(
poll_runtime_fds( poll_runtime_fds(wayland_read.as_fd(), wake.poll_fd(), Some(Duration::ZERO),).unwrap(),
wayland_read.as_raw_fd(),
wake.poll_fd().as_raw_fd(),
Some(Duration::ZERO),
)
.unwrap(),
RuntimePollReadiness { RuntimePollReadiness {
wayland: true, wayland: true,
wake: true, wake: true,
@@ -475,8 +422,8 @@ mod tests {
} }
impl PreparedWaylandRead for FakePreparedWaylandRead { impl PreparedWaylandRead for FakePreparedWaylandRead {
fn connection_raw_fd(&self) -> RawFd { fn connection_fd(&self) -> BorrowedFd<'_> {
self.stream.as_raw_fd() self.stream.as_fd()
} }
fn read(mut self) -> Result<usize, WaylandError> { fn read(mut self) -> Result<usize, WaylandError> {
@@ -684,13 +631,12 @@ mod tests {
#[test] #[test]
fn runtime_poll_retries_interruption() { fn runtime_poll_retries_interruption() {
let mut calls = 0; let mut calls = 0;
let readiness = poll_runtime_fds_with(10, 11, None, |pollfds, _| { let readiness = poll_runtime_fds_with(None, |_| {
calls += 1; calls += 1;
if calls == 1 { if calls == 1 {
return Err(io::Error::from(ErrorKind::Interrupted)); return Err(io::Error::from(ErrorKind::Interrupted));
} }
pollfds[1].revents = libc::POLLIN; Ok([PollFlags::empty(), PollFlags::IN])
Ok(1)
}) })
.unwrap(); .unwrap();
@@ -706,11 +652,8 @@ mod tests {
#[test] #[test]
fn runtime_poll_rejects_invalid_descriptor_readiness() { fn runtime_poll_rejects_invalid_descriptor_readiness() {
let err = poll_runtime_fds_with(10, 11, None, |pollfds, _| { let err =
pollfds[1].revents = libc::POLLNVAL; poll_runtime_fds_with(None, |_| Ok([PollFlags::empty(), PollFlags::NVAL])).unwrap_err();
Ok(1)
})
.unwrap_err();
assert!(err.to_string().contains("runtime wake")); assert!(err.to_string().contains("runtime wake"));
assert!(err.to_string().contains("readiness")); assert!(err.to_string().contains("readiness"));
@@ -718,9 +661,9 @@ mod tests {
#[test] #[test]
fn runtime_poll_timeout_reports_no_readiness() { fn runtime_poll_timeout_reports_no_readiness() {
let readiness = poll_runtime_fds_with(10, 11, Some(Duration::ZERO), |_, timeout_ms| { let readiness = poll_runtime_fds_with(Some(Duration::ZERO), |timeout_ms| {
assert_eq!(timeout_ms, 0); assert_eq!(timeout_ms, 0);
Ok(0) Ok([PollFlags::empty(); 2])
}) })
.unwrap(); .unwrap();
+97 -108
View File
@@ -1,8 +1,11 @@
use std::io; use std::io;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use rustix::event::{self, EventfdFlags, PollFd, PollFlags, Timespec};
use rustix::io::{self as rustix_io, Errno};
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct RuntimeWakeSource { pub(crate) struct RuntimeWakeSource {
fd: Arc<OwnedFd>, fd: Arc<OwnedFd>,
@@ -20,33 +23,43 @@ pub(super) enum TerminalReadinessPolicy {
} }
pub(super) fn validate_poll_readiness( pub(super) fn validate_poll_readiness(
pollfd: &libc::pollfd, revents: PollFlags,
label: &str, label: &str,
terminal_policy: TerminalReadinessPolicy, terminal_policy: TerminalReadinessPolicy,
) -> io::Result<bool> { ) -> io::Result<bool> {
let readable = pollfd.revents & libc::POLLIN != 0; let readable = revents.contains(PollFlags::IN);
if pollfd.revents & libc::POLLNVAL != 0 { if revents.contains(PollFlags::NVAL) {
return Err(io::Error::other(format!( return Err(io::Error::other(format!(
"{label} poll descriptor failed with readiness {:#x}", "{label} poll descriptor failed with readiness {:#x}",
pollfd.revents revents.bits()
))); )));
} }
let terminal = pollfd.revents & (libc::POLLERR | libc::POLLHUP); let terminal = revents.intersects(PollFlags::ERR | PollFlags::HUP);
if terminal != 0 && !(terminal_policy == TerminalReadinessPolicy::ReadBuffered && readable) { if terminal && !(terminal_policy == TerminalReadinessPolicy::ReadBuffered && readable) {
return Err(io::Error::other(format!( return Err(io::Error::other(format!(
"{label} poll descriptor failed with readiness {:#x}", "{label} poll descriptor failed with readiness {:#x}",
pollfd.revents revents.bits()
))); )));
} }
if pollfd.revents != 0 && !readable { if !revents.is_empty() && !readable {
return Err(io::Error::other(format!( return Err(io::Error::other(format!(
"{label} poll descriptor returned unexpected readiness {:#x}", "{label} poll descriptor returned unexpected readiness {:#x}",
pollfd.revents revents.bits()
))); )));
} }
Ok(readable) Ok(readable)
} }
/// `poll(2)` with a millisecond timeout from [`timeout_to_poll_ms`], where a
/// negative value blocks.
pub(super) fn poll_for_ms(fds: &mut [PollFd<'_>], timeout_ms: i32) -> io::Result<usize> {
let timeout = (timeout_ms >= 0).then(|| Timespec {
tv_sec: (timeout_ms / 1_000).into(),
tv_nsec: (timeout_ms % 1_000 * 1_000_000).into(),
});
Ok(event::poll(fds, timeout.as_ref())?)
}
pub(super) fn poll_with_retry<T>( pub(super) fn poll_with_retry<T>(
timeout: Option<Duration>, timeout: Option<Duration>,
mut attempt: impl FnMut(i32) -> io::Result<Option<T>>, mut attempt: impl FnMut(i32) -> io::Result<Option<T>>,
@@ -67,15 +80,9 @@ pub(super) fn poll_with_retry<T>(
impl RuntimeWakeSource { impl RuntimeWakeSource {
pub(crate) fn try_new() -> io::Result<Self> { pub(crate) fn try_new() -> io::Result<Self> {
// SAFETY: eventfd returns a new owned descriptor on success. EFD_NONBLOCK // NONBLOCK keeps both producer writes and consumer drains bounded, and
// keeps both producer writes and consumer drains bounded, and EFD_CLOEXEC // CLOEXEC prevents subprocesses from extending the descriptor lifetime.
// prevents subprocesses from extending the runtime descriptor lifetime. let fd = event::eventfd(0, EventfdFlags::NONBLOCK | EventfdFlags::CLOEXEC)?;
let raw_fd = unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
if raw_fd < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: raw_fd was just returned by eventfd and has not been wrapped.
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
Ok(Self { fd: Arc::new(fd) }) Ok(Self { fd: Arc::new(fd) })
} }
@@ -93,32 +100,20 @@ impl RuntimeWakeSource {
/// One eventfd read consumes the entire accumulated counter, so a second /// One eventfd read consumes the entire accumulated counter, so a second
/// successful-path read would only issue a guaranteed-EAGAIN syscall. /// successful-path read would only issue a guaranteed-EAGAIN syscall.
pub(crate) fn drain(&self) -> io::Result<bool> { pub(crate) fn drain(&self) -> io::Result<bool> {
let mut value = [0_u8; size_of::<u64>()];
loop { loop {
let mut value = 0_u64; match rustix_io::read(&self.fd, &mut value) {
// SAFETY: value points to a writable u64 and the owned eventfd remains Ok(read) if read == value.len() => return Ok(true),
// valid for the duration of this read. Ok(read) => {
let result = unsafe { return Err(io::Error::new(
libc::read( io::ErrorKind::UnexpectedEof,
self.fd.as_raw_fd(), format!("runtime wake eventfd returned a short read ({read} bytes)"),
(&raw mut value).cast(), ));
size_of::<u64>(),
)
};
if result == size_of::<u64>() as isize {
return Ok(true);
}
if result < 0 {
let err = io::Error::last_os_error();
match err.kind() {
io::ErrorKind::Interrupted => continue,
io::ErrorKind::WouldBlock => return Ok(false),
_ => return Err(err),
} }
Err(Errno::INTR) => {}
Err(Errno::WOULDBLOCK) => return Ok(false),
Err(errno) => return Err(errno.into()),
} }
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("runtime wake eventfd returned a short read ({result} bytes)"),
));
} }
} }
@@ -126,24 +121,15 @@ impl RuntimeWakeSource {
#[cfg(test)] #[cfg(test)]
pub(crate) fn wait_readable(&self, timeout: Option<Duration>) -> io::Result<bool> { pub(crate) fn wait_readable(&self, timeout: Option<Duration>) -> io::Result<bool> {
let readable = poll_with_retry(timeout, |timeout_ms| { let readable = poll_with_retry(timeout, |timeout_ms| {
let mut pollfd = libc::pollfd { let mut pollfds = [PollFd::new(&self.fd, PollFlags::IN)];
fd: self.fd.as_raw_fd(), if poll_for_ms(&mut pollfds, timeout_ms)? == 0 {
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd is valid and the Arc-owned descriptor remains open
// throughout this bounded or producer-woken wait.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, timeout_ms) };
if ready == 0 {
return Ok(None); return Ok(None);
} }
if ready < 0 { let revents = pollfds[0].revents();
return Err(io::Error::last_os_error()); if !validate_poll_readiness(revents, "runtime wake", TerminalReadinessPolicy::Reject)? {
}
if !validate_poll_readiness(&pollfd, "runtime wake", TerminalReadinessPolicy::Reject)? {
return Err(io::Error::other(format!( return Err(io::Error::other(format!(
"runtime wake poll reported readiness without a readable descriptor ({:#x})", "runtime wake poll reported readiness without a readable descriptor ({:#x})",
pollfd.revents revents.bits()
))); )));
} }
Ok(Some(())) Ok(Some(()))
@@ -154,6 +140,27 @@ impl RuntimeWakeSource {
Ok(false) Ok(false)
} }
} }
/// Reports whether a wake arrives within `timeout`, without draining it.
/// Any other readiness is an error, so a broken descriptor cannot pass for
/// silence in a test that expects no wake.
#[cfg(test)]
pub(crate) fn is_readable_within(&self, timeout: Duration) -> io::Result<bool> {
let readable = poll_with_retry(Some(timeout), |timeout_ms| {
let mut pollfds = [PollFd::new(&self.fd, PollFlags::IN)];
if poll_for_ms(&mut pollfds, timeout_ms)? == 0 {
return Ok(None);
}
validate_poll_readiness(
pollfds[0].revents(),
"runtime wake",
TerminalReadinessPolicy::Reject,
)
.map(Some)
})?;
Ok(readable == Some(true))
}
} }
pub(super) fn timeout_to_poll_ms(timeout: Option<Duration>) -> i32 { pub(super) fn timeout_to_poll_ms(timeout: Option<Duration>) -> i32 {
@@ -169,34 +176,22 @@ pub(super) fn timeout_to_poll_ms(timeout: Option<Duration>) -> i32 {
impl RuntimeWakeHandle { impl RuntimeWakeHandle {
pub(crate) fn wake(&self) -> io::Result<()> { pub(crate) fn wake(&self) -> io::Result<()> {
let value = 1_u64; let value = 1_u64.to_ne_bytes();
loop { loop {
// SAFETY: value points to a readable u64 and the shared eventfd remains match rustix_io::write(&self.fd, &value) {
// valid for the duration of this write. Ok(written) if written == value.len() => return Ok(()),
let result = unsafe { Ok(written) => {
libc::write( return Err(io::Error::new(
self.fd.as_raw_fd(), io::ErrorKind::WriteZero,
(&raw const value).cast(), format!("runtime wake eventfd returned a short write ({written} bytes)"),
size_of::<u64>(), ));
)
};
if result == size_of::<u64>() as isize {
return Ok(());
}
if result < 0 {
let err = io::Error::last_os_error();
match err.kind() {
io::ErrorKind::Interrupted => continue,
// A saturated eventfd is already readable, so the wake is
// successfully coalesced rather than lost.
io::ErrorKind::WouldBlock => return Ok(()),
_ => return Err(err),
} }
Err(Errno::INTR) => {}
// A saturated eventfd is already readable, so the wake is
// successfully coalesced rather than lost.
Err(Errno::WOULDBLOCK) => return Ok(()),
Err(errno) => return Err(errno.into()),
} }
return Err(io::Error::new(
io::ErrorKind::WriteZero,
format!("runtime wake eventfd returned a short write ({result} bytes)"),
));
} }
} }
} }
@@ -207,21 +202,13 @@ mod tests {
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{fs, thread}; use std::{fs, thread};
use rustix::pipe;
use rustix::process::Pid;
use rustix::thread as rustix_thread;
use super::*; use super::*;
fn poll_readable(source: &RuntimeWakeSource, timeout_ms: i32) -> bool { fn wait_until_task_is_blocked_in_poll(tid: Pid) {
let mut pollfd = libc::pollfd {
fd: source.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd is valid for the duration of this call.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, timeout_ms) };
assert!(ready >= 0, "poll failed: {}", io::Error::last_os_error());
ready > 0 && pollfd.revents & libc::POLLIN != 0
}
fn wait_until_task_is_blocked_in_poll(tid: libc::pid_t) {
let stat_path = format!("/proc/self/task/{tid}/stat"); let stat_path = format!("/proc/self/task/{tid}/stat");
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
@@ -248,9 +235,9 @@ mod tests {
let source = RuntimeWakeSource::try_new().unwrap(); let source = RuntimeWakeSource::try_new().unwrap();
source.handle().wake().unwrap(); source.handle().wake().unwrap();
assert!(poll_readable(&source, 0)); assert!(source.is_readable_within(Duration::ZERO).unwrap());
assert!(source.drain().unwrap()); assert!(source.drain().unwrap());
assert!(!poll_readable(&source, 0)); assert!(!source.is_readable_within(Duration::ZERO).unwrap());
} }
#[test] #[test]
@@ -259,10 +246,7 @@ mod tests {
let handle = source.handle(); let handle = source.handle();
let (poll_entry_tx, poll_entry_rx) = mpsc::channel(); let (poll_entry_tx, poll_entry_rx) = mpsc::channel();
let poller = thread::spawn(move || { let poller = thread::spawn(move || {
// SAFETY: gettid has no preconditions and returns the calling Linux poll_entry_tx.send(rustix_thread::gettid()).unwrap();
// thread's id, used only to observe its scheduler state in this test.
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t };
poll_entry_tx.send(tid).unwrap();
assert!(source.wait_readable(Some(Duration::from_secs(1))).unwrap()); assert!(source.wait_readable(Some(Duration::from_secs(1))).unwrap());
}); });
@@ -281,7 +265,7 @@ mod tests {
} }
assert!(source.drain().unwrap()); assert!(source.drain().unwrap());
assert!(!poll_readable(&source, 0)); assert!(!source.is_readable_within(Duration::ZERO).unwrap());
} }
#[test] #[test]
@@ -299,6 +283,15 @@ mod tests {
assert!(!source.drain().unwrap()); assert!(!source.drain().unwrap());
} }
#[test]
fn readiness_without_a_wake_is_an_error_rather_than_silence() {
let (read, write) = pipe::pipe().unwrap();
drop(write);
let source = RuntimeWakeSource { fd: Arc::new(read) };
assert!(source.is_readable_within(Duration::ZERO).is_err());
}
#[test] #[test]
fn wait_readable_times_out_without_a_wake() { fn wait_readable_times_out_without_a_wake() {
let source = RuntimeWakeSource::try_new().unwrap(); let source = RuntimeWakeSource::try_new().unwrap();
@@ -307,22 +300,18 @@ mod tests {
#[test] #[test]
fn terminal_readiness_policy_distinguishes_streams_from_wake_descriptors() { fn terminal_readiness_policy_distinguishes_streams_from_wake_descriptors() {
let pollfd = libc::pollfd { let revents = PollFlags::IN | PollFlags::HUP;
fd: 7,
events: libc::POLLIN,
revents: libc::POLLIN | libc::POLLHUP,
};
assert!( assert!(
validate_poll_readiness( validate_poll_readiness(
&pollfd, revents,
"buffered stream", "buffered stream",
TerminalReadinessPolicy::ReadBuffered, TerminalReadinessPolicy::ReadBuffered,
) )
.unwrap() .unwrap()
); );
assert!( assert!(
validate_poll_readiness(&pollfd, "runtime wake", TerminalReadinessPolicy::Reject,) validate_poll_readiness(revents, "runtime wake", TerminalReadinessPolicy::Reject)
.is_err() .is_err()
); );
} }
+2 -10
View File
@@ -114,21 +114,13 @@ pub(super) fn setup_signal_handlers(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::os::fd::AsRawFd; use std::time::Duration;
use super::*; use super::*;
use crate::backend::wayland::RuntimeWakeSource; use crate::backend::wayland::RuntimeWakeSource;
fn wait_for_wake(source: &RuntimeWakeSource) { fn wait_for_wake(source: &RuntimeWakeSource) {
let mut pollfd = libc::pollfd { assert!(source.is_readable_within(Duration::from_secs(1)).unwrap());
fd: source.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: source retains the descriptor throughout this bounded wait.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
assert_eq!(ready, 1);
assert_ne!(pollfd.revents & libc::POLLIN, 0);
source.drain().unwrap(); source.drain().unwrap();
} }
+2 -10
View File
@@ -205,12 +205,11 @@ fn is_gnome_copied_files_mime(mime_type: &str) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io;
use std::os::unix; use std::os::unix;
use super::*; use super::*;
use crate::backend::wayland::clipboard::image::choose_supported_mime; use crate::backend::wayland::clipboard::image::choose_supported_mime;
use crate::test_temp::TempDir; use crate::test_temp::{self, TempDir};
use std::fs; use std::fs;
#[test] #[test]
@@ -265,16 +264,9 @@ mod tests {
#[test] #[test]
#[cfg(unix)] #[cfg(unix)]
fn uri_list_paste_rejects_fifo_without_blocking() { fn uri_list_paste_rejects_fifo_without_blocking() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let temp = TempDir::new().unwrap(); let temp = TempDir::new().unwrap();
let fifo_path = temp.path().join("not-an-image"); let fifo_path = temp.path().join("not-an-image");
let c_path = CString::new(fifo_path.as_os_str().as_bytes()).unwrap(); test_temp::make_fifo(&fifo_path);
// SAFETY: `c_path` is a NUL-terminated string that outlives the call,
// and mkfifo only reads it.
let result = unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) };
assert_eq!(result, 0, "mkfifo failed: {}", io::Error::last_os_error());
let uri = file_uri_for_path(&fifo_path); let uri = file_uri_for_path(&fifo_path);
let offered = vec![TEXT_URI_LIST_MIME.to_string()]; let offered = vec![TEXT_URI_LIST_MIME.to_string()];
+5 -8
View File
@@ -16,7 +16,9 @@
//! device or keymap problem can never panic across the libinput FFI edge. //! device or keymap problem can never panic across the libinput FFI edge.
use std::io; use std::io;
use std::os::fd::{AsRawFd, OwnedFd}; use std::os::fd::OwnedFd;
use rustix::io as rustix_io;
use std::sync::mpsc::{Receiver, TryRecvError, channel}; use std::sync::mpsc::{Receiver, TryRecvError, channel};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -188,15 +190,10 @@ impl InputMonitor {
/// A reader wedged inside libinput is left to finish on its own rather than /// A reader wedged inside libinput is left to finish on its own rather than
/// blocking overlay shutdown; its descriptors close with the process. /// blocking overlay shutdown; its descriptors close with the process.
fn stop(&mut self) { fn stop(&mut self) {
let byte = [1_u8]; if let Err(errno) = rustix_io::write(&self.stop, &[1]) {
// SAFETY: the write end is owned and open for the duration of the call,
// and `byte` is a valid one-byte buffer.
let written =
unsafe { libc::write(self.stop.as_raw_fd(), byte.as_ptr().cast(), byte.len()) };
if written < 0 {
log::warn!( log::warn!(
"Failed to signal the system input reader: {}", "Failed to signal the system input reader: {}",
io::Error::last_os_error() io::Error::from(errno)
); );
} }
let Some(join) = self.join.take() else { let Some(join) = self.join.take() else {
@@ -13,9 +13,12 @@ use input::event::pointer::{Axis, ButtonState, PointerScrollEvent};
use input::event::tablet_tool::{TabletToolEvent, TipState}; use input::event::tablet_tool::{TabletToolEvent, TipState};
use input::event::{Event, EventTrait, KeyboardEvent, PointerEvent}; use input::event::{Event, EventTrait, KeyboardEvent, PointerEvent};
use input::{DeviceCapability, Libinput, LibinputInterface}; use input::{DeviceCapability, Libinput, LibinputInterface};
use rustix::event::{self, PollFd, PollFlags, Timespec};
use rustix::io::Errno;
use rustix::pipe::{self, PipeFlags};
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io; use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::fd::OwnedFd;
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::path::Path; use std::path::Path;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
@@ -28,17 +31,7 @@ use repeat::{KeyboardKeys, RepeatPress, RepeatState};
/// Non-blocking, close-on-exec pipe used only to interrupt the reader's poll. /// Non-blocking, close-on-exec pipe used only to interrupt the reader's poll.
pub(super) fn stop_pipe() -> io::Result<(OwnedFd, OwnedFd)> { pub(super) fn stop_pipe() -> io::Result<(OwnedFd, OwnedFd)> {
let mut fds = [0 as RawFd; 2]; pipe::pipe_with(PipeFlags::CLOEXEC).map_err(io::Error::from)
// SAFETY: `fds` is a writable two-element array, the shape pipe2(2) expects.
let result = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
if result < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: both descriptors were just returned by pipe2 and are unwrapped.
let read = unsafe { OwnedFd::from_raw_fd(fds[0]) };
// SAFETY: as above for the write end.
let write = unsafe { OwnedFd::from_raw_fd(fds[1]) };
Ok((read, write))
} }
/// Open evdev nodes for libinput. Unprivileged `open(2)`: it succeeds when the /// Open evdev nodes for libinput. Unprivileged `open(2)`: it succeeds when the
@@ -239,47 +232,33 @@ fn wait_for_input(
stop_read: &OwnedFd, stop_read: &OwnedFd,
timeout: Option<Duration>, timeout: Option<Duration>,
) -> io::Result<PollOutcome> { ) -> io::Result<PollOutcome> {
// A repeat deadline in the past polls with 0 and returns Timeout at once. // A repeat deadline in the past polls with zero and returns Timeout at once.
let timeout_ms = timeout.map_or(-1, |timeout| { let timeout = timeout
timeout.as_millis().min(i32::MAX as u128) as i32 .map(Timespec::try_from)
}); .transpose()
.map_err(io::Error::other)?;
loop { loop {
let mut fds = [ let mut fds = [
libc::pollfd { PollFd::new(libinput, PollFlags::IN),
fd: libinput.as_raw_fd(), PollFd::new(stop_read, PollFlags::IN),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: stop_read.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
]; ];
// SAFETY: `fds` is a valid two-element array and both descriptors are match event::poll(&mut fds, timeout.as_ref()) {
// owned (or borrowed from an owner) for the duration of the call. A Ok(0) => return Ok(PollOutcome::Timeout),
// negative timeout blocks until one becomes ready. Ok(_) => {}
let ready = unsafe { libc::poll(fds.as_mut_ptr(), 2, timeout_ms) }; Err(Errno::INTR) => continue,
if ready < 0 { Err(errno) => return Err(errno.into()),
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(err);
} }
if ready == 0 { if !fds[1].revents().is_empty() {
return Ok(PollOutcome::Timeout);
}
if fds[1].revents != 0 {
return Ok(PollOutcome::Stop); return Ok(PollOutcome::Stop);
} }
if fds[0].revents & (libc::POLLERR | libc::POLLNVAL | libc::POLLHUP) != 0 { let revents = fds[0].revents();
if revents.intersects(PollFlags::ERR | PollFlags::NVAL | PollFlags::HUP) {
return Err(io::Error::other(format!( return Err(io::Error::other(format!(
"libinput poll descriptor failed with readiness {:#x}", "libinput poll descriptor failed with readiness {:#x}",
fds[0].revents revents.bits()
))); )));
} }
if fds[0].revents & libc::POLLIN != 0 { if revents.contains(PollFlags::IN) {
return Ok(PollOutcome::Ready); return Ok(PollOutcome::Ready);
} }
} }
+8 -15
View File
@@ -5,14 +5,14 @@
//! silently pick overlay mode and `system` can warn with actionable guidance //! silently pick overlay mode and `system` can warn with actionable guidance
//! before anything is spawned. //! before anything is spawned.
use std::ffi::CString; use rustix::fs::{self as rustix_fs, Access};
use std::os::unix::ffi::OsStrExt; use rustix::io::Errno;
use std::path::Path; use std::path::Path;
use crate::env_vars; use crate::env_vars;
use std::ffi::OsString; use std::ffi::OsString;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::{env, fs, io}; use std::{env, fs};
const DEV_INPUT_DIR: &str = "/dev/input"; const DEV_INPUT_DIR: &str = "/dev/input";
/// Seat to follow when the session does not name one. libinput's udev backend /// Seat to follow when the session does not name one. libinput's udev backend
@@ -211,18 +211,11 @@ fn classify_seat_nodes(seat: &str) -> EventNodeAccess {
} }
fn node_path_access(path: &Path) -> NodePathAccess { fn node_path_access(path: &Path) -> NodePathAccess {
let Ok(c_path) = CString::new(path.as_os_str().as_bytes()) else { match rustix_fs::access(path, Access::READ_OK) {
return NodePathAccess::Unknown; Ok(()) => NodePathAccess::Readable,
}; Err(Errno::ACCESS | Errno::PERM) => NodePathAccess::PermissionDenied,
// SAFETY: `c_path` is a valid NUL-terminated C string that outlives the Err(Errno::NOENT | Errno::NOTDIR | Errno::NODEV) => NodePathAccess::Absent,
// call, and `access(2)` only reads it. Err(_) => NodePathAccess::Unknown,
if unsafe { libc::access(c_path.as_ptr(), libc::R_OK) } == 0 {
return NodePathAccess::Readable;
}
match io::Error::last_os_error().raw_os_error() {
Some(libc::EACCES | libc::EPERM) => NodePathAccess::PermissionDenied,
Some(libc::ENOENT | libc::ENOTDIR | libc::ENODEV) => NodePathAccess::Absent,
_ => NodePathAccess::Unknown,
} }
} }
+2 -16
View File
@@ -217,7 +217,6 @@ impl<T> Drop for PortalTaskExitGuard<T> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::future; use std::future;
use std::os::fd::AsRawFd;
use tokio::task; use tokio::task;
@@ -225,13 +224,7 @@ mod tests {
use crate::backend::wayland::RuntimeWakeSource; use crate::backend::wayland::RuntimeWakeSource;
fn wait_for_wake(wake: &RuntimeWakeSource) { fn wait_for_wake(wake: &RuntimeWakeSource) {
let mut pollfd = libc::pollfd { assert!(wake.is_readable_within(Duration::from_secs(1)).unwrap());
fd: wake.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: the descriptor and pollfd remain valid during the bounded wait.
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 1_000) }, 1);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@@ -264,14 +257,7 @@ mod tests {
}); });
drop(task); drop(task);
task::yield_now().await; task::yield_now().await;
let mut pollfd = libc::pollfd { assert!(!wake.is_readable_within(Duration::ZERO).unwrap());
fd: wake.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: the descriptor and pollfd are valid for this non-blocking poll.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
assert_eq!(ready, 0);
} }
#[tokio::test] #[tokio::test]
+7 -20
View File
@@ -379,12 +379,14 @@ impl<T> Drop for RuntimeOperationExitGuard<T> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::os::fd::AsRawFd;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc; use std::sync::mpsc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{fs, thread}; use std::{fs, thread};
use rustix::process::Pid;
use rustix::thread as rustix_thread;
use super::*; use super::*;
use crate::backend::wayland::RuntimeWakeSource; use crate::backend::wayland::RuntimeWakeSource;
@@ -396,17 +398,10 @@ mod tests {
} }
fn wait_for_wake(wake: &RuntimeWakeSource) { fn wait_for_wake(wake: &RuntimeWakeSource) {
let mut pollfd = libc::pollfd { assert!(wake.is_readable_within(Duration::from_secs(1)).unwrap());
fd: wake.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd and the runtime wake descriptor remain valid for the bounded wait.
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 1_000) }, 1);
assert_ne!(pollfd.revents & libc::POLLIN, 0);
} }
fn wait_until_thread_is_blocked_in_poll(tid: libc::pid_t) { fn wait_until_thread_is_blocked_in_poll(tid: Pid) {
let stat_path = format!("/proc/self/task/{tid}/stat"); let stat_path = format!("/proc/self/task/{tid}/stat");
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
@@ -516,9 +511,7 @@ mod tests {
.unwrap(); .unwrap();
let (tid_tx, tid_rx) = mpsc::channel(); let (tid_tx, tid_rx) = mpsc::channel();
let poller = thread::spawn(move || { let poller = thread::spawn(move || {
// SAFETY: gettid has no preconditions and is used only to observe this test thread. tid_tx.send(rustix_thread::gettid()).unwrap();
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t };
tid_tx.send(tid).unwrap();
wait_for_wake(&wake); wait_for_wake(&wake);
}); });
let tid = tid_rx.recv().unwrap(); let tid = tid_rx.recv().unwrap();
@@ -592,13 +585,7 @@ mod tests {
) )
); );
assert!(!controller.is_active()); assert!(!controller.is_active());
let mut pollfd = libc::pollfd { assert!(!wake.is_readable_within(Duration::ZERO).unwrap());
fd: wake.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd and the runtime wake descriptor are valid for this non-blocking poll.
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 0) }, 0);
assert_eq!( assert_eq!(
controller controller
.try_submit(42, "test-after-spawn-failure", || 2) .try_submit(42, "test-after-spawn-failure", || 2)
+5 -24
View File
@@ -470,9 +470,7 @@ fn assert_send_static<T: Send + 'static>() {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::io::ErrorKind; use std::fs;
use std::os::fd::AsRawFd;
use std::{fs, io};
use crate::test_temp; use crate::test_temp;
@@ -503,27 +501,10 @@ mod tests {
} }
fn wait_for_runtime_wake(wake: &RuntimeWakeSource) { fn wait_for_runtime_wake(wake: &RuntimeWakeSource) {
let mut pollfd = libc::pollfd { assert!(
fd: wake.poll_fd().as_raw_fd(), wake.is_readable_within(Duration::from_secs(1)).unwrap(),
events: libc::POLLIN, "persistence worker did not wake runtime within one second"
revents: 0, );
};
loop {
// SAFETY: pollfd and the runtime-owned descriptor remain valid for
// this bounded test wait.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
if ready > 0 {
assert_ne!(pollfd.revents & libc::POLLIN, 0);
return;
}
if ready == 0 {
panic!("persistence worker did not wake runtime within one second");
}
let err = io::Error::last_os_error();
if err.kind() != ErrorKind::Interrupted {
panic!("runtime wake poll failed: {err}");
}
}
} }
#[test] #[test]
@@ -1,3 +1,5 @@
use rustix::process as rustix_process;
use crate::config::ConfigDocument; use crate::config::ConfigDocument;
use crate::{config, test_temp}; use crate::{config, test_temp};
@@ -387,8 +389,7 @@ fn a_read_only_config_fails_the_write_and_leaves_the_file_alone() {
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir"); fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file"); fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
// SAFETY: geteuid has no preconditions and cannot fail. if rustix_process::geteuid().is_root() {
if unsafe { libc::geteuid() } == 0 {
// Root ignores these modes; the scenario cannot be staged. // Root ignores these modes; the scenario cannot be staged.
return; return;
} }
@@ -136,6 +136,8 @@ impl WaylandState {
mod tests { mod tests {
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use rustix::process as rustix_process;
use crate::config::ConfigDocument; use crate::config::ConfigDocument;
use crate::input::Tool; use crate::input::Tool;
use crate::{config, test_temp}; use crate::{config, test_temp};
@@ -367,8 +369,7 @@ redo = [\"Ctrl+Alt+Shift+Q\"]
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir"); fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file"); fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
// SAFETY: geteuid has no preconditions and cannot fail. if rustix_process::geteuid().is_root() {
if unsafe { libc::geteuid() } == 0 {
return; return;
} }
assert!(result.is_err(), "a read-only config must fail the write"); assert!(result.is_err(), "a read-only config must fail the write");
@@ -122,6 +122,8 @@ impl WaylandState {
mod tests { mod tests {
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use rustix::process as rustix_process;
use crate::config::{ColorSpec, ConfigDocument}; use crate::config::{ColorSpec, ConfigDocument};
use crate::{config, test_temp}; use crate::{config, test_temp};
@@ -343,8 +345,7 @@ color = \"#33F54F\"
fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir"); fs::set_permissions(temp.path(), fs::Permissions::from_mode(0o755)).expect("restore dir");
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file"); fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore file");
// SAFETY: geteuid has no preconditions and cannot fail. if rustix_process::geteuid().is_root() {
if unsafe { libc::geteuid() } == 0 {
return; return;
} }
assert!(result.is_err(), "a read-only config must fail the write"); assert!(result.is_err(), "a read-only config must fail the write");
+7 -12
View File
@@ -1,4 +1,3 @@
use std::os::fd::AsRawFd;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, mpsc}; use std::sync::{Arc, Mutex, mpsc};
@@ -163,23 +162,19 @@ async fn completion_wakes_an_already_blocked_runtime_poll_after_publication() {
.unwrap(); .unwrap();
started.notified().await; started.notified().await;
let wake_fd = runtime_wake.poll_fd().as_raw_fd();
let (polling_tx, polling_rx) = mpsc::sync_channel(0); let (polling_tx, polling_rx) = mpsc::sync_channel(0);
let poller = thread::spawn(move || { let poller = thread::spawn(move || {
let mut pollfd = libc::pollfd {
fd: wake_fd,
events: libc::POLLIN,
revents: 0,
};
polling_tx.send(()).unwrap(); polling_tx.send(()).unwrap();
// SAFETY: the runtime wake source outlives this bounded poll and owns assert!(
// the descriptor for the entire test. runtime_wake
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 1_000) }, 1); .is_readable_within(Duration::from_secs(1))
assert_ne!(pollfd.revents & libc::POLLIN, 0); .unwrap()
);
runtime_wake
}); });
polling_rx.recv().unwrap(); polling_rx.recv().unwrap();
release.notify_one(); release.notify_one();
poller.join().unwrap(); let runtime_wake = poller.join().unwrap();
runtime_wake.drain().unwrap(); runtime_wake.drain().unwrap();
assert!(matches!( assert!(matches!(
+94 -142
View File
@@ -2,6 +2,8 @@ use std::env;
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use lexopt::{Arg, Parser};
use crate::domain::Language; use crate::domain::Language;
use crate::tray_action::TrayAction; use crate::tray_action::TrayAction;
use crate::{build_info, i18n, runtime_capabilities}; use crate::{build_info, i18n, runtime_capabilities};
@@ -105,96 +107,70 @@ impl Cli {
I: IntoIterator<Item = T>, I: IntoIterator<Item = T>,
T: Into<OsString>, T: Into<OsString>,
{ {
let mut args = args let mut parser = Parser::from_iter(args.into_iter().map(Into::into));
.into_iter()
.map(|arg| {
arg.into()
.into_string()
.map_err(|_| "arguments must be valid UTF-8".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
if args.is_empty() {
args.push("wayscriber".to_string());
}
let mut cli = Cli::default(); let mut cli = Cli::default();
let mut index = 1;
while index < args.len() { while let Some(arg) = parser.next().map_err(|error| parse_error(&error))? {
let arg = &args[index]; match arg {
match arg.as_str() { Arg::Short('h') => return Ok(CliOutcome::Help),
"-h" | "--help" => return Ok(CliOutcome::Help), Arg::Short('V') => return Ok(CliOutcome::Version),
"-V" | "--version" => return Ok(CliOutcome::Version), Arg::Long("help") => {
"-d" | "--daemon" => cli.daemon = true, reject_attached_value(&mut parser, "--help")?;
"--daemon-toggle" => cli.daemon_toggle = true, return Ok(CliOutcome::Help);
"--daemon-action" => {
index += 1;
cli.daemon_action = Some(value_after(&args, index, "--daemon-action")?);
} }
"--light-toggle" => cli.light_toggle = true, Arg::Long("version") => {
"--light-draw-toggle" => cli.light_draw_toggle = true, reject_attached_value(&mut parser, "--version")?;
"--light-draw-on" => cli.light_draw_on = true, return Ok(CliOutcome::Version);
"--light-draw-off" => cli.light_draw_off = true,
"-a" | "--active" => cli.active = true,
"-m" | "--mode" => {
index += 1;
cli.mode = Some(value_after(&args, index, "--mode")?);
} }
"--no-tray" => cli.no_tray = true, Arg::Short('d') | Arg::Long("daemon") => cli.daemon = true,
"--freeze-on-show" => cli.freeze_on_show = true, Arg::Long("daemon-toggle") => cli.daemon_toggle = true,
"--clear-session" => cli.clear_session = true, Arg::Long("daemon-action") => {
"--clear-tool-state" => cli.clear_tool_state = true, cli.daemon_action = Some(string_value(&mut parser, "--daemon-action")?);
"--session-info" => cli.session_info = true,
"--rename-session" => {
index += 1;
cli.rename_session = Some(value_after(&args, index, "--rename-session")?);
} }
"--session-file" => { Arg::Long("light-toggle") => cli.light_toggle = true,
index += 1; Arg::Long("light-draw-toggle") => cli.light_draw_toggle = true,
Arg::Long("light-draw-on") => cli.light_draw_on = true,
Arg::Long("light-draw-off") => cli.light_draw_off = true,
Arg::Short('a') | Arg::Long("active") => cli.active = true,
Arg::Short('m') => cli.mode = Some(string_value(&mut parser, "-m")?),
Arg::Long("mode") => cli.mode = Some(string_value(&mut parser, "--mode")?),
Arg::Long("no-tray") => cli.no_tray = true,
Arg::Long("freeze-on-show") => cli.freeze_on_show = true,
Arg::Long("clear-session") => cli.clear_session = true,
Arg::Long("clear-tool-state") => cli.clear_tool_state = true,
Arg::Long("session-info") => cli.session_info = true,
Arg::Long("rename-session") => {
cli.rename_session = Some(string_value(&mut parser, "--rename-session")?);
}
Arg::Long("session-file") => {
cli.session_file = cli.session_file =
Some(PathBuf::from(value_after(&args, index, "--session-file")?)); Some(PathBuf::from(option_value(&mut parser, "--session-file")?));
} }
"--freeze" => cli.freeze = true, Arg::Long("freeze") => cli.freeze = true,
"--exit-after-capture" => cli.exit_after_capture = true, Arg::Long("exit-after-capture") => cli.exit_after_capture = true,
"--no-exit-after-capture" => cli.no_exit_after_capture = true, Arg::Long("no-exit-after-capture") => cli.no_exit_after_capture = true,
"--resume-session" => cli.resume_session = true, Arg::Long("resume-session") => cli.resume_session = true,
"--no-resume-session" => cli.no_resume_session = true, Arg::Long("no-resume-session") => cli.no_resume_session = true,
"--about" => cli.about = true, Arg::Long("about") => cli.about = true,
"--check-update" => cli.check_update = true, Arg::Long("check-update") => cli.check_update = true,
"--language" => { Arg::Long("language") => {
index += 1; cli.language = Some(parse_language(&string_value(&mut parser, "--language")?)?);
cli.language = Some(parse_language(&value_after(&args, index, "--language")?)?);
} }
"--list-languages" => cli.list_languages = true, Arg::Long("list-languages") => cli.list_languages = true,
runtime_capabilities::RUNTIME_CAPABILITIES_FLAG => { Arg::Long(name)
if runtime_capabilities::RUNTIME_CAPABILITIES_FLAG.strip_prefix("--")
== Some(name) =>
{
cli.runtime_capabilities = true; cli.runtime_capabilities = true;
} }
_ if arg.starts_with("--daemon-action=") => { Arg::Long(name) => {
cli.daemon_action = Some(value_from_equals(arg, "--daemon-action")?); let option = format!("--{name}");
reject_attached_value(&mut parser, &option)?;
return Err(unknown_argument(&option));
} }
_ if arg.starts_with("--mode=") => { Arg::Short(flag) => return Err(format!("unknown short option '-{flag}'")),
cli.mode = Some(value_from_equals(arg, "--mode")?); Arg::Value(value) => return Err(unknown_argument(&value.display().to_string())),
}
_ if arg.starts_with("--session-file=") => {
cli.session_file =
Some(PathBuf::from(value_from_equals(arg, "--session-file")?));
}
_ if arg.starts_with("--rename-session=") => {
cli.rename_session = Some(value_from_equals(arg, "--rename-session")?);
}
_ if arg.starts_with("--language=") => {
cli.language = Some(parse_language(&value_from_equals(arg, "--language")?)?);
}
_ if is_short_option_cluster(arg) => {
match parse_short_option_cluster(&args, index, &mut cli)? {
ShortOptionOutcome::Continue(next_index) => index = next_index,
ShortOptionOutcome::Help => return Ok(CliOutcome::Help),
ShortOptionOutcome::Version => return Ok(CliOutcome::Version),
}
}
_ => return Err(format!("unknown argument '{arg}'")),
} }
index += 1;
} }
cli.validate()?; cli.validate()?;
@@ -525,77 +501,53 @@ fn parse_language(value: &str) -> Result<Language, String> {
}) })
} }
fn is_short_option_cluster(arg: &str) -> bool { /// Reads an option's value, either attached (`--mode=x`, `-mx`, `-m=x`) or as
arg.starts_with('-') && !arg.starts_with("--") && arg.len() > 2 /// the next argument. A next argument starting with `-` is the next option, so
/// a value that starts with `-` has to be attached.
fn option_value(parser: &mut Parser, name: &str) -> Result<OsString, String> {
let requires_value = || format!("{name} requires a value");
if let Some(value) = parser.optional_value() {
return if value.is_empty() {
Err(requires_value())
} else {
Ok(value)
};
}
let value = parser.value().map_err(|_| requires_value())?;
if value.as_encoded_bytes().starts_with(b"-") {
return Err(requires_value());
}
Ok(value)
} }
enum ShortOptionOutcome { fn string_value(parser: &mut Parser, name: &str) -> Result<String, String> {
Continue(usize), option_value(parser, name)?
Help, .into_string()
Version, .map_err(|_| "arguments must be valid UTF-8".to_string())
} }
fn parse_short_option_cluster( /// Rejects `--flag=value` for an option that is about to stop parsing, which
args: &[String], /// would otherwise skip the check `Parser::next` makes on its following call.
index: usize, fn reject_attached_value(parser: &mut Parser, name: &str) -> Result<(), String> {
cli: &mut Cli, match parser.optional_value() {
) -> Result<ShortOptionOutcome, String> { Some(value) => Err(unknown_argument(&format!("{name}={}", value.display()))),
let arg = &args[index]; None => Ok(()),
for (offset, flag) in arg[1..].char_indices() { }
match flag { }
'h' => return Ok(ShortOptionOutcome::Help),
'V' => return Ok(ShortOptionOutcome::Version), fn parse_error(error: &lexopt::Error) -> String {
'd' => cli.daemon = true, match error {
'a' => cli.active = true, lexopt::Error::UnexpectedValue { option, value } => {
'm' => { unknown_argument(&format!("{option}={}", value.display()))
let value_start = 1 + offset + flag.len_utf8();
let value = if value_start < arg.len() {
attached_short_value(arg, value_start, "-m")?
} else {
value_after(args, index + 1, "-m")?
};
cli.mode = Some(value);
return Ok(ShortOptionOutcome::Continue(if value_start < arg.len() {
index
} else {
index + 1
}));
}
_ => return Err(format!("unknown short option '-{flag}'")),
} }
other => other.to_string(),
} }
Ok(ShortOptionOutcome::Continue(index))
} }
fn value_after(args: &[String], index: usize, name: &str) -> Result<String, String> { fn unknown_argument(arg: &str) -> String {
let value = args format!("unknown argument '{arg}'")
.get(index)
.ok_or_else(|| format!("{name} requires a value"))?;
if value.starts_with('-') {
return Err(format!("{name} requires a value"));
}
Ok(value.clone())
}
fn attached_short_value(arg: &str, value_start: usize, name: &str) -> Result<String, String> {
let value = arg[value_start..]
.strip_prefix('=')
.unwrap_or_else(|| &arg[value_start..]);
if value.is_empty() {
return Err(format!("{name} requires a value"));
}
Ok(value.to_string())
}
fn value_from_equals(arg: &str, name: &str) -> Result<String, String> {
let value = arg
.split_once('=')
.map(|(_, value)| value)
.unwrap_or_default();
if value.is_empty() {
return Err(format!("{name} requires a value"));
}
Ok(value.to_string())
} }
fn conflict(left: &str, right: &str) -> String { fn conflict(left: &str, right: &str) -> String {
+103
View File
@@ -1,3 +1,5 @@
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
use std::path::PathBuf; use std::path::PathBuf;
use super::{Cli, CliOutcome}; use super::{Cli, CliOutcome};
@@ -601,3 +603,104 @@ fn list_languages_is_a_print_and_exit_command() {
); );
} }
} }
#[test]
fn a_value_starting_with_a_dash_must_be_attached() {
for args in [
&["wayscriber", "--mode", "-x"][..],
&["wayscriber", "-m", "-x"],
&["wayscriber", "--mode", "--active"],
] {
assert!(
Cli::try_parse_from(args.iter().copied())
.unwrap_err()
.ends_with("requires a value"),
"{args:?}"
);
}
assert_eq!(
parse_cli(["wayscriber", "--mode=-x"]).mode.as_deref(),
Some("-x")
);
assert_eq!(
parse_cli(["wayscriber", "-m=-x"]).mode.as_deref(),
Some("-x")
);
assert_eq!(
parse_cli(["wayscriber", "-m-x"]).mode.as_deref(),
Some("-x")
);
}
#[test]
fn an_empty_attached_value_is_missing_but_a_separate_one_is_not() {
for (args, name) in [
(&["wayscriber", "--mode="][..], "--mode"),
(&["wayscriber", "-m="], "-m"),
(&["wayscriber", "--language="], "--language"),
] {
assert_eq!(
Cli::try_parse_from(args.iter().copied()).unwrap_err(),
format!("{name} requires a value")
);
}
assert_eq!(
parse_cli(["wayscriber", "--mode", ""]).mode.as_deref(),
Some("")
);
}
#[test]
fn unknown_and_misused_options_are_rejected() {
for (args, expected) in [
(&["wayscriber", "--nope"][..], "unknown argument '--nope'"),
(&["wayscriber", "--nope=1"], "unknown argument '--nope=1'"),
(
&["wayscriber", "--active=1"],
"unknown argument '--active=1'",
),
(&["wayscriber", "--help=1"], "unknown argument '--help=1'"),
(
&["wayscriber", "--version=1"],
"unknown argument '--version=1'",
),
(&["wayscriber", "stray"], "unknown argument 'stray'"),
(&["wayscriber", "-"], "unknown argument '-'"),
(&["wayscriber", "--", "stray"], "unknown argument 'stray'"),
(&["wayscriber", "-x"], "unknown short option '-x'"),
(&["wayscriber", "-dx"], "unknown short option '-x'"),
] {
assert_eq!(
Cli::try_parse_from(args.iter().copied()).unwrap_err(),
expected,
"{args:?}"
);
}
}
#[test]
fn a_session_path_keeps_its_bytes_but_other_values_must_be_utf8() {
let raw_path = OsString::from_vec(b"/tmp/s\xff.wayscriber-session".to_vec());
let outcome = Cli::try_parse_from([
OsString::from("wayscriber"),
OsString::from("--active"),
OsString::from("--session-file"),
raw_path.clone(),
])
.unwrap();
let CliOutcome::Run(cli) = outcome else {
panic!("expected CLI run outcome, got {outcome:?}");
};
assert_eq!(cli.session_file, Some(PathBuf::from(raw_path)));
let error = Cli::try_parse_from([
OsString::from("wayscriber"),
OsString::from("--mode"),
OsString::from_vec(b"\xff".to_vec()),
])
.unwrap_err();
assert_eq!(error, "arguments must be valid UTF-8");
}
+9 -10
View File
@@ -6,6 +6,8 @@ use crate::paths::{daemon_lock_file, daemon_pid_file};
use crate::session::try_lock_exclusive; use crate::session::try_lock_exclusive;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use log::warn; use log::warn;
#[cfg(unix)]
use rustix::process::{self as rustix_process, Pid, Signal};
use std::fs; use std::fs;
use std::fs::{File, OpenOptions}; use std::fs::{File, OpenOptions};
use std::io::ErrorKind; use std::io::ErrorKind;
@@ -136,16 +138,13 @@ pub(super) fn signal_daemon_pid(pid: u32) -> Result<()> {
return Err(anyhow!("invalid daemon pid {pid}")); return Err(anyhow!("invalid daemon pid {pid}"));
} }
// SAFETY: `pid` has been checked to be a positive Unix process id and let target = Pid::from_raw(pid).ok_or_else(|| anyhow!("invalid daemon pid {pid}"))?;
// `SIGUSR1` is a valid signal constant. rustix_process::kill_process(target, Signal::USR1).map_err(|errno| {
if unsafe { libc::kill(pid, libc::SIGUSR1) } != 0 { anyhow!(
return Err(anyhow!( "failed to signal wayscriber daemon {pid}: {}",
"failed to signal wayscriber daemon {}: {}", io::Error::from(errno)
pid, )
io::Error::last_os_error() })
));
}
Ok(())
} }
#[cfg(not(unix))] #[cfg(not(unix))]
+29 -52
View File
@@ -3,10 +3,12 @@ use std::{io, mem, process, thread};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::{info, warn}; use log::{info, warn};
use rustix::event::{self, PollFd, PollFlags};
use rustix::io::Errno;
use std::fs; use std::fs;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::fd::BorrowedFd;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
@@ -922,48 +924,31 @@ fn wait_for_daemon_lifecycle(
deadline_source: Option<&BootDeadlineSource>, deadline_source: Option<&BootDeadlineSource>,
overlay_exit_fd: Option<BorrowedFd<'_>>, overlay_exit_fd: Option<BorrowedFd<'_>>,
) -> Result<DaemonLifecycleReadiness> { ) -> Result<DaemonLifecycleReadiness> {
let mut pollfds = vec![libc::pollfd { let mut pollfds = vec![PollFd::from_borrowed_fd(
fd: daemon_wake.poll_fd().as_raw_fd(), daemon_wake.poll_fd(),
events: libc::POLLIN, PollFlags::IN,
revents: 0, )];
}];
if let Some(watcher) = command_watcher { if let Some(watcher) = command_watcher {
pollfds.push(libc::pollfd { pollfds.push(PollFd::from_borrowed_fd(watcher.poll_fd(), PollFlags::IN));
fd: watcher.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
});
} }
let command_index = command_watcher.map(|_| 1); let command_index = command_watcher.map(|_| 1);
let deadline_index = deadline_source.map(|source| { let deadline_index = deadline_source.map(|source| {
let index = pollfds.len(); let index = pollfds.len();
pollfds.push(libc::pollfd { pollfds.push(PollFd::from_borrowed_fd(source.poll_fd(), PollFlags::IN));
fd: source.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
});
index index
}); });
let child_index = overlay_exit_fd.map(|fd| { let child_index = overlay_exit_fd.map(|fd| {
let index = pollfds.len(); let index = pollfds.len();
pollfds.push(libc::pollfd { pollfds.push(PollFd::from_borrowed_fd(fd, PollFlags::IN));
fd: fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
});
index index
}); });
loop { loop {
// SAFETY: the descriptor remains owned by `daemon_wake` throughout poll. let ready = match event::poll(&mut pollfds, None) {
let ready = unsafe { Ok(ready) => ready,
libc::poll( Err(Errno::INTR) => continue,
pollfds.as_mut_ptr(), Err(errno) => {
pollfds return Err(io::Error::from(errno)).context("daemon lifecycle poll failed");
.len() }
.try_into()
.expect("poll descriptor count fits"),
-1,
)
}; };
if ready == 0 { if ready == 0 {
return Ok(DaemonLifecycleReadiness { return Ok(DaemonLifecycleReadiness {
@@ -971,36 +956,28 @@ fn wait_for_daemon_lifecycle(
deadline: false, deadline: false,
}); });
} }
if ready < 0 {
let err = io::Error::last_os_error();
if err.kind() == ErrorKind::Interrupted {
continue;
}
return Err(err).context("daemon lifecycle poll failed");
}
for pollfd in &pollfds { for pollfd in &pollfds {
let terminal = pollfd.revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL); let revents = pollfd.revents();
if terminal != 0 { if revents.intersects(PollFlags::ERR | PollFlags::HUP | PollFlags::NVAL) {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"daemon lifecycle descriptor returned terminal readiness {:#x}", "daemon lifecycle descriptor returned terminal readiness {:#x}",
pollfd.revents revents.bits()
)); ));
} }
} }
let daemon_ready = pollfds[0].revents & libc::POLLIN != 0; let is_readable = |index: Option<usize>| {
let command_ready = command_index index
.and_then(|index| pollfds.get(index)) .and_then(|index| pollfds.get(index))
.is_some_and(|pollfd| pollfd.revents & libc::POLLIN != 0); .is_some_and(|pollfd| pollfd.revents().contains(PollFlags::IN))
let deadline_ready = deadline_index };
.and_then(|index| pollfds.get(index)) let daemon_ready = is_readable(Some(0));
.is_some_and(|pollfd| pollfd.revents & libc::POLLIN != 0); let command_ready = is_readable(command_index);
let child_ready = child_index let deadline_ready = is_readable(deadline_index);
.and_then(|index| pollfds.get(index)) let child_ready = is_readable(child_index);
.is_some_and(|pollfd| pollfd.revents & libc::POLLIN != 0);
if !daemon_ready && !command_ready && !deadline_ready && !child_ready { if !daemon_ready && !command_ready && !deadline_ready && !child_ready {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"daemon wake descriptor returned invalid readiness {:#x}", "daemon wake descriptor returned invalid readiness {:#x}",
pollfds[0].revents pollfds[0].revents().bits()
)); ));
} }
if daemon_ready { if daemon_ready {
+2 -2
View File
@@ -1,4 +1,4 @@
use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::fd::{AsFd, BorrowedFd};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
@@ -108,7 +108,7 @@ impl OverlayLifecycle {
&candidate.program, &candidate.program,
&launch.arguments, &launch.arguments,
launch.environment, launch.environment,
daemon_watchdog.as_raw_fd(), daemon_watchdog.as_fd(),
)?; )?;
self.child.start(child)?; self.child.start(child)?;
self.child self.child
+11 -11
View File
@@ -2,7 +2,6 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, ErrorKind}; use std::io::{self, ErrorKind};
use std::os::fd::AsRawFd;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[cfg(test)] #[cfg(test)]
@@ -11,6 +10,7 @@ use std::sync::LazyLock;
use std::sync::Mutex; use std::sync::Mutex;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use rustix::fs::{self as rustix_fs, FlockOperation};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -224,10 +224,13 @@ fn try_open_journal_lock(root: &Path, nonblocking: bool) -> Result<Option<File>>
if !file.metadata()?.is_file() { if !file.metadata()?.is_file() {
bail!("action journal lock is not a regular file"); bail!("action journal lock is not a regular file");
} }
let operation = libc::LOCK_EX | if nonblocking { libc::LOCK_NB } else { 0 }; let operation = if nonblocking {
// SAFETY: file owns the descriptor; flock retains no pointer. FlockOperation::NonBlockingLockExclusive
if unsafe { libc::flock(file.as_raw_fd(), operation) } != 0 { } else {
let error = io::Error::last_os_error(); FlockOperation::LockExclusive
};
if let Err(errno) = rustix_fs::flock(&file, operation) {
let error = io::Error::from(errno);
if nonblocking && error.kind() == ErrorKind::WouldBlock { if nonblocking && error.kind() == ErrorKind::WouldBlock {
return Ok(None); return Ok(None);
} }
@@ -237,12 +240,9 @@ fn try_open_journal_lock(root: &Path, nonblocking: bool) -> Result<Option<File>>
} }
fn unlock(file: &File) -> Result<()> { fn unlock(file: &File) -> Result<()> {
// SAFETY: file owns the descriptor; flock retains no pointer. rustix_fs::flock(file, FlockOperation::Unlock)
if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) } == 0 { .map_err(io::Error::from)
Ok(()) .context("failed to unlock action journal")
} else {
Err(io::Error::last_os_error()).context("failed to unlock action journal")
}
} }
fn write_record<T: Serialize>(path: &Path, value: &T) -> Result<()> { fn write_record<T: Serialize>(path: &Path, value: &T) -> Result<()> {
+4 -12
View File
@@ -1,5 +1,7 @@
use std::{env, thread}; use std::{env, thread};
use rustix::fs::{self as rustix_fs, FlockOperation};
use crate::{test_env, test_temp}; use crate::{test_env, test_temp};
use super::*; use super::*;
@@ -179,21 +181,11 @@ fn event_loop_claim_and_finish_defer_instead_of_waiting_for_locks() {
.write(true) .write(true)
.open(decision_path) .open(decision_path)
.unwrap(); .unwrap();
assert_eq!( rustix_fs::flock(&held_decision, FlockOperation::LockExclusive).unwrap();
// 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
);
let ActionFinishOutcome::Deferred(action) = action.try_finish(true, None).unwrap() else { let ActionFinishOutcome::Deferred(action) = action.try_finish(true, None).unwrap() else {
panic!("contended command finish should defer"); panic!("contended command finish should defer");
}; };
assert_eq!( rustix_fs::flock(&held_decision, FlockOperation::Unlock).unwrap();
// 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
);
assert!(matches!( assert!(matches!(
action.try_finish(true, None).unwrap(), action.try_finish(true, None).unwrap(),
ActionFinishOutcome::Complete ActionFinishOutcome::Complete
+12 -27
View File
@@ -1,5 +1,5 @@
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; use std::os::fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd};
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::thread::Builder; use std::thread::Builder;
@@ -7,6 +7,8 @@ use std::time::Duration;
use std::{env, fs, io, process, thread}; use std::{env, fs, io, process, thread};
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use rustix::event::{self, PollFd, PollFlags};
use rustix::io::{self as rustix_io, Errno, FdFlags};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy}; use crate::durable_io::{AtomicWriteOptions, OverwriteMode, PermissionPolicy, SymlinkPolicy};
@@ -535,11 +537,9 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> {
// inherited descriptor. The environment marker is removed immediately so // inherited descriptor. The environment marker is removed immediately so
// no later exec can reinterpret the descriptor number. // no later exec can reinterpret the descriptor number.
let descriptor = unsafe { OwnedFd::from_raw_fd(raw) }; let descriptor = unsafe { OwnedFd::from_raw_fd(raw) };
// SAFETY: descriptor is owned and F_SETFD changes only exec inheritance. rustix_io::fcntl_setfd(&descriptor, FdFlags::CLOEXEC)
if unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 { .map_err(io::Error::from)
return Err(io::Error::last_os_error()) .context("failed to protect daemon watchdog descriptor")?;
.context("failed to protect daemon watchdog descriptor");
}
super::linux::validate_pidfd(descriptor.as_fd()) super::linux::validate_pidfd(descriptor.as_fd())
.context("inherited daemon watchdog is not a live pidfd")?; .context("inherited daemon watchdog is not a live pidfd")?;
// SAFETY: application startup has not launched runtime worker threads yet; // SAFETY: application startup has not launched runtime worker threads yet;
@@ -550,27 +550,12 @@ pub(crate) fn start_daemon_watchdog_from_environment() -> Result<()> {
Builder::new() Builder::new()
.name("wayscriber-daemon-watchdog".into()) .name("wayscriber-daemon-watchdog".into())
.spawn(move || { .spawn(move || {
let mut pollfd = libc::pollfd { let mut pollfds = [PollFd::new(&descriptor, PollFlags::IN)];
fd: descriptor.as_raw_fd(), while event::poll(&mut pollfds, None) == Err(Errno::INTR) {}
events: libc::POLLIN, // SAFETY: readiness means the owning daemon is gone, and any other
revents: 0, // result is an unusable parent-death channel. Neither can safely
}; // preserve overlay ownership, so this is an irrevocable fail-stop.
loop { unsafe { super::linux::fail_stop(70) }
// SAFETY: pollfd points to one initialized entry and the
// descriptor remains owned by this thread.
let result = unsafe { libc::poll(&raw mut pollfd, 1, -1) };
if result > 0 {
// SAFETY: loss of the owning daemon is an irrevocable
// fail-stop condition for its internal overlay child.
unsafe { super::linux::fail_stop(70) }
}
if result < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted {
continue;
}
// SAFETY: an unusable parent-death channel cannot safely
// preserve overlay ownership.
unsafe { super::linux::fail_stop(70) }
}
}) })
.context("failed to start daemon watchdog")?; .context("failed to start daemon watchdog")?;
Ok(()) Ok(())
@@ -2,6 +2,7 @@ use std::io::ErrorKind;
use std::time::Duration; use std::time::Duration;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use rustix::fs::FlockOperation;
use super::super::BootClock; use super::super::BootClock;
use super::super::action::PreparedAction; use super::super::action::PreparedAction;
@@ -37,14 +38,14 @@ fn claim_command_action_inner(
let path = control_path(&command_root(), command_identity); let path = control_path(&command_root(), command_identity);
let decision = open_lock(&path.join("decision.lock"), false)?; let decision = open_lock(&path.join("decision.lock"), false)?;
if nonblocking { if nonblocking {
match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) { match flock(&decision, FlockOperation::NonBlockingLockExclusive) {
Ok(()) => {} Ok(()) => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(None), Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(None),
Err(error) => return Err(error).context("failed to try command-action decision lock"), Err(error) => return Err(error).context("failed to try command-action decision lock"),
} }
} else { } else {
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; 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 mut control = read_control(&path)?;
let exact = |status: &ActionStatus| match status { let exact = |status: &ActionStatus| match status {
@@ -193,14 +194,14 @@ fn finish_command_action_inner(
let path = control_path(&command_root(), command_identity); let path = control_path(&command_root(), command_identity);
let decision = open_lock(&path.join("decision.lock"), false)?; let decision = open_lock(&path.join("decision.lock"), false)?;
if nonblocking { if nonblocking {
match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) { match flock(&decision, FlockOperation::NonBlockingLockExclusive) {
Ok(()) => {} Ok(()) => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(false), Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(false),
Err(error) => return Err(error).context("failed to try command-action decision lock"), Err(error) => return Err(error).context("failed to try command-action decision lock"),
} }
} else { } else {
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; 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 mut control = read_control(&path)?;
let applied = result == CommandActionResult::Applied; let applied = result == CommandActionResult::Applied;
+9 -3
View File
@@ -1,6 +1,7 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{Result, anyhow, bail}; use anyhow::{Result, anyhow, bail};
use rustix::fs::FlockOperation;
#[cfg(test)] #[cfg(test)]
use super::super::wire::CommandControl; use super::super::wire::CommandControl;
@@ -61,7 +62,8 @@ impl ClaimedCommand {
journal.prepare_command(&expected.identity, &expected.target_daemon_token, action)?; journal.prepare_command(&expected.identity, &expected.target_daemon_token, action)?;
let deadline = let deadline =
BootDeadline::from_nanos(expected.submission_clock.authorization_deadline_boottime_ns); 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"); let _ = journal.abandon(&prepared, "command decision lock was not reacquired");
return Err(error); return Err(error);
} }
@@ -71,7 +73,11 @@ impl ClaimedCommand {
let reason = "command was canceled during action preparation"; let reason = "command was canceled during action preparation";
journal.abandon(&prepared, reason)?; journal.abandon(&prepared, reason)?;
let recovery_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; 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)?; let mut latest = read_control(&self.control_path)?;
if latest != current if latest != current
|| !matches!( || !matches!(
@@ -221,6 +227,6 @@ impl ClaimedCommand {
impl Drop for ClaimedCommand { impl Drop for ClaimedCommand {
fn drop(&mut self) { fn drop(&mut self) {
let _ = flock(&self.decision_lock, libc::LOCK_UN); let _ = flock(&self.decision_lock, FlockOperation::Unlock);
} }
} }
+17 -4
View File
@@ -3,6 +3,7 @@ use std::time::Duration;
use std::{fs, process, thread}; use std::{fs, process, thread};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use rustix::fs::FlockOperation;
#[cfg(test)] #[cfg(test)]
use anyhow::anyhow; use anyhow::anyhow;
@@ -62,8 +63,16 @@ impl ClientCommand {
create_private_directory(&stage)?; create_private_directory(&stage)?;
let decision_lock = open_lock(&stage.join("decision.lock"), true)?; let decision_lock = open_lock(&stage.join("decision.lock"), true)?;
let caller_lease = open_lock(&stage.join("caller.lease"), true)?; let caller_lease = open_lock(&stage.join("caller.lease"), true)?;
lock_until(&caller_lease, libc::LOCK_SH, authorization_deadline)?; lock_until(
lock_until(&decision_lock, libc::LOCK_EX, authorization_deadline)?; &caller_lease,
FlockOperation::LockShared,
authorization_deadline,
)?;
lock_until(
&decision_lock,
FlockOperation::LockExclusive,
authorization_deadline,
)?;
let mut control = CommandControl { let mut control = CommandControl {
protocol_version: DAEMON_COMMAND_PROTOCOL_VERSION, protocol_version: DAEMON_COMMAND_PROTOCOL_VERSION,
@@ -154,7 +163,7 @@ impl ClientCommand {
let slice = now let slice = now
.checked_add(Duration::from_millis(20))? .checked_add(Duration::from_millis(20))?
.min(self.response_deadline); .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 { if slice < self.response_deadline {
continue; continue;
} }
@@ -201,7 +210,11 @@ impl ClientCommand {
let _ = unlock(&self.caller_lease); let _ = unlock(&self.caller_lease);
return Ok(TerminalCommandResult::AdmittedIndeterminate(reason)); 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)?; let mut control = read_control(&self.control_path)?;
if cancel_open(&mut control)? { if cancel_open(&mut control)? {
write_control(&self.control_path, &control)?; write_control(&self.control_path, &control)?;
+15 -12
View File
@@ -1,13 +1,13 @@
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, ErrorKind}; use std::io::{self, ErrorKind};
use std::os::fd::AsRawFd;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use rustix::fs::{self as rustix_fs, FlockOperation};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use crate::durable_io::AtomicWriteOptions; use crate::durable_io::AtomicWriteOptions;
@@ -279,22 +279,25 @@ pub(super) fn open_lock(path: &Path, create_new: bool) -> Result<File> {
Ok(file) Ok(file)
} }
pub(super) fn flock(file: &File, operation: libc::c_int) -> io::Result<()> { pub(super) fn flock(file: &File, operation: FlockOperation) -> io::Result<()> {
// SAFETY: file owns a valid descriptor and flock does not retain it. rustix_fs::flock(file, operation).map_err(io::Error::from)
if unsafe { libc::flock(file.as_raw_fd(), operation) } == 0 { }
Ok(())
} else { fn nonblocking(operation: FlockOperation) -> FlockOperation {
Err(io::Error::last_os_error()) match operation {
FlockOperation::LockShared => FlockOperation::NonBlockingLockShared,
FlockOperation::LockExclusive => FlockOperation::NonBlockingLockExclusive,
other => other,
} }
} }
pub(super) fn unlock(file: &File) -> Result<()> { 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( pub(super) fn lock_until(
file: &File, file: &File,
operation: libc::c_int, operation: FlockOperation,
deadline: BootDeadline, deadline: BootDeadline,
) -> Result<()> { ) -> Result<()> {
if try_lock_until(file, operation, deadline)? { if try_lock_until(file, operation, deadline)? {
@@ -306,11 +309,11 @@ pub(super) fn lock_until(
pub(super) fn try_lock_until( pub(super) fn try_lock_until(
file: &File, file: &File,
operation: libc::c_int, operation: FlockOperation,
deadline: BootDeadline, deadline: BootDeadline,
) -> Result<bool> { ) -> Result<bool> {
loop { loop {
match flock(file, operation | libc::LOCK_NB) { match flock(file, nonblocking(operation)) {
Ok(()) => return Ok(true), Ok(()) => return Ok(true),
Err(error) if error.kind() == ErrorKind::WouldBlock => { Err(error) if error.kind() == ErrorKind::WouldBlock => {
if BootClock::now()? >= deadline { if BootClock::now()? >= deadline {
@@ -341,7 +344,7 @@ pub(super) fn try_admission_lock(root: &Path, deadline: BootDeadline) -> Result<
} }
Err(error) => return Err(error), 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)) Ok(Some(file))
} else { } else {
Ok(None) Ok(None)
+12 -7
View File
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use rustix::fs::FlockOperation;
use super::super::wire::{ use super::super::wire::{
ActionStatus, CallerDisposition, ChildReportStatus, CommandDecision, CommandResponse, ActionStatus, CallerDisposition, ChildReportStatus, CommandDecision, CommandResponse,
@@ -191,7 +192,11 @@ impl CommandOwner {
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
let retry_deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; 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); return Ok(None);
} }
let Ok(mut control) = read_control(&path) else { let Ok(mut control) = read_control(&path) else {
@@ -316,7 +321,7 @@ impl CommandOwner {
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; 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 mut control = read_control(&path)?;
if control.identity == reference.command_identity if control.identity == reference.command_identity
&& control.queue_order == reference.queue_order && control.queue_order == reference.queue_order
@@ -341,13 +346,13 @@ impl CommandOwner {
observed: &super::super::wire::CommandControl, observed: &super::super::wire::CommandControl,
) -> Result<()> { ) -> Result<()> {
let caller_lease = open_lock(&path.join("caller.lease"), false)?; 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(()) => {} Ok(()) => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(()), Err(error) if error.kind() == ErrorKind::WouldBlock => return Ok(()),
Err(error) => return Err(error).context("failed to inspect unqueued caller lease"), Err(error) => return Err(error).context("failed to inspect unqueued caller lease"),
} }
let decision = open_lock(&path.join("decision.lock"), false)?; let decision = open_lock(&path.join("decision.lock"), false)?;
match flock(&decision, libc::LOCK_EX | libc::LOCK_NB) { match flock(&decision, FlockOperation::NonBlockingLockExclusive) {
Ok(()) => {} Ok(()) => {}
Err(error) if error.kind() == ErrorKind::WouldBlock => { Err(error) if error.kind() == ErrorKind::WouldBlock => {
unlock(&caller_lease)?; unlock(&caller_lease)?;
@@ -390,7 +395,7 @@ impl CommandOwner {
} }
let decision_lock = open_lock(&path.join("decision.lock"), false)?; let decision_lock = open_lock(&path.join("decision.lock"), false)?;
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; 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); return Ok(None);
} }
let mut control = read_control(&path)?; 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)? { for entry in read_dir_bounded(&controls_dir(&self.root), MAX_DIRECTORY_ENTRIES_PER_SCAN)? {
let path = entry.path(); let path = entry.path();
let lease = open_lock(&path.join("caller.lease"), false)?; 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; continue;
} }
let decision = open_lock(&path.join("decision.lock"), false)?; 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)?; unlock(&lease)?;
continue; continue;
} }
+2 -1
View File
@@ -3,6 +3,7 @@ use std::path::Path;
use std::time::Duration; use std::time::Duration;
use anyhow::{Result, anyhow, bail}; use anyhow::{Result, anyhow, bail};
use rustix::fs::FlockOperation;
use super::super::BootClock; use super::super::BootClock;
use super::super::wire::{ use super::super::wire::{
@@ -32,7 +33,7 @@ pub(super) fn recover_previous_generation(root: &Path, daemon_token: &str) -> Re
continue; continue;
}; };
let deadline = BootClock::now()?.checked_add(Duration::from_millis(200))?; 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 { let Ok(mut control) = read_control(&path) else {
unlock(&decision_lock)?; unlock(&decision_lock)?;
quarantine_entry(root, &path, QuarantineKind::Control)?; quarantine_entry(root, &path, QuarantineKind::Control)?;
+2 -1
View File
@@ -3,6 +3,7 @@ use std::path::Path;
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use rustix::fs::FlockOperation;
use super::super::{BootClock, BootIdentity}; use super::super::{BootClock, BootIdentity};
use super::MAX_COMMAND_STAGING_DIRECTORIES; use super::MAX_COMMAND_STAGING_DIRECTORIES;
@@ -24,7 +25,7 @@ pub(super) fn recover_staging(root: &Path) -> Result<()> {
let _ = fs::remove_dir_all(path); let _ = fs::remove_dir_all(path);
continue; continue;
}; };
if flock(&lease, libc::LOCK_EX | libc::LOCK_NB).is_ok() { if flock(&lease, FlockOperation::NonBlockingLockExclusive).is_ok() {
fs::remove_dir_all(path)?; fs::remove_dir_all(path)?;
} }
} }
+8 -6
View File
@@ -1,6 +1,8 @@
use std::env; use std::env;
use std::time::Duration; use std::time::Duration;
use rustix::fs::FlockOperation;
use crate::{test_env, test_temp}; use crate::{test_env, test_temp};
use super::super::BootClock; use super::super::BootClock;
@@ -208,7 +210,7 @@ fn contended_published_command_claim_is_deferred_and_retried() {
&token, &token,
) )
.unwrap(); .unwrap();
flock(&client.decision_lock, libc::LOCK_EX).unwrap(); flock(&client.decision_lock, FlockOperation::LockExclusive).unwrap();
assert!(owner.claim_next().unwrap().is_none()); assert!(owner.claim_next().unwrap().is_none());
let now = BootClock::now().unwrap(); let now = BootClock::now().unwrap();
@@ -252,7 +254,7 @@ fn contended_ref_less_recovery_is_deferred_and_retried() {
&control.identity, &control.identity,
)) ))
.unwrap(); .unwrap();
flock(&client.decision_lock, libc::LOCK_EX).unwrap(); flock(&client.decision_lock, FlockOperation::LockExclusive).unwrap();
assert!(owner.claim_next().unwrap().is_none()); assert!(owner.claim_next().unwrap().is_none());
unlock(&client.decision_lock).unwrap(); unlock(&client.decision_lock).unwrap();
@@ -287,7 +289,7 @@ fn terminal_collection_defers_while_admission_is_busy() {
drop(client); drop(client);
let admission = open_lock(&owner.root.join("admission.lock"), false).unwrap(); 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); assert_eq!(owner.collect_terminal().unwrap(), 0);
let now = BootClock::now().unwrap(); let now = BootClock::now().unwrap();
let retry = owner let retry = owner
@@ -715,20 +717,20 @@ fn lock_deadline_is_reported_without_parsing_error_text() {
let path = root.join("structured-timeout.lock"); let path = root.join("structured-timeout.lock");
let held = open_lock(&path, true).unwrap(); let held = open_lock(&path, true).unwrap();
let contender = open_lock(&path, false).unwrap(); let contender = open_lock(&path, false).unwrap();
flock(&held, libc::LOCK_EX).unwrap(); flock(&held, FlockOperation::LockExclusive).unwrap();
let deadline = BootClock::now() let deadline = BootClock::now()
.unwrap() .unwrap()
.checked_add(Duration::from_millis(5)) .checked_add(Duration::from_millis(5))
.unwrap(); .unwrap();
assert!(!try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap()); assert!(!try_lock_until(&contender, FlockOperation::LockExclusive, deadline).unwrap());
unlock(&held).unwrap(); unlock(&held).unwrap();
let deadline = BootClock::now() let deadline = BootClock::now()
.unwrap() .unwrap()
.checked_add(Duration::from_millis(5)) .checked_add(Duration::from_millis(5))
.unwrap(); .unwrap();
assert!(try_lock_until(&contender, libc::LOCK_EX, deadline).unwrap()); assert!(try_lock_until(&contender, FlockOperation::LockExclusive, deadline).unwrap());
unlock(&contender).unwrap(); unlock(&contender).unwrap();
}); });
} }
+73 -184
View File
@@ -1,12 +1,21 @@
use std::ffi::CString; use std::ffi::CString;
use std::fs::{File, Metadata, OpenOptions, Permissions}; use std::fs::{File, Metadata, OpenOptions, Permissions};
use std::io::{self, Read}; use std::io::{self, Read};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{fmt, fs, process, ptr}; use std::{fmt, fs, process, ptr};
use rustix::event::{self, PollFd, PollFlags, Timespec};
use rustix::fs::inotify::{self, CreateFlags, WatchFlags};
use rustix::io::{self as rustix_io, Errno};
use rustix::process::{self as rustix_process, Pid, PidfdFlags};
use rustix::time::{
self as rustix_time, DynamicClockId, Itimerspec, TimerfdClockId, TimerfdFlags,
TimerfdTimerFlags,
};
const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id"; const BOOT_ID_PATH: &str = "/proc/sys/kernel/random/boot_id";
const TIME_NAMESPACE_PATH: &str = "/proc/self/ns/time"; const TIME_NAMESPACE_PATH: &str = "/proc/self/ns/time";
const PID_NAMESPACE_PATH: &str = "/proc/self/ns/pid"; const PID_NAMESPACE_PATH: &str = "/proc/self/ns/pid";
@@ -63,33 +72,6 @@ pub(crate) struct CommandQueueWatcher {
buffered_offset: usize, buffered_offset: usize,
} }
fn fill_random(bytes: &mut [u8]) -> io::Result<()> {
let mut filled = 0;
while filled < bytes.len() {
// SAFETY: the remaining slice is writable for its full length. getrandom
// writes at most that length and does not retain the pointer.
let result = unsafe {
libc::getrandom(bytes[filled..].as_mut_ptr().cast(), bytes.len() - filled, 0)
};
if result > 0 {
filled += result as usize;
continue;
}
if result == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"getrandom returned zero bytes",
));
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(error);
}
Ok(())
}
fn lowercase_hex(bytes: &[u8], output: &mut fmt::Formatter<'_>) -> fmt::Result { fn lowercase_hex(bytes: &[u8], output: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in bytes { for byte in bytes {
write!(output, "{byte:02x}")?; write!(output, "{byte:02x}")?;
@@ -100,7 +82,7 @@ fn lowercase_hex(bytes: &[u8], output: &mut fmt::Formatter<'_>) -> fmt::Result {
impl ProtocolId { impl ProtocolId {
pub(crate) fn generate() -> io::Result<Self> { pub(crate) fn generate() -> io::Result<Self> {
let mut bytes = [0; 16]; let mut bytes = [0; 16];
fill_random(&mut bytes)?; getrandom::fill(&mut bytes)?;
Ok(Self(bytes)) Ok(Self(bytes))
} }
} }
@@ -120,7 +102,7 @@ impl fmt::Display for ProtocolId {
impl ProtocolToken { impl ProtocolToken {
pub(crate) fn generate() -> io::Result<Self> { pub(crate) fn generate() -> io::Result<Self> {
let mut bytes = [0; 32]; let mut bytes = [0; 32];
fill_random(&mut bytes)?; getrandom::fill(&mut bytes)?;
Ok(Self(bytes)) Ok(Self(bytes))
} }
} }
@@ -198,8 +180,7 @@ pub(crate) fn read_bounded_private_file(path: &Path, cap: usize) -> io::Result<V
/// Whether this user owns the file or directory and nobody else may use it. /// Whether this user owns the file or directory and nobody else may use it.
pub(crate) fn is_private(metadata: &Metadata) -> bool { pub(crate) fn is_private(metadata: &Metadata) -> bool {
// SAFETY: geteuid has no preconditions and cannot fail. metadata.uid() == rustix_process::geteuid().as_raw() && metadata.mode() & 0o077 == 0
metadata.uid() == unsafe { libc::geteuid() } && metadata.mode() & 0o077 == 0
} }
/// Creates `path` as a directory only this user may use, or makes an existing /// Creates `path` as a directory only this user may use, or makes an existing
@@ -215,8 +196,7 @@ pub(crate) fn create_private_directory(path: &Path) -> anyhow::Result<()> {
} }
let metadata = fs::symlink_metadata(path)?; let metadata = fs::symlink_metadata(path)?;
// SAFETY: geteuid has no preconditions and cannot fail. if !metadata.is_dir() || metadata.uid() != rustix_process::geteuid().as_raw() {
if !metadata.is_dir() || metadata.uid() != unsafe { libc::geteuid() } {
anyhow::bail!("{} is not a no-follow private directory", path.display()); anyhow::bail!("{} is not a no-follow private directory", path.display());
} }
fs::set_permissions(path, Permissions::from_mode(0o700))?; fs::set_permissions(path, Permissions::from_mode(0o700))?;
@@ -389,15 +369,10 @@ pub(crate) fn process_start_ticks(pid: u32) -> io::Result<u64> {
pub(crate) fn open_pidfd(pid: u32) -> io::Result<OwnedFd> { pub(crate) fn open_pidfd(pid: u32) -> io::Result<OwnedFd> {
let pid = i32::try_from(pid) let pid = i32::try_from(pid)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "pid does not fit pid_t"))?; .ok()
// SAFETY: pidfd_open has no pointer arguments. The returned descriptor is .and_then(Pid::from_raw)
// owned by the caller on success. .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "pid does not fit pid_t"))?;
let raw = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) }; Ok(rustix_process::pidfd_open(pid, PidfdFlags::empty())?)
if raw < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: raw is a new pidfd not wrapped elsewhere.
Ok(unsafe { OwnedFd::from_raw_fd(raw as i32) })
} }
/// Blocks until the process behind `fd` exits or `timeout` elapses, returning /// Blocks until the process behind `fd` exits or `timeout` elapses, returning
@@ -413,27 +388,14 @@ pub(crate) fn wait_for_pidfd_exit(fd: BorrowedFd<'_>, timeout: Duration) -> io::
if remaining.is_zero() { if remaining.is_zero() {
return Ok(false); return Ok(false);
} }
let mut poll_fd = libc::pollfd { let mut pollfds = [PollFd::from_borrowed_fd(fd, PollFlags::IN)];
fd: fd.as_raw_fd(), let remaining = Timespec::try_from(remaining).map_err(io::Error::other)?;
events: libc::POLLIN, match event::poll(&mut pollfds, Some(&remaining)) {
revents: 0, Ok(ready) => return Ok(ready != 0),
};
let millis = i32::try_from(remaining.as_millis()).unwrap_or(i32::MAX);
// SAFETY: one owned pollfd, valid for the call; the descriptor is
// borrowed for at least this long.
let ready = unsafe { libc::poll(&raw mut poll_fd, 1, millis) };
if ready < 0 {
let error = io::Error::last_os_error();
// A signal arriving mid-wait is expected on this thread. // A signal arriving mid-wait is expected on this thread.
if error.kind() == io::ErrorKind::Interrupted { Err(Errno::INTR) => {}
continue; Err(errno) => return Err(errno.into()),
}
return Err(error);
} }
if ready == 0 {
return Ok(false);
}
return Ok(true);
} }
} }
@@ -459,15 +421,7 @@ pub(crate) fn validate_pidfd(fd: BorrowedFd<'_>) -> io::Result<()> {
impl BootClock { impl BootClock {
pub(crate) fn now() -> io::Result<BootDeadline> { pub(crate) fn now() -> io::Result<BootDeadline> {
let mut value = libc::timespec { let value = rustix_time::clock_gettime_dynamic(DynamicClockId::Boottime)?;
tv_sec: 0,
tv_nsec: 0,
};
// SAFETY: value is a valid writable timespec and CLOCK_BOOTTIME has no
// additional preconditions.
if unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &raw mut value) } != 0 {
return Err(io::Error::last_os_error());
}
if value.tv_sec < 0 || !(0..1_000_000_000).contains(&value.tv_nsec) { if value.tv_sec < 0 || !(0..1_000_000_000).contains(&value.tv_nsec) {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidData, io::ErrorKind::InvalidData,
@@ -508,17 +462,11 @@ impl BootDeadline {
impl BootDeadlineSource { impl BootDeadlineSource {
pub(crate) fn try_new() -> io::Result<Self> { pub(crate) fn try_new() -> io::Result<Self> {
// SAFETY: timerfd_create returns a new owned descriptor on success. let fd = rustix_time::timerfd_create(
let raw = unsafe { TimerfdClockId::Boottime,
libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_NONBLOCK | libc::TFD_CLOEXEC) TimerfdFlags::NONBLOCK | TimerfdFlags::CLOEXEC,
}; )?;
if raw < 0 { Ok(Self { fd })
return Err(io::Error::last_os_error());
}
Ok(Self {
// SAFETY: raw is a newly created descriptor not wrapped elsewhere.
fd: unsafe { OwnedFd::from_raw_fd(raw) },
})
} }
pub(crate) fn poll_fd(&self) -> BorrowedFd<'_> { pub(crate) fn poll_fd(&self) -> BorrowedFd<'_> {
@@ -528,82 +476,45 @@ impl BootDeadlineSource {
pub(crate) fn arm(&self, deadline: BootDeadline) -> io::Result<()> { pub(crate) fn arm(&self, deadline: BootDeadline) -> io::Result<()> {
let seconds = deadline.0 / 1_000_000_000; let seconds = deadline.0 / 1_000_000_000;
let nanos = deadline.0 % 1_000_000_000; let nanos = deadline.0 % 1_000_000_000;
let value = libc::itimerspec { let value = Itimerspec {
it_interval: libc::timespec { it_interval: Timespec::default(),
tv_sec: 0, it_value: Timespec {
tv_nsec: 0, tv_sec: i64::try_from(seconds).map_err(|_| {
},
it_value: libc::timespec {
tv_sec: libc::time_t::try_from(seconds).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "deadline seconds overflow") io::Error::new(io::ErrorKind::InvalidInput, "deadline seconds overflow")
})?, })?,
tv_nsec: libc::c_long::try_from(nanos).map_err(|_| { tv_nsec: i64::try_from(nanos).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "deadline nanos overflow") io::Error::new(io::ErrorKind::InvalidInput, "deadline nanos overflow")
})?, })?,
}, },
}; };
// SAFETY: value is a fully initialized itimerspec and fd owns a timerfd. rustix_time::timerfd_settime(&self.fd, TimerfdTimerFlags::ABSTIME, &value)?;
if unsafe {
libc::timerfd_settime(
self.fd.as_raw_fd(),
libc::TFD_TIMER_ABSTIME,
&raw const value,
ptr::null_mut(),
)
} != 0
{
return Err(io::Error::last_os_error());
}
Ok(()) Ok(())
} }
pub(crate) fn disarm(&self) -> io::Result<()> { pub(crate) fn disarm(&self) -> io::Result<()> {
let value = libc::itimerspec { let value = Itimerspec {
it_interval: libc::timespec { it_interval: Timespec::default(),
tv_sec: 0, it_value: Timespec::default(),
tv_nsec: 0,
},
it_value: libc::timespec {
tv_sec: 0,
tv_nsec: 0,
},
}; };
// SAFETY: value is initialized and fd owns a timerfd. rustix_time::timerfd_settime(&self.fd, TimerfdTimerFlags::empty(), &value)?;
if unsafe {
libc::timerfd_settime(self.fd.as_raw_fd(), 0, &raw const value, ptr::null_mut())
} != 0
{
return Err(io::Error::last_os_error());
}
Ok(()) Ok(())
} }
pub(crate) fn drain(&self) -> io::Result<bool> { pub(crate) fn drain(&self) -> io::Result<bool> {
let mut expirations = 0_u64; let mut expirations = [0_u8; size_of::<u64>()];
loop { loop {
// SAFETY: expirations is writable and fd remains valid for the read. match rustix_io::read(&self.fd, &mut expirations) {
let result = unsafe { Ok(read) if read == expirations.len() => return Ok(true),
libc::read( Ok(read) => {
self.fd.as_raw_fd(), return Err(io::Error::new(
(&raw mut expirations).cast(), io::ErrorKind::UnexpectedEof,
size_of::<u64>(), format!("timerfd returned a short read ({read} bytes)"),
) ));
}; }
if result == size_of::<u64>() as isize { Err(Errno::INTR) => {}
return Ok(true); Err(Errno::WOULDBLOCK) => return Ok(false),
Err(errno) => return Err(errno.into()),
} }
if result < 0 {
let error = io::Error::last_os_error();
return match error.kind() {
io::ErrorKind::Interrupted => continue,
io::ErrorKind::WouldBlock => Ok(false),
_ => Err(error),
};
}
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("timerfd returned a short read ({result} bytes)"),
));
} }
} }
} }
@@ -611,27 +522,15 @@ impl BootDeadlineSource {
impl CommandQueueWatcher { impl CommandQueueWatcher {
pub(crate) fn try_new(queue_path: &Path) -> io::Result<Self> { pub(crate) fn try_new(queue_path: &Path) -> io::Result<Self> {
let (queue_fd, queue_identity) = open_nofollow_directory(queue_path)?; let (queue_fd, queue_identity) = open_nofollow_directory(queue_path)?;
// SAFETY: inotify_init1 returns a newly owned descriptor on success. let fd = inotify::init(CreateFlags::NONBLOCK | CreateFlags::CLOEXEC)?;
let raw = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
if raw < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: raw is a new descriptor not wrapped elsewhere.
let fd = unsafe { OwnedFd::from_raw_fd(raw) };
let path = CString::new(queue_path.as_os_str().as_encoded_bytes()) let path = CString::new(queue_path.as_os_str().as_encoded_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "queue path contains NUL"))?; .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "queue path contains NUL"))?;
let mask = libc::IN_MOVED_TO // IN_IGNORED and IN_Q_OVERFLOW always arrive, so they are not requested here.
| libc::IN_CREATE let mask = WatchFlags::MOVED_TO
| libc::IN_DELETE_SELF | WatchFlags::CREATE
| libc::IN_MOVE_SELF | WatchFlags::DELETE_SELF
| libc::IN_IGNORED | WatchFlags::MOVE_SELF;
| libc::IN_Q_OVERFLOW; let watch_descriptor = inotify::add_watch(&fd, &path, mask)?;
// SAFETY: path is NUL-terminated and fd owns an inotify instance.
let watch_descriptor =
unsafe { libc::inotify_add_watch(fd.as_raw_fd(), path.as_ptr(), mask) };
if watch_descriptor < 0 {
return Err(io::Error::last_os_error());
}
revalidate_path_identity(queue_path, queue_identity)?; revalidate_path_identity(queue_path, queue_identity)?;
Ok(Self { Ok(Self {
fd, fd,
@@ -657,30 +556,20 @@ impl CommandQueueWatcher {
self.buffered_length = 0; self.buffered_length = 0;
self.buffered_offset = 0; self.buffered_offset = 0;
loop { loop {
// SAFETY: buffer is writable for its full fixed length and fd is match rustix_io::read(&self.fd, &mut self.buffer[..]) {
// a nonblocking inotify descriptor. Ok(0) => {
let read = unsafe { return Err(io::Error::new(
libc::read( io::ErrorKind::UnexpectedEof,
self.fd.as_raw_fd(), "inotify returned EOF",
self.buffer.as_mut_ptr().cast(), ));
self.buffer.len(), }
) Ok(read) => {
}; self.buffered_length = read;
if read > 0 { return Ok(true);
self.buffered_length = read as usize; }
return Ok(true); Err(Errno::INTR) => {}
} Err(Errno::WOULDBLOCK) => return Ok(false),
if read == 0 { Err(errno) => return Err(errno.into()),
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"inotify returned EOF",
));
}
let error = io::Error::last_os_error();
match error.kind() {
io::ErrorKind::Interrupted => continue,
io::ErrorKind::WouldBlock => return Ok(false),
_ => return Err(error),
} }
} }
} }
+21 -25
View File
@@ -1,13 +1,30 @@
use std::os::fd::AsRawFd; use std::os::fd::{AsFd, BorrowedFd};
use std::process::Command; use std::process::Command;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{env, process}; use std::{env, process};
use rustix::event::{self, PollFd, PollFlags, Timespec};
use crate::tray_action::TrayAction; use crate::tray_action::TrayAction;
use crate::{env_vars, test_env, test_temp}; use crate::{env_vars, test_env, test_temp};
use super::*; use super::*;
/// Polls for input. Any other readiness fails the test, so a broken descriptor
/// cannot pass for silence.
fn is_readable(fd: BorrowedFd<'_>, timeout: Duration) -> bool {
let mut pollfds = [PollFd::from_borrowed_fd(fd, PollFlags::IN)];
let timeout = Timespec::try_from(timeout).unwrap();
let ready = event::poll(&mut pollfds, Some(&timeout)).unwrap();
let revents = pollfds[0].revents();
assert!(
ready == 0 || revents == PollFlags::IN,
"unexpected readiness {:#x}",
revents.bits()
);
ready == 1
}
#[test] #[test]
fn production_mode_is_closed_to_published_v2_after_cutover() { fn production_mode_is_closed_to_published_v2_after_cutover() {
assert_eq!( assert_eq!(
@@ -71,15 +88,7 @@ fn boottime_deadline_source_arms_absolutely_and_drains_once() {
.unwrap(); .unwrap();
source.arm(deadline).unwrap(); source.arm(deadline).unwrap();
let mut poll_fd = libc::pollfd { assert!(is_readable(source.poll_fd(), Duration::from_millis(500)));
fd: source.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: poll_fd remains valid for this bounded poll.
let ready = unsafe { libc::poll(&raw mut poll_fd, 1, 500) };
assert_eq!(ready, 1);
assert_ne!(poll_fd.revents & libc::POLLIN, 0);
assert!(source.drain().unwrap()); assert!(source.drain().unwrap());
assert!(!source.drain().unwrap()); assert!(!source.drain().unwrap());
source.disarm().unwrap(); source.disarm().unwrap();
@@ -128,14 +137,7 @@ fn pidfd_wait_times_out_while_alive_and_returns_once_exited() {
#[test] #[test]
fn self_pidfd_is_not_readable_while_process_is_alive() { fn self_pidfd_is_not_readable_while_process_is_alive() {
let pidfd = linux::open_self_pidfd().unwrap(); let pidfd = linux::open_self_pidfd().unwrap();
let mut poll_fd = libc::pollfd { assert!(!is_readable(pidfd.as_fd(), Duration::ZERO));
fd: pidfd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: poll_fd remains valid for the zero-timeout observation.
let ready = unsafe { libc::poll(&raw mut poll_fd, 1, 0) };
assert_eq!(ready, 0);
} }
#[test] #[test]
@@ -189,13 +191,7 @@ fn command_queue_watch_reports_atomic_publication_and_rejects_replacement() {
) )
.unwrap(); .unwrap();
let mut poll_fd = libc::pollfd { assert!(is_readable(watcher.poll_fd(), Duration::from_millis(500)));
fd: watcher.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: watcher owns the descriptor throughout the bounded poll.
assert_eq!(unsafe { libc::poll(&raw mut poll_fd, 1, 500) }, 1);
assert!(watcher.drain().unwrap().scan_pending); assert!(watcher.drain().unwrap().scan_pending);
fs::rename(&queue, temp.path().join("old-queue")).unwrap(); fs::rename(&queue, temp.path().join("old-queue")).unwrap();
+11 -17
View File
@@ -8,6 +8,9 @@ use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(target_os = "linux")]
use rustix::fs::{self as rustix_fs, RenameFlags};
use std::ffi::{CString, OsStr, OsString}; use std::ffi::{CString, OsStr, OsString};
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
@@ -594,23 +597,14 @@ fn metadata_mode(_metadata: &fs::Metadata) -> Option<u32> {
pub fn rename_no_replace(source: &Path, target: &Path) -> io::Result<()> { pub fn rename_no_replace(source: &Path, target: &Path) -> io::Result<()> {
let source = path_to_cstring(source)?; let source = path_to_cstring(source)?;
let target = path_to_cstring(target)?; let target = path_to_cstring(target)?;
// SAFETY: The C strings are valid, NUL-terminated paths. AT_FDCWD makes both rustix_fs::renameat_with(
// paths relative to the process cwd, matching std::fs::rename path semantics. rustix_fs::CWD,
let result = unsafe { &source,
libc::syscall( rustix_fs::CWD,
libc::SYS_renameat2, &target,
libc::AT_FDCWD, RenameFlags::NOREPLACE,
source.as_ptr(), )
libc::AT_FDCWD, .map_err(io::Error::from)
target.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
+7 -15
View File
@@ -4,19 +4,11 @@ use std::fs::File;
use std::io; use std::io;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::io::AsRawFd; use rustix::fs::{self as rustix_fs, FlockOperation};
#[cfg(unix)] #[cfg(unix)]
fn flock(file: &File, op: libc::c_int) -> io::Result<()> { fn flock(file: &File, operation: FlockOperation) -> io::Result<()> {
let fd = file.as_raw_fd(); rustix_fs::flock(file, operation).map_err(io::Error::from)
// 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())
}
} }
#[cfg(not(unix))] #[cfg(not(unix))]
@@ -30,7 +22,7 @@ fn flock(_file: &File, _op: i32) -> io::Result<()> {
pub fn lock_shared(file: &File) -> io::Result<()> { pub fn lock_shared(file: &File) -> io::Result<()> {
#[cfg(unix)] #[cfg(unix)]
{ {
flock(file, libc::LOCK_SH) flock(file, FlockOperation::LockShared)
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
@@ -41,7 +33,7 @@ pub fn lock_shared(file: &File) -> io::Result<()> {
pub fn lock_exclusive(file: &File) -> io::Result<()> { pub fn lock_exclusive(file: &File) -> io::Result<()> {
#[cfg(unix)] #[cfg(unix)]
{ {
flock(file, libc::LOCK_EX) flock(file, FlockOperation::LockExclusive)
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
@@ -52,7 +44,7 @@ pub fn lock_exclusive(file: &File) -> io::Result<()> {
pub fn try_lock_exclusive(file: &File) -> io::Result<()> { pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
#[cfg(unix)] #[cfg(unix)]
{ {
flock(file, libc::LOCK_EX | libc::LOCK_NB) flock(file, FlockOperation::NonBlockingLockExclusive)
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
@@ -63,7 +55,7 @@ pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
pub fn unlock(file: &File) -> io::Result<()> { pub fn unlock(file: &File) -> io::Result<()> {
#[cfg(unix)] #[cfg(unix)]
{ {
flock(file, libc::LOCK_UN) flock(file, FlockOperation::Unlock)
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
+2 -5
View File
@@ -1,11 +1,10 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ffi::CString;
use std::os::unix; use std::os::unix;
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration; use std::time::Duration;
use std::{fs, thread}; use std::{fs, thread};
use crate::test_env; use crate::{test_env, test_temp};
use super::*; use super::*;
@@ -314,9 +313,7 @@ fn a_symlinked_override_is_refused() {
fn a_fifo_override_is_refused_without_waiting_for_a_writer() { fn a_fifo_override_is_refused_without_waiting_for_a_writer() {
let directory = tempfile::tempdir().expect("override directory"); let directory = tempfile::tempdir().expect("override directory");
let path = directory.path().join("zh-CN.toml"); let path = directory.path().join("zh-CN.toml");
let raw_path = CString::new(path.as_os_str().as_encoded_bytes()).expect("path"); test_temp::make_fifo(&path);
// SAFETY: `raw_path` is a valid NUL-terminated path for the call.
assert_eq!(unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) }, 0);
let (sender, receiver) = mpsc::channel(); let (sender, receiver) = mpsc::channel();
thread::spawn(move || { thread::spawn(move || {
+36 -55
View File
@@ -1,5 +1,5 @@
use std::io; use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; use std::os::fd::{AsRawFd, OwnedFd};
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@@ -17,6 +17,13 @@ use std::thread::Builder;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
#[cfg(not(test))]
use rustix::io as rustix_io;
use rustix::io::Errno;
use rustix::net::{self, AddressFamily, SocketFlags, SocketType};
#[cfg(not(test))]
use rustix::process::Signal;
use rustix::process::{self as rustix_process, Pid, WaitOptions};
use crate::daemon::protocol_v2::ProtocolToken; use crate::daemon::protocol_v2::ProtocolToken;
#[cfg(not(test))] #[cfg(not(test))]
@@ -152,9 +159,8 @@ pub(super) fn start() -> Result<ProcessBroker> {
}), }),
}; };
if let Err(error) = broker.request(BrokerOperation::Ping) { if let Err(error) = broker.request(BrokerOperation::Ping) {
// SAFETY: pid is the raw-clone broker child created above. if let Some(broker_pid) = Pid::from_raw(pid) {
unsafe { let _ = rustix_process::kill_process(broker_pid, Signal::KILL);
libc::kill(pid, libc::SIGKILL);
} }
wait_for_broker_process(pid); wait_for_broker_process(pid);
return Err(error).context("process broker exec/authentication handshake failed"); return Err(error).context("process broker exec/authentication handshake failed");
@@ -164,37 +170,20 @@ pub(super) fn start() -> Result<ProcessBroker> {
#[cfg(not(test))] #[cfg(not(test))]
fn duplicate_for_exec(descriptor: &OwnedFd) -> Result<OwnedFd> { fn duplicate_for_exec(descriptor: &OwnedFd) -> Result<OwnedFd> {
// SAFETY: F_DUPFD_CLOEXEC duplicates the live descriptor at or above five. rustix_io::fcntl_dupfd_cloexec(descriptor, 5)
let duplicate = unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 5) }; .map_err(io::Error::from)
if duplicate < 0 { .context("failed to stage broker descriptor")
return Err(io::Error::last_os_error()).context("failed to stage broker descriptor");
}
// SAFETY: fcntl returned a fresh owned descriptor.
Ok(unsafe { OwnedFd::from_raw_fd(duplicate) })
} }
fn socket_pair(label: &str) -> Result<(OwnedFd, OwnedFd)> { fn socket_pair(label: &str) -> Result<(OwnedFd, OwnedFd)> {
let mut sockets = [0; 2]; net::socketpair(
// SAFETY: sockets has room for the returned descriptor pair. AddressFamily::UNIX,
if unsafe { SocketType::SEQPACKET,
libc::socketpair( SocketFlags::CLOEXEC,
libc::AF_UNIX, None,
libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC, )
0, .map_err(io::Error::from)
sockets.as_mut_ptr(), .with_context(|| format!("failed to create {label} socketpair"))
)
} != 0
{
return Err(io::Error::last_os_error())
.with_context(|| format!("failed to create {label} socketpair"));
}
// SAFETY: socketpair returned two new descriptors.
Ok(unsafe {
(
OwnedFd::from_raw_fd(sockets[0]),
OwnedFd::from_raw_fd(sockets[1]),
)
})
} }
/// Whether the broker process exits within `grace`, reaping it if it does. /// Whether the broker process exits within `grace`, reaping it if it does.
@@ -203,20 +192,17 @@ pub(super) fn broker_exits_within(child_pid: libc::pid_t, grace: Duration) -> bo
return true; return true;
} }
let Some(pid) = Pid::from_raw(child_pid) else {
return true;
};
let deadline = Instant::now() + grace; let deadline = Instant::now() + grace;
let mut status = 0;
loop { loop {
// SAFETY: child_pid names the raw-clone broker child owned by its guard. match rustix_process::waitpid(Some(pid), WaitOptions::NOHANG) {
let result = unsafe { libc::waitpid(child_pid, &raw mut status, libc::WNOHANG) }; Ok(Some(_)) => return true,
if result == child_pid { Ok(None) => {}
return true; Err(Errno::INTR) => continue,
}
if result < 0 {
if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
// Already reaped: nothing is left to wait for. // Already reaped: nothing is left to wait for.
return true; Err(_) => return true,
} }
if Instant::now() >= deadline { if Instant::now() >= deadline {
return false; return false;
@@ -229,16 +215,11 @@ pub(super) fn wait_for_broker_process(child_pid: libc::pid_t) {
if child_pid <= 0 { if child_pid <= 0 {
return; return;
} }
let mut status = 0; let Some(pid) = Pid::from_raw(child_pid) else {
loop { return;
// SAFETY: child_pid names the raw-clone broker child owned by its guard. };
let result = unsafe { libc::waitpid(child_pid, &raw mut status, 0) }; while matches!(
if result == child_pid { rustix_process::waitpid(Some(pid), WaitOptions::empty()),
break; Err(Errno::INTR)
} ) {}
if result < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
break;
}
} }
+31 -37
View File
@@ -2,7 +2,7 @@ use std::collections::VecDeque;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::io; use std::io;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, TryLockError, Weak}; use std::sync::{Arc, Mutex, MutexGuard, OnceLock, TryLockError, Weak};
#[cfg(test)] #[cfg(test)]
@@ -10,6 +10,9 @@ use std::thread::JoinHandle;
use std::time::Duration; use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use rustix::io::Errno;
use rustix::net::{self, SendFlags};
use rustix::process::{self as rustix_process, Pid, Signal};
use crate::daemon::protocol_v2::ProtocolId; use crate::daemon::protocol_v2::ProtocolId;
@@ -87,8 +90,8 @@ enum ExchangeWait {
/// The transport policy and watchdog a spawn request carries, kept together so /// The transport policy and watchdog a spawn request carries, kept together so
/// the private spawn path stays inside the argument-count lint. /// the private spawn path stays inside the argument-count lint.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
struct SpawnOptions { struct SpawnOptions<'descriptor> {
watchdog: Option<RawFd>, watchdog: Option<BorrowedFd<'descriptor>>,
wait: ExchangeWait, wait: ExchangeWait,
} }
@@ -119,7 +122,7 @@ pub(crate) fn current() -> Result<ProcessBroker> {
impl Drop for ProcessBrokerGuard { impl Drop for ProcessBrokerGuard {
fn drop(&mut self) { fn drop(&mut self) {
let was_healthy = self.broker.inner.healthy.swap(false, Ordering::AcqRel); let was_healthy = self.broker.inner.healthy.swap(false, Ordering::AcqRel);
let asked_to_stop = signal_shutdown(self.broker.inner.shutdown.as_raw_fd()).is_ok(); let asked_to_stop = signal_shutdown(self.broker.inner.shutdown.as_fd()).is_ok();
// A broker whose exchange failed may be stuck and never read the // A broker whose exchange failed may be stuck and never read the
// shutdown request. Waiting on it without a bound would hang the very // shutdown request. Waiting on it without a bound would hang the very
// exit that lets the service manager start a fresh daemon. // exit that lets the service manager start a fresh daemon.
@@ -129,11 +132,11 @@ impl Drop for ProcessBrokerGuard {
self.broker.inner.child_pid, self.broker.inner.child_pid,
UNHEALTHY_BROKER_EXIT_GRACE, UNHEALTHY_BROKER_EXIT_GRACE,
)); ));
if must_kill && self.broker.inner.child_pid > 0 { if must_kill
// SAFETY: child_pid is the broker process owned by this guard. && self.broker.inner.child_pid > 0
unsafe { && let Some(pid) = Pid::from_raw(self.broker.inner.child_pid)
libc::kill(self.broker.inner.child_pid, libc::SIGKILL); {
} let _ = rustix_process::kill_process(pid, Signal::KILL);
} }
#[cfg(test)] #[cfg(test)]
if let Some(thread) = self if let Some(thread) = self
@@ -163,29 +166,20 @@ impl Drop for ProcessBrokerGuard {
} }
} }
fn signal_shutdown(descriptor: RawFd) -> io::Result<()> { fn signal_shutdown(descriptor: BorrowedFd<'_>) -> io::Result<()> {
let byte = [GRACEFUL_SHUTDOWN_BYTE]; let byte = [GRACEFUL_SHUTDOWN_BYTE];
loop { loop {
// SAFETY: descriptor is the broker shutdown socket and byte is readable. match net::send(descriptor, &byte, SendFlags::NOSIGNAL) {
let written = unsafe { Ok(1) => return Ok(()),
libc::send( Ok(_) => {
descriptor, return Err(io::Error::new(
byte.as_ptr().cast(), ErrorKind::WriteZero,
byte.len(), "short broker shutdown write",
libc::MSG_NOSIGNAL, ));
) }
}; Err(Errno::INTR) => {}
if written == 1 { Err(errno) => return Err(errno.into()),
return Ok(());
} }
if written < 0 && io::Error::last_os_error().kind() == ErrorKind::Interrupted {
continue;
}
return Err(if written < 0 {
io::Error::last_os_error()
} else {
io::Error::new(ErrorKind::WriteZero, "short broker shutdown write")
});
} }
} }
@@ -253,7 +247,7 @@ impl ProcessBroker {
fn request_with_descriptors( fn request_with_descriptors(
&self, &self,
operation: BrokerOperation, operation: BrokerOperation,
descriptors: &[RawFd], descriptors: &[BorrowedFd<'_>],
wait: ExchangeWait, wait: ExchangeWait,
) -> Result<(BrokerOutcome, Vec<OwnedFd>)> { ) -> Result<(BrokerOutcome, Vec<OwnedFd>)> {
let request_id = ProtocolId::generate()?.to_string(); let request_id = ProtocolId::generate()?.to_string();
@@ -271,9 +265,9 @@ impl ProcessBroker {
bail!("process broker transport is no longer usable"); bail!("process broker transport is no longer usable");
} }
let exchange = (|| -> Result<(BrokerResponse, Vec<OwnedFd>)> { let exchange = (|| -> Result<(BrokerResponse, Vec<OwnedFd>)> {
set_socket_timeout(self.inner.socket.as_raw_fd(), exchange_timeout)?; set_socket_timeout(self.inner.socket.as_fd(), exchange_timeout)?;
send_packet(self.inner.socket.as_raw_fd(), &packet, descriptors)?; send_packet(self.inner.socket.as_fd(), &packet, descriptors)?;
let (packet, descriptors) = recv_packet(self.inner.socket.as_raw_fd())?; let (packet, descriptors) = recv_packet(self.inner.socket.as_fd())?;
let response: BrokerResponse = serde_json::from_slice(&packet)?; let response: BrokerResponse = serde_json::from_slice(&packet)?;
if response.request_id != request_id { if response.request_id != request_id {
bail!("broker response identity mismatch"); bail!("broker response identity mismatch");
@@ -368,7 +362,7 @@ impl ProcessBroker {
let (input, input_descriptor) = encode_blob(input, MAX_INPUT_BYTES)?; let (input, input_descriptor) = encode_blob(input, MAX_INPUT_BYTES)?;
let request_descriptors = input_descriptor let request_descriptors = input_descriptor
.as_ref() .as_ref()
.map(|descriptor| vec![descriptor.as_raw_fd()]) .map(|descriptor| vec![descriptor.as_fd()])
.unwrap_or_default(); .unwrap_or_default();
let (outcome, descriptors) = self.request_with_descriptors( let (outcome, descriptors) = self.request_with_descriptors(
BrokerOperation::Run { BrokerOperation::Run {
@@ -428,7 +422,7 @@ impl ProcessBroker {
let (input, input_descriptor) = encode_blob(input, super::manifest::input_cap(kind))?; let (input, input_descriptor) = encode_blob(input, super::manifest::input_cap(kind))?;
let request_descriptors = input_descriptor let request_descriptors = input_descriptor
.as_ref() .as_ref()
.map(|descriptor| vec![descriptor.as_raw_fd()]) .map(|descriptor| vec![descriptor.as_fd()])
.unwrap_or_default(); .unwrap_or_default();
let (outcome, descriptors) = self.request_with_descriptors( let (outcome, descriptors) = self.request_with_descriptors(
BrokerOperation::Publish { BrokerOperation::Publish {
@@ -530,7 +524,7 @@ impl ProcessBroker {
program: &OsStr, program: &OsStr,
arguments: I, arguments: I,
environment: Vec<(OsString, Option<OsString>)>, environment: Vec<(OsString, Option<OsString>)>,
watchdog: RawFd, watchdog: BorrowedFd<'_>,
) -> Result<BrokerChild> ) -> Result<BrokerChild>
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
@@ -556,7 +550,7 @@ impl ProcessBroker {
program: &OsStr, program: &OsStr,
arguments: I, arguments: I,
environment: Vec<(OsString, Option<OsString>)>, environment: Vec<(OsString, Option<OsString>)>,
options: SpawnOptions, options: SpawnOptions<'_>,
) -> Result<BrokerChild> ) -> Result<BrokerChild>
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
+40 -75
View File
@@ -1,7 +1,6 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::mem::MaybeUninit; use std::os::fd::{AsFd, BorrowedFd};
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio}; use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
use std::sync::Arc; use std::sync::Arc;
@@ -10,6 +9,10 @@ use std::time::Duration;
use std::{mem, thread}; use std::{mem, thread};
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
use rustix::event::{self, PollFd, PollFlags, Timespec};
use rustix::fs::{self as rustix_fs, OFlags};
use rustix::io::Errno;
use rustix::process::{self as rustix_process, Pid, Signal, WaitId, WaitIdOptions};
use crate::daemon::protocol_v2::{BootClock, BootDeadline}; use crate::daemon::protocol_v2::{BootClock, BootDeadline};
@@ -25,7 +28,10 @@ use super::wire::{HelperKind, MAX_STDERR_BYTES, OutputMode, STDOUT_CAP_EXCEEDED}
const PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2); const PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2);
/// How often a pipe thread waiting on an idle pipe checks whether to give up. /// How often a pipe thread waiting on an idle pipe checks whether to give up.
const PIPE_POLL_INTERVAL_MS: libc::c_int = 20; const PIPE_POLL_INTERVAL: Timespec = Timespec {
tv_sec: 0,
tv_nsec: 20_000_000,
};
const PIPE_HELD_OPEN: &str = "broker helper pipe is still open after the helper exited"; const PIPE_HELD_OPEN: &str = "broker helper pipe is still open after the helper exited";
@@ -63,12 +69,9 @@ pub(super) fn terminate_owned_children(children: &mut BTreeMap<String, Child>) {
} }
pub(super) fn kill_child_process_group(child: &mut Child) { pub(super) fn kill_child_process_group(child: &mut Child) {
let pid = child.id(); // Broker children use a fresh process group equal to their PID.
if let Ok(group) = i32::try_from(pid) { if rustix_process::kill_process_group(Pid::from_child(child), Signal::KILL).is_ok() {
// SAFETY: broker children use a fresh process group equal to their PID. return;
if unsafe { libc::kill(-group, libc::SIGKILL) } == 0 {
return;
}
} }
let _ = child.kill(); let _ = child.kill();
} }
@@ -145,48 +148,28 @@ impl Drop for OwnedProcess {
} }
fn child_status_unreaped(child: &Child) -> io::Result<Option<i32>> { fn child_status_unreaped(child: &Child) -> io::Result<Option<i32>> {
let mut info = MaybeUninit::<libc::siginfo_t>::zeroed();
// WNOWAIT pins the zombie leader and therefore its process-group identity. // WNOWAIT pins the zombie leader and therefore its process-group identity.
// SAFETY: info is writable and child is owned by this broker. let options = WaitIdOptions::EXITED | WaitIdOptions::NOHANG | WaitIdOptions::NOWAIT;
if unsafe { let Some(status) = rustix_process::waitid(WaitId::Pid(Pid::from_child(child)), options)? else {
libc::waitid(
libc::P_PID,
child.id(),
info.as_mut_ptr(),
libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
)
} != 0
{
return Err(io::Error::last_os_error());
}
// SAFETY: waitid initialized info after success.
let info = unsafe { info.assume_init() };
// SAFETY: info was zeroed before the call and waitid only writes a SIGCHLD
// record into it, so the pid field it reads is initialized; it stays zero
// when WNOHANG found no waitable child.
if unsafe { info.si_pid() } == 0 {
return Ok(None); return Ok(None);
}
// SAFETY: CLD terminal records initialize si_status.
let detail = unsafe { info.si_status() };
let status = match info.si_code {
libc::CLD_EXITED => detail,
libc::CLD_KILLED | libc::CLD_DUMPED => 128_i32.saturating_add(detail),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"waitid returned a nonterminal child status",
));
}
}; };
Ok(Some(status)) if let Some(code) = status.exit_status() {
return Ok(Some(code));
}
if let Some(signal) = status.terminating_signal() {
return Ok(Some(128_i32.saturating_add(signal)));
}
Err(io::Error::new(
io::ErrorKind::InvalidData,
"waitid returned a nonterminal child status",
))
} }
pub(super) fn publish_bounded( pub(super) fn publish_bounded(
mut command: Command, mut command: Command,
input: Vec<u8>, input: Vec<u8>,
timeout: Duration, timeout: Duration,
shutdown_fd: RawFd, shutdown_fd: BorrowedFd<'static>,
) -> Result<PublishOutput> { ) -> Result<PublishOutput> {
if shutdown_requested(shutdown_fd)? { if shutdown_requested(shutdown_fd)? {
return Err(anyhow!("broker publication cancelled during shutdown")); return Err(anyhow!("broker publication cancelled during shutdown"));
@@ -254,17 +237,10 @@ fn write_input_until(
mut stdin: ChildStdin, mut stdin: ChildStdin,
input: &[u8], input: &[u8],
deadline: BootDeadline, deadline: BootDeadline,
shutdown_fd: RawFd, shutdown_fd: BorrowedFd<'static>,
) -> io::Result<()> { ) -> io::Result<()> {
let descriptor = stdin.as_raw_fd(); let flags = rustix_fs::fcntl_getfl(&stdin)?;
// SAFETY: fcntl reads and updates descriptor-local flags. rustix_fs::fcntl_setfl(&stdin, flags | OFlags::NONBLOCK)?;
let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFL) };
// SAFETY: `stdin` keeps the descriptor open, and F_SETFL only adds
// O_NONBLOCK to the flags just read from it.
if flags < 0 || unsafe { libc::fcntl(descriptor, libc::F_SETFL, flags | libc::O_NONBLOCK) } != 0
{
return Err(io::Error::last_os_error());
}
let mut offset = 0; let mut offset = 0;
while offset < input.len() { while offset < input.len() {
if shutdown_requested(shutdown_fd)? { if shutdown_requested(shutdown_fd)? {
@@ -298,7 +274,7 @@ pub(super) fn run_bounded(
timeout: Duration, timeout: Duration,
output_cap: usize, output_cap: usize,
output_mode: OutputMode, output_mode: OutputMode,
shutdown_fd: RawFd, shutdown_fd: BorrowedFd<'static>,
) -> Result<BoundedOutput> { ) -> Result<BoundedOutput> {
if shutdown_requested(shutdown_fd)? { if shutdown_requested(shutdown_fd)? {
return Err(anyhow!("broker helper cancelled during shutdown")); return Err(anyhow!("broker helper cancelled during shutdown"));
@@ -421,7 +397,7 @@ struct AbandonablePipe<P> {
abandon: Arc<AtomicBool>, abandon: Arc<AtomicBool>,
} }
impl<P: AsRawFd> AbandonablePipe<P> { impl<P: AsFd> AbandonablePipe<P> {
fn new(pipe: P, abandon: &Arc<AtomicBool>) -> Self { fn new(pipe: P, abandon: &Arc<AtomicBool>) -> Self {
Self { Self {
pipe, pipe,
@@ -429,42 +405,31 @@ impl<P: AsRawFd> AbandonablePipe<P> {
} }
} }
fn wait_until_ready(&self, events: libc::c_short) -> io::Result<()> { fn wait_until_ready(&self, events: PollFlags) -> io::Result<()> {
let mut pollfd = libc::pollfd { let mut pollfds = [PollFd::new(&self.pipe, events)];
fd: self.pipe.as_raw_fd(),
events,
revents: 0,
};
loop { loop {
if self.abandon.load(Ordering::Acquire) { if self.abandon.load(Ordering::Acquire) {
return Err(io::Error::new(io::ErrorKind::TimedOut, PIPE_HELD_OPEN)); return Err(io::Error::new(io::ErrorKind::TimedOut, PIPE_HELD_OPEN));
} }
// SAFETY: pollfd points to one initialized entry, and the pipe match event::poll(&mut pollfds, Some(&PIPE_POLL_INTERVAL)) {
// stays open for as long as `self` owns it. Ok(0) | Err(Errno::INTR) => {}
let result = unsafe { libc::poll(&raw mut pollfd, 1, PIPE_POLL_INTERVAL_MS) }; Ok(_) => return Ok(()),
if result > 0 { Err(errno) => return Err(errno.into()),
return Ok(());
}
if result < 0 {
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(error);
}
} }
} }
} }
} }
impl<P: Read + AsRawFd> Read for AbandonablePipe<P> { impl<P: Read + AsFd> Read for AbandonablePipe<P> {
fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
self.wait_until_ready(libc::POLLIN)?; self.wait_until_ready(PollFlags::IN)?;
self.pipe.read(buffer) self.pipe.read(buffer)
} }
} }
impl<P: Write + AsRawFd> Write for AbandonablePipe<P> { impl<P: Write + AsFd> Write for AbandonablePipe<P> {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> { fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.wait_until_ready(libc::POLLOUT)?; self.wait_until_ready(PollFlags::OUT)?;
// A writable pipe has room for at least PIPE_BUF bytes, so a write of // A writable pipe has room for at least PIPE_BUF bytes, so a write of
// no more than that cannot block on a reader that stopped reading. // no more than that cannot block on a reader that stopped reading.
self.pipe.write(&buffer[..buffer.len().min(libc::PIPE_BUF)]) self.pipe.write(&buffer[..buffer.len().min(libc::PIPE_BUF)])
+80 -113
View File
@@ -1,5 +1,5 @@
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, VecDeque};
use std::os::fd::{AsRawFd, OwnedFd, RawFd}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::process::{Child, ExitCode, Stdio}; use std::process::{Child, ExitCode, Stdio};
use std::thread::Builder; use std::thread::Builder;
@@ -7,6 +7,9 @@ use std::time::Duration;
use std::{env, io, mem}; use std::{env, io, mem};
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use rustix::event::{self, PollFd, PollFlags};
use rustix::io::{self as rustix_io, Errno, FdFlags};
use rustix::process::{self as rustix_process, Pid, Signal};
use crate::daemon::protocol_v2::ProtocolId; use crate::daemon::protocol_v2::ProtocolId;
use crate::env_vars; use crate::env_vars;
@@ -41,10 +44,17 @@ pub(crate) fn run_internal_broker_if_requested() -> Option<ExitCode> {
{ {
return Some(ExitCode::from(126)); return Some(ExitCode::from(126));
} }
// SAFETY: both numbers were just checked to be open broker sockets, and
// nothing in this process closes them before it exits.
let (fd, shutdown_fd) = unsafe {
(
BorrowedFd::borrow_raw(fd),
BorrowedFd::borrow_raw(shutdown_fd),
)
};
// Restore CLOEXEC before any runtime helper can inherit broker internals. // Restore CLOEXEC before any runtime helper can inherit broker internals.
for descriptor in [fd, shutdown_fd] { for descriptor in [fd, shutdown_fd] {
// SAFETY: both descriptors were validated as inherited broker channels. if rustix_io::fcntl_setfd(descriptor, FdFlags::CLOEXEC).is_err() {
if unsafe { libc::fcntl(descriptor, libc::F_SETFD, libc::FD_CLOEXEC) } != 0 {
return Some(ExitCode::from(126)); return Some(ExitCode::from(126));
} }
} }
@@ -63,7 +73,9 @@ pub(crate) fn run_internal_broker_if_requested() -> Option<ExitCode> {
fn validate_broker_socket(fd: RawFd) -> io::Result<()> { fn validate_broker_socket(fd: RawFd) -> io::Result<()> {
let mut socket_type = 0_i32; let mut socket_type = 0_i32;
let mut length = mem::size_of::<i32>() as libc::socklen_t; let mut length = mem::size_of::<i32>() as libc::socklen_t;
// SAFETY: the output slots are valid for getsockopt. // SAFETY: the output slots are valid for getsockopt, which also rejects a
// number that is not an open descriptor. A borrowed descriptor cannot be
// used here because it requires the number to be open already.
if unsafe { if unsafe {
libc::getsockopt( libc::getsockopt(
fd, fd,
@@ -85,7 +97,11 @@ fn validate_broker_socket(fd: RawFd) -> io::Result<()> {
Ok(()) Ok(())
} }
fn broker_loop(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> { fn broker_loop(
socket: BorrowedFd<'static>,
shutdown_fd: BorrowedFd<'static>,
token: &str,
) -> Result<()> {
let mut ownership = BrokerOwnership::default(); let mut ownership = BrokerOwnership::default();
loop { loop {
if wait_for_request(socket, shutdown_fd)? == BrokerWake::Shutdown { if wait_for_request(socket, shutdown_fd)? == BrokerWake::Shutdown {
@@ -139,7 +155,7 @@ fn broker_loop(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> {
let response_descriptors = wire_response let response_descriptors = wire_response
.descriptors .descriptors
.iter() .iter()
.map(AsRawFd::as_raw_fd) .map(AsFd::as_fd)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
send_packet(socket, &bytes, &response_descriptors)?; send_packet(socket, &bytes, &response_descriptors)?;
if take_graceful_shutdown_signal(shutdown_fd)? { if take_graceful_shutdown_signal(shutdown_fd)? {
@@ -155,39 +171,27 @@ enum BrokerWake {
Shutdown, Shutdown,
} }
fn wait_for_request(socket: RawFd, shutdown_fd: RawFd) -> io::Result<BrokerWake> { fn wait_for_request(socket: BorrowedFd<'_>, shutdown_fd: BorrowedFd<'_>) -> io::Result<BrokerWake> {
let mut descriptors = [ let mut descriptors = [
libc::pollfd { PollFd::from_borrowed_fd(socket, PollFlags::IN),
fd: socket, PollFd::from_borrowed_fd(shutdown_fd, PollFlags::IN),
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: shutdown_fd,
events: libc::POLLIN,
revents: 0,
},
]; ];
let unusable = PollFlags::HUP | PollFlags::ERR | PollFlags::NVAL;
loop { loop {
// SAFETY: descriptors points to two initialized pollfd entries. match event::poll(&mut descriptors, None) {
let result = unsafe { libc::poll(descriptors.as_mut_ptr(), descriptors.len() as _, -1) }; Ok(_) => {}
if result < 0 { Err(Errno::INTR) => continue,
let error = io::Error::last_os_error(); Err(errno) => return Err(errno.into()),
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(error);
} }
if descriptors[1].revents & libc::POLLIN != 0 && take_graceful_shutdown_signal(shutdown_fd)? let socket_events = descriptors[0].revents();
{ let shutdown_events = descriptors[1].revents();
if shutdown_events.contains(PollFlags::IN) && take_graceful_shutdown_signal(shutdown_fd)? {
return Ok(BrokerWake::Shutdown); return Ok(BrokerWake::Shutdown);
} }
if descriptors[0].revents & libc::POLLIN != 0 { if socket_events.contains(PollFlags::IN) {
return Ok(BrokerWake::Request); return Ok(BrokerWake::Request);
} }
if descriptors[0].revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0 if socket_events.intersects(unusable) || shutdown_events.intersects(unusable) {
|| descriptors[1].revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL) != 0
{
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
"broker control channel became unusable", "broker control channel became unusable",
@@ -198,6 +202,14 @@ fn wait_for_request(socket: RawFd, shutdown_fd: RawFd) -> io::Result<BrokerWake>
#[cfg(test)] #[cfg(test)]
pub(super) fn run_loop_for_test(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> { pub(super) fn run_loop_for_test(socket: RawFd, shutdown_fd: RawFd, token: &str) -> Result<()> {
// SAFETY: the test broker thread owns both descriptors and keeps them open
// until this loop returns.
let (socket, shutdown_fd) = unsafe {
(
BorrowedFd::borrow_raw(socket),
BorrowedFd::borrow_raw(shutdown_fd),
)
};
broker_loop(socket, shutdown_fd, token) broker_loop(socket, shutdown_fd, token)
} }
@@ -242,7 +254,7 @@ fn handle_operation(
operation: BrokerOperation, operation: BrokerOperation,
descriptors: &mut VecDeque<OwnedFd>, descriptors: &mut VecDeque<OwnedFd>,
ownership: &mut BrokerOwnership, ownership: &mut BrokerOwnership,
shutdown_fd: RawFd, shutdown_fd: BorrowedFd<'static>,
) -> Result<BrokerWireResponse> { ) -> Result<BrokerWireResponse> {
if shutdown_requested(shutdown_fd)? { if shutdown_requested(shutdown_fd)? {
bail!("broker operation cancelled during shutdown"); bail!("broker operation cancelled during shutdown");
@@ -350,20 +362,20 @@ fn handle_operation(
), ),
BrokerOperation::Signal { handle, signal } => { BrokerOperation::Signal { handle, signal } => {
reject_descriptors(descriptors)?; reject_descriptors(descriptors)?;
if !matches!( let signal = match signal {
signal, libc::SIGUSR1 => Signal::USR1,
libc::SIGUSR1 | libc::SIGUSR2 | libc::SIGTERM | libc::SIGKILL libc::SIGUSR2 => Signal::USR2,
) { libc::SIGTERM => Signal::TERM,
bail!("signal is not allowed by broker manifest"); libc::SIGKILL => Signal::KILL,
} _ => bail!("signal is not allowed by broker manifest"),
};
let child = ownership let child = ownership
.children .children
.get(&handle) .get(&handle)
.ok_or_else(|| anyhow!("unknown broker child handle"))?; .ok_or_else(|| anyhow!("unknown broker child handle"))?;
// SAFETY: the broker retains the exact unreaped child handle. rustix_process::kill_process(Pid::from_child(child), signal)
if unsafe { libc::kill(child.id() as i32, signal) } != 0 { .map_err(io::Error::from)
return Err(io::Error::last_os_error()).context("broker child signal failed"); .context("broker child signal failed")?;
}
Ok(wire_outcome(BrokerOutcome::Acknowledged)) Ok(wire_outcome(BrokerOutcome::Acknowledged))
} }
BrokerOperation::TryWait { handle } => { BrokerOperation::TryWait { handle } => {
@@ -409,7 +421,7 @@ fn spawn_helper(
request: SpawnRequest, request: SpawnRequest,
descriptors: &mut VecDeque<OwnedFd>, descriptors: &mut VecDeque<OwnedFd>,
children: &mut BTreeMap<String, Child>, children: &mut BTreeMap<String, Child>,
shutdown_fd: RawFd, shutdown_fd: BorrowedFd<'static>,
) -> Result<BrokerWireResponse> { ) -> Result<BrokerWireResponse> {
let SpawnRequest { let SpawnRequest {
kind, kind,
@@ -504,22 +516,17 @@ fn reject_descriptors(descriptors: &VecDeque<OwnedFd>) -> Result<()> {
} }
fn set_cloexec(descriptor: &OwnedFd, enabled: bool) -> Result<()> { fn set_cloexec(descriptor: &OwnedFd, enabled: bool) -> Result<()> {
// SAFETY: fcntl reads and updates descriptor-local flags. let current = rustix_io::fcntl_getfd(descriptor)
let current = unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_GETFD) }; .map_err(io::Error::from)
if current < 0 { .context("failed to read descriptor flags")?;
return Err(io::Error::last_os_error()).context("failed to read descriptor flags");
}
let updated = if enabled { let updated = if enabled {
current | libc::FD_CLOEXEC current | FdFlags::CLOEXEC
} else { } else {
current & !libc::FD_CLOEXEC current - FdFlags::CLOEXEC
}; };
// SAFETY: `descriptor` is borrowed open for the call, and `updated` only rustix_io::fcntl_setfd(descriptor, updated)
// changes FD_CLOEXEC in the flags just read from it. .map_err(io::Error::from)
if unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_SETFD, updated) } != 0 { .context("failed to update descriptor flags")
return Err(io::Error::last_os_error()).context("failed to update descriptor flags");
}
Ok(())
} }
fn wire_outcome(outcome: BrokerOutcome) -> BrokerWireResponse { fn wire_outcome(outcome: BrokerOutcome) -> BrokerWireResponse {
@@ -551,30 +558,18 @@ fn canonical_lower_hex(value: &str, length: usize) -> bool {
mod tests { mod tests {
use std::process::Command; use std::process::Command;
use rustix::net::{self, AddressFamily, SendFlags, SocketFlags, SocketType};
use super::*; use super::*;
use std::os::fd::FromRawFd;
fn test_socket_pair() -> (OwnedFd, OwnedFd) { fn test_socket_pair() -> (OwnedFd, OwnedFd) {
let mut sockets = [0; 2]; net::socketpair(
assert_eq!( AddressFamily::UNIX,
// SAFETY: sockets has room for both returned descriptors. SocketType::SEQPACKET,
unsafe { SocketFlags::CLOEXEC,
libc::socketpair( None,
libc::AF_UNIX, )
libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC, .unwrap()
0,
sockets.as_mut_ptr(),
)
},
0
);
// SAFETY: socketpair returned two fresh owned descriptors.
unsafe {
(
OwnedFd::from_raw_fd(sockets[0]),
OwnedFd::from_raw_fd(sockets[1]),
)
}
} }
#[test] #[test]
@@ -583,8 +578,7 @@ mod tests {
let (shutdown_writer, shutdown_reader) = test_socket_pair(); let (shutdown_writer, shutdown_reader) = test_socket_pair();
drop(shutdown_writer); drop(shutdown_writer);
let Err(error) = wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd()) let Err(error) = wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()) else {
else {
panic!("hangup must unwind through destructive broker cleanup"); panic!("hangup must unwind through destructive broker cleanup");
}; };
@@ -597,21 +591,13 @@ mod tests {
let (shutdown_writer, shutdown_reader) = test_socket_pair(); let (shutdown_writer, shutdown_reader) = test_socket_pair();
let signal = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE]; let signal = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE];
assert_eq!( assert_eq!(
// SAFETY: both the descriptor and one-byte buffer are valid. net::send(&shutdown_writer, &signal, SendFlags::NOSIGNAL).unwrap(),
unsafe {
libc::send(
shutdown_writer.as_raw_fd(),
signal.as_ptr().cast(),
signal.len(),
libc::MSG_NOSIGNAL,
)
},
1 1
); );
drop(shutdown_writer); drop(shutdown_writer);
assert!(matches!( assert!(matches!(
wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd()), wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()),
Ok(BrokerWake::Shutdown) Ok(BrokerWake::Shutdown)
)); ));
} }
@@ -622,20 +608,11 @@ mod tests {
let (shutdown_writer, shutdown_reader) = test_socket_pair(); let (shutdown_writer, shutdown_reader) = test_socket_pair();
let invalid = [2_u8]; let invalid = [2_u8];
assert_eq!( assert_eq!(
// SAFETY: both the descriptor and one-byte buffer are valid. net::send(&shutdown_writer, &invalid, SendFlags::NOSIGNAL).unwrap(),
unsafe {
libc::send(
shutdown_writer.as_raw_fd(),
invalid.as_ptr().cast(),
invalid.len(),
libc::MSG_NOSIGNAL,
)
},
1 1
); );
let Err(error) = wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd()) let Err(error) = wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()) else {
else {
panic!("invalid packet must not authorize ownership release"); panic!("invalid packet must not authorize ownership release");
}; };
@@ -648,20 +625,11 @@ mod tests {
let (shutdown_writer, shutdown_reader) = test_socket_pair(); let (shutdown_writer, shutdown_reader) = test_socket_pair();
let invalid = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE; 2]; let invalid = [super::super::transport::GRACEFUL_SHUTDOWN_BYTE; 2];
assert_eq!( assert_eq!(
// SAFETY: both the descriptor and two-byte buffer are valid. net::send(&shutdown_writer, &invalid, SendFlags::NOSIGNAL).unwrap(),
unsafe {
libc::send(
shutdown_writer.as_raw_fd(),
invalid.as_ptr().cast(),
invalid.len(),
libc::MSG_NOSIGNAL,
)
},
2 2
); );
let Err(error) = wait_for_request(control_reader.as_raw_fd(), shutdown_reader.as_raw_fd()) let Err(error) = wait_for_request(control_reader.as_fd(), shutdown_reader.as_fd()) else {
else {
panic!("oversized packet must not authorize ownership release"); panic!("oversized packet must not authorize ownership release");
}; };
@@ -681,7 +649,6 @@ mod tests {
drop(ownership); drop(ownership);
// SAFETY: signal zero only probes the test-owned provider after teardown. assert!(rustix_process::test_kill_process(Pid::from_raw(pid).unwrap()).is_err());
assert_ne!(unsafe { libc::kill(pid, 0) }, 0);
} }
} }
+34 -51
View File
@@ -1,5 +1,5 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use std::os::fd::AsRawFd; use std::os::fd::AsFd;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
@@ -7,10 +7,23 @@ use std::sync::mpsc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{env, fs, iter, thread}; use std::{env, fs, iter, thread};
use rustix::net::{self, Shutdown};
use rustix::process::{self as rustix_process, Pid, Signal};
use crate::daemon::protocol_v2; use crate::daemon::protocol_v2;
use crate::test_fake_helper::Role; use crate::test_fake_helper::Role;
use crate::{env_vars, process_broker, test_env, test_fake_helper, test_temp}; use crate::{env_vars, process_broker, test_env, test_fake_helper, test_temp};
fn process_exists(pid: libc::pid_t) -> bool {
Pid::from_raw(pid).is_some_and(|pid| rustix_process::test_kill_process(pid).is_ok())
}
fn kill_for_cleanup(pid: libc::pid_t) {
if let Some(pid) = Pid::from_raw(pid) {
let _ = rustix_process::kill_process(pid, Signal::KILL);
}
}
use super::*; use super::*;
fn wire_arguments(arguments: &[&str]) -> Vec<super::wire::OsWire> { fn wire_arguments(arguments: &[&str]) -> Vec<super::wire::OsWire> {
@@ -28,10 +41,7 @@ fn release_test_provider(release_path: &Path, proof_path: &Path, provider_pid: i
return; return;
} }
if Instant::now() >= deadline { if Instant::now() >= deadline {
// SAFETY: cleanup keeps a failing regression test from leaking its provider. kill_for_cleanup(provider_pid);
unsafe {
libc::kill(provider_pid, libc::SIGKILL);
}
panic!("successful provider could not act after normal broker shutdown"); panic!("successful provider could not act after normal broker shutdown");
} }
thread::sleep(Duration::from_millis(5)); thread::sleep(Duration::from_millis(5));
@@ -540,8 +550,7 @@ fn process_group_guard_cleans_up_before_ownership_transfer() {
drop(child); drop(child);
// SAFETY: signal zero only probes the test-owned helper PID after guard cleanup. assert!(!process_exists(pid));
assert_ne!(unsafe { libc::kill(pid, 0) }, 0);
} }
#[test] #[test]
@@ -762,8 +771,7 @@ fn broker_guard_preempts_an_active_operation_and_kills_its_group() {
"broker shutdown waited for the active operation" "broker shutdown waited for the active operation"
); );
assert!(run.join().unwrap().is_err()); assert!(run.join().unwrap().is_err());
// SAFETY: signal zero only probes the test-owned helper PID. assert!(!process_exists(helper_pid));
assert_ne!(unsafe { libc::kill(helper_pid, 0) }, 0);
} }
#[test] #[test]
@@ -778,7 +786,7 @@ fn owned_child_inherits_daemon_pidfd_without_leaking_broker_copy() {
OsStr::new("sleep"), OsStr::new("sleep"),
[OsStr::new("30")], [OsStr::new("30")],
Vec::new(), Vec::new(),
watchdog.as_raw_fd(), watchdog.as_fd(),
) )
.unwrap(); .unwrap();
let inherited_pidfd = fs::read_dir(format!("/proc/{}/fd", child.id())) let inherited_pidfd = fs::read_dir(format!("/proc/{}/fd", child.id()))
@@ -891,10 +899,7 @@ fn a_descendant_outside_the_helper_group_cannot_hold_the_broker_on_its_pipes() {
.ok() .ok()
.and_then(|pid| pid.trim().parse::<libc::pid_t>().ok()) .and_then(|pid| pid.trim().parse::<libc::pid_t>().ok())
{ {
// SAFETY: cleanup of the descendant this test started in its own session. kill_for_cleanup(pid);
unsafe {
libc::kill(pid, libc::SIGKILL);
}
} }
let error = result.expect_err("output still held open must not read as complete"); let error = result.expect_err("output still held open must not read as complete");
@@ -950,8 +955,7 @@ fn normal_broker_shutdown_releases_successful_provider_descendant() {
.trim() .trim()
.parse::<i32>() .parse::<i32>()
.unwrap(); .unwrap();
// SAFETY: signal zero only checks the test-owned provider. assert!(process_exists(provider_pid));
assert_eq!(unsafe { libc::kill(provider_pid, 0) }, 0);
drop(guard); drop(guard);
@@ -984,28 +988,19 @@ fn shutdown_channel_peer_loss_kills_retained_provider() {
.trim() .trim()
.parse::<i32>() .parse::<i32>()
.unwrap(); .unwrap();
// SAFETY: signal zero only checks the test-owned provider. assert!(process_exists(provider_pid));
assert_eq!(unsafe { libc::kill(provider_pid, 0) }, 0);
// Simulate abrupt parent loss without writing the graceful-shutdown packet. // Simulate abrupt parent loss without writing the graceful-shutdown packet.
assert_eq!( net::shutdown(&guard.broker().inner.shutdown, Shutdown::Both).unwrap();
// SAFETY: the descriptor belongs to this test's live broker guard.
unsafe { libc::shutdown(guard.broker().inner.shutdown.as_raw_fd(), libc::SHUT_RDWR) },
0
);
drop(guard); drop(guard);
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
// SAFETY: signal zero only probes the recorded test provider PID. if !process_exists(provider_pid) {
if unsafe { libc::kill(provider_pid, 0) } != 0 {
break; break;
} }
if Instant::now() >= deadline { if Instant::now() >= deadline {
// SAFETY: cleanup keeps a failing regression test from leaking its provider. kill_for_cleanup(provider_pid);
unsafe {
libc::kill(provider_pid, libc::SIGKILL);
}
panic!("provider survived abnormal broker channel loss"); panic!("provider survived abnormal broker channel loss");
} }
thread::sleep(Duration::from_millis(5)); thread::sleep(Duration::from_millis(5));
@@ -1069,22 +1064,17 @@ fn retained_publication_replacement_disposes_the_previous_provider() {
.unwrap(); .unwrap();
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
// SAFETY: signal zero only probes the recorded test provider PID. if !process_exists(first_pid) {
if unsafe { libc::kill(first_pid, 0) } != 0 {
break; break;
} }
if Instant::now() >= deadline { if Instant::now() >= deadline {
// SAFETY: cleanup keeps a failing regression test from leaking its helpers. kill_for_cleanup(first_pid);
unsafe { kill_for_cleanup(second_pid);
libc::kill(first_pid, libc::SIGKILL);
libc::kill(second_pid, libc::SIGKILL);
}
panic!("replaced publication provider remained alive"); panic!("replaced publication provider remained alive");
} }
thread::sleep(Duration::from_millis(5)); thread::sleep(Duration::from_millis(5));
} }
// SAFETY: signal zero only checks the current test-owned provider. assert!(process_exists(second_pid));
assert_eq!(unsafe { libc::kill(second_pid, 0) }, 0);
drop(guard); drop(guard);
release_test_provider(&second_release_path, &second_proof_path, second_pid); release_test_provider(&second_release_path, &second_proof_path, second_pid);
@@ -1146,20 +1136,15 @@ fn failed_publication_replacement_preserves_the_current_provider() {
.trim() .trim()
.parse::<i32>() .parse::<i32>()
.unwrap(); .unwrap();
// SAFETY: signal zero only checks the current test-owned provider. assert!(process_exists(current_pid));
assert_eq!(unsafe { libc::kill(current_pid, 0) }, 0);
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
// SAFETY: signal zero only probes the failed test provider PID. if !process_exists(failed_pid) {
if unsafe { libc::kill(failed_pid, 0) } != 0 {
break; break;
} }
if Instant::now() >= deadline { if Instant::now() >= deadline {
// SAFETY: cleanup keeps a failing regression test from leaking its helpers. kill_for_cleanup(current_pid);
unsafe { kill_for_cleanup(failed_pid);
libc::kill(current_pid, libc::SIGKILL);
libc::kill(failed_pid, libc::SIGKILL);
}
panic!("failed replacement provider survived cleanup"); panic!("failed replacement provider survived cleanup");
} }
thread::sleep(Duration::from_millis(5)); thread::sleep(Duration::from_millis(5));
@@ -1205,8 +1190,7 @@ fn retained_publication_kills_failed_or_input_stalled_provider_groups() {
} }
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
// SAFETY: signal zero only probes the recorded test child PID. if !process_exists(provider_pid) {
if unsafe { libc::kill(provider_pid, 0) } != 0 {
break; break;
} }
assert!( assert!(
@@ -1307,8 +1291,7 @@ fn broker_shutdown_preempts_retained_publication_stdin_writer() {
release_test_provider(&current_release_path, &current_proof_path, current_pid); release_test_provider(&current_release_path, &current_proof_path, current_pid);
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
// SAFETY: signal zero only probes the recorded test child PID. if !process_exists(provider_pid) {
if unsafe { libc::kill(provider_pid, 0) } != 0 {
break; break;
} }
assert!( assert!(
+130 -268
View File
@@ -1,10 +1,18 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use std::io::{self, IoSlice, IoSliceMut};
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; use std::os::fd::{BorrowedFd, OwnedFd};
use std::time::Duration; use std::time::Duration;
use std::{io, mem, ptr};
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use rustix::event::{self, PollFd, PollFlags, Timespec};
use rustix::fs::{self as rustix_fs, FileType, MemfdFlags};
use rustix::io::{self as rustix_io, Errno};
use rustix::net::sockopt::{self, Timeout};
use rustix::net::{
self, RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags, SendAncillaryBuffer,
SendAncillaryMessage, SendFlags,
};
use super::wire::{ use super::wire::{
BlobWire, INLINE_BLOB_BYTES, MAX_OUTPUT_BYTES, MAX_PACKET_BYTES, MAX_PACKET_DESCRIPTORS, BlobWire, INLINE_BLOB_BYTES, MAX_OUTPUT_BYTES, MAX_PACKET_BYTES, MAX_PACKET_DESCRIPTORS,
@@ -13,80 +21,47 @@ use super::wire::{
pub(super) const GRACEFUL_SHUTDOWN_BYTE: u8 = 1; pub(super) const GRACEFUL_SHUTDOWN_BYTE: u8 = 1;
pub(super) fn set_socket_timeout(fd: RawFd, timeout: Duration) -> io::Result<()> { pub(super) fn set_socket_timeout(fd: BorrowedFd<'_>, timeout: Duration) -> io::Result<()> {
let timeout = timeout.max(Duration::from_millis(1)); let timeout = timeout.max(Duration::from_millis(1));
let value = libc::timeval { for option in [Timeout::Send, Timeout::Recv] {
tv_sec: libc::time_t::try_from(timeout.as_secs()) sockopt::set_socket_timeout(fd, option, Some(timeout))?;
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "socket timeout overflow"))?,
tv_usec: libc::suseconds_t::from(timeout.subsec_micros()),
};
for option in [libc::SO_SNDTIMEO, libc::SO_RCVTIMEO] {
// SAFETY: value is initialized and setsockopt copies it.
if unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
option,
(&raw const value).cast(),
mem::size_of::<libc::timeval>() as libc::socklen_t,
)
} != 0
{
return Err(io::Error::last_os_error());
}
} }
Ok(()) Ok(())
} }
pub(super) fn shutdown_requested(descriptor: RawFd) -> io::Result<bool> { pub(super) fn shutdown_requested(descriptor: BorrowedFd<'_>) -> io::Result<bool> {
let mut pollfd = libc::pollfd { let mut pollfds = [PollFd::from_borrowed_fd(descriptor, PollFlags::IN)];
fd: descriptor, event::poll(&mut pollfds, Some(&Timespec::default()))?;
events: libc::POLLIN, let revents = pollfds[0].revents();
revents: 0, if revents.contains(PollFlags::NVAL) {
};
// SAFETY: pollfd points to one initialized entry.
let result = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
if result < 0 {
return Err(io::Error::last_os_error());
}
if pollfd.revents & libc::POLLNVAL != 0 {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
"broker shutdown channel became invalid", "broker shutdown channel became invalid",
)); ));
} }
Ok(pollfd.revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0) Ok(revents.intersects(PollFlags::IN | PollFlags::HUP | PollFlags::ERR))
} }
pub(super) fn take_graceful_shutdown_signal(descriptor: RawFd) -> io::Result<bool> { pub(super) fn take_graceful_shutdown_signal(descriptor: BorrowedFd<'_>) -> io::Result<bool> {
let mut pollfd = libc::pollfd { let mut pollfds = [PollFd::from_borrowed_fd(descriptor, PollFlags::IN)];
fd: descriptor,
events: libc::POLLIN,
revents: 0,
};
loop { loop {
// SAFETY: pollfd points to one initialized entry. match event::poll(&mut pollfds, Some(&Timespec::default())) {
let result = unsafe { libc::poll(&raw mut pollfd, 1, 0) }; Ok(0) => return Ok(false),
if result < 0 { Ok(_) => {}
let error = io::Error::last_os_error(); Err(Errno::INTR) => continue,
if error.kind() == io::ErrorKind::Interrupted { Err(errno) => return Err(errno.into()),
continue;
}
return Err(error);
} }
if result == 0 { let revents = pollfds[0].revents();
return Ok(false); if revents.contains(PollFlags::NVAL) {
}
if pollfd.revents & libc::POLLNVAL != 0 {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
"broker shutdown channel became invalid", "broker shutdown channel became invalid",
)); ));
} }
if pollfd.revents & libc::POLLIN != 0 { if revents.contains(PollFlags::IN) {
return receive_graceful_shutdown_signal(descriptor); return receive_graceful_shutdown_signal(descriptor);
} }
if pollfd.revents & (libc::POLLHUP | libc::POLLERR) != 0 { if revents.intersects(PollFlags::HUP | PollFlags::ERR) {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
"broker shutdown channel closed without a graceful signal", "broker shutdown channel closed without a graceful signal",
@@ -96,27 +71,17 @@ pub(super) fn take_graceful_shutdown_signal(descriptor: RawFd) -> io::Result<boo
} }
} }
fn receive_graceful_shutdown_signal(descriptor: RawFd) -> io::Result<bool> { fn receive_graceful_shutdown_signal(descriptor: BorrowedFd<'_>) -> io::Result<bool> {
let mut byte = 0_u8; let mut byte = [0_u8];
loop { loop {
// MSG_TRUNC makes an oversized SOCK_SEQPACKET message report its full length. // TRUNC makes an oversized SOCK_SEQPACKET message report its full length.
// SAFETY: byte is a valid one-byte destination for this live broker socket. let flags = RecvFlags::DONTWAIT | RecvFlags::TRUNC;
let received = unsafe { let received = match net::recv(descriptor, &mut byte, flags) {
libc::recv( Ok((_, received)) => received,
descriptor, Err(Errno::INTR) => continue,
(&raw mut byte).cast(), Err(errno) => return Err(errno.into()),
1,
libc::MSG_DONTWAIT | libc::MSG_TRUNC,
)
}; };
if received < 0 { if received == 1 && byte[0] == GRACEFUL_SHUTDOWN_BYTE {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(error);
}
if received == 1 && byte == GRACEFUL_SHUTDOWN_BYTE {
return Ok(true); return Ok(true);
} }
return Err(io::Error::new( return Err(io::Error::new(
@@ -166,28 +131,11 @@ pub(super) fn decode_blob(
let mut bytes = vec![0_u8; length]; let mut bytes = vec![0_u8; length];
let mut offset = 0; let mut offset = 0;
while offset < bytes.len() { while offset < bytes.len() {
// SAFETY: the destination range is writable and the validated match rustix_io::pread(&descriptor, &mut bytes[offset..], offset as u64) {
// memfd has exactly the declared immutable length. Ok(0) => bail!("broker memfd ended before its declared length"),
let read = unsafe { Ok(read) => offset += read,
libc::pread( Err(Errno::INTR) => {}
descriptor.as_raw_fd(), Err(errno) => return Err(io::Error::from(errno).into()),
bytes[offset..].as_mut_ptr().cast(),
bytes.len() - offset,
offset as libc::off_t,
)
};
if read > 0 {
offset += read as usize;
} else if read < 0
&& io::Error::last_os_error().kind() == io::ErrorKind::Interrupted
{
continue;
} else {
return Err(if read == 0 {
anyhow!("broker memfd ended before its declared length")
} else {
io::Error::last_os_error().into()
});
} }
} }
Ok(bytes) Ok(bytes)
@@ -196,206 +144,120 @@ pub(super) fn decode_blob(
} }
fn sealed_memfd(bytes: &[u8]) -> Result<OwnedFd> { fn sealed_memfd(bytes: &[u8]) -> Result<OwnedFd> {
let name = c"wayscriber-broker-payload"; let descriptor = rustix_fs::memfd_create(
// SAFETY: name is a valid C string and flags request a private, sealable fd. c"wayscriber-broker-payload",
let raw = MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING,
unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING) }; )
if raw < 0 { .map_err(io::Error::from)
return Err(io::Error::last_os_error()).context("broker memfd creation failed"); .context("broker memfd creation failed")?;
}
// SAFETY: memfd_create returned a new owned descriptor.
let descriptor = unsafe { OwnedFd::from_raw_fd(raw) };
let mut offset = 0; let mut offset = 0;
while offset < bytes.len() { while offset < bytes.len() {
// SAFETY: the source range is readable for the requested length. match rustix_io::write(&descriptor, &bytes[offset..]) {
let written = unsafe { Ok(0) => bail!("short write while creating broker memfd"),
libc::write( Ok(written) => offset += written,
descriptor.as_raw_fd(), Err(Errno::INTR) => {}
bytes[offset..].as_ptr().cast(), Err(errno) => return Err(io::Error::from(errno).into()),
bytes.len() - offset,
)
};
if written > 0 {
offset += written as usize;
} else if written < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
} else {
return Err(if written == 0 {
anyhow!("short write while creating broker memfd")
} else {
io::Error::last_os_error().into()
});
} }
} }
// SAFETY: fcntl changes only seal metadata on the owned memfd. rustix_fs::fcntl_add_seals(&descriptor, REQUIRED_MEMFD_SEALS)
if unsafe { .map_err(io::Error::from)
libc::fcntl( .context("failed to seal broker memfd")?;
descriptor.as_raw_fd(),
libc::F_ADD_SEALS,
REQUIRED_MEMFD_SEALS,
)
} < 0
{
return Err(io::Error::last_os_error()).context("failed to seal broker memfd");
}
Ok(descriptor) Ok(descriptor)
} }
fn validate_sealed_memfd(descriptor: &OwnedFd, length: usize) -> Result<()> { fn validate_sealed_memfd(descriptor: &OwnedFd, length: usize) -> Result<()> {
let mut stat = MaybeUninit::<libc::stat>::uninit(); let stat = rustix_fs::fstat(descriptor)
// SAFETY: stat points to writable storage for fstat. .map_err(io::Error::from)
if unsafe { libc::fstat(descriptor.as_raw_fd(), stat.as_mut_ptr()) } != 0 { .context("failed to inspect broker memfd")?;
return Err(io::Error::last_os_error()).context("failed to inspect broker memfd"); if FileType::from_raw_mode(stat.st_mode) != FileType::RegularFile
} || u64::try_from(stat.st_size).ok() != Some(length as u64)
// SAFETY: fstat initialized stat after success. {
let stat = unsafe { stat.assume_init() };
if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG || stat.st_size != length as libc::off_t {
bail!("broker memfd shape does not match its declaration"); bail!("broker memfd shape does not match its declaration");
} }
// SAFETY: F_GET_SEALS reads descriptor metadata. let sealed = rustix_fs::fcntl_get_seals(descriptor)
let seals = unsafe { libc::fcntl(descriptor.as_raw_fd(), libc::F_GET_SEALS) }; .is_ok_and(|seals| seals.contains(REQUIRED_MEMFD_SEALS));
if seals < 0 || seals & REQUIRED_MEMFD_SEALS != REQUIRED_MEMFD_SEALS { if !sealed {
bail!("broker memfd is not immutably sealed"); bail!("broker memfd is not immutably sealed");
} }
Ok(()) Ok(())
} }
pub(super) fn send_packet(fd: RawFd, packet: &[u8], descriptors: &[RawFd]) -> io::Result<()> { pub(super) fn send_packet(
fd: BorrowedFd<'_>,
packet: &[u8],
descriptors: &[BorrowedFd<'_>],
) -> io::Result<()> {
if descriptors.len() > MAX_PACKET_DESCRIPTORS { if descriptors.len() > MAX_PACKET_DESCRIPTORS {
return Err(io::Error::new( return Err(io::Error::new(
io::ErrorKind::InvalidInput, io::ErrorKind::InvalidInput,
"too many broker packet descriptors", "too many broker packet descriptors",
)); ));
} }
let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(MAX_PACKET_DESCRIPTORS))];
let mut control = SendAncillaryBuffer::new(&mut space);
if !descriptors.is_empty() && !control.push(SendAncillaryMessage::ScmRights(descriptors)) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"broker packet descriptors do not fit their control buffer",
));
}
let payload = [IoSlice::new(packet)];
loop { loop {
let mut iovec = libc::iovec { match net::sendmsg(fd, &payload, &mut control, SendFlags::NOSIGNAL) {
iov_base: packet.as_ptr().cast_mut().cast(), Ok(sent) if sent == packet.len() => return Ok(()),
iov_len: packet.len(), Ok(_) => {
}; return Err(io::Error::new(
let control_len = if descriptors.is_empty() { io::ErrorKind::WriteZero,
0 "short seqpacket send",
} else { ));
// SAFETY: CMSG_SPACE only performs arithmetic on its argument.
unsafe { libc::CMSG_SPACE(mem::size_of_val(descriptors) as libc::c_uint) as usize }
};
let mut control = vec![0_usize; control_len.div_ceil(mem::size_of::<usize>())];
// SAFETY: zero is a valid initial state for msghdr.
let mut message: libc::msghdr = unsafe { mem::zeroed() };
message.msg_iov = &raw mut iovec;
message.msg_iovlen = 1;
if !descriptors.is_empty() {
message.msg_control = control.as_mut_ptr().cast();
message.msg_controllen = control_len;
// SAFETY: the control buffer has CMSG_SPACE bytes.
unsafe {
let header = libc::CMSG_FIRSTHDR(&raw const message);
(*header).cmsg_level = libc::SOL_SOCKET;
(*header).cmsg_type = libc::SCM_RIGHTS;
(*header).cmsg_len =
libc::CMSG_LEN(mem::size_of_val(descriptors) as libc::c_uint) as usize;
ptr::copy_nonoverlapping(
descriptors.as_ptr(),
libc::CMSG_DATA(header).cast::<RawFd>(),
descriptors.len(),
);
} }
Err(Errno::INTR) => {}
Err(errno) => return Err(errno.into()),
} }
// SAFETY: message references live packet/control buffers.
let sent = unsafe { libc::sendmsg(fd, &raw const message, libc::MSG_NOSIGNAL) };
if sent == packet.len() as isize {
return Ok(());
}
if sent < 0 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(if sent < 0 {
io::Error::last_os_error()
} else {
io::Error::new(io::ErrorKind::WriteZero, "short seqpacket send")
});
} }
} }
pub(super) fn recv_packet(fd: RawFd) -> io::Result<(Vec<u8>, Vec<OwnedFd>)> { pub(super) fn recv_packet(fd: BorrowedFd<'_>) -> io::Result<(Vec<u8>, Vec<OwnedFd>)> {
let mut buffer = vec![0_u8; MAX_PACKET_BYTES + 1]; let mut buffer = vec![0_u8; MAX_PACKET_BYTES + 1];
loop { let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(MAX_PACKET_DESCRIPTORS))];
let mut iovec = libc::iovec { let mut control = RecvAncillaryBuffer::new(&mut space);
iov_base: buffer.as_mut_ptr().cast(), let received = loop {
iov_len: buffer.len(), let mut payload = [IoSliceMut::new(&mut buffer)];
}; match net::recvmsg(fd, &mut payload, &mut control, RecvFlags::CMSG_CLOEXEC) {
// SAFETY: CMSG_SPACE only performs arithmetic on its argument. Ok(received) => break received,
let control_len = unsafe { Err(Errno::INTR) => {}
libc::CMSG_SPACE((MAX_PACKET_DESCRIPTORS * mem::size_of::<RawFd>()) as libc::c_uint) Err(errno) => return Err(errno.into()),
as usize
};
let mut control = vec![0_usize; control_len.div_ceil(mem::size_of::<usize>())];
// SAFETY: zero is a valid initial state for msghdr.
let mut message: libc::msghdr = unsafe { mem::zeroed() };
message.msg_iov = &raw mut iovec;
message.msg_iovlen = 1;
message.msg_control = control.as_mut_ptr().cast();
message.msg_controllen = control_len;
// SAFETY: message references writable packet/control buffers.
let read = unsafe { libc::recvmsg(fd, &raw mut message, libc::MSG_CMSG_CLOEXEC) };
if read > 0 {
let read = read as usize;
let mut descriptors = Vec::new();
// SAFETY: recvmsg initialized ancillary headers in the buffer.
unsafe {
let mut header = libc::CMSG_FIRSTHDR(&raw const message);
while !header.is_null() {
if (*header).cmsg_level != libc::SOL_SOCKET
|| (*header).cmsg_type != libc::SCM_RIGHTS
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unexpected broker packet ancillary data",
));
}
let base_len = libc::CMSG_LEN(0) as usize;
if (*header).cmsg_len < base_len {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"malformed broker descriptor payload",
));
}
let data_len = (*header).cmsg_len - base_len;
if !data_len.is_multiple_of(mem::size_of::<RawFd>()) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"misaligned broker descriptor payload",
));
}
let count = data_len / mem::size_of::<RawFd>();
let data = libc::CMSG_DATA(header).cast::<RawFd>();
for index in 0..count {
descriptors.push(OwnedFd::from_raw_fd(*data.add(index)));
}
header = libc::CMSG_NXTHDR(&raw const message, header);
}
}
if read > MAX_PACKET_BYTES
|| message.msg_flags & (libc::MSG_TRUNC | libc::MSG_CTRUNC) != 0
|| descriptors.len() > MAX_PACKET_DESCRIPTORS
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"broker packet exceeds its data or descriptor cap",
));
}
buffer.truncate(read);
return Ok((buffer, descriptors));
} }
if read == 0 { };
return Err(io::Error::new( if received.bytes == 0 {
io::ErrorKind::UnexpectedEof, return Err(io::Error::new(
"broker socket closed", io::ErrorKind::UnexpectedEof,
)); "broker socket closed",
} ));
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(error);
} }
// Descriptors left in `control` close when it drops, so every rejection
// below releases what the packet carried.
let mut descriptors = Vec::new();
for message in control.drain() {
let RecvAncillaryMessage::ScmRights(passed) = message else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unexpected broker packet ancillary data",
));
};
descriptors.extend(passed);
}
if received.bytes > MAX_PACKET_BYTES
|| received
.flags
.intersects(ReturnFlags::TRUNC | ReturnFlags::CTRUNC)
|| descriptors.len() > MAX_PACKET_DESCRIPTORS
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"broker packet exceeds its data or descriptor cap",
));
}
buffer.truncate(received.bytes);
Ok((buffer, descriptors))
} }
+5 -2
View File
@@ -3,6 +3,7 @@ use std::os::fd::OwnedFd;
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use rustix::fs::SealFlags;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub(super) const BROKER_FD: i32 = 3; pub(super) const BROKER_FD: i32 = 3;
@@ -19,8 +20,10 @@ pub(super) const MAX_OUTPUT_BYTES: usize = 256 * 1024 * 1024;
pub(super) const MAX_STDERR_BYTES: usize = 64 * 1024; pub(super) const MAX_STDERR_BYTES: usize = 64 * 1024;
pub(super) const MAX_OWNED_CHILDREN: usize = 64; pub(super) const MAX_OWNED_CHILDREN: usize = 64;
pub(super) const MAX_PACKET_DESCRIPTORS: usize = 2; pub(super) const MAX_PACKET_DESCRIPTORS: usize = 2;
pub(super) const REQUIRED_MEMFD_SEALS: i32 = pub(super) const REQUIRED_MEMFD_SEALS: SealFlags = SealFlags::WRITE
libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_SEAL; .union(SealFlags::GROW)
.union(SealFlags::SHRINK)
.union(SealFlags::SEAL);
/// Failure text used when a `Complete`-mode helper writes past its stdout cap. /// Failure text used when a `Complete`-mode helper writes past its stdout cap.
/// ///
+49 -83
View File
@@ -1,8 +1,7 @@
use std::ffi::{CString, OsStr, OsString}; use std::ffi::{CString, OsStr, OsString};
use std::fs::{self, File, OpenOptions}; use std::fs::{self, File, OpenOptions};
use std::io::{self, Write}; use std::io::{self, Write};
use std::mem::MaybeUninit; use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::ffi::OsStrExt; use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::path::PathBuf; use std::path::PathBuf;
@@ -10,6 +9,11 @@ use std::process;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(target_os = "linux")]
use rustix::fs::RenameFlags;
use rustix::fs::{self as rustix_fs, AtFlags, Mode, OFlags};
use rustix::io::Errno;
use crate::runtime_ui_state::{RuntimeStateFileIdentity, RuntimeStateResolvedParent}; use crate::runtime_ui_state::{RuntimeStateFileIdentity, RuntimeStateResolvedParent};
static NEXT_SIBLING_ID: AtomicU64 = AtomicU64::new(1); static NEXT_SIBLING_ID: AtomicU64 = AtomicU64::new(1);
@@ -62,8 +66,8 @@ impl PinnedParent {
}) })
} }
fn fd(&self) -> libc::c_int { fn fd(&self) -> BorrowedFd<'_> {
self.inner.file.as_raw_fd() self.inner.file.as_fd()
} }
fn sync(&self) -> io::Result<()> { fn sync(&self) -> io::Result<()> {
@@ -77,7 +81,7 @@ impl PinnedParent {
return Ok(self.inner.observed_path.clone()); return Ok(self.inner.observed_path.clone());
} }
let proc_path = PathBuf::from(format!("/proc/self/fd/{}", self.fd())); let proc_path = PathBuf::from(format!("/proc/self/fd/{}", self.fd().as_raw_fd()));
let current = fs::read_link(proc_path)?; let current = fs::read_link(proc_path)?;
if !fs::metadata(&current) if !fs::metadata(&current)
.is_ok_and(|metadata| file_identity(&metadata) == self.inner.identity) .is_ok_and(|metadata| file_identity(&metadata) == self.inner.identity)
@@ -100,15 +104,13 @@ pub(super) struct PinnedPath {
impl PinnedPath { impl PinnedPath {
pub(super) fn open_read(&self) -> io::Result<File> { pub(super) fn open_read(&self) -> io::Result<File> {
let leaf = os_str_c_string(&self.leaf)?; let leaf = os_str_c_string(&self.leaf)?;
// SAFETY: the parent fd and CString remain valid for the duration of the call. let fd = rustix_fs::openat(
let fd = unsafe { self.parent.fd(),
libc::openat( &leaf,
self.parent.fd(), OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
leaf.as_ptr(), Mode::empty(),
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK, )?;
) Ok(File::from(fd))
};
file_from_fd(fd)
} }
pub(super) fn reported_path(&self) -> io::Result<PathBuf> { pub(super) fn reported_path(&self) -> io::Result<PathBuf> {
@@ -119,40 +121,23 @@ impl PinnedPath {
self.parent.sync() self.parent.sync()
} }
fn create_new(&self, mode: libc::mode_t) -> io::Result<File> { fn create_new(&self, mode: Mode) -> io::Result<File> {
let leaf = os_str_c_string(&self.leaf)?; let leaf = os_str_c_string(&self.leaf)?;
// SAFETY: the parent fd and CString remain valid for the duration of the call. let fd = rustix_fs::openat(
let fd = unsafe { self.parent.fd(),
libc::openat( &leaf,
self.parent.fd(), OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
leaf.as_ptr(), mode,
libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW, )?;
mode, Ok(File::from(fd))
)
};
file_from_fd(fd)
} }
fn exists_nofollow(&self) -> io::Result<bool> { fn exists_nofollow(&self) -> io::Result<bool> {
let leaf = os_str_c_string(&self.leaf)?; let leaf = os_str_c_string(&self.leaf)?;
let mut stat = MaybeUninit::<libc::stat>::uninit(); match rustix_fs::statat(self.parent.fd(), &leaf, AtFlags::SYMLINK_NOFOLLOW) {
// SAFETY: the pointers are valid and stat points to writable storage. Ok(_) => Ok(true),
let result = unsafe { Err(Errno::NOENT) => Ok(false),
libc::fstatat( Err(errno) => Err(errno.into()),
self.parent.fd(),
leaf.as_ptr(),
stat.as_mut_ptr(),
libc::AT_SYMLINK_NOFOLLOW,
)
};
if result == 0 {
return Ok(true);
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(error)
} }
} }
} }
@@ -184,18 +169,20 @@ impl Drop for CleanupPath {
} }
pub(super) fn create_synced_temp(path: &PinnedPath, bytes: &[u8]) -> io::Result<CleanupPath> { pub(super) fn create_synced_temp(path: &PinnedPath, bytes: &[u8]) -> io::Result<CleanupPath> {
let path = create_unique_sibling(path, "tmp", |candidate| match candidate.create_new(0o600) { let path = create_unique_sibling(path, "tmp", |candidate| {
Ok(mut file) => { match candidate.create_new(Mode::RUSR | Mode::WUSR) {
let result = file.write_all(bytes).and_then(|()| file.sync_all()); Ok(mut file) => {
drop(file); let result = file.write_all(bytes).and_then(|()| file.sync_all());
if let Err(error) = result { drop(file);
let _ = remove_file(candidate); if let Err(error) = result {
return Err(error); let _ = remove_file(candidate);
return Err(error);
}
Ok(true)
} }
Ok(true) Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
Err(error) => Err(error),
} }
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(false),
Err(error) => Err(error),
})?; })?;
Ok(CleanupPath::new(path)) Ok(CleanupPath::new(path))
} }
@@ -236,21 +223,14 @@ pub(super) fn rename_noreplace(source: &PinnedPath, destination: &PinnedPath) ->
{ {
let source_leaf = os_str_c_string(&source.leaf)?; let source_leaf = os_str_c_string(&source.leaf)?;
let destination_leaf = os_str_c_string(&destination.leaf)?; let destination_leaf = os_str_c_string(&destination.leaf)?;
// SAFETY: both parent fds and CStrings remain valid for the call. rustix_fs::renameat_with(
let result = unsafe { source.parent.fd(),
libc::renameat2( &source_leaf,
source.parent.fd(), destination.parent.fd(),
source_leaf.as_ptr(), &destination_leaf,
destination.parent.fd(), RenameFlags::NOREPLACE,
destination_leaf.as_ptr(), )
libc::RENAME_NOREPLACE, .map_err(io::Error::from)
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
{ {
@@ -264,13 +244,7 @@ pub(super) fn rename_noreplace(source: &PinnedPath, destination: &PinnedPath) ->
pub(super) fn remove_file(path: &PinnedPath) -> io::Result<()> { pub(super) fn remove_file(path: &PinnedPath) -> io::Result<()> {
let leaf = os_str_c_string(&path.leaf)?; let leaf = os_str_c_string(&path.leaf)?;
// SAFETY: the parent fd and CString remain valid for the duration of the call. rustix_fs::unlinkat(path.parent.fd(), &leaf, AtFlags::empty()).map_err(io::Error::from)
let result = unsafe { libc::unlinkat(path.parent.fd(), leaf.as_ptr(), 0) };
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
} }
fn validate_leaf(leaf: &OsStr) -> io::Result<()> { fn validate_leaf(leaf: &OsStr) -> io::Result<()> {
@@ -288,14 +262,6 @@ fn os_str_c_string(value: &OsStr) -> io::Result<CString> {
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL byte")) .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains a NUL byte"))
} }
fn file_from_fd(fd: libc::c_int) -> io::Result<File> {
if fd < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: openat returned a new owned descriptor on success.
Ok(unsafe { File::from_raw_fd(fd) })
}
fn file_identity(metadata: &fs::Metadata) -> RuntimeStateFileIdentity { fn file_identity(metadata: &fs::Metadata) -> RuntimeStateFileIdentity {
RuntimeStateFileIdentity { RuntimeStateFileIdentity {
device: metadata.dev(), device: metadata.dev(),
+1 -5
View File
@@ -1,6 +1,4 @@
use std::ffi::CString;
use std::fs; use std::fs;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::{PermissionsExt, symlink}; use std::os::unix::fs::{PermissionsExt, symlink};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -257,9 +255,7 @@ fn target_symlink_is_rejected_and_neither_path_is_modified() {
fn fifo_is_rejected_without_blocking_for_a_writer() { fn fifo_is_rejected_without_blocking_for_a_writer() {
let temp = test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let path = temp.path().join("runtime-ui.toml"); let path = temp.path().join("runtime-ui.toml");
let c_path = CString::new(path.as_os_str().as_bytes()).unwrap(); test_temp::make_fifo(&path);
// SAFETY: the CString is NUL-terminated and remains live for the call.
assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o600) }, 0);
let started = Instant::now(); let started = Instant::now();
assert!(RuntimeUiStateStore::new(path).inspect().is_err()); assert!(RuntimeUiStateStore::new(path).inspect().is_err());
assert!(started.elapsed() < Duration::from_secs(1)); assert!(started.elapsed() < Duration::from_secs(1));
+10 -17
View File
@@ -1,6 +1,8 @@
use std::ffi::CString; use std::ffi::CString;
use anyhow::{Context, Result, anyhow}; use anyhow::{Context, Result, anyhow};
#[cfg(target_os = "linux")]
use rustix::fs::{self as rustix_fs, RenameFlags};
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io; use std::io;
@@ -386,23 +388,14 @@ fn rename_artifacts_with_rollback(artifacts: &[MoveArtifact]) -> Result<()> {
pub(crate) fn rename_artifact_no_replace(source: &Path, target: &Path) -> io::Result<()> { pub(crate) fn rename_artifact_no_replace(source: &Path, target: &Path) -> io::Result<()> {
let source = path_to_cstring(source)?; let source = path_to_cstring(source)?;
let target = path_to_cstring(target)?; let target = path_to_cstring(target)?;
// SAFETY: The C strings are valid, NUL-terminated paths. AT_FDCWD makes both paths rustix_fs::renameat_with(
// relative to the current process, matching std::fs::rename path semantics. rustix_fs::CWD,
let result = unsafe { &source,
libc::syscall( rustix_fs::CWD,
libc::SYS_renameat2, &target,
libc::AT_FDCWD, RenameFlags::NOREPLACE,
source.as_ptr(), )
libc::AT_FDCWD, .map_err(io::Error::from)
target.as_ptr(),
libc::RENAME_NOREPLACE,
)
};
if result == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
+2 -20
View File
@@ -1,11 +1,7 @@
#[cfg(unix)] #[cfg(unix)]
use std::ffi::CString;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::{PermissionsExt, symlink}; use std::os::unix::fs::{PermissionsExt, symlink};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{fs, io, process}; use std::{fs, process};
use crate::{test_env, test_temp}; use crate::{test_env, test_temp};
@@ -250,7 +246,7 @@ fn named_file_validation_rejects_missing_directory_shaped_path() {
fn named_file_validation_rejects_fifo_target() { fn named_file_validation_rejects_fifo_target() {
let temp = test_temp::tempdir().unwrap(); let temp = test_temp::tempdir().unwrap();
let path = temp.path().join("session-fifo.wayscriber-session"); let path = temp.path().join("session-fifo.wayscriber-session");
make_fifo(&path); test_temp::make_fifo(&path);
for (label, result) in [ for (label, result) in [
( (
@@ -394,17 +390,3 @@ fn can_create_probe(parent: &Path) -> bool {
Err(_) => false, Err(_) => false,
} }
} }
#[cfg(unix)]
fn make_fifo(path: &Path) {
let raw_path = CString::new(path.as_os_str().as_bytes()).expect("fifo path has no NUL bytes");
// SAFETY: raw_path is a valid, NUL-terminated filesystem path for this process.
let result = unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) };
assert_eq!(
result,
0,
"mkfifo {} failed: {}",
path.display(),
io::Error::last_os_error()
);
}
+2 -2
View File
@@ -13,7 +13,7 @@ use super::*;
use crate::session::snapshot::tests::sample_snapshot; use crate::session::snapshot::tests::sample_snapshot;
use crate::session::snapshot::{load, save}; use crate::session::snapshot::{load, save};
use crate::session::{CompressionMode, SessionOptions}; use crate::session::{CompressionMode, SessionOptions};
use crate::test_temp::tempdir; use crate::test_temp::{self, tempdir};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{Seek, Write}; use std::io::{Seek, Write};
use std::path::Path; use std::path::Path;
@@ -427,7 +427,7 @@ fn save_as_and_gzip_preserve_the_allocated_header_generation() {
fn fifo_marker_probe_never_blocks_and_oversized_generations_do_not_raise_the_sequence() { fn fifo_marker_probe_never_blocks_and_oversized_generations_do_not_raise_the_sequence() {
let temp = tempdir().unwrap(); let temp = tempdir().unwrap();
let options = options(temp.path()); let options = options(temp.path());
super::super::tests::make_fifo(&options.clear_marker_file_path()); test_temp::make_fifo(&options.clear_marker_file_path());
assert_eq!( assert_eq!(
ArtifactSetView::probe(&options) ArtifactSetView::probe(&options)
.next_generation(UnreadablePolicy::Refuse) .next_generation(UnreadablePolicy::Refuse)
+2 -20
View File
@@ -22,19 +22,15 @@ use crate::draw::frame::{MAX_COMPOUND_DEPTH, ShapeSnapshot, UndoAction};
use crate::draw::{ArrowStyle, Color, FontDescriptor, Frame, Shape}; use crate::draw::{ArrowStyle, Color, FontDescriptor, Frame, Shape};
use crate::input::EraserMode; use crate::input::EraserMode;
use crate::session::options::{CompressionMode, SessionOptions}; use crate::session::options::{CompressionMode, SessionOptions};
use crate::test_temp::tempdir; use crate::test_temp::{make_fifo, tempdir};
use crate::time_utils::now_rfc3339; use crate::time_utils::now_rfc3339;
#[cfg(unix)]
use std::ffi::CString;
use std::fs::{File, Permissions}; use std::fs::{File, Permissions};
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::mpsc; use std::sync::mpsc;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use std::{fs, io, thread}; use std::{fs, thread};
fn sample_frame() -> Frame { fn sample_frame() -> Frame {
let mut frame = Frame::new(); let mut frame = Frame::new();
@@ -281,20 +277,6 @@ fn write_contentless_session(path: &Path) {
.expect("contentless session write"); .expect("contentless session write");
} }
#[cfg(unix)]
pub(super) fn make_fifo(path: &Path) {
let raw_path = CString::new(path.as_os_str().as_bytes()).expect("fifo path has no NUL bytes");
// SAFETY: raw_path is a valid, NUL-terminated filesystem path for this process.
let result = unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) };
assert_eq!(
result,
0,
"mkfifo {} failed: {}",
path.display(),
io::Error::last_os_error()
);
}
pub(super) fn set_modified(path: &Path, modified: SystemTime) { pub(super) fn set_modified(path: &Path, modified: SystemTime) {
File::options() File::options()
.write(true) .write(true)
+2 -21
View File
@@ -8,14 +8,9 @@ use crate::session::snapshot::{BoardPagesSnapshot, BoardSnapshot};
use crate::session::{ use crate::session::{
CompressionMode, SessionOptions, SessionSnapshot, ToolStateSnapshot, save_snapshot, CompressionMode, SessionOptions, SessionSnapshot, ToolStateSnapshot, save_snapshot,
}; };
#[cfg(unix)] use std::fs;
use std::ffi::CString;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
use std::path::Path;
use std::{fs, io};
fn transparent_line_snapshot() -> SessionSnapshot { fn transparent_line_snapshot() -> SessionSnapshot {
let mut frame = Frame::new(); let mut frame = Frame::new();
@@ -632,7 +627,7 @@ fn inspect_named_file_rejects_special_lock_without_blocking() {
save_snapshot(&snapshot, &options).expect("save_snapshot should succeed"); save_snapshot(&snapshot, &options).expect("save_snapshot should succeed");
let lock_path = options.lock_file_path(); let lock_path = options.lock_file_path();
fs::remove_file(&lock_path).expect("remove named session lock"); fs::remove_file(&lock_path).expect("remove named session lock");
make_fifo(&lock_path); test_temp::make_fifo(&lock_path);
let err = inspect_session(&options).expect_err("special lock should be rejected"); let err = inspect_session(&options).expect_err("special lock should be rejected");
@@ -722,17 +717,3 @@ fn clear_named_file_rejects_symlink_primary_without_removing_artifacts() {
"clear must not remove selected sidecars after primary validation fails" "clear must not remove selected sidecars after primary validation fails"
); );
} }
#[cfg(unix)]
fn make_fifo(path: &Path) {
let raw_path = CString::new(path.as_os_str().as_bytes()).expect("fifo path has no NUL bytes");
// SAFETY: raw_path is a valid, NUL-terminated filesystem path for this process.
let result = unsafe { libc::mkfifo(raw_path.as_ptr(), 0o600) };
assert_eq!(
result,
0,
"mkfifo {} failed: {}",
path.display(),
io::Error::last_os_error()
);
}
+3 -2
View File
@@ -15,6 +15,8 @@ use std::os::unix::fs as unix_fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{env, fs, io, process}; use std::{env, fs, io, process};
use rustix::process as rustix_process;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use crate::durable_io; use crate::durable_io;
@@ -124,8 +126,7 @@ fn play(arguments: &[OsString]) -> Result<()> {
let report: Vec<u8> = match role { let report: Vec<u8> = match role {
Role::Exit => return Ok(()), Role::Exit => return Ok(()),
Role::ReportProcessGroup => { Role::ReportProcessGroup => {
// SAFETY: getpgrp has no preconditions and cannot fail. let leader = rustix_process::getpgrp().as_raw_pid() == process::id() as libc::pid_t;
let leader = unsafe { libc::getpgrp() } == process::id() as libc::pid_t;
if leader { if leader {
"process-group-leader" "process-group-leader"
} else { } else {
+18
View File
@@ -1 +1,19 @@
#[cfg(unix)]
use std::path::Path;
#[cfg(unix)]
use rustix::fs::{self as rustix_fs, FileType, Mode};
pub(crate) use tempfile::{TempDir, tempdir}; pub(crate) use tempfile::{TempDir, tempdir};
/// Creates a FIFO that only the current user can open.
#[cfg(unix)]
pub(crate) fn make_fifo(path: &Path) {
rustix_fs::mknodat(
rustix_fs::CWD,
path,
FileType::Fifo,
Mode::RUSR | Mode::WUSR,
0,
)
.unwrap_or_else(|errno| panic!("mkfifo {} failed: {errno}", path.display()));
}
+6 -23
View File
@@ -527,7 +527,8 @@ mod tests {
use std::fs; use std::fs;
use glib::MainContext; use glib::MainContext;
use std::os::fd::AsRawFd; use rustix::process::Pid;
use rustix::thread as rustix_thread;
use std::sync::mpsc; use std::sync::mpsc;
use super::*; use super::*;
@@ -570,18 +571,10 @@ mod tests {
} }
fn wake_is_readable(source: &RuntimeWakeSource) -> bool { fn wake_is_readable(source: &RuntimeWakeSource) -> bool {
let mut pollfd = libc::pollfd { source.is_readable_within(Duration::ZERO).unwrap()
fd: source.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd and the source descriptor are valid for this non-blocking poll.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
assert!(ready >= 0);
ready > 0 && pollfd.revents & libc::POLLIN != 0
} }
fn wait_until_thread_is_blocked_in_poll(tid: libc::pid_t) { fn wait_until_thread_is_blocked_in_poll(tid: Pid) {
let stat_path = format!("/proc/self/task/{tid}/stat"); let stat_path = format!("/proc/self/task/{tid}/stat");
let deadline = Instant::now() + Duration::from_secs(1); let deadline = Instant::now() + Duration::from_secs(1);
loop { loop {
@@ -814,18 +807,8 @@ mod tests {
let (wake, _health, publisher) = channel(); let (wake, _health, publisher) = channel();
let (tid_tx, tid_rx) = mpsc::channel(); let (tid_tx, tid_rx) = mpsc::channel();
let poller = thread::spawn(move || { let poller = thread::spawn(move || {
// SAFETY: gettid has no preconditions and is used only to observe this test thread. tid_tx.send(rustix_thread::gettid()).unwrap();
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t }; assert!(wake.is_readable_within(Duration::from_secs(1)).unwrap());
tid_tx.send(tid).unwrap();
let mut pollfd = libc::pollfd {
fd: wake.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd and the source descriptor remain valid for the bounded wait.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 1_000) };
assert_eq!(ready, 1);
assert_ne!(pollfd.revents & libc::POLLIN, 0);
}); });
let tid = tid_rx.recv().unwrap(); let tid = tid_rx.recv().unwrap();
+3 -17
View File
@@ -125,8 +125,8 @@ pub(super) fn run(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::cell::Cell; use std::cell::Cell;
use std::os::fd::AsRawFd;
use std::rc::Rc; use std::rc::Rc;
use std::time::Duration;
use super::*; use super::*;
use crate::backend::wayland::RuntimeWakeSource; use crate::backend::wayland::RuntimeWakeSource;
@@ -140,16 +140,8 @@ mod tests {
clean: Cell::new(false), clean: Cell::new(false),
}); });
let mut pollfd = libc::pollfd { assert!(wake.is_readable_within(Duration::ZERO).unwrap());
fd: wake.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd and the source descriptor are valid for this non-blocking poll.
let ready = unsafe { libc::poll(&raw mut pollfd, 1, 0) };
assert_eq!(ready, 1);
assert!(health.failed()); assert!(health.failed());
assert_ne!(pollfd.revents & libc::POLLIN, 0);
} }
#[test] #[test]
@@ -162,13 +154,7 @@ mod tests {
}); });
assert!(!health.failed()); assert!(!health.failed());
let mut pollfd = libc::pollfd { assert!(!wake.is_readable_within(Duration::ZERO).unwrap());
fd: wake.poll_fd().as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
// SAFETY: pollfd and the source descriptor are valid for this non-blocking poll.
assert_eq!(unsafe { libc::poll(&raw mut pollfd, 1, 0) }, 0);
} }
#[test] #[test]
+6 -3
View File
@@ -7,6 +7,9 @@ use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle}; use std::thread::{self, JoinHandle};
use std::{fmt, io}; use std::{fmt, io};
#[cfg(test)]
use rustix::thread as rustix_thread;
use super::{ use super::{
SIGNAL_WRITE_FD, begin_listener_teardown, clear_registered_signals, create_pipe, SIGNAL_WRITE_FD, begin_listener_teardown, clear_registered_signals, create_pipe,
dispatch_pending_signals, finish_listener_teardown, install_handler, publish_listener_active, dispatch_pending_signals, finish_listener_teardown, install_handler, publish_listener_active,
@@ -259,9 +262,9 @@ impl Drop for SignalListener {
fn listener_thread(read_fd: RawFd, shared: &ListenerShared) { fn listener_thread(read_fd: RawFd, shared: &ListenerShared) {
#[cfg(test)] #[cfg(test)]
{ {
// SAFETY: gettid has no preconditions and is used only by tests to // Used only by tests to observe that this exact thread reached its
// observe that this exact thread reached its blocking read. // blocking read.
let tid = unsafe { libc::syscall(libc::SYS_gettid) as libc::pid_t }; let tid = rustix_thread::gettid().as_raw_pid();
shared.thread_tid.store(tid, Ordering::Release); shared.thread_tid.store(tid, Ordering::Release);
} }
+13
View File
@@ -343,6 +343,19 @@ fn clustered_help_and_version_exit_zero_without_runtime_startup() {
.stdout_starts_with("wayscriber "); .stdout_starts_with("wayscriber ");
} }
#[cfg(unix)]
#[test]
fn internal_broker_entry_rejects_descriptors_that_are_not_broker_sockets() {
run_command(
wayscriber_cmd()
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_FD", "3")
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_SHUTDOWN_FD", "4")
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_TOKEN", "a".repeat(64)),
)
.failure()
.code(126);
}
#[test] #[test]
fn invalid_arguments_exit_two() { fn invalid_arguments_exit_two() {
for (args, diagnostic) in [ for (args, diagnostic) in [
+2
View File
@@ -334,11 +334,13 @@ internal static class ChecksCommand
[RepositoryNames.Gtk4LayerShell] = [RepositoryNames.Gtk4LayerShell], [RepositoryNames.Gtk4LayerShell] = [RepositoryNames.Gtk4LayerShell],
["input"] = ["libinput"], ["input"] = ["libinput"],
["ksni"] = [], ["ksni"] = [],
["lexopt"] = [],
["libc"] = [], ["libc"] = [],
["log"] = [], ["log"] = [],
["pango"] = ["pango"], ["pango"] = ["pango"],
["pangocairo"] = ["pango", "cairo"], ["pangocairo"] = ["pango", "cairo"],
["png"] = [], ["png"] = [],
["rustix"] = [],
["schemars"] = [], ["schemars"] = [],
["serde"] = [], ["serde"] = [],
["serde_ignored"] = [], ["serde_ignored"] = [],