From 498d0bd1cb6b67652aba2a2cba8de38528d64d36 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Sun, 8 Nov 2026 23:20:17 +0900 Subject: [PATCH 1/2] Fix use of the freed default stream context by the stream error code FG(default_context) is a request resource that zend_close_rsrc_list() frees at request shutdown, but the pointer is only reset in the RINIT of ext/standard. php_stream_error_operation_end() falls back to FG(default_context) when no context is given, and sapi_read_standard_form_data() buffers the POST body before RINIT, so when the temporary file for a body over 16 KiB cannot be created, the error path reads the previous request's freed context. Clear FG(default_context) when its resource is destroyed. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: Claude Mythos 5.1 Claude-Session: https://claude.ai/code/session_01LUfkv6dbRLGQe2UL9SsR2y --- ext/standard/file.c | 3 + .../tests/stream_error_default_context.phpt | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 sapi/cgi/tests/stream_error_default_context.phpt diff --git a/ext/standard/file.c b/ext/standard/file.c index ff68374928c5..e64167df3283 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -111,6 +111,9 @@ PHPAPI int php_le_stream_context(void) static ZEND_RSRC_DTOR_FUNC(file_context_dtor) { php_stream_context *context = (php_stream_context*)res->ptr; + if (context == FG(default_context)) { + FG(default_context) = NULL; + } if (Z_TYPE(context->options) != IS_UNDEF) { zval_ptr_dtor(&context->options); ZVAL_UNDEF(&context->options); diff --git a/sapi/cgi/tests/stream_error_default_context.phpt b/sapi/cgi/tests/stream_error_default_context.phpt new file mode 100644 index 000000000000..240bd9342be7 --- /dev/null +++ b/sapi/cgi/tests/stream_error_default_context.phpt @@ -0,0 +1,63 @@ +--TEST-- +Stream errors before RINIT do not use the default stream context freed by the previous request +--SKIPIF-- + +--FILE-- + '1', + 'REQUEST_METHOD' => 'POST', + 'SCRIPT_FILENAME' => "$dir/index.php", + 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', + 'CONTENT_LENGTH' => (string) strlen($body), +]; +foreach (['PATH', 'USE_ZEND_ALLOC', 'USE_TRACKED_ALLOC', 'ASAN_OPTIONS', 'LSAN_OPTIONS', 'UBSAN_OPTIONS'] as $name) { + if (getenv($name) !== false) { + $env[$name] = getenv($name); + } +} + +/* The first request creates the default stream context, the second one fails to spill its body */ +$proc = proc_open( + [$php, '-n', '-d', 'display_errors=0', '-d', 'log_errors=1', '-d', "sys_temp_dir=$dir/missing", '-T', '2'], + [0 => ['file', "$dir/body", 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + $dir, + $env +); +$stdout = stream_get_contents($pipes[1]); +$stderr = stream_get_contents($pipes[2]); +proc_close($proc); + +var_dump(substr_count($stdout, "ok 0\n")); +echo $stderr; +?> +--CLEAN-- + +--EXPECTF-- +int(2) +PHP Warning: PHP Request Startup: Unable to create temporary file, Check permissions in temporary files directory. in Unknown on line 0 +PHP Warning: PHP Request Startup: POST data can't be buffered; all data discarded in Unknown on line 0 +PHP Warning: PHP Request Startup: Unable to create temporary file, Check permissions in temporary files directory. in Unknown on line 0 +PHP Warning: PHP Request Startup: POST data can't be buffered; all data discarded in Unknown on line 0 + +Elapsed time: %s sec From d89d0bcfd601d39cf7ddb787cc6b55fe04344502 Mon Sep 17 00:00:00 2001 From: Go Kudo Date: Tue, 10 Nov 2026 15:40:09 +0900 Subject: [PATCH 2/2] [ci skip] update NEWS --- NEWS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS b/NEWS index 697cc4de957a..3103c9e5634d 100644 --- a/NEWS +++ b/NEWS @@ -61,6 +61,9 @@ PHP NEWS callback). (Ilia Alshanetsky) . Fixed leaks and data loss when user filter leaves buckets on input brigade. (ndossche, Matthias Goergens, Jakub Zelenka) + . Fixed use-after-free of the previous request's default stream context when + a stream error is raised during request startup, such as when a POST body + cannot be buffered to a temporary file. (Go Kudo) . Fixed bug GH-24175 (Use-after-free when pclose() closes a stream from its user filter). (Edmond)