Skip to content

Shell timeout doesn't cover stdout/stderr reads; sequential reads can deadlock #65

Description

@tcdent

Bug

In src/tools/io.rs, execute_shell() reads stdout and stderr sequentially before applying the timeout to wait():

// Read ALL of stdout (blocks until EOF)
if let Some(stdout) = stdout { /* read all lines */ }
// Read ALL of stderr (blocks until EOF)  
if let Some(stderr) = stderr { /* read all lines */ }

// THEN apply timeout to wait()
let status = tokio::time::timeout(timeout_secs, child.0.wait()).await;

Problems

  1. Timeout doesn't cover I/O reads — If a command holds stdout open (e.g. tail -f, a hung process), reader.next_line().await blocks forever. The tokio::time::timeout is never reached.

  2. Sequential reads can deadlock — If a process fills the stderr pipe buffer (~64KB) while the parent is still reading stdout, the child blocks on stderr write, the parent blocks on stdout read waiting for EOF → deadlock.

Fix

Wrap stdout, stderr reads and wait in a single tokio::join! under one timeout:

let result = tokio::time::timeout(
    Duration::from_secs(timeout_secs),
    async {
        let stdout_fut = async { /* read all stdout */ };
        let stderr_fut = async { /* read all stderr */ };
        let wait_fut = child.0.wait();
        tokio::join!(stdout_fut, stderr_fut, wait_fut)
    }
).await;

Found during code review of the cancellation PR (#61).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions