leanprover / leanprover/lean4

IO.Process.output hangs on large input strings

Open
#14,000 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P-medium
Dominant language
Lean
Stars
9.2k
Forks
990
Avg merge
1d 17h
Merged PRs (30d)
175

Description

Prerequisites
Description

If a child process (say cat) called by IO.Process.output is sent a large
number of bytes (say 196K + 1 bytes on Linux), then the capacity of the
pipes fill up and the program deadlocks.

Context

Chat
This occurred when I was calling a program with several megabytes of data. When looking into the problem, I found two more issues that I will submit separately.

  1. IO.Process.output/IO.Process.run can continue after throwing an error. This is easily triggered by the underlying process sending byte 255 and then closing standard error. If the programmer thinks some untrusted code is no longer running when it still is, that could be very bad (Bug #14001).
  2. IO.Process.run fails if the stdout or stderr is not UTF8. This is caused because it calls IO.Process.output that returns its stdout and stderr as strings (Bug #14003).

I think that a part of the solution is to create a IO.Process.binOutput, that I will propose separately.

Steps to Reproduce

Run the following code on a Linux machine with 64K buffers.

To determine the pipe capacity on your Linux system, you can run the following command:

dd if=/dev/zero bs=1 | sleep 999

Wait a few seconds, hit control-C, and observe how many bytes were produced.

Save the following into a file, say Test.lean.

def testProcessOutputWithCatSize (numBytes : Nat) : IO Unit := do
  IO.println s!"Testing running cat with string of size {numBytes}"
  let input := String.ofList (List.replicate numBytes 'a') -- A string of length numBytes.
  let spawnArgs : IO.Process.SpawnArgs := { cmd := "cat", args := #[] }
  let startTime ← IO.monoNanosNow
  let out ← IO.Process.output spawnArgs (some input)
  let endTime ← IO.monoNanosNow
  if out.exitCode == 0 && out.stdout.trimAscii == input && out.stderr == "" then
    let duration := (Float.ofNat (endTime - startTime)) / 1_000_000_000.0
    IO.println s!"SUCCESS: Passed in {duration} seconds."
  else
    IO.println "FAILURE: Failed."
    return

#version

def main(_args : List String) : IO Unit := do
  testProcessOutputWithCatSize 1000000

Then, run lean --run Test.lean.

Expected behavior:
It should run to completion in well under a second.

Actual behavior:
It hangs forever. Note that reducing the number of bytes to a smaller value
makes the problem vanish.

Versions

Lean 4.32.0-nightly-2026-06-10
Linux, with a 64K pipe buffer.

Additional Information
Impact

This impacts both IO.Process.output and IO.Process.run (that calls
IO.Process.output).

Proposed Solution: In IO.Process.output, the main process writes data into the stdin of the child process. If this were done as a task, the issue would be resolved. If this is done as a dedicated task, it will not affect the thread maximum, but of course that is one more thread for the kernel to handle.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the IO.Process.output and IO.Process.run entry points, then run the reproducer in Test.lean on Linux with a large input such as 1,000,000 bytes. Trace how input is written while stdout and stderr are collected. Done means the test completes without hanging, returns exit code 0, and preserves the expected stdout and empty stderr.

Written by the indexing model from the issue text.

Assessment

Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.