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);
}
}