[php-src] Issue #23264: Streams: set_error_handler() callback frees the structured-handler context
[email protected] (edorian)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <0C95DqNCCtO5IupdPxPCcyJfu2oksoOYtTNYIxQ2Y34@main.internal.php.net> |
Issue: https://github.com/php/php-src/issues/23264
Author: edorian
### Description
## Description
Fuzzing on the new PHP 8.6 streams API, found by Ryan @ Calif.io.
---
The following code:
```php
<?php
class OversizedReadForLegacyHandler
{
public $context;
public function stream_open($path, $mode, $options, &$openedPath): bool
{
return true;
}
public function stream_read(int $count): string
{
return str_repeat('A', $count + 1);
}
public function stream_eof(): bool
{
return false;
}
public function stream_stat(): array
{
return [];
}
}
stream_wrapper_register('legacy-handler-uaf', OversizedReadForLegacyHandler::class);
$context = stream_context_create(['stream' => ['error_mode' => StreamErrorMode::Error]]);
$stream = fopen('legacy-handler-uaf://input', 'r', false, $context);
unset($context);
set_error_handler(static function (int $severity, string $message) use (&$stream): bool {
fclose($stream);
return true;
});
var_dump(fread($stream, 1));
```
Resulted in this output:
```
=================================================================
==ERROR: AddressSanitizer: heap-use-after-free on address 0x... at pc 0x... bp 0x... sp 0x...
READ of size 8 at 0x... thread T0
#0 0x... in php_stream_context_get_option main/streams/streams.c:2299
#1 0x... in php_stream_report_errors main/streams/stream_errors.c:430
#2 0x... in php_stream_error_operation_end main/streams/stream_errors.c:463
#3 0x... in php_stream_error_operation_end_for_stream main/streams/stream_errors.c:555
#4 0x... in zif_fread ext/standard/file.c:1623
freed by thread T0 here:
#0 0x... in __interceptor_free
#1 0x... in php_stream_context_free main/streams/streams.c:2267
#2 0x... in file_context_dtor ext/standard/file.c:119
#3 0x... in zend_resource_dtor Zend/zend_list.c:72
#4 0x... in zend_list_delete Zend/zend_list.c:48
#5 0x... in php_stream_free main/streams/streams.c:426
#6 0x... in zif_fclose ext/standard/file.c:779
#7 0x... in zend_error_zstr_at Zend/zend.c:1584
```
But I expected this output instead:
```
string(1) "A"
```
* * *
`php_stream_report_errors()` emits legacy warnings via `php_error_docref()` in a loop. If userland has `set_error_handler()` registered, that call re-enters user code once per recorded error. `fclose()`ing the stream drops the last reference to its `context`
Validated that the ASAN issues goes away with this patch, but not confident that this is the correct solution.
```diff
--- a/main/streams/stream_errors.c
+++ b/main/streams/stream_errors.c
@@ -404,6 +404,10 @@
static void php_stream_report_errors(const php_stream_context *context, const php_stream_error_operation *op,
php_stream_error_mode error_mode, bool is_terminating)
{
+ if (context) {
+ GC_ADDREF(context->res);
+ }
+
switch (error_mode) {
case PHP_STREAM_ERROR_MODE_ERROR: {
const php_stream_error_entry *entry = op->first_error;
@@ -428,13 +432,17 @@
const zval *handler
= context ? php_stream_context_get_option(context, "stream", "error_handler") : NULL;
if (handler) {
zval errors_array;
php_stream_error_create_array(&errors_array, op->first_error);
php_stream_call_error_handler(handler, &errors_array);
zval_ptr_dtor(&errors_array);
}
+
+ if (context) {
+ zend_list_delete(context->res);
+ }
}
```
Rebuilt with this patch applied and re-ran the reproducer above: no crash, output matches the expected block exactly. Also re-ran with a variant that has the legacy handler additionally open a *replacement* `stream_context_create()` before the lookup runs — unpatched, this redirects the structured-handler dispatch to the replacement context; patched, it does not, confirming the fix closes both the crash and the cross-context redirection.
### PHP Version
```
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_
---
## Full test cases
<details>
<summary>Two full test cases</summary>
```php
<?php
class OversizedReadForLegacyHandler
{
public $context;
public function stream_open($path, $mode, $options, &$openedPath): bool
{
return true;
}
public function stream_read(int $count): string
{
return str_repeat('A', $count + 1);
}
public function stream_eof(): bool
{
return false;
}
public function stream_stat(): array
{
return [];
}
}
stream_wrapper_register('legacy-handler-uaf', OversizedReadForLegacyHandler::class);
$context = stream_context_create(['stream' => ['error_mode' => StreamErrorMode::Error]]);
$stream = fopen('legacy-handler-uaf://input', 'r', false, $context);
unset($context);
$replacement = null;
set_error_handler(static function (int $severity, string $message) use (&$stream, &$replacement): bool {
fclose($stream);
$replacement = stream_context_create([
'stream' => [
'error_mode' => StreamErrorMode::Silent,
'error_handler' => static function (array $errors): void {
echo "replacement handler: {$errors[0]->code->name}\n";
},
],
]);
return true;
});
var_dump(fread($stream, 1));
```
</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_