Skip to content

opcache: set ZEND_ACC_PRELOADED for compile-only preloaded enums - #23103

Open
Nibbler999 wants to merge 1 commit into
php:PHP-8.4from
Nibbler999:fix-preload-enum-observer-compile-only
Open

opcache: set ZEND_ACC_PRELOADED for compile-only preloaded enums#23103
Nibbler999 wants to merge 1 commit into
php:PHP-8.4from
Nibbler999:fix-preload-enum-observer-compile-only

Conversation

@Nibbler999

Copy link
Copy Markdown

Disclosure: this description, the patch and its test were drafted with an LLM.

Summary

GH-17835 fixed GH-17715 for enums that preload declares. It does not cover enums that preload only compiles, and those still crash on 8.4 and 8.5.

// preload.php
opcache_compile_file(__DIR__ . '/e.php');

// e.php
enum E { case Foo; }

// request
E::cases();   // SIGSEGV with any Observer API extension loaded

Cause

zend_enum_register_func() sets ZEND_ACC_PRELOADED inside the if (EG(active)) branch. zend_persist_class_method() keys on that flag to choose ZEND_MAP_PTR_NEW_STATIC() over ZEND_MAP_PTR_NEW(), i.e. a run_time_cache slot that survives into every request — which is what a class living in SHM permanently needs.

An enum that preload reaches via opcache_compile_file() is not linked while the preload script executes. It is linked from preload_link(), which runs after the preload request has ended, so EG(active) is already false, the else branch is taken and the flag is never set.

gdb on the crash, on an unpatched 8.5.9:

Program received signal SIGSEGV, Segmentation fault.
#0  zend_observer_fcall_begin_specialized (...) at Zend/zend_observer.h:92
#1  ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER ()   at Zend/zend_vm_execute.h:2281
#2  execute_ex (...)                         at Zend/zend_vm_execute.h:116541

function : cases        type: 1 (internal)     T: 1
fn_flags : 0x2002011  = PUBLIC|STATIC|HAS_RETURN_TYPE|ARENA_ALLOCATED
                        ZEND_ACC_PRELOADED (1<<10) absent
run_time_cache : 0

ZEND_OBSERVER_DATA() computes RUN_TIME_CACHE(...) + handle = NULL + 0 and dereferences it — the same null deref, and the same Zend/zend_observer.h:92, that GH-17715's UBSAN output reports.

Fix

Move the tag out of the EG(active) branch so both linking paths set it. The compile-only case then follows the identical, already-proven route as the declared case; no new mechanism is introduced.

Test

ext/opcache/tests/preload_enum_observed_compile_only.phpt mirrors the existing preload_enum_observed.phpt and reuses its preload_enum.inc fixture, changing only how preload reaches it.

phpGH-17835 made zend_enum_register_func() tag an enum's generated
cases()/from()/tryFrom() with ZEND_ACC_PRELOADED, so that
zend_persist_class_method() gives them a static run_time_cache map_ptr,
which survives into every request. The tag is set inside the
if (EG(active)) branch.

An enum that preload reaches through opcache_compile_file() rather than by
executing its file is linked from preload_link(), which runs after the
preload request has ended, so EG(active) is already false and the tag is
never applied. Those functions get a non-static map_ptr that nothing
initialises again, so RUN_TIME_CACHE() is NULL in a fresh request and the
first observed call dereferences NULL + the observer handle:

    #0 zend_observer_fcall_begin_specialized  Zend/zend_observer.h:92
    php#1 ZEND_DO_FCALL_SPEC_OBSERVER_HANDLER    Zend/zend_vm_execute.h:2281

Move the tag out of the EG(active) branch so both linking paths set it.
The compile-only case then takes exactly the same route as the already
fixed declared case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant