2026年6月1日(月) 16:30 Jakub Zelenka <[email protected]>:

> Hi,
>
> On Mon, Jun 1, 2026 at 8:06 AM Go Kudo <[email protected]> wrote:
>
>>
>>
>> 2026年5月17日(日) 0:19 Go Kudo <[email protected]>:
>>
>>> Hi internals,
>>>
>>> I'd like to start the discussion for a new RFC, OPcache Static Cache.
>>>
>>> RFC: https://wiki.php.net/rfc/opcache_static_cache
>>> Implementation: https://github.com/php/php-src/pull/22052
>>>
>>
>>
> The FPM shared hosting part is a problem and I don't think this can be
> default and probably cannot even be optional. The reason is that we
> consider data leaks between pools as security issues so I don't think we
> can have some feature that is actually causing a security issue. It will be
> a bit tricky to decide what to do if this passes in the current form
> because we would probably need to apply security fix and disable it. If you
> really want to have it enabled, we would need to explicitly state in the
> policy and docs that pool boundary is no longer considered as a security
> boundary which would be quite problematic for some shared hosting that rely
> on it. Maybe the solution would be to allow it only if there is one pool
> enabled.
>
> Kind regards,
>
> Jakub
>

Hi Jakub, Larry, internals,

Thank you both for the feedback.

Updated RFC and Implementation:

- https://wiki.php.net/rfc/opcache_static_cache
- https://github.com/php/php-src/pull/22052

I want to pause before moving to a vote and make sure we agree on the
security/default model first. Jakub raised this after my Intent to Vote
mail,
and I think it is a substantive security concern rather than something to
push
through at the last minute.

To summarize the two concerns as I understand them:

Larry's concern is that if Static Cache is default-off, it becomes much less
useful as a portable primitive for libraries. A router, DI container,
metadata
cache, or framework component cannot reasonably rely on a feature that is
usually disabled by default.
Jakub's concern is that, in FPM, pool boundaries are treated as security
boundaries. If Static Cache creates one shared data channel across all pools
in one FPM master, then it can turn a default-on feature into a cross-pool
data leak. That is not acceptable as a PHP/FPM security model.

I agree with both points.

My previous model was "one OPcache Static Cache shared-memory segment is one
trust domain". That was clear, but it did not solve the FPM case if the same
FPM master hosts multiple mutually untrusted pools. It effectively required
operators to disable the feature or run separate FPM masters, which is not a
satisfying answer if the feature is default-enabled.

I have therefore changed the implementation direction for FPM.

The FPM master no longer creates one global Static Cache backend shared by
all
pools. Instead, it creates a separate Static Cache partition for each
configured
worker pool before children are forked. Each partition owns its own volatile
and pinned backends, including the backend header, entry table, allocator
state,
mutation epoch, lookup/status surface, and lock file. During child
initialization, the child activates the partition belonging to its FPM
worker
pool.

The active partition is selected from the FPM worker pool that owns the
child.
It is not selected from request data, the Host header, SCRIPT_FILENAME,
environment variables, cache keys, or userland input.

As a result, in FPM:

explicit Static Cache APIs operate on the active pool partition;
pinned static state is stored and restored from the active pool partition;
status reporting is pool-local;
explicit Static Cache clearing is pool-local;
request-shutdown publication is pool-local;
script invalidation and the Static Cache part of reset handling operate on
the active pool partition rather than on a global cross-pool backend.

I also added an FPM PHPT that starts two pools and verifies that:

a value stored with the volatile Static Cache API in pool alpha is not
visible from pool beta;
a #[OPcache\PinnedStatic] static value initialized in pool alpha is still
at the default value in pool beta;
after initializing both pools, returning to alpha still observes alpha's
own values.

I plan to extend the test coverage to status reporting and clear/reset-style
operations, so the intended pool-local behavior is covered explicitly.

I have also added a new INI directive:

opcache.static_cache.allow_unsafe_runtime=0

The default is 0. When this directive is 0, Static Cache is disabled in
persistent server runtimes where PHP cannot provide a safe storage partition
comparable to the FPM per-pool partitioning described above.

In other words, SAPIs where PHP cannot identify or enforce a comparable
tenant
boundary do not get Static Cache merely because the backend memory size
defaults are non-zero. An administrator who intentionally treats such a
runtime
as a single trust domain can opt in explicitly by setting
opcache.static_cache.allow_unsafe_runtime=1.

With the default setting, Static Cache is available only for the SAPIs
where I
think PHP can either provide the required boundary or where the SAPI is not
a
traditional shared-hosting server runtime:

fpm: available by default, because Static Cache is partitioned per FPM
worker pool;
cli: available by default, because it is not a persistent shared server
runtime;
phpdbg: available by default, for the same reason as CLI;
embed: available by default, because the embedding application owns the
runtime and trust boundary. This is important for modern embedded
application-server runtimes such as FrankenPHP, where PHP is embedded into
a host runtime rather than run as a traditional shared-hosting SAPI. In that
model, making Static Cache unavailable by default would significantly reduce
its usefulness for the same library-adoption reasons Larry described.

For embed, the important caveat is that PHP itself cannot know the
multi-tenant policy of the host application. If an embedding application
intentionally hosts mutually untrusted tenants inside one persistent
embedded
PHP runtime, then that embedded runtime is one Static Cache trust domain.
Such
a host should disable Static Cache for those tenants, or explicitly accept
the
shared-runtime trust model.

For other persistent SAPIs, such as apache2handler, litespeed, and generic
cgi-fcgi under an external process manager, Static Cache is unavailable by
default unless opcache.static_cache.allow_unsafe_runtime=1 is set.

When Static Cache is disabled by this policy, the backend behaves as
unavailable: StaticCacheInfo reports that the backend is not available,
explicit APIs fail or throw according to the existing error mode, and
attribute-backed statics retain normal request-local semantics.

This changes the proposal in a meaningful way, so I will update the RFC to
version 1.4.0 and treat it as a major RFC change. The previous Intent to
Vote
is canceled; I will not open voting until the new cooldown has elapsed and a
new Intent to Vote has been sent.

The remaining question is whether this default/security model is acceptable.

I see three possible models:

A. Default-on for all SAPIs, with FPM partitioned per pool.

This is closest to Larry's library-adoption concern. FPM is the concrete
case
where PHP has a visible multi-pool boundary inside one persistent master,
and
it is explicitly isolated. Other SAPIs follow the existing OPcache runtime
model: one persistent PHP runtime is one trust domain.

B. Default-on only where PHP can provide a safe boundary, or where the SAPI
is
not a persistent shared-hosting server runtime; explicit administrator
opt-in otherwise.

This is the model now implemented by the new INI directive. FPM is
default-on because PHP can enforce per-pool storage partitioning. CLI and
phpdbg are not shared server runtimes. Embed is default-on because the
embedding application owns the runtime boundary, and because this is
important for embedded application-server runtimes such as FrankenPHP. For
apache2handler, litespeed/lsphp, and generic cgi-fcgi under an external
process manager, Static Cache is unavailable by default unless the
administrator explicitly opts in with
opcache.static_cache.allow_unsafe_runtime=1.

C. Default-off everywhere, with explicit opt-in.

This is the most conservative security model, but it also largely loses the
portability benefit Larry is asking for. Libraries would still need to treat
Static Cache as an optional acceleration path rather than a primitive they
can normally rely on.

I do not want to minimize legacy shared-hosting deployments. They still
exist,
and PHP should not break their security assumptions. At the same time, I
think
we should be careful not to optimize the default entirely around the most
conservative legacy multi-tenant model if doing so makes the feature much
less
useful for the deployments and libraries that are likely to benefit from it.

The hosting landscape has changed significantly. Many modern PHP
applications
are deployed on VPS, cloud instances, containers, managed application
platforms, or per-application FPM pools, and increasingly on application
server
runtimes as well. In those cases, the relevant runtime is already a single
application or a single trust domain. Traditional shared hosting and
control-panel hosting are still important, but they are not the only default
deployment model we should design around.

That is why I would like to avoid C unless it is truly necessary from a
security-policy perspective. It protects the most conservative
shared-runtime
case, but it also imposes a large cost on library and framework adoption.
If a
library cannot assume that Static Cache is normally available, then the
feature
becomes much closer to an optional site-specific optimization than to a
portable runtime primitive.

My current preference is B.

It keeps Static Cache default-enabled where PHP can provide a safe storage
boundary, especially FPM with per-pool partitions, and it requires an
explicit
administrator decision where PHP cannot enforce such a boundary. It also
keeps
Static Cache available by default for CLI/phpdbg and embed-based application
server runtimes such as FrankenPHP, where disabling it by default would
weaken
the intended library-facing use case. This seems to address the concrete FPM
shared-hosting issue without making the feature globally default-off.

I am also open to A if Jakub and others are comfortable saying that, outside
FPM, the persistent PHP runtime/process group is the trust domain, and that
shared-hosting deployments that do not treat that runtime as trusted must
disable the feature. Conversely, if the security position is that arbitrary
Static Cache data must never be default-enabled unless PHP can enforce the
tenant boundary, then I think B is the natural fallback rather than C.

For documentation, I propose to make the trust model explicit:

In FPM, Static Cache storage is separated per FPM worker pool.
A Static Cache partition is one trust domain.
Outside FPM, Static Cache is scoped to the persistent PHP runtime/process
group provided by the SAPI. It is not automatically scoped to virtual hosts,
document roots, or OS users unless those are already separated into distinct
PHP runtimes by the SAPI or process manager.
In persistent server SAPIs where PHP cannot identify or enforce a comparable
partition boundary, Static Cache is disabled by default unless
opcache.static_cache.allow_unsafe_runtime=1 is set.
The embed SAPI is enabled by default because the embedding application owns
the runtime boundary, and because this is important for embedded
application-server runtimes such as FrankenPHP. If an embed host uses one
persistent embedded PHP runtime for mutually untrusted tenants, that host
must treat it as one Static Cache trust domain or disable Static Cache.
opcache.restrict_api and disable_functions may restrict management APIs,
but they are not the primary isolation boundary for attribute-backed Static
Cache state.
Shared-hosting operators must not enable Static Cache in a runtime shared by
mutually untrusted tenants unless that runtime is intentionally treated as a
shared trust domain.

Jakub: With the FPM per-pool partitioning and the new default-off policy for
unsafe persistent runtimes described above, would FPM default-on still be a
security concern from your point of view? Also, does the SAPI allow-list
(fpm, cli, phpdbg, and embed) seem like the right boundary for
allow_unsafe_runtime=0, given the embed/FrankenPHP use case?

Larry: Does this compromise preserve enough of the default-on behavior for
the
library-adoption use case? In particular, FPM remains default-on, CLI/phpdbg
remain available, and embed remains available for application-server
runtimes
such as FrankenPHP, while SAPIs without a PHP-visible tenant boundary
require
an explicit administrator opt-in.

If either of you sees a better model than A/B/C, I would rather adjust the
RFC
now than try to resolve this during or after voting.

Best regards,
Go Kudo

Reply via email to