reactphp/stream

Closed STDIN returns endless stream of random data

clue opened this issue ยท 11 comments

clue commented

While working on #37, I've tested the following simple and quite common code to check possible combinations of readable/writable streams and closing/ending either side:

$stdout = new Stream(STDOUT, $loop);
$stdout->pause();

$stdin = new Stream(STDIN, $loop);
$stdin->pipe($stdout);

Here's what works as expected:

$ php test.php #interactive STDIO
$ echo test | test.php # STDIN ends once receiving input
$ true | php test.php # STDIN ends immediately
$ php test.php < /dev/null # STDIN ends immediately
$ php test.php < /dev/zero # endless STDIN stream
$ php test.php < /dev/urandom #endless STDIN stream
$ php test.php > /dev/null # STDOUT closes when trying to write, thus pausing STDIN

Here's what does not work (on most of my systems, more below):

$ php test.php <&- # no STDIN should error and/or close
$ php test.php >&- # no STDOUT should error and/or close when trying to write

It turns out explicitly passing no handle actually results in a default stream for all STDIO streams (STDIN, STDOUT and STERR). Arguably, this may be a sane default behavior if this default stream would follow expected behavior for closed streams, but unfortunately it does not.

Unfortunately, this default stream behaves just like an endless stream of random data. In fact, it actually is /dev/urandom. This can be verified by checking /proc/{PID}/fd. And it turns out this is not in fact related to ReactPHP at all and can be reproduced with the most simple PHP scripts.

Here's a very simple gist to reproduce this output:

$ LANG=en php -r 'var_dump(!!fstat(STDIN));passthru("ls -o /proc/".getmypid()."/fd");' <&-

This SHOULD show false (because STDIN is not a valid stream) and SHOULD not include file descriptor 0 in the listing.

Note that I could reproduce this in multiple setups, but have seen other setups where this does not occur. As such, I'm opening this ticket to gather more information on why this happens and how this could possibly be avoided.

Unless I'm missing something obvious, it looks like this may boil down to an issue in PHP itself, but I'd rather collect more evidence first.

clue commented

PHP 7.1.2 from Docker php:7.1
STDIN defaults to dev/urandom

PHP 7.0.16 from Docker php:7.0
STDIN defaults to dev/urandom

PHP 7.0.15 on Ubuntu 16.04:
STDIN defaults to /dev/urandom

PHP 5.6.8 from Docker php:5.6
STDIN defaults to dev/urandom

PHP 5.5.38 from Docker php:5.5
STDIN defaults to dev/urandom

PHP 5.4.45 from Docker php:5.4
STDIN defaults to dev/urandom

PHP 5.3.29 from Docker php:5.3
โœ… STDIN is closed

PHP 5.3.10 on Ubuntu 12.04:
โœ… STDIN is closed

I executed the gist and got the following:

$ LANG=en php -r 'var_dump(!!fstat(STDIN));passthru("ls -o /proc/".getmypid()."/fd");' <&-
Command line code:1:
bool(false)
total 0
lr-x------ 1 legionth 64 Mar 14 15:40 0 -> pipe:[174916]
lrwx------ 1 legionth 64 Mar 14 15:40 1 -> /dev/pts/17
lrwx------ 1 legionth 64 Mar 14 15:40 2 -> /dev/pts/17

This looks good to me, nothing about dev/urandom. ๐Ÿค”

Executed this with this version:

$ php --version
PHP 7.0.8-0ubuntu0.16.04.3 (cli) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies
    with Zend OPcache v7.0.8-0ubuntu0.16.04.3, Copyright (c) 1999-2016, by Zend Technologies
    with Xdebug v2.4.0, Copyright (c) 2002-2016, by Derick Rethans

Just tried this with Amp, it does indeed dump random data with the NativeDriver.

<?php

require "vendor/autoload.php";

use Amp\Loop;

// Loop::set(new Loop\NativeDriver);

Loop::run(function () {
    Loop::onReadable(STDIN, function ($watcherId) {
        var_dump("READ", fread(STDIN, 8192));
        var_dump("EOF", feof(STDIN));

        if (feof(STDIN)) {
            Loop::cancel($watcherId);
        }
    });
});

EvDriver shows the same behavior. However, this doesn't happen with uv nor event.

UvDriver fails with the following error:

Fatal error:  uv_poll_init_socket(): uv_poll_init failed in UvDriver.php on line 182

EventDriver fails with the following error:

Warning:  Event::add(): Epoll ADD(1) on fd 0 failed.  Old events were 0; read change was 1 (add); write change was 0 (none): Operation not permitted in EventDriver.php on line 238
Warning:  Event::add(): Failed adding event in EventDriver.php on line 238
clue commented

@kelunik This looks like you're affected by the same issue. Can you report the PHP version and the output of the gist as given above?

It looks like this may in fact be a PHP issue, but we have yet to dive deeper. Some systems seem to be not affected (see @legionth's output).

In case you're affected, this really affects the most simple scripts, you can really confuse any interactive CLI tool, like psysh, boris, those building on top of Symfony console etc. etc.

LANG=en php -r 'var_dump(!!fstat(STDIN));passthru("ls -o /proc/".getmypid()."/fd");' <&-
bool(true)
total 0
lr-x------ 1 kelunik 64 Mar 14 15:54 0 -> /dev/urandom
lrwx------ 1 kelunik 64 Mar 14 15:54 1 -> /dev/pts/1
lrwx------ 1 kelunik 64 Mar 14 15:54 2 -> /dev/pts/1
lr-x------ 1 kelunik 64 Mar 14 15:54 3 -> pipe:[5172278]

Running on PHP 7.1.3RC1. I don't think this is related to PHP, but probably rather the underlying system. I'm on Ubuntu 16.04.2 LTS.

Ok, that's not true, it's not done by the system, (1) because Uv and Event wouldn't fail in that case and (2) because head -c 8 <&- errors out like it should.

@clue FD 0 just gets reused. As 0 isn't used yet, that's taken.

โ”Œ[kelunik@kelunik] - [~/GitHub/php/php-src] - [16:11:04] - [master]
โ””[5415] $ LANG=en php -r 'var_dump(!!fstat(STDIN));passthru("ls -o /proc/".getmypid()."/fd");' <&-
bool(true)
total 0
lr-x------ 1 kelunik 64 Mar 14 16:13 0 -> /dev/urandom
lrwx------ 1 kelunik 64 Mar 14 16:13 1 -> /dev/pts/1
lrwx------ 1 kelunik 64 Mar 14 16:13 2 -> /dev/pts/1
lr-x------ 1 kelunik 64 Mar 14 16:13 3 -> pipe:[5183812]

โ”Œ[kelunik@kelunik] - [~/GitHub/php/php-src] - [16:13:07] - [master]
โ””[5416] $ LANG=en php -r 'var_dump(!!fstat(STDIN));passthru("ls -o /proc/".getmypid()."/fd");'
bool(true)
total 0
lrwx------ 1 kelunik 64 Mar 14 16:13 0 -> /dev/pts/1
lrwx------ 1 kelunik 64 Mar 14 16:13 1 -> /dev/pts/1
lrwx------ 1 kelunik 64 Mar 14 16:13 2 -> /dev/pts/1
lr-x------ 1 kelunik 64 Mar 14 16:13 3 -> /dev/urandom
lr-x------ 1 kelunik 64 Mar 14 16:13 4 -> pipe:[5183821]

This is due to https://github.com/php/php-src/blob/16ae9f82e82e2aea5d7deaf8f9a9c825a56dfcc1/ext/standard/php_fopen_wrapper.c#L265 just duplicating instead of registering STDIN as an already closed stream. Thanks to @bwoebi for finding the right place.

clue commented

Thanks to @kelunik for reporting this upstream: https://bugs.php.net/bug.php?id=74252

It looks like there's little we can do about this on our end, so we may as well document and close this? ๐Ÿ‘

clue commented

For the reference only: I'm no longer able to reproduce this issue with a default PHP installation on Ubuntu (PHP 7.0.22-0ubuntu0.17.04.1):

$ LANG=en php -r 'var_dump(!!fstat(STDIN));passthru("ls -o /proc/".getmypid()."/fd");' <&-
Command line code:1:
bool(false)
total 0
lr-x------ 1 me 64 Nov  1 13:29 0 -> pipe:[58524]
lrwx------ 1 me 64 Nov  1 13:29 1 -> /dev/pts/1
lrwx------ 1 me 64 Nov  1 13:29 2 -> /dev/pts/1

I am having the same problem with 7.2-fpm-alpine on docker.