Commit 92128ac93fa for php.net

commit 92128ac93fa3911bcedb6ee2656680939959965b
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date:   Sun Jun 14 23:16:32 2026 +0200

    Fix stream_context_set_option() mutating the default context (#22235)

    Since GH-20524, _php_stream_open_wrapper_ex() attaches the context to a
    stream that has none, including the implicitly substituted default
    context. Sharing the default context by reference let a later
    stream_context_set_option() on the stream mutate the global default
    context, leaking options into every other context-less stream.

    Only attach explicitly provided contexts. Stream errors already fall
    back to the default context when the stream has none, so error handling
    is unaffected.

diff --git a/ext/standard/tests/streams/stream_context_set_option_no_default_leak.phpt b/ext/standard/tests/streams/stream_context_set_option_no_default_leak.phpt
new file mode 100644
index 00000000000..788b6d8a467
--- /dev/null
+++ b/ext/standard/tests/streams/stream_context_set_option_no_default_leak.phpt
@@ -0,0 +1,29 @@
+--TEST--
+stream_context_set_option() on a context-less stream must not leak into the default context
+--FILE--
+<?php
+$a = fopen('php://memory', 'r+');
+stream_context_set_option($a, 'http', 'filename', 'test.txt');
+
+// The default context must stay untouched.
+var_dump(stream_context_get_options(stream_context_get_default()));
+
+// A later context-less stream must not inherit the option.
+$b = fopen('php://memory', 'r+');
+var_dump(stream_context_get_options($b));
+
+// The stream the option was set on keeps it for itself.
+var_dump(stream_context_get_options($a));
+?>
+--EXPECT--
+array(0) {
+}
+array(0) {
+}
+array(1) {
+  ["http"]=>
+  array(1) {
+    ["filename"]=>
+    string(8) "test.txt"
+  }
+}
diff --git a/main/streams/streams.c b/main/streams/streams.c
index 715bbcfe037..171748e6a08 100644
--- a/main/streams/streams.c
+++ b/main/streams/streams.c
@@ -2248,7 +2248,12 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod
 		stream->open_filename = __zend_orig_filename ? __zend_orig_filename : __zend_filename;
 		stream->open_lineno = __zend_orig_lineno ? __zend_orig_lineno : __zend_lineno;
 #endif
-		if (stream->ctx == NULL && context != NULL && !persistent) {
+		/* Attach an explicitly provided context to the stream, but never the
+		 * default context: sharing it by reference would let a later
+		 * stream_context_set_option() on the stream mutate the global default
+		 * context, leaking options into every other stream. Stream errors fall
+		 * back to the default context on their own when the stream has none. */
+		if (stream->ctx == NULL && context != NULL && context != FG(default_context) && !persistent) {
 			php_stream_context_set(stream, context);
 		}
 	}