ANT-2026-KP4YD2Z6 · php/php-src

heap-buffer-overflow high

Severity Claude high · Security research firm high · Maintainer -

Discovered by Claude Mythos Preview

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Ada Logics.

ANT-2026-KP4YD2Z6: DBA flatfile length parsing triggers heap buffer overflow

In ext/dba/libflatfile/flatfile.c, the flatfile_delete(), flatfile_findkey(), flatfile_firstkey() and flatfile_nextkey() routines read a record-length line with php_stream_gets() and convert it with atoi() into a size_t. A length line of "-1\n" yields SIZE_MAX, which passes the num >= buf_size check but wraps to 1023 in num + FLATFILE_BLOCK_SIZE, so buf is erealloc'd to ~1 KB while php_stream_read() is asked for SIZE_MAX bytes. The buffered stream path then memcpy's the entire attacker-controlled stream read-buffer into the 1 KB chunk, overflowing it by kilobytes and corrupting adjacent Zend heap metadata for potential code execution.

Target

Project: php/php-src
Commit: 63f304dc37e0fe6c
Location: ext/dba/libflatfile/flatfile.c:113
Discovery: static analysis — not yet dynamically reproduced

Technical Details

atoi() on an attacker-supplied length line can return a negative int, which when assigned to size_t num becomes SIZE_MAX; the subsequent buf_size = num + FLATFILE_BLOCK_SIZE integer-wraps to 1023, so erealloc(buf, buf_size) produces a ~1 KB allocation while php_stream_read(dba->fp, buf, num) requests SIZE_MAX bytes. The buffered read implementation drains its whole read-buffer via memcpy into buf, writing well past the end of the heap chunk.

Reproduction

  1. Craft a flatfile whose first record-length line is "-1\n" followed by several KB of payload bytes
  2. Cause the target application to dba_open() this file with the 'flatfile' handler (e.g. via an upload directory or a cache/index file written by a less-trusted component)
  3. Trigger dba_fetch()/dba_exists()/dba_firstkey(), which calls flatfile_findkey()/flatfile_firstkey()
  4. atoi("-1") → SIZE_MAX in size_t; buf is erealloc'd to 1023 bytes while php_stream_read requests SIZE_MAX
  5. Stream read-buffer is memcpy'd past the end of buf, corrupting adjacent Zend heap metadata

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Suggested Fix

Parse record lengths as unsigned with explicit range validation — reject negative or implausibly large values before using them to size allocations or stream reads.

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-KP4YD2Z6.


Reference: ANT-2026-KP4YD2Z6
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Ada Logics.

Verdict
true positive
Severity
high
UPSTREAM FIX

The change that resolved this finding.

diff --git a/ext/dba/libflatfile/flatfile.c b/ext/dba/libflatfile/flatfile.c
index bd76ecfdd0ae..a68fed7e38c4 100644
--- a/ext/dba/libflatfile/flatfile.c
+++ b/ext/dba/libflatfile/flatfile.c
@@ -37,6 +37,18 @@
 
 #define FLATFILE_BLOCK_SIZE 1024
 
+/* Parse the length prefix in `buf` into `num` and grow `buf` to hold it.
+ * atoi() narrows a malformed (e.g. negative) length to a huge size_t whose
+ * `+ FLATFILE_BLOCK_SIZE` would overflow erealloc(); the macro yields true in
+ * that case so the caller stops reading and the read stays within `buf_size`. */
+#define FLATFILE_GROW_BUF(num, buf, buf_size) ( \
+	(num) = atoi(buf), \
+	(num) >= (buf_size) && ( \
+		(num) > SIZE_MAX - FLATFILE_BLOCK_SIZE \
+		|| ((buf) = erealloc((buf), (buf_size) = (num) + FLATFILE_BLOCK_SIZE), 0) \
+	) \
+)
+
 /*
  * ret = -1 means that database was opened for read-only
  * ret = 0  success
@@ -112,10 +124,8 @@ int flatfile_delete(flatfile *dba, datum key_datum) {
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		pos = php_stream_tell(dba->fp);
 
@@ -135,10 +145,8 @@ int flatfile_delete(flatfile *dba, datum key_datum) {
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		/* read in the value */
 		num = php_stream_read(dba->fp, buf, num);
@@ -162,10 +170,8 @@ int flatfile_findkey(flatfile *dba, datum key_datum) {
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		num = php_stream_read(dba->fp, buf, num);
 
@@ -178,10 +184,8 @@ int flatfile_findkey(flatfile *dba, datum key_datum) {
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		num = php_stream_read(dba->fp, buf, num);
 	}
@@ -202,10 +206,8 @@ datum flatfile_firstkey(flatfile *dba) {
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		num = php_stream_read(dba->fp, buf, num);
 
@@ -218,10 +220,8 @@ datum flatfile_firstkey(flatfile *dba) {
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		num = php_stream_read(dba->fp, buf, num);
 	}
@@ -244,20 +244,16 @@ datum flatfile_nextkey(flatfile *dba) {
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		num = php_stream_read(dba->fp, buf, num);
 
 		if (!php_stream_gets(dba->fp, buf, 15)) {
 			break;
 		}
-		num = atoi(buf);
-		if (num >= buf_size) {
-			buf_size = num + FLATFILE_BLOCK_SIZE;
-			buf = erealloc(buf, buf_size);
+		if (FLATFILE_GROW_BUF(num, buf, buf_size)) {
+			break;
 		}
 		num = php_stream_read(dba->fp, buf, num);
 
diff --git a/ext/dba/tests/dba_flatfile_oob.phpt b/ext/dba/tests/dba_flatfile_oob.phpt
new file mode 100644
index 000000000000..3328e1dcba90
--- /dev/null
+++ b/ext/dba/tests/dba_flatfile_oob.phpt
@@ -0,0 +1,31 @@
+--TEST--
+DBA FlatFile handler bounds with a malformed (negative) length field
+--EXTENSIONS--
+dba
+--SKIPIF--
+<?php
+require_once __DIR__ . '/setup/setup_dba_tests.inc';
+check_skip('flatfile');
+?>
+--FILE--
+<?php
+$db_file = __DIR__ . '/dba_flatfile_oob.db';
+// A negative length narrows to a huge size_t and previously overran the read buffer.
+file_put_contents($db_file, "-1\n" . str_repeat('A', 200000));
+
+$db = dba_open($db_file, 'r', 'flatfile');
+var_dump(dba_firstkey($db));
+var_dump(dba_exists("AAAA", $db));
+var_dump(dba_fetch("AAAA", $db));
+dba_close($db);
+echo "done\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/dba_flatfile_oob.db');
+?>
+--EXPECT--
+bool(false)
+bool(false)
+bool(false)
+done

https://github.com/php/php-src/commit/a9c7dfefbdcab6b6d2a5c5692f5dcfb5676814ae

TIMELINE

Dates from discovery through public reveal.

  1. 2026-04-24 Reported to tracker
  2. 2026-05-08 Sent to maintainer
  3. 2026-05-12 Maintainer acknowledged
  4. 2026-07-01 Patch released
  5. 2026-08-17 Publicly revealed
PROVENANCE

SHA-3-512 hash:

8c92fb5c1e0fd2ac6e9dd30b3e1a44735f9152e0fc6c835e00ba4d985a2dae970f65ec54a03e368841019b7d1b5a3a43e13018a6938c14cfcc1d0852548a9036

Committed 2026-05-08 09:19 PT

Revealed 2026-08-17 10:47 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-KP4YD2Z6",
  "bug_class": "Memory Corruption / Heap Buffer Overflow",
  "claude_severity": "high",
  "commit_sha": "63f304dc37e0fe6c",
  "created_at": "2026-04-24T21:28:30+00:00",
  "description": "In ext/dba/libflatfile/flatfile.c, the flatfile_delete(), flatfile_findkey(), flatfile_firstkey() and flatfile_nextkey() routines read a record-length line with php_stream_gets() and convert it with atoi() into a size_t. A length line of \"-1\\n\" yields SIZE_MAX, which passes the `num >= buf_size` check but wraps to 1023 in `num + FLATFILE_BLOCK_SIZE`, so buf is erealloc'd to ~1 KB while php_stream_read() is asked for SIZE_MAX bytes. The buffered stream path then memcpy's the entire attacker-controlled stream read-buffer into the 1 KB chunk, overflowing it by kilobytes and corrupting adjacent Zend heap metadata for potential code execution.",
  "discovered_at": "2026-04-22T00:00:00+00:00",
  "location": "ext/dba/libflatfile/flatfile.c:113",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "php/php-src",
  "reproduction": [
    "1. Craft a flatfile whose first record-length line is \"-1\\n\" followed by several KB of payload bytes",
    "2. Cause the target application to dba_open() this file with the 'flatfile' handler (e.g. via an upload directory or a cache/index file written by a less-trusted component)",
    "3. Trigger dba_fetch()/dba_exists()/dba_firstkey(), which calls flatfile_findkey()/flatfile_firstkey()",
    "4. atoi(\"-1\") → SIZE_MAX in size_t; buf is erealloc'd to 1023 bytes while php_stream_read requests SIZE_MAX",
    "5. Stream read-buffer is memcpy'd past the end of buf, corrupting adjacent Zend heap metadata"
  ],
  "technical_details": "atoi() on an attacker-supplied length line can return a negative int, which when assigned to size_t `num` becomes SIZE_MAX; the subsequent `buf_size = num + FLATFILE_BLOCK_SIZE` integer-wraps to 1023, so `erealloc(buf, buf_size)` produces a ~1 KB allocation while `php_stream_read(dba->fp, buf, num)` requests SIZE_MAX bytes. The buffered read implementation drains its whole read-buffer via memcpy into buf, writing well past the end of the heap chunk.",
  "title": "DBA flatfile length parsing triggers heap buffer overflow",
  "vendor_severity": "high"
}