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:
parent
a47fdff1dd
commit
13b5ac1704
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -616,7 +616,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moonset"
|
name = "moonset"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moonset"
|
name = "moonset"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "Wayland session power menu with GTK4 and Layer Shell"
|
description = "Wayland session power menu with GTK4 and Layer Shell"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
Architectural and design decisions for Moonset, in reverse chronological order.
|
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
|
## 2026-03-31 – Fourth audit: release profile, GResource compression, lock stderr, sync markers
|
||||||
|
|
||||||
- **Who**: ClaudeCode, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
|
|||||||
@ -40,7 +40,9 @@ fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(),
|
|||||||
log::debug!("Power action: {action} ({program} {args:?})");
|
log::debug!("Power action: {action} ({program} {args:?})");
|
||||||
let mut child = Command::new(program)
|
let mut child = Command::new(program)
|
||||||
.args(args)
|
.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())
|
.stderr(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| PowerError::CommandFailed {
|
.map_err(|e| PowerError::CommandFailed {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user