Commit 0ff02e86dc7 for php
commit 0ff02e86dc7db58ada9ef521245025906d24053a
Author: Edmond <1571649+EdmondDantes@users.noreply.github.com>
Date: Wed Oct 14 20:33:17 2026 +0300
std: Fix memory leak of the last response headers after a nested HTTP request (#24201)
* Fix leak of the last response headers after a nested HTTP request
php_stream_url_wrap_http() releases BG(last_http_headers) before the request
and copies the new headers over it after. A request made while the first one
runs, from its notification callback, stores its own headers there in between,
and the copy overwrote that array without releasing it.
* Release the last response headers once, after the request
The release before the request is dropped: a single release after it covers
both the previous request's headers and those stored by a nested one.
diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c
index 2cc9e081ae7..687c2f70d4a 100644
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -1272,16 +1272,15 @@ php_stream *php_stream_url_wrap_http(php_stream_wrapper *wrapper, const char *pa
ZVAL_UNDEF(&headers);
- zval_ptr_dtor(&BG(last_http_headers));
- ZVAL_UNDEF(&BG(last_http_headers));
-
stream = php_stream_url_wrap_http_ex(
wrapper, path, mode, options, opened_path, context,
PHP_URL_REDIRECT_MAX, HTTP_WRAPPER_HEADER_INIT, &headers STREAMS_CC);
- if (!Z_ISUNDEF(headers)) {
- ZVAL_COPY(&BG(last_http_headers), &headers);
+ /* After the request: one made from its notification callback may have stored its own headers here. */
+ zval_ptr_dtor(&BG(last_http_headers));
+ ZVAL_COPY(&BG(last_http_headers), &headers);
+ if (!Z_ISUNDEF(headers)) {
if (FAILURE == zend_set_local_var_str(
"http_response_header", sizeof("http_response_header")-1, &headers, 0)) {
zval_ptr_dtor(&headers);
diff --git a/ext/standard/tests/http/http_response_header_nested_request.phpt b/ext/standard/tests/http/http_response_header_nested_request.phpt
new file mode 100644
index 00000000000..d562818f74e
--- /dev/null
+++ b/ext/standard/tests/http/http_response_header_nested_request.phpt
@@ -0,0 +1,41 @@
+--TEST--
+http_get_last_response_headers() after a request made from a notification callback
+--SKIPIF--
+<?php require 'server.inc'; http_server_skipif(); ?>
+--INI--
+allow_url_fopen=1
+--FILE--
+<?php
+require 'server.inc';
+
+$responses = [
+ "data://text/plain,HTTP/1.0 200 Ok\r\nContent-Type: text/plain\r\nOuter: 1\r\n\r\nouter",
+ "data://text/plain,HTTP/1.0 200 Ok\r\nInner: 1\r\n\r\ninner",
+];
+
+['pid' => $pid, 'uri' => $uri] = http_server($responses, $output);
+
+$nested = false;
+$ctx = stream_context_create([], ['notification' => function ($code) use (&$nested, $uri) {
+ if ($code === STREAM_NOTIFY_MIME_TYPE_IS && !$nested) {
+ $nested = true;
+ var_dump(file_get_contents($uri));
+ }
+}]);
+
+var_dump(file_get_contents($uri, false, $ctx));
+var_dump(http_get_last_response_headers());
+
+http_server_kill($pid);
+?>
+--EXPECT--
+string(5) "inner"
+string(5) "outer"
+array(3) {
+ [0]=>
+ string(15) "HTTP/1.0 200 Ok"
+ [1]=>
+ string(24) "Content-Type: text/plain"
+ [2]=>
+ string(8) "Outer: 1"
+}