Commit 53681619e95 for php
commit 53681619e957d79c9770d7b2a9e0452c3bf16275
Author: David Carlier <devnexen@gmail.com>
Date: Tue Nov 3 19:26:35 2026 +0000
sapi/cli: php_cli_server_client_save_header() crash on repeated headers
The merged value was not marked persistent local, which tripped the
ZEND_RC_DEBUG assertions, and the second value leaked. Spellings differing
only by case also left headers_original_case pointing at the value freed by
the headers update. Each table now owns a reference to the value it holds.
Close GH-25142
diff --git a/NEWS b/NEWS
index d79257caf33..42263e05c51 100644
--- a/NEWS
+++ b/NEWS
@@ -54,6 +54,8 @@ PHP NEWS
. Fixed bug GH-24697 (PHP_HTTP_PARSER_STRICT build option doesn't work).
(ndossche, devnexen)
. Fixed bug GH-9670 (List -C and -q in the CLI help). (Juan Versolato Lopes)
+ . Fixed crash and leak in the built-in server on repeated request headers.
+ (David Carlier)
- COM:
. Fixed bug GH-25008 (com_event_sink() and com_print_typeinfo() crash
diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c
index 6b17f083c12..671d5d5cd15 100644
--- a/sapi/cli/php_cli_server.c
+++ b/sapi/cli/php_cli_server.c
@@ -1408,9 +1408,8 @@ static void php_cli_server_request_ctor(php_cli_server_request *req) /* {{{ */
req->query_string = NULL;
req->query_string_len = 0;
zend_hash_init(&req->headers, 0, NULL, cli_header_value_dtor, 1);
- /* No destructor is registered as the value pointed by is the same as for &req->headers */
GC_MAKE_PERSISTENT_LOCAL(&req->headers);
- zend_hash_init(&req->headers_original_case, 0, NULL, NULL, 1);
+ zend_hash_init(&req->headers_original_case, 0, NULL, cli_header_value_dtor, 1);
GC_MAKE_PERSISTENT_LOCAL(&req->headers_original_case);
req->content = NULL;
req->content_len = 0;
@@ -1701,16 +1700,22 @@ static void php_cli_server_client_save_header(php_cli_server_client *client)
} else {
zend_string *curval = Z_STR_P(entry);
zend_string *newval = zend_string_safe_alloc(1, ZSTR_LEN(curval), ZSTR_LEN(client->current_header_value) + 2, /* persistent */true);
+ GC_MAKE_PERSISTENT_LOCAL(newval);
memcpy(ZSTR_VAL(newval), ZSTR_VAL(curval), ZSTR_LEN(curval));
memcpy(ZSTR_VAL(newval) + ZSTR_LEN(curval), ", ", 2);
memcpy(ZSTR_VAL(newval) + ZSTR_LEN(curval) + 2, ZSTR_VAL(client->current_header_value), ZSTR_LEN(client->current_header_value) + 1);
ZVAL_STR(&tmp, newval);
+ zend_string_release_ex(client->current_header_value, /* persistent */ true);
}
/* Add/Update the wrapped zend_string to the HashTable */
zend_hash_update(&client->request.headers, lc_header_name, &tmp);
+ /* Each table owns a reference to the new value. The old value can outlive the update
+ * above, as a repeat spelled differently keeps the first spelling's entry in
+ * headers_original_case. */
+ zend_string_addref(Z_STR(tmp));
zend_hash_update(&client->request.headers_original_case, client->current_header_name, &tmp);
zend_string_release_ex(lc_header_name, /* persistent */ true);
diff --git a/sapi/cli/tests/gh16137_duplicate_headers.phpt b/sapi/cli/tests/gh16137_duplicate_headers.phpt
new file mode 100644
index 00000000000..5fdc149de37
--- /dev/null
+++ b/sapi/cli/tests/gh16137_duplicate_headers.phpt
@@ -0,0 +1,36 @@
+--TEST--
+GH-16137: CLI server handling of repeated request headers
+--SKIPIF--
+<?php
+include "skipif.inc";
+?>
+--FILE--
+<?php
+include "php_cli_server.inc";
+php_cli_server_start('$h = getallheaders(); unset($h["Host"], $h["Connection"]); var_dump($h);');
+$ctx = stream_context_create(['http' => ['header' => [
+ 'X-Foo: a',
+ 'X-Foo: b',
+ 'Cookie: Foo',
+ 'cOOKIE: Bar',
+ 'Set-Cookie: first',
+ 'set-cookie: second',
+]]]);
+echo file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS, false, $ctx);
+echo file_get_contents("http://" . PHP_CLI_SERVER_ADDRESS);
+?>
+--EXPECT--
+array(5) {
+ ["X-Foo"]=>
+ string(4) "a, b"
+ ["Cookie"]=>
+ string(3) "Foo"
+ ["cOOKIE"]=>
+ string(8) "Foo, Bar"
+ ["Set-Cookie"]=>
+ string(5) "first"
+ ["set-cookie"]=>
+ string(6) "second"
+}
+array(0) {
+}