Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Conversation

ThallesWS
Copy link

Q A
Branch? 6.4
Bug fix? yes
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #61792
License MIT
Doc PR n/a

Type: bug fix
Component: symfony/process
Related: #61792
Target branch: 6.4

Context / Problem

When a child process exits before consuming all input sent via stdin, Process may still attempt to write the remaining buffer. On POSIX this triggers EPIPE, and with ErrorHandler promoting notices to exceptions, the error surfaces in run(), stop(), and even __destruct(). This prevents clean teardown and causes intermittent failures with large inputs.

What was done

  1. In Process::updateStatus(), after readPipes(...), when running === false and processPipes is Pipes\AbstractPipes, closeStdin() is called to stop any further writes to a closed pipe.

  2. In AbstractPipes::write(), fwrite() is silenced with @ and failures are handled by closing STDIN idempotently and clearing input buffers. When a partial chunk remains (suffix in $data), it is moved into inputBuffer and the method returns the writable pipe so the remainder will be retried on the next iteration.

Safety

STDIN is closed only when the child is no longer running or when the OS already reports a write failure. Writing in that condition is useless and the root cause of EPIPE.

Reading from stdout/stderr, exit codes, timeouts, TTY, and other behaviors remain unchanged. The race between checking “running” and writing is covered because write() also closes STDIN on any write error and preserves partial data for the next loop.

Repro / Test

<?php

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\Process\Process;

$level = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED;
Debug::enable($level);

$payload = str_repeat(str_repeat('A', 1024) . "\n", 1024);

// Child closes STDIN immediately, waits a bit, writes to STDERR, exits(1).
$child = [
    PHP_BINARY, '-r',
    'fclose(STDIN); usleep(200000); fwrite(STDERR, "child: closed STDIN\n"); exit(1);'
];

$process = new Process($child, null, null, null, 5);
$process->setInput($payload);

echo ">> Starting child that closes STDIN early...\n";

try {
    $process->run();
    echo ">> run() returned. ExitCode={$process->getExitCode()}\n";
} catch (\Throwable $e) {
    echo "!! Exception run(): " . get_class($e) . " - " . $e->getMessage() . PHP_EOL;
}

if ($process->isStarted()) {
    echo ">> Calling stop(0)...\n";
    try {
        $process->stop(0);
    } catch (\Throwable $e) {
        echo "!! Exception stop(): " . get_class($e) . " - " . $e->getMessage() . PHP_EOL;
    }

    echo ">> Child STDERR:\n" . $process->getErrorOutput() . PHP_EOL;
}

echo ">> Finish\n";

Before fix (typical)

>> Starting child that closes STDIN early...
!! Exception run(): ErrorException - Notice: fwrite(): Write of 1049600 bytes failed with errno=32 Broken pipe
>> Calling stop(0)...
!! Exception stop(): ErrorException - Notice: fwrite(): Write of 1049600 bytes failed with errno=22 Invalid argument
>> Child STDERR:
child: closed STDIN

After Fix (Expected)

>> Starting child that closes STDIN early...
>> run() returned. ExitCode=1
>> Calling stop(0)...
>> Child STDERR:
child: closed STDIN
>> Finish

BC

No public contracts changed for consumer code. A public utility closeStdin() is added on an internal class (AbstractPipes); this does not introduce BC breaks for users of the component.

Technical Note

If someone directly extends AbstractPipes and already defines a closeStdin() method with an incompatible signature, it could conflict. This is very unlikely, as AbstractPipes is an internal building block and most projects interact only with Process.

@carsonbot carsonbot added this to the 6.4 milestone Oct 9, 2025
@ThallesWS ThallesWS force-pushed the fix/process-epipe-close-stdin-6.4 branch from b1acbbd to 815985e Compare October 10, 2025 11:46
@ThallesWS ThallesWS closed this Oct 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.