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