Skip to content

Immutability is silently not enforced when opcache is enabled #15

Description

@lisachenko

Summary

With opcache enabled, ImmutableHandler::install() reports success but no write is ever blocked. Writing to a property of an immutable object outside a constructor or static method just succeeds — no LogicException, no fatal error, no warning:

$object = new TestObject(['publicProperty' => 100]);
$object->publicProperty = 42;   // expected: "Immutable object could be modified only in
                                // constructor or static methods". Actual: 42, silently.

Reproduced on both supported minors — PHP 8.4.19 + z-engine 8.4.2 and PHP 8.5.9 + z-engine 8.5.0 — with opcache.enable_cli=1. Turning opcache off restores correct behaviour on both. This is not a regression from the recent stable-z-engine upgrade (#14): it reproduces identically against the older 8.4.x-dev/8.5.x-dev lines.

Why the test suite does not catch it

tests/Functional/*.phpt carry no --INI-- section, and CI runs shivammathur/setup-php, whose CLI builds have opcache.enable_cli=Off. So every test runs in exactly the configuration where the handlers work. On a distro build with opcache.enable_cli=On (which is how this was found), 5 of the 8 functional tests fail — every one that asserts a write is rejected — and they fail by producing no output at all, because the exception that should have been raised never happens.

Root cause is upstream

install() hooks ImmutableInterface with setInterfaceGetsImplementedHandler(), and that callback installs create_object, write_property, get_property_ptr_ptr and unset_property on each implementor. Under opcache the callback does run and the create_object handler does take effect — only the property handlers are lost. This is reproducible with plain z-engine calls and no code from this package, so it is tracked upstream as lisachenko/z-engine#238.

Installing the same handlers directly on a named class works fine under opcache; it is specific to installing them from inside the interface hook.

What this package should do about it regardless

Even once the engine side is fixed, a silent no-op is the wrong failure mode for a library whose entire purpose is enforcing a constraint. Worth having:

  1. A loud failure or warning at install() time when the environment is one where the handlers will not take effect, so an application cannot come up believing its objects are protected when they are not.
  2. A regression test that runs with opcache enabled, so the configuration that breaks this is exercised rather than avoided — an --INI-- block on the functional tests, or a dedicated CI leg with opcache.enable_cli=1.
  3. A note in the README requirements stating the constraint until the engine side is resolved.

Environment

  • PHP 8.4.19 (NTS) and PHP 8.5.9 (NTS), linux-x64
  • lisachenko/z-engine 8.4.2 / 8.5.0 (also reproduced on 8.4.x-dev / 8.5.x-dev)
  • ffi.enable=1, opcache.jit=off, opcache.enable_cli=1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions