[php-src] Issue #24685: Memory leak in php_cli_server_client_save_header when joining duplicate headers
| From: | m-this | Date: | Sun, 25 Oct 2026 13:18:08 +0000 |
| Subject: | [php-src] Issue #24685: Memory leak in php_cli_server_client_save_header when joining duplicate headers | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253180@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24685
Author: m-this
### Description
php_cli_server_client_save_header() in sapi/cli/php_cli_server.c leaks the
value of a duplicate request header. When a request repeats a header name, the duplicate value is
copied into a joined string but the original current_header_value string is never
released.
### Details
In the merge branch (8.4.26, lines 1699-1707), a duplicate header is joined with the value already
stored in the hash table:
```c
} 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);
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);
}
```
newval is stored in the hash tables, but client->current_header_value
is only copied out of and then dropped at line 1718:
```c
client->current_header_value = NULL;
```
In the non-duplicate branch (entry == NULL), ZVAL_STR(&tmp,
client->current_header_value) hands the string to the hash table, which owns and frees it
at request teardown. The merge branch copies the bytes of current_header_value into
newval but never releases current_header_value itself, so the string
leaks.
### PoC
Start the built-in server and send a request with two headers of the same name:
```python
import socket
s = socket.create_connection(("127.0.0.1", 8000))
s.sendall(b"GET / HTTP/1.1\r\nX: 1\r\nX: 2\r\n\r\n")
```
Under a LeakSanitizer build this leaks 32 bytes per connection. The leak is linear in the number of
connections that carry a duplicate header.
### LeakSanitizer output
```
Direct leak of 32 byte(s) in 1 object(s) allocated from:
#0 0x7f8b7e8f4c57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x558100a29061 in __zend_malloc /src/Zend/zend_alloc.c:3294
#2 0x5581010b0e47 in zend_string_alloc /src/Zend/zend_string.h:176
#3 0x5581010b0e47 in zend_string_init /src/Zend/zend_string.h:198
#4 0x5581010b0e47 in php_cli_server_client_read_request_on_header_value
/src/sapi/cli/php_cli_server.c:1765
#5 0x5581010a2803 in php_http_parser_execute /src/sapi/cli/php_http_parser.c:1209
#6 0x5581010b7797 in php_cli_server_client_read_request /src/sapi/cli/php_cli_server.c:1884
#7 0x5581010b856f in php_cli_server_recv_event_read_request /src/sapi/cli/php_cli_server.c:2643
...
SUMMARY: AddressSanitizer: 32 byte(s) leaked in 1 allocation(s).
```
### Impact
A small server-side leak (32 bytes) per request that sends a duplicate header. The built-in server
is single-process, so a long-running instance receiving such requests grows memory slowly. No crash
or memory corruption.
### Suggested fix
Release the incoming value in the merge branch after it has been copied:
```c
ZVAL_STR(&tmp, newval);
zend_string_release_ex(client->current_header_value, /* persistent */ true);
```
### PHP Version
8.4.26; the same code is present on PHP-8.6 and master (8.7.0-dev).