php / php/php-src

Using stream wrappers breaks proc_open

Open
#17,943 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Bug Category: Streams Status: Verified
Dominant language
C
Stars
40.4k
Forks
8.1k
Avg merge
2d 13h
Merged PRs (30d)
96

Description

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

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 PHP reproduction in a.php and trace proc_open through stream_cast and _php_stream_cast, focusing on the supplied stream-wrapper setup and the posix_spawn Bad file descriptor result. Reproduce it on PHP 8.4.4 with /usr/bin/ls; done means proc_open completes without the warning when the wrapper is active.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, php
Domain
backend, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.