@@ -166,7 +166,7 @@ final class DebugServer
166166 /** @var array{id: string, label: string, file: string, line: int, condition?: string}|null */
167167 private array |null $ activeBreakpoint = null ;
168168
169- /** @param array{command?: list<string>, context?: string, breakpoint?: string, steps?: int, connectionTimeout?: float, executionTimeout?: float, traceOnly?: bool, maxSteps?: int, jsonOutput?: bool, breakpoints?: list<array{file: string, line: int|string, condition?: string}>, readTimeout?: float, watches?: list<string>, pretty?: bool, maxValueBytes?: int|null, maxDepth?: int|null} $options */
169+ /** @param array{command?: list<string>, context?: string, breakpoint?: string, steps?: int, connectionTimeout?: float, executionTimeout?: float, traceOnly?: bool, maxSteps?: int, jsonOutput?: bool, breakpoints?: list<array{file: string, line: int|string, condition?: string}>, readTimeout?: float, watches?: list<string>, pretty?: bool, maxValueBytes?: int|null, maxDepth?: int|null, phpBinary?: string } $options */
170170 public function __construct (
171171 private readonly string $ targetScript ,
172172 private readonly int $ debugPort ,
@@ -354,8 +354,14 @@ private function executeTargetScript(): void
354354 $ xdebugFlag = XdebugFinder::getXdebugFlag ();
355355 $ xdebugPart = $ xdebugFlag !== '' ? $ xdebugFlag . ' ' : '' ;
356356
357+ // Allow callers (e.g. xback --php=...) to override the spawned
358+ // PHP binary while keeping $command[0] as the literal 'php'.
359+ $ phpBinary = ($ this ->options ['phpBinary ' ] ?? '' ) !== ''
360+ ? (string ) $ this ->options ['phpBinary ' ]
361+ : 'php ' ;
362+
357363 $ cmd = sprintf (
358- 'XDEBUG_SESSION=xdebug-mcp php %s '
364+ 'XDEBUG_SESSION=xdebug-mcp %s %s '
359365 . '-dxdebug.mode=debug,trace '
360366 . '-dxdebug.start_with_request=yes '
361367 . '-dxdebug.client_host=127.0.0.1 '
@@ -372,6 +378,7 @@ private function executeTargetScript(): void
372378 . '-derror_log=/tmp/php.log '
373379 . '-dauto_prepend_file=%s '
374380 . '%s ' ,
381+ escapeshellarg ($ phpBinary ),
375382 $ xdebugPart ,
376383 $ this ->debugPort ,
377384 escapeshellarg ($ prependFilter ),
@@ -391,8 +398,13 @@ private function executeTargetScript(): void
391398 $ xdebugFlag = XdebugFinder::getXdebugFlag ();
392399 $ xdebugPart = $ xdebugFlag !== '' ? $ xdebugFlag . ' ' : '' ;
393400
401+ // Honor an optional PHP-binary override.
402+ $ phpBinary = ($ this ->options ['phpBinary ' ] ?? '' ) !== ''
403+ ? (string ) $ this ->options ['phpBinary ' ]
404+ : 'php ' ;
405+
394406 $ cmd = sprintf (
395- 'XDEBUG_SESSION=xdebug-mcp php %s '
407+ 'XDEBUG_SESSION=xdebug-mcp %s %s '
396408 . '-dxdebug.mode=debug,trace '
397409 . '-dxdebug.start_with_request=yes '
398410 . '-dxdebug.client_host=127.0.0.1 '
@@ -405,6 +417,7 @@ private function executeTargetScript(): void
405417 . '-dxdebug.connect_timeout_ms=5000 '
406418 . '-dauto_prepend_file=%s '
407419 . '%s ' ,
420+ escapeshellarg ($ phpBinary ),
408421 $ xdebugPart ,
409422 $ this ->debugPort ,
410423 escapeshellarg ($ prependFilter ),
@@ -2305,6 +2318,18 @@ private function formatVariableValue(string $value, string $type): string
23052318 };
23062319 }
23072320
2321+ /**
2322+ * Strip XML 1.0 illegal control characters from a DBGp byte stream.
2323+ *
2324+ * Xdebug emits raw bytes (e.g., NUL inside anonymous-class names from PHP 8.3+)
2325+ * that libxml's strict parser rejects. We delete every byte that is illegal in
2326+ * XML 1.0 while preserving tab/LF/CR and any high-bit bytes (UTF-8 sequences).
2327+ */
2328+ private static function sanitizeDbgpXml (string $ xml ): string
2329+ {
2330+ return preg_replace ('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/ ' , '' , $ xml ) ?? $ xml ;
2331+ }
2332+
23082333 /**
23092334 * Parse XML response safely without error suppression
23102335 */
@@ -2318,7 +2343,7 @@ private function parseXmlResponse(string $xmlString): SimpleXMLElement|null
23182343 $ useErrors = libxml_use_internal_errors (true );
23192344 libxml_clear_errors ();
23202345
2321- $ xml = simplexml_load_string ($ xmlString );
2346+ $ xml = simplexml_load_string (self :: sanitizeDbgpXml ( $ xmlString) );
23222347
23232348 // Get any errors that occurred
23242349 $ errors = libxml_get_errors ();
@@ -2805,7 +2830,11 @@ public function getCurrentVariables(): array
28052830 }
28062831
28072832 $ variables = [];
2808- $ xml = simplexml_load_string ($ response );
2833+ $ useErrors = libxml_use_internal_errors (true );
2834+ libxml_clear_errors ();
2835+ $ xml = simplexml_load_string (self ::sanitizeDbgpXml ($ response ));
2836+ libxml_clear_errors ();
2837+ libxml_use_internal_errors ($ useErrors );
28092838 if ($ xml && (property_exists ($ xml , 'property ' ) && $ xml ->property !== null )) {
28102839 foreach ($ xml ->property as $ prop ) {
28112840 $ name = (string ) $ prop ['name ' ];
@@ -2944,7 +2973,11 @@ private function getJsonEncodeOutput(string $varName): string|null
29442973 return null ;
29452974 }
29462975
2947- $ xml = simplexml_load_string ($ response );
2976+ $ useErrors = libxml_use_internal_errors (true );
2977+ libxml_clear_errors ();
2978+ $ xml = simplexml_load_string (self ::sanitizeDbgpXml ($ response ));
2979+ libxml_clear_errors ();
2980+ libxml_use_internal_errors ($ useErrors );
29482981 if (! $ xml || (! property_exists ($ xml , 'property ' ) || $ xml ->property === null )) {
29492982 return null ;
29502983 }
@@ -3448,7 +3481,11 @@ private function captureCurrentDebugState(int $breakNumber, array|null $breakpoi
34483481 private function parseStackFrames (string $ stackXml ): array
34493482 {
34503483 try {
3451- $ xml = simplexml_load_string ($ stackXml );
3484+ $ useErrors = libxml_use_internal_errors (true );
3485+ libxml_clear_errors ();
3486+ $ xml = simplexml_load_string (self ::sanitizeDbgpXml ($ stackXml ));
3487+ libxml_clear_errors ();
3488+ libxml_use_internal_errors ($ useErrors );
34523489 if (! $ xml || (! property_exists ($ xml , 'stack ' ) || $ xml ->stack === null )) {
34533490 return [];
34543491 }
@@ -3547,7 +3584,11 @@ private function extractLocationFromBreakResponse(string $response): string
35473584 private function extractLocationDataFromBreakResponse (string $ response ): array |null
35483585 {
35493586 try {
3550- $ xml = simplexml_load_string ($ response );
3587+ $ useErrors = libxml_use_internal_errors (true );
3588+ libxml_clear_errors ();
3589+ $ xml = simplexml_load_string (self ::sanitizeDbgpXml ($ response ));
3590+ libxml_clear_errors ();
3591+ libxml_use_internal_errors ($ useErrors );
35513592 if ($ xml ) {
35523593 // Register xdebug namespace
35533594 $ xml ->registerXPathNamespace ('xdebug ' , 'https://xdebug.org/dbgp/xdebug ' );
0 commit comments