Skip to content

daemon: node stdout reader loops instead of terminating on a read error (asymmetric with stderr reader) → busy-loop / log flood on a persistent read error #2763

Description

@phil-opp

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions