Skip to content

61 critical pipe payload deadlock

Part VII: Ecosystem, Runtime & Agent-Specific | Challenge §61

61. Bidirectional Pipe Payload Deadlock

Source: Antigravity 01_io_and_formatting.md (RA)

Severity: Critical | Frequency: Situational | Detectability: Hard | Token Spend: High | Time: Critical | Context: Low

The Problem

UNIX pipes have a finite kernel buffer (typically 64KB on Linux). If a CLI tool simultaneously reads a large payload from stdin AND writes a large response to stdout, both sides can block waiting for the other to drain — a classic deadlock. The tool waits for the agent to read its stdout output so it can write more; the agent waits for the tool to finish reading stdin so it can process the response. Neither side unblocks; the process hangs forever.

# Agent sends 200KB JSON payload to tool and expects 200KB response:
$ echo "$large_json" | my-tool transform > result.json
# Timeline:
# 1. Agent starts writing 200KB to tool's stdin
# 2. Tool starts reading stdin, starts writing response to stdout
# 3. Tool's stdout fills the 64KB pipe buffer — tool blocks waiting for reader
# 4. Agent is still writing stdin — hasn't started reading stdout yet
# 5. Tool is blocked on stdout write; agent is blocked on stdin write
# → DEADLOCK. Both processes hang forever.

This manifests silently: both processes are "running" (non-zero CPU is possible), no timeout detection by either side, no error message.

Impact

  • Full timeout budget burned with no useful output
  • Process requires external kill signal; any partial output is lost
  • No error message produced by either side
  • Harder to detect than §50 (stdin deadlock): both sides are actively writing, not just blocking on read

Solutions

Use temporary files for large payloads instead of pipes:

# Avoid: pipe large data
echo "$large_json" | my-tool transform

# Good: use file reference
echo "$large_json" > /tmp/input.json
my-tool transform --input-file /tmp/input.json > result.json

Schema declares maximum stdin payload size:

{
  "stdin_input": {
    "max_bytes": 65536,
    "overflow_flag": "--input-file",
    "overflow_hint": "For payloads >64KB, use --input-file <path> instead of stdin"
  }
}

Framework enforces size limit on stdin reads:

# Framework reads stdin with size limit:
data = sys.stdin.buffer.read(MAX_STDIN_BYTES)
if len(data) >= MAX_STDIN_BYTES:
    exit_with_error("STDIN_TOO_LARGE", "Payload exceeds 64KB. Use --input-file instead.")

For framework design: - Framework MUST enforce a maximum stdin payload size (default: 64KB) and fail with exit 2 if exceeded, directing the caller to use --input-file instead - The --input-file flag MUST be auto-generated by the framework for any command that accepts stdin input - Framework MUST document the pipe buffer limit prominently in the agent integration guide

Evaluation

Score Condition
0 Bidirectional pipe with large payloads deadlocks silently; no --input-file alternative; no stdin size limit
1 --input-file flag available on some commands; no stdin size enforcement; overflow not detected
2 Framework enforces stdin size limit; STDIN_TOO_LARGE error (exit 2) with --input-file hint when exceeded
3 stdin_input.max_bytes declared in schema; --input-file auto-generated for all stdin-accepting commands; limit documented

Check: Pipe a payload larger than 64KB to any command that also produces large output — verify the tool exits with STDIN_TOO_LARGE rather than deadlocking.


Agent Workaround

Never use bidirectional pipes with large payloads; always use --input-file for payloads over the safe threshold:

import subprocess, json, tempfile, os

PIPE_SAFE_BYTES = 32 * 1024  # conservative: 32KB, well under 64KB pipe buffer

def run_with_payload(
    cmd: list[str],
    payload: dict | str,
) -> dict:
    payload_str = json.dumps(payload) if isinstance(payload, dict) else payload
    payload_bytes = payload_str.encode()

    if len(payload_bytes) > PIPE_SAFE_BYTES:
        # Payload too large for safe piping — use a temp file
        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".json", delete=False
        ) as f:
            f.write(payload_str)
            tmp_path = f.name

        try:
            result = subprocess.run(
                [*cmd, "--input-file", tmp_path],
                capture_output=True, text=True,
                stdin=subprocess.DEVNULL,
            )
        finally:
            os.unlink(tmp_path)
    else:
        # Small payload: safe to use stdin pipe
        result = subprocess.run(
            cmd,
            input=payload_str,
            capture_output=True, text=True,
        )

    return json.loads(result.stdout)

Limitation: If the tool has no --input-file flag and requires stdin for large payloads, the only safe option is to split the payload into chunks below the pipe buffer size — this is only possible for array-type payloads; for single large objects there is no workaround other than asking the tool author to add --input-file support