Using stream wrappers breaks proc_open
まだ誰も着手していません。
- 主要言語
- C
- スター
- 40.4k
- フォーク
- 8.1k
- 平均マージ
- 2日 13時間
- マージ済み PR(30日)
- 96
説明
Description
The following code (from https://packagist.org/packages/dg/bypass-finals, which is where the issue is happening):
<?php
class Wrapper {
/** @var resource|null Stream context, which may be set by stream functions */
public $context;
/** @var resource|null File handle, which may be set by stream functions */
public $handle;
public function dir_closedir(): void
{
closedir($this->handle);
}
public function dir_opendir(string $path, int $options): bool
{
$this->handle = $this->context
? $this->native('opendir', $path, $this->context)
: $this->native('opendir', $path);
return (bool) $this->handle;
}
public function dir_readdir()
{
return readdir($this->handle);
}
public function dir_rewinddir(): bool
{
rewinddir($this->handle);
return true;
}
public function mkdir(string $path, int $mode, int $options): bool
{
$recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE);
return $this->context
? $this->native('mkdir', $path, $mode, $recursive, $this->context)
: $this->native('mkdir', $path, $mode, $recursive);
}
public function rename(string $pathFrom, string $pathTo): bool
{
return $this->context
? $this->native('rename', $pathFrom, $pathTo, $this->context)
: $this->native('rename', $pathFrom, $pathTo);
}
public function rmdir(string $path, int $options): bool
{
return $this->context
? $this->native('rmdir', $path, $this->context)
: $this->native('rmdir', $path);
}
public function stream_cast(int $castAs)
{
return $this->handle;
}
public function stream_close(): void
{
fclose($this->handle);
}
public function stream_eof(): bool
{
return feof($this->handle);
}
public function stream_flush(): bool
{
return fflush($this->handle);
}
public function stream_lock(int $operation): bool
{
return $operation
? flock($this->handle, $operation)
: true;
}
public function stream_metadata(string $path, int $option, $value): bool
{
switch ($option) {
case STREAM_META_TOUCH:
return $this->native('touch', $path, $value[0] ?? time(), $value[1] ?? time());
case STREAM_META_OWNER_NAME:
case STREAM_META_OWNER:
return $this->native('chown', $path, $value);
case STREAM_META_GROUP_NAME:
case STREAM_META_GROUP:
return $this->native('chgrp', $path, $value);
case STREAM_META_ACCESS:
return $this->native('chmod', $path, $value);
default:
return false;
}
}
public function stream_open(string $path, string $mode, int $options = 0, ?string &$openedPath = null): bool
{
$usePath = (bool) ($options & STREAM_USE_PATH);
$this->handle = $this->context
? $this->native('fopen', $path, $mode, $usePath, $this->context)
: $this->native('fopen', $path, $mode, $usePath);
return (bool) $this->handle;
}
public function stream_read(int $count)
{
return fread($this->handle, $count);
}
public function stream_seek(int $offset, int $whence = SEEK_SET): bool
{
if (stream_get_meta_data($this->handle)['seekable']) {
return fseek($this->handle, $offset, $whence) === 0;
}
return false;
}
public function stream_set_option(int $option, int $arg1, ?int $arg2)
{
switch ($option) {
case STREAM_OPTION_BLOCKING:
return stream_set_blocking($this->handle, (bool) $arg1);
case STREAM_OPTION_READ_BUFFER:
return stream_set_read_buffer($this->handle, $arg2);
case STREAM_OPTION_WRITE_BUFFER:
return stream_set_write_buffer($this->handle, $arg2);
case STREAM_OPTION_READ_TIMEOUT:
return stream_set_timeout($this->handle, $arg1, $arg2);
default:
return false;
}
}
public function stream_stat()
{
return fstat($this->handle);
}
public function stream_tell()
{
return ftell($this->handle);
}
public function stream_truncate(int $newSize): bool
{
return ftruncate($this->handle, $newSize);
}
public function stream_write(string $data)
{
return fwrite($this->handle, $data);
}
public function unlink(string $path): bool
{
return $this->native('unlink', $path);
}
public function url_stat(string $path, int $flags)
{
if ($flags & STREAM_URL_STAT_QUIET) {
set_error_handler(function () {
return true;
});
}
try {
$func = $flags & STREAM_URL_STAT_LINK ? 'lstat' : 'stat';
return $this->native($func, $path);
} catch (\RuntimeException $e) {
// SplFileInfo::isFile throws exception
return false;
} finally {
if ($flags & STREAM_URL_STAT_QUIET) {
restore_error_handler();
}
}
}
/**
* Temporarily restores the native protocol handler to perform operations.
*/
private function native(string $func)
{
stream_wrapper_restore('file');
try {
return $func(...array_slice(func_get_args(), 1));
} finally {
stream_wrapper_unregister('file');
stream_wrapper_register('file', self::class);
}
}
}
stream_wrapper_unregister('file');
stream_wrapper_register('file', Wrapper::class);
$proc = \proc_open(
'/usr/bin/ls',
[["file", "/dev/null", "r"], STDOUT, STDERR],
$pipes,
);
Resulted in this output:
Warning: proc_open(): posix_spawn() failed: Bad file descriptor in /Users/daniil/repos/php-src/a.php on line 226
But I expected this output instead: No error.
Stepping through the code, it seems strange to me that this would fail, as after stream_cast returns the real underlying stream resource, it is then re-passed to _php_stream_cast, behaving just like as if no stream wrapper is in use.
Another issue is the fact that stream_cast is even part of the stream wrapper API in the first place, given that its only real function is to return the inner resource (regardless of the cast type); a get_inner_resource method would've been way less confusing...
PHP Version
PHP 8.4.4
Operating System
Ubuntu 24.04
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
a.php の PHP 再現から始め、stream_cast と _php_stream_cast を通じて proc_open を追跡し、提供された stream wrapper の設定と posix_spawn の「Bad file descriptor」結果に焦点を当ててください。PHP 8.4.4 で /usr/bin/ls を使って再現してください。wrapper が有効な状態で proc_open が警告なしに完了すれば完了です。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- c, php
- 領域
- backend, operating-systems
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 停滞
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 35/100