Hi David,

> Doesn't PHP with ZTS (Zend Thread Safety) offers the same capabilities?

No, it's a different thing. ZTS gives each thread its own copy of the
engine and extension globals, so several threads can run requests in
parallel in the same process. But in both models, RINIT and RSHUTDOWN
run once per script execution.

In worker mode, a thread runs one script that handles many requests in
a loop, so RINIT and RSHUTDOWN only run when that script starts and
ends. ZTS doesn't change that.

By the way, FrankenPHP always uses
ZTS, because it's written in Go, and Go relies heavily on threads
(goroutines rely on system threads under the hood).

Best

On Thu, Oct 29, 2026 at 7:20 PM David Maye <[email protected]> wrote:
>
> Hello Kévin,
>
> I have a question. Doesn't PHP with ZTS (Zend Thread Safety) offers the same 
> capabilities?
>
> I remember I read something about it. What you described reminded me of it. 
> Although as I'm not an expert in the area I can't tell.
>
> The following link is from the PHP internal book for PHP lifecycle. (Although 
> it says php7, I understand that it works the same for PHP8).
> https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html
>
> I sent a screenshot of the page in attachment.
>
> I'm curious if this has to do with the question.
>
> Kind regards,
> David Maye
>
>
> On Oct 29, 2026 6:47 PM, Kévin Dunglas <[email protected]> wrote:
>
> Hello internals,
>
> As you may know, FrankenPHP and some other SAPIs (like the
> experimental ones written in Rust) have optional support for a "worker
> mode". The main idea behind these worker modes is to use long-running
> scripts, started when the web server starts, to handle multiple HTTP
> requests in a loop.
>
> This allows keeping states in memory across requests - such as the
> Composer autoloader or Symfony and Laravel services - instead of
> re-computing them at the beginning of each new request. This also
> allows maintaining persistent connections to remote services across
> requests.
>
> Worker mode dramatically improves performance, at the cost of no
> longer isolating state between requests and losing the memory leak
> tolerance provided by the traditional "fire and forget" model. This
> mode has become increasingly popular since the release of FrankenPHP,
> and is now natively supported by a large part of the ecosystem
> (Symfony, Laravel, Yii...).
>
> Currently, the engine and extensions have no knowledge of this, and
> have no way to detect when a "real" HTTP request starts or ends. As
> expected, they are only aware of the start (and end) of the main,
> long-running script. FrankenPHP (and other SAPIs with a similar mode)
> reset "real HTTP request" related data by poking into the engine and
> first-party extensions
> (https://github.com/php/frankenphp/blob/7f50be9d86261bf7cff9b051ab4fb572e95020ed/frankenphp.c#L400-L646).
> This approach is fragile, and the internal code resetting these states
> can change without warning in a new PHP version.
>
> Each omission results in a cross-request leak or a memory safety bug.
> FrankenPHP fixed several of them this year:
>
> - a session ID leaking to other clients when ext/session is a shared
> module, because FrankenPHP can only reset the session globals when it
> is built against the session headers:
> https://github.com/php/frankenphp/security/advisories/GHSA-v3ph-cgqh-r8p5
> - a use-after-free of php://temp streams, because nothing releases
> SG(request_info).request_body between worker requests and FrankenPHP's
> own cleanup freed streams it didn't own:
> https://github.com/php/frankenphp/security/advisories/GHSA-cm3q-jw2q-9rh5
> - a double free because sapi_deactivate() frees
> SG(request_info).content_type_dup and current_user without clearing
> them (fixed for PHP 8.6: https://github.com/php/php-src/pull/22974):
> https://github.com/php/frankenphp/security/advisories/GHSA-h764-4wh7-m7j2
> - http_get_last_response_headers() returning headers fetched by a
> previous request:
> https://github.com/php/frankenphp/security/advisories/GHSA-pmvg-8483-mw84
> - libxml_get_errors() returning errors of a previous request, with
> parse-error fragments and server paths:
> https://github.com/php/frankenphp/security/advisories/GHSA-4gfw-9v29-gp9h
>
> Third-party extensions detect the boundaries themselves, each in its
> own way. Xdebug and the Datadog profiler replace sapi_module.activate
> and sapi_module.deactivate when the SAPI is FrankenPHP. Datadog's
> tracer and AppSec, and New Relic in an open pull request, hook
> frankenphp_handle_request(). Blackfire's Symfony bridge, Tideways and
> Sentry wrap each request with start and stop calls in userland.
>
> Calling RSHUTDOWN and RINIT between worker requests doesn't work:
> these handlers mix state that needs resetting with state the script
> keeps. For instance, the standard extension's RSHUTDOWN releases the
> strtok() state, but also destroys register_tick_function() callbacks
> and user stream filters. The session extension's RSHUTDOWN writes and
> closes the session, but also frees the save handlers registered at
> boot. The libxml extension's RSHUTDOWN drops the external entity
> loader. Only each extension's maintainers know which of their globals
> belong to which kind.
>
> I'd like to add two optional module hooks, called by the SAPI between
> two logical requests of the same script:
>
> - request_end: release the state tied to the finished request, but
> keep everything still reachable from userland;
> - request_begin: set up the next request once its headers and
> superglobals are available (session.auto_start, Xdebug's triggers).
>
> Two engine functions, php_request_end() and php_request_begin(), would
> run the output and SAPI steps that worker SAPIs copy today from
> php_request_shutdown() and php_request_startup(), import the
> superglobals of the new request (except $_ENV, which comes from the
> process), set the execution timer, and call the hooks.
>
> Releasing the request body takes two small changes: php://input
> streams hold a reference to the body, and freeing the body, for
> instance when the script closes it, clears
> SG(request_info).request_body. Core extensions would implement the
> hooks first; third-party extensions could adopt them at their pace.
> SAPIs that don't call these functions see no behavior change.
>
> I drafted a working patch for master, with the standard and session
> hooks: https://github.com/dunglas/php-src/tree/request-boundary-hooks.
> FrankenPHP doesn't build against master yet, so I tested it with a
> patched version of FrankenPHP and it works.
>
> As this is an internal API with no userland or BC impact, an RFC isn't
> strictly needed.
>
> I made a similar but broader proposal almost 3 years ago
> (https://externals.io/message/122027). I think time has shown that
> worker modes are here to stay, and this less intrusive proposal could
> fix several maintenance issues affecting the whole ecosystem.
>
> So, I would like your opinion on this before working more on it.
>
> Best,
> --
> Kévin Dunglas
>
> https://dunglas.dev
>
>

Reply via email to