[php-src] Issue #23262: stream_get_meta_data() uses the stream after error_handler closes it
[email protected] (edorian)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <GihK5HNfQDarqZuKJDLVazWTFPzbZVIimuw3G6YgdoY@main.internal.php.net> |
Issue: https://github.com/php/php-src/issues/23262
Author: edorian
### Description
## Description
Fuzzing on the new PHP 8.6 streams API:
---
The following code:
```php
<?php
class InvalidEofStream
{
public $context;
public function stream_open($path, $mode, $options, &$openedPath): bool
{
return true;
}
public function stream_eof()
{
return [];
}
public function stream_stat(): array
{
return [];
}
}
stream_wrapper_register('invalid-eof', InvalidEofStream::class);
$stream = null;
$context = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Silent,
'error_handler' => static function (array $errors) use (&$stream): void {
echo "handler: {$errors[0]->code->name}\n";
fclose($stream);
},
],
]);
$stream = fopen('invalid-eof://input', 'r', false, $context);
var_dump(stream_get_meta_data($stream));
```
Resulted in this output:
```
handler: UserspaceInvalidReturn
=================================================================
==ERROR: AddressSanitizer: heap-use-after-free on address 0x... at pc 0x... bp 0x... sp 0x...
READ of size 1 at 0x... thread T0
#0 0x... in zval_get_type Zend/zend_types.h:685
#1 0x... in zif_stream_get_meta_data ext/standard/streamsfuncs.c:568
#2 0x... in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER Zend/zend_vm_execute.h:1393
#3 0x... in execute_ex Zend/zend_vm_execute.h:110749
freed by thread T0 here:
#0 0x... in __interceptor_free
#1 0x... in __zend_free Zend/zend_alloc.c:3571
#2 0x... in _efree Zend/zend_alloc.c:2788
#3 0x... in php_stream_free main/streams/streams.c:422
#4 0x... in zif_fclose ext/standard/file.c:779
#5 0x... in php_stream_call_error_handler main/streams/stream_errors.c:375
#6 0x... in php_stream_report_errors main/streams/stream_errors.c:436
#7 0x... in php_stream_error_operation_end main/streams/stream_errors.c:463
#8 0x... in php_stream_error_operation_end_for_stream main/streams/stream_errors.c:555
#9 0x... in zif_stream_get_meta_data ext/standard/streamsfuncs.c:566
```
But I expected this output instead:
```
handler: UserspaceInvalidReturn
array(10) {
["timed_out"]=>
bool(false)
["blocked"]=>
bool(true)
["eof"]=>
bool(true)
["wrapper_data"]=>
object(InvalidEofStream)#3 (1) {
["context"]=>
resource(5) of type (stream-context)
}
["wrapper_type"]=>
string(10) "user-space"
["stream_type"]=>
string(10) "user-space"
["mode"]=>
string(1) "r"
["unread_bytes"]=>
int(0)
["seekable"]=>
bool(true)
["uri"]=>
string(19) "invalid-eof://input"
}
```
For validation, I used the following patch, but I haven't thought trough this, so it's just a bug report. Not a PR.
```diff
--- a/ext/standard/streamsfuncs.c
+++ b/ext/standard/streamsfuncs.c
@@ -560,7 +560,6 @@
php_stream_error_operation_begin();
if (!php_stream_populate_meta_data(stream, return_value)) {
add_assoc_bool(return_value, "timed_out", 0);
add_assoc_bool(return_value, "blocked", 1);
add_assoc_bool(return_value, "eof", php_stream_eof(stream));
}
- php_stream_error_operation_end_for_stream(stream);
if (!Z_ISUNDEF(stream->wrapperdata)) {
@@ -594,5 +593,6 @@
add_assoc_bool(return_value, "seekable", (stream->ops->seek) && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0);
if (stream->orig_path) {
add_assoc_string(return_value, "uri", stream->orig_path);
}
+ php_stream_error_operation_end_for_stream(stream);
}
```
---
## Additional test cases
<details><summary>
Expand</summary>
```php
<?php
class InvalidEofStream
{
public $context;
public function stream_open($path, $mode, $options, &$openedPath): bool { return true; }
public function stream_eof() { return []; }
public function stream_stat(): array { return []; }
}
stream_wrapper_register('invalid-eof', InvalidEofStream::class);
$stream = null;
$context = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Silent,
'error_handler' => static function (array $errors) use (&$stream): void {
echo "handler: {$errors[0]->code->name}\n";
fclose($stream);
},
],
]);
$stream = fopen('invalid-eof://input', 'r', false, $context);
var_dump(stream_get_meta_data($stream));
echo "--- case 2 ---\n";
class InvalidEofAliasStream
{
public $context;
public function stream_open($path, $mode, $options, &$openedPath): bool { return true; }
public function stream_eof() { return []; }
public function stream_stat(): array { return []; }
}
stream_wrapper_register('invalid-eof-alias', InvalidEofAliasStream::class);
$stream2 = null;
$replacement = null;
$context2 = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Silent,
'error_handler' => static function (array $errors) use (&$stream2, &$replacement): void {
echo "handler: {$errors[0]->code->name}\n";
fclose($stream2);
$replacement = fopen('php://memory', 'w+');
},
],
]);
$stream2 = fopen('invalid-eof-alias://input', 'r', false, $context2);
var_export(stream_get_meta_data($stream2));
echo "\nreplacement_resource_is_stream=" . (get_resource_type($replacement) === 'stream' ? 'yes' : 'no') . "\n";
echo "--- case 3 ---\n";
class MissingEofControlledStream
{
public $context;
public function stream_open($path, $mode, $options, &$openedPath): bool { return true; }
public function stream_stat(): array { return []; }
}
stream_wrapper_register('missing-eof-controlled', MissingEofControlledStream::class);
$stream3 = null;
$replacement3 = null;
$context3 = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Silent,
'error_handler' => static function (array $errors) use (&$stream3, &$replacement3): void {
fclose($stream3);
$replacement3 = str_repeat("\0", 200);
$chosen = pack('P', 0x4141414141414140);
for ($i = 0; $i < 8; $i++) {
$replacement3[56 + $i] = $chosen[$i];
}
$stringTypeInfo = pack('V', 0x106);
for ($i = 0; $i < 4; $i++) {
$replacement3[64 + $i] = $stringTypeInfo[$i];
}
},
],
]);
$stream3 = fopen('missing-eof-controlled://input', 'r', false, $context3);
stream_get_meta_data($stream3);
echo "case 3 completed without crash\n";
```
Expected (patched) output:
```
handler: UserspaceInvalidReturn
array(10) {
["timed_out"]=>
bool(false)
["blocked"]=>
bool(true)
["eof"]=>
bool(true)
["wrapper_data"]=>
object(InvalidEofStream)#3 (1) {
["context"]=>
resource(5) of type (stream-context)
}
["wrapper_type"]=>
string(10) "user-space"
["stream_type"]=>
string(10) "user-space"
["mode"]=>
string(1) "r"
["unread_bytes"]=>
int(0)
["seekable"]=>
bool(true)
["uri"]=>
string(19) "invalid-eof://input"
}
--- case 2 ---
handler: UserspaceInvalidReturn
array (
'timed_out' => false,
'blocked' => true,
'eof' => true,
'wrapper_data' =>
\InvalidEofAliasStream::__set_state(array(
'context' => NULL,
)),
'wrapper_type' => 'user-space',
'stream_type' => 'user-space',
'mode' => 'r',
'unread_bytes' => 0,
'seekable' => true,
'uri' => 'invalid-eof-alias://input',
)
replacement_resource_is_stream=yes
--- case 3 ---
case 3 completed without crash
```
</details>
### PHP Version
```plain
PHP 8.6.0-dev (cli) (built: Aug 11 2026 17:26:03) (ZTS DEBUG)
Copyright © The PHP Group and Contributors
Zend Engine v4.6.0-dev, Copyright © Zend by Perforce
with Zend OPcache v8.6.0-dev, Copyright ©, by Zend by Perforce
```
### Operating System
_No response_