GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — FrankenPHP worker mode bypasses the normal request-activation checks · Issue #122 · php-debugger/php-debugger

FrankenPHP worker mode bypasses the normal request-activation checks #122

New issue
Copy link
New issue
Copy link
Open
Open
FrankenPHP worker mode bypasses the normal request-activation checks#122
Copy link
Labels
bugSomething isn't workingSomething isn't workingcompatibilityperformanceBenchmarks, overhead, performance trackingBenchmarks, overhead, performance tracking

Description

@carlos-granados
carlos-granados
opened on Oct 4, 2026
Issue body actions

Summary

Our FrankenPHP worker-mode support decides whether to start a debug session in
sapi_activate, using a raw substring scan of the request data instead of the
normal request-activation path. As a result several documented activation rules
are not applied to worker requests: xdebug.trigger_value,
xdebug.start_with_request=no, triggers arriving via POST/env, and
XDEBUG_SESSION_START / XDEBUG_SESSION_STOP.

Xdebug has since grown its own worker-mode support in
b4052057
("Fixed issue #2403: Reinit debugger for requests in FrankenPHP worker mode"),
built on a different mechanism that does not have these gaps. That commit was
deliberately not transplanted during the Xdebug sync (#121), because taking
it as-is would replace our implementation and drop the overhead optimisation
this fork exists for. This issue records the gaps and proposes how to close them.

Related: #63 (closed) and #64 cover the observer/statement-handler mechanics,
which our implementation already addresses. This issue is about the activation
checks
, which it does not.

Root cause

xdebug_init_auto_globals() is called only from PHP_RINIT (xdebug.c:577),
and RINIT does not run per request in a FrankenPHP worker — only
sapi_module.activate does. So superglobals are never force-built for a worker
request, and anything that reads them is unreliable there.

That is why src/debugger/frankenphp.c falls back to scanning
SG(request_info).cookie_data and .query_string directly
(has_trigger_in_string() / has_debug_trigger()), and decides up front:

/* src/debugger/frankenphp.c:122 */
if (xdebug_lib_start_with_request() || has_debug_trigger()) {
    XG_DBG(context).do_connect_to_client = 1;
    XG_BASE(observer_active) = 1;
} else {
    XG_BASE(observer_active) = 0;
}

This bypasses xdebug_debug_init_if_requested_at_startup() (src/debugger/com.c:971),
which is what every non-worker request goes through.

Gaps

Gap Code we bypass Consequence in worker mode
xdebug.trigger_value / shared secret not checked trigger_enabled() → xdebug_lib_has_shared_secret(), does_shared_secret_match() (src/lib/lib.c:569-580) Security-relevant: with a shared secret configured, a bare XDEBUG_TRIGGER= activates a session
start_with_request=no not honoured xdebug_lib_never_start_with_request() (src/lib/lib.c:314) We only test == YES, so a stray trigger cookie activates despite no
Trigger sources limited to raw cookie header + query string xdebug_lib_find_in_globals() checks _GET, _POST, _COOKIE, real superglobals, then getenv() (src/lib/lib.c:393+) POST-body, $_ENV, $_SERVER and env-var triggers never fire
XDEBUG_SESSION_START side effects missing xdebug_handle_start_session() (src/debugger/com.c:869) — sets the session cookie, calls xdebug_update_ide_key() No session cookie set; IDE key not taken from the trigger value
XDEBUG_SESSION_STOP not handled xdebug_handle_stop_session() (src/debugger/com.c) A session cannot be stopped in worker mode
start_session_result cache never reset reset only in xdebug_debugger_rinit() (src/debugger/debugger.c:895) Not a bug today (we never call xdebug_handle_start_session()), but it becomes one the moment we do: the worker's first answer would stick for its whole life

XDEBUG_IGNORE is a partial case: it is reached, via do_connect_to_client →
xdebug_debug_init_if_requested_on_connect_to_client() → xdebug_should_ignore()
(src/debugger/debugger.c:354). But should_ignore() reads superglobals that
nothing forces here, so it may see the previous request's data or none. Unreliable
rather than absent.

Why this is not a small patch

The correct checks need superglobals. Superglobals are not available in
sapi_activate — which is exactly why upstream defers the decision to the first
user code executed in the request:

  • sapi_activate resets per-request flags and sets context.do_request_reinit = 1; it decides nothing
  • xdebug_execute_user_code_begin() checks xdebug_frankenphp_reinit_pending() and calls xdebug_frankenphp_request_reinit()
  • that forces five superglobals with zend_is_auto_global_str(), then calls xdebug_debug_init_if_requested_at_startup() — the same function a normal request uses
  • rinit clears the flag, so it doubles as a reliable "this is a worker request" detector

Upstream can do this because its observer is always installed, so the hook point
is always reached. Reaching it requires observer_active == true, and our
optimisation's whole point is to set observer_active before the request runs,
from the cheap scan. Adopting the deferred path naively means the observer runs
for every worker request, and we lose the near-zero-overhead property for
untriggered ones.

Things our implementation has that upstream's does not, and that must survive any
change here:

  • CG(compiler_options) |= ZEND_COMPILE_EXTENDED_STMT forced process-wide at MINIT (the fix for FrankenPHP and disabled statement handler #64)
  • xdebug_disable_opcache_optimizer() per sapi_activate (INI values are restored when a worker generation ends, so MINIT alone is not enough)
  • xdebug_frankenphp_in_use() making xdebug_observer_init() install handlers unconditionally, so the engine's per-zend_function cache does not blacklist them for the worker's life (the fix for FrankenPHP and disabled observer #63)
  • draining remote_poll_pending so breakpoints pushed while the worker was busy are picked up

Proposed approach

Keep the cheap scan, but demote it to a conservative negative filter, and use
upstream's deferred mechanism for everything else:

  1. In sapi_activate: if start_with_request=yes → arm. Else if the raw scan finds no candidate trigger name in the cookie header or query string, getenv() has none, and the request method is not POST → leave observer_active = 0 and return. Zero cost, and this is the common case.
  2. Otherwise arm observer_active and a do_request_reinit flag, and let the real xdebug_debug_init_if_requested_at_startup() decide inside the request, as upstream does.
  3. Reset XG_DBG(start_session_result) = -1 in sapi_activate.
  4. Keep all four items listed above.

Open question: POST-body triggers either force the slow path for every POST
request, or stay unsupported. Worth deciding explicitly rather than by omission.

Testing

tests/frankenphp/run-worker-tests.sh + tests/frankenphp/dbgp_listener.py already
drive a real worker in Docker and assert that breakpoints still fire after
un-debugged requests, but exercise none of the gaps above. Cases to add:

  • trigger present but xdebug.trigger_value mismatch → must not activate
  • start_with_request=no plus a trigger → must not activate
  • XDEBUG_IGNORE present → must not activate
  • XDEBUG_SESSION_START → session cookie set; XDEBUG_SESSION_STOP → both cookies cleared
  • trigger in a POST body (whatever we decide above)

tests/frankenphp/Dockerfile currently hardcodes dunglas/frankenphp:1-php8.4, so
testing across versions needs the PHP_VERSION plumbing that upstream's
1aea1ae7
("Add 'make test-frankenphp' which also sets the current PHP version") was adding.
That commit was skipped in #121 for the same reason, and is worth folding in here.

Activity

  1. carlos-granados
    added
    bugSomething isn't workingSomething isn't working
    performanceBenchmarks, overhead, performance trackingBenchmarks, overhead, performance tracking
    compatibility
    on Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingSomething isn't workingcompatibilityperformanceBenchmarks, overhead, performance trackingBenchmarks, overhead, performance tracking

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.