diff --git a/Cargo.lock b/Cargo.lock index 41a046d..895ccf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,7 +616,7 @@ dependencies = [ [[package]] name = "moonset" -version = "0.8.2" +version = "0.8.3" dependencies = [ "dirs", "gdk-pixbuf", diff --git a/Cargo.toml b/Cargo.toml index 76a2fee..f3cbc9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moonset" -version = "0.8.2" +version = "0.8.3" edition = "2024" description = "Wayland session power menu with GTK4 and Layer Shell" license = "MIT" diff --git a/DECISIONS.md b/DECISIONS.md index 6364ef6..72d4dd7 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -2,6 +2,13 @@ Architectural and design decisions for Moonset, in reverse chronological order. +## 2026-04-24 – Audit fix: avoid latent stdout pipe deadlock in run_command (v0.8.3) + +- **Who**: ClaudeCode, Dom +- **Why**: Audit found that `run_command` piped the child's `stdout` but never drained it, then called blocking `child.wait()`. A child writing more than one OS pipe buffer (~64 KB on Linux) would block on `write()` while the parent blocked in `wait()` — classic pipe deadlock, broken only by the 30 s SIGKILL timeout. Current callers (`systemctl`, `niri msg`, `loginctl`) do not emit that much output, but the structure was fragile and would bite on any future command or changed behaviour. +- **Tradeoffs**: stdout is now fully discarded. If a future caller needs stdout, it will have to drain it concurrently with `wait()` (separate reader thread). +- **How**: Replace `.stdout(Stdio::piped())` with `.stdout(Stdio::null())` in `run_command`. `stderr` stays piped — it is drained after `wait()`, which is safe because `wait()` already reaped the child and no further writes can occur. + ## 2026-03-31 – Fourth audit: release profile, GResource compression, lock stderr, sync markers - **Who**: ClaudeCode, Dom diff --git a/src/power.rs b/src/power.rs index 06732bb..d8eb508 100644 --- a/src/power.rs +++ b/src/power.rs @@ -40,7 +40,9 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), log::debug!("Power action: {action} ({program} {args:?})"); let mut child = Command::new(program) .args(args) - .stdout(Stdio::piped()) + // stdout is discarded — piping without draining would deadlock if a + // command ever wrote more than one OS pipe buffer before wait() returned. + .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() .map_err(|e| PowerError::CommandFailed {