From ad68c06a7b99daf9e1e0979363c81a490d21e7de Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:51:53 +0000 Subject: [PATCH 1/2] 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. --- ext/standard/http_fopen_wrapper.c | 2 + .../http_response_header_nested_request.phpt | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 ext/standard/tests/http/http_response_header_nested_request.phpt diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 2cc9e081ae7d..bfe4bbee9f11 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -1280,6 +1280,8 @@ php_stream *php_stream_url_wrap_http(php_stream_wrapper *wrapper, const char *pa PHP_URL_REDIRECT_MAX, HTTP_WRAPPER_HEADER_INIT, &headers STREAMS_CC); if (!Z_ISUNDEF(headers)) { + /* A request made from a notification callback may have stored its own headers meanwhile. */ + zval_ptr_dtor(&BG(last_http_headers)); ZVAL_COPY(&BG(last_http_headers), &headers); if (FAILURE == zend_set_local_var_str( 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 000000000000..d562818f74e1 --- /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-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $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" +} From b98fc1f6f9fb93080378e28b871896e6281a6d5a Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:21 +0300 Subject: [PATCH 2/2] 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. --- ext/standard/http_fopen_wrapper.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index bfe4bbee9f11..687c2f70d4ab 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -1272,18 +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)) { - /* A request made from a notification callback may have stored its own headers meanwhile. */ - zval_ptr_dtor(&BG(last_http_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);