Note: This issue was created by a scheduled automated Claude code-review check. It was found by randomly selecting a component (daemon/src/spawn) and reviewing it for correctness issues. Please treat the severity assessment as a starting point for human triage, not a confirmed defect.
Summary
The per-node stdout log reader and stderr log reader in binaries/daemon/src/spawn/prepared.rs are meant to be symmetric, but they handle a read error differently:
- the stderr reader sets
finished = true on Err(...), so the loop terminates;
- the stdout reader sets
finished = false on Err(...), so the loop keeps going.
If a node's stdout pipe ever returns a persistent (non-EOF) read error, the stdout task spins in a tight loop: each iteration read_until returns Err immediately (leaving raw empty), the task logs a Warn, pushes an empty LogLine onto the log channel, and loops again — a CPU busy-loop that also floods the daemon log and the log channel. The stderr reader, given the same condition, exits cleanly.
Affected code
binaries/daemon/src/spawn/prepared.rs
stdout reader (loops on error):
// prepared.rs:582-596
finished = match child_stdout
.read_until(b'\n', &mut raw)
.await
.wrap_err_with(|| {
format!("failed to read stdout line from spawned node {node_id}")
}) {
Ok(0) => true,
Ok(_) => false,
Err(err) => {
logger_c
.log(LogLevel::Warn, Some("daemon".into()), format!("{err:?}"))
.await;
false // <-- keeps looping on a read error
}
};
stderr reader (terminates on error — the intended behavior):
// prepared.rs:646-658
finished = match child_stderr
.read_until(b'\n', &mut raw)
.await
.wrap_err_with(|| {
format!("failed to read stderr line from spawned node {node_id}")
}) {
Ok(0) => true,
Ok(_) => false,
Err(err) => {
tracing::warn!("{err:?}");
true // <-- terminates the loop
}
};
Impact
- On a persistent stdout read error: an unbounded busy-loop in the stdout logging task — pegging a core, spamming the daemon log with repeated
Warn lines, and flooding the log channel with empty LogLines. The task never exits until the process does.
- Severity is low in practice: most terminal conditions on a child pipe surface as
Ok(0) (EOF), and transient errors like EINTR are retried by tokio internally, so a repeating non-EOF error is uncommon. But the two readers are clearly intended to behave identically, and the stdout side lacks the loop-terminating guard the stderr side has.
Suggested fix
Make the stdout error arm match the stderr arm — terminate the loop after logging:
Err(err) => {
logger_c
.log(LogLevel::Warn, Some("daemon".into()), format!("{err:?}"))
.await;
true // terminate, matching the stderr reader
}
(Alternatively, if the intent is to tolerate a transient error and keep reading, both readers should share that policy with a bounded retry / backoff rather than an unconditional immediate re-read — but the simplest correct fix is to make stdout symmetric with stderr.)
Affected files
binaries/daemon/src/spawn/prepared.rs (stdout reader error arm, ~line 590-595; cf. stderr reader ~line 654-657)
Summary
The per-node stdout log reader and stderr log reader in
binaries/daemon/src/spawn/prepared.rsare meant to be symmetric, but they handle a read error differently:finished = trueonErr(...), so the loop terminates;finished = falseonErr(...), so the loop keeps going.If a node's stdout pipe ever returns a persistent (non-EOF) read error, the stdout task spins in a tight loop: each iteration
read_untilreturnsErrimmediately (leavingrawempty), the task logs aWarn, pushes an emptyLogLineonto the log channel, and loops again — a CPU busy-loop that also floods the daemon log and the log channel. The stderr reader, given the same condition, exits cleanly.Affected code
binaries/daemon/src/spawn/prepared.rsstdout reader (loops on error):
stderr reader (terminates on error — the intended behavior):
Impact
Warnlines, and flooding the log channel with emptyLogLines. The task never exits until the process does.Ok(0)(EOF), and transient errors likeEINTRare retried by tokio internally, so a repeating non-EOF error is uncommon. But the two readers are clearly intended to behave identically, and the stdout side lacks the loop-terminating guard the stderr side has.Suggested fix
Make the stdout error arm match the stderr arm — terminate the loop after logging:
(Alternatively, if the intent is to tolerate a transient error and keep reading, both readers should share that policy with a bounded retry / backoff rather than an unconditional immediate re-read — but the simplest correct fix is to make stdout symmetric with stderr.)
Affected files
binaries/daemon/src/spawn/prepared.rs(stdout reader error arm, ~line 590-595; cf. stderr reader ~line 654-657)