fix: audit fix — avoid latent stdout pipe deadlock in run_command (v0.8.3)

Piping stdout without draining while blocking in child.wait() risks deadlock
if a command writes more than one OS pipe buffer (~64 KB on Linux). Current
callers (systemctl, niri msg, loginctl) stay well under that, but the
structure was fragile. stdout is now discarded; stderr continues to be
captured for error reporting.
This commit is contained in:
nevaforget 2026-04-24 13:01:48 +02:00
parent a47fdff1dd
commit 13b5ac1704
4 changed files with 12 additions and 3 deletions

2
Cargo.lock generated
View File

@ -616,7 +616,7 @@ dependencies = [
[[package]]
name = "moonset"
version = "0.8.2"
version = "0.8.3"
dependencies = [
"dirs",
"gdk-pixbuf",

View File

@ -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"

View File

@ -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

View File

@ -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 {