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) {
+}