[php-src] Issue #24509: HTTP wrapper leaks Location buffer when an empty Location header is followed by another Location header
| From: | IzieStratt | Date: | Wed, 21 Oct 2026 20:39:59 +0000 |
| Subject: | [php-src] Issue #24509: HTTP wrapper leaks Location buffer when an empty Location header is followed by another Location header | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253066@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24509
Author: IzieStratt
### Description
The following code:
server.php (run first,
php server.php):
```php
<?php
$srv = stream_socket_server("tcp://127.0.0.1:18233", $errno, $errstr);
$conn = stream_socket_accept($srv);
while (($line = fgets($conn, 8192)) !== false) { if (trim($line) === '') break; }
// empty Location value, then a real one
fwrite($conn, "HTTP/1.1 302 Found\r\nLocation:\r\nLocation: /abcdefghijklmno\r\n\r\n");
fclose($conn);
```
client.php (debug build, php -d report_memleaks=1 client.php):
```php
<?php
@file_get_contents("http://127.0.0.1:18233/");
```
Resulted in this output:
```
ext/standard/http_fopen_wrapper.c(341) : Freeing 0x... (16 bytes), script=.../client.php
=== Total 1 memory leaks detected ===
```
But I expected this output instead:
```
(no leaks reported)
```
Root cause: the wrapper stores the Location value using location_len == 0 as the
"nothing allocated yet" sentinel (ext/standard/http_fopen_wrapper.c:340 in 8.5.12, :327 on
master):
```c
if (header_info->location_len == 0) {
header_info->location = emalloc(last_header_value_len + 1);
} else if (header_info->location_len <= last_header_value_len) {
header_info->location = erealloc(header_info->location, last_header_value_len + 1);
}
header_info->location_len = last_header_value_len;
```
An empty Location: header allocates a 1-byte buffer and then sets location_len =
0, restoring the sentinel state while the pointer is still held. A second Location header
takes the emalloc branch again and the first buffer is leaked. Each empty-then-nonempty sequence
leaks one buffer per request.
Control (two non-empty Location headers): 0 leaks. Reproduces on current master (8.7.0-dev) as well.
Suggested patch (verified against master: 0 leaks after patching; repeated non-empty Location
headers still take the erealloc path and behave as before). header_info is
zero-initialized by php_stream_http_response_header_info_init, so the pointer works as
the sentinel:
```diff
--- a/ext/standard/http_fopen_wrapper.c
+++ b/ext/standard/http_fopen_wrapper.c
@@ -324,7 +324,9 @@ static zend_string *php_stream_http_response_headers_parse(php_stream_wrapper *w
zend_string_efree(last_header_line);
return NULL;
}
- if (header_info->location_len == 0) {
+ /* An empty Location value leaves location_len at 0, so the buffer
+ * pointer, not the length, must be the "already allocated" test. */
+ if (header_info->location == NULL) {
header_info->location = emalloc(last_header_value_len + 1);
} else if (header_info->location_len <= last_header_value_len) {
header_info->location = erealloc(header_info->location, last_header_value_len + 1);
```
### PHP Version
```plain
PHP 8.5.12 (also reproduces on master, 8.7.0-dev)
```
### Operating System
Linux x86_64