Commit beae627b5d1 for php
commit beae627b5d1e3350b83fd2391c18103a0369921a
Author: Go Kudo <zeriyoshi@gmail.com>
Date: Tue Nov 10 15:41:34 2026 +0900
8.6: Fix UAF of the default stream context in the stream error code (#25321)
* 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 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LUfkv6dbRLGQe2UL9SsR2y
* [ci skip] update NEWS
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/NEWS b/NEWS
index 468191a7b90..7edde295167 100644
--- a/NEWS
+++ b/NEWS
@@ -96,6 +96,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)
. Fixed bug GH-15701 (Build failure: incompatible funopen() seeker pointer
diff --git a/ext/standard/file.c b/ext/standard/file.c
index ff68374928c..e64167df328 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 00000000000..240bd9342be
--- /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--
+<?php include "skipif.inc"; ?>
+--FILE--
+<?php
+
+include "include.inc";
+
+$php = get_cgi_path();
+reset_env_vars();
+
+$dir = __DIR__ . '/stream_error_default_context';
+@mkdir($dir);
+file_put_contents("$dir/index.php", '<?php file_get_contents(__FILE__); echo "ok ", count($_POST), "\n";');
+/* Larger than SAPI_POST_BLOCK_SIZE, so the request body is spilled to a temporary file */
+$body = 'a=' . str_repeat('a', 20000);
+/* php-cgi -T reads CONTENT_LENGTH bytes of stdin for each request */
+file_put_contents("$dir/body", $body . $body);
+
+$env = [
+ 'REDIRECT_STATUS' => '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--
+<?php
+$dir = __DIR__ . '/stream_error_default_context';
+@unlink("$dir/index.php");
+@unlink("$dir/body");
+@rmdir($dir);
+?>
+--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