From 2ed260fb1bd6a7f337ef73be4a8fe4fbb1f7302b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 16:26:34 +0000 Subject: [PATCH 1/2] php: set BIN, which mkone's install rule needs `mk install' linked php and then answered /rc/lib/rcmain:24 *eval*:1: null list in concatenation mk: cp 6.out $BIN/php : exit status=rc 57534: error `mkone's rule is `cp $prereq $BIN/$TARG', `$BIN/php' is a concatenation in rc, and concatenating an empty list with a string is an error rather than an empty result -- so the message names rcmain and not the mkfile, which is the "mk names the wrong line" shape again. `sys/src/ape/config' supplies APEBIN and not BIN, so every other APE command mkfile assigns BIN itself. This one includes ` Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs --- sys/src/ape/cmd/php/mkfile | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sys/src/ape/cmd/php/mkfile b/sys/src/ape/cmd/php/mkfile index 66dff2e51..eda6666fa 100644 --- a/sys/src/ape/cmd/php/mkfile +++ b/sys/src/ape/cmd/php/mkfile @@ -2,6 +2,30 @@ APEXPROOT=../../../../.. TARG=php +# ***`mkone's INSTALL RULE IS `cp $prereq $BIN/$TARG' AND NOTHING HERE +# SET `BIN'.*** rc answered +# +# /rc/lib/rcmain:24 *eval*:1: null list in concatenation +# mk: cp 6.out $BIN/php : exit status=rc 57534: error +# +# -- `$BIN/php' is a CONCATENATION in rc, and concatenating an empty +# list with a string is an error rather than an empty result. *The +# message names the shell and the line inside `rcmain', not the +# mkfile*, which is the "mk names the wrong line" shape a third time. +# +# `sys/src/ape/config' supplies `APEBIN' and not `BIN', so **every +# other APE command mkfile assigns this itself** -- `cmd/bzip2', +# `cmd/m4' and the rest all carry the same line. This file includes +# ` Date: Thu, 5 Nov 2026 16:39:04 +0000 Subject: [PATCH 2/2] php: opcache destroyed globals its constructor never built `6.out --version' printed PHP 8.5.12 (cli) (built: Nov 5 2026 16:19:35) (NTS) Zend Engine v4.5.12 and then died with `fault read addr=0x4 pc=0x7ef0f7'. `accel_globals->key' is allocated by `accel_globals_ctor', whose only caller is `accel_startup' -- the zend_extension's startup -- and `zend_extensions.c:194' has the whole body of `zend_extension_startup' inside `#if ZEND_EXTENSIONS_SUPPORT', which `zend_portability.h:191' sets to 1 for dlopen and for Windows and 0 otherwise. Plan 9 has no dlopen, so no zend_extension's startup runs here at all. opcache's MSHUTDOWN is an ordinary module shutdown, gated by nothing, and calls `accel_shutdown', which takes its `!ZCG(enabled) || !accel_startup_ok' arm and destroys the globals regardless -- `zend_string_free(NULL)', and `GC_FLAGS' reads `gc.u.type_info' at offset 4, which is the address the kernel named. Guarded, which is what the next statement in the same function already does for `preloaded_internal_run_time_cache' and what `zend_accel_blacklist_shutdown' opens with four lines earlier in the same call. Upstream's, and worth reporting with the zend_atomic.h typo. The rest of that path was read for the same shape: `zend_jit_shutdown' is behind HAVE_JIT, the blacklist has its own guard, and unregistering ini entries that were never registered walks an empty set. The diagnosis needed one command and no rebuild. `php_cli.c:1318's `-h' arm returns while `module_started' and `sapi_started' are still 0 -- they are set at :1337 and :1359 -- so it runs exit() and neither shutdown. `./6.out -h' exits clean, which cleared libap's exit path, the stdio teardown and php_ini_builder_deinit at once and left two functions where nine calls had been candidates. `acid 6.out' and `src(0x7ef0f7)' then named zend_string.h:401 outright. Prediction: `php -v' exits 0 now. What would refute it is a fault at a different line, which would be the next member of the same family, since every zend_extension on this platform is constructed by nobody. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WGAwvvTwDg2yknFkmZ3qzs --- CLAUDE.md | 27 ++++++- docs/notes/php.md | 72 +++++++++++++++++++ .../php/ext/opcache/ZendAccelerator.c | 29 +++++++- 3 files changed, 125 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e00ccfb0c..11d7d1c17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1202,8 +1202,31 @@ silent: - **itcl** — done. 10 failures, all upstream or Tcl 9's, two settled at source, none ours. -**php — ***ALL 514 TRANSLATION UNITS COMPILE***, measured on the HOST, -and the notes are in `docs/notes/php.md`.** 8.5.12, vendored at +**php — ***IT LINKS AND IT RUNS***: `PHP 8.5.12 (cli) (NTS)`, Zend +Engine v4.5.12, printed by the binary on the VM. Notes in +`docs/notes/php.md`.** **`php -v` then FAULTED at exit and the cause +is a platform consequence**: `ZEND_EXTENSIONS_SUPPORT` is 0 without +dlopen (`zend_portability.h:200`), so `zend_extension_startup`'s body +is compiled out and **no zend_extension's startup ever runs here** -- +while opcache's MSHUTDOWN is an ordinary MODULE shutdown, gated by +nothing, and destroys globals its ctor never built: +`zend_string_free(NULL)`, `GC_FLAGS` reading offset 4, which is what +`fault read addr=0x4` said. Guarded, as the next line of the same +function and `zend_accel_blacklist_shutdown` already are; upstream's, +and the second upstream php bug this campaign. +***THE DIAGNOSIS COST ONE COMMAND AND NO REBUILD***: `php -h` returns +before `module_started`/`sapi_started` are set (`php_cli.c:1318` +against `:1337`/`:1359`), so it runs `exit()` and neither shutdown -- +it exits clean, which cleared libap's teardown and left two functions +standing where nine calls had been candidates. Then `acid 6.out` and +`src(pc)` named `zend_string.h:401` outright. *A CLI with an early +exit path is a bisect someone else already wrote.* +**Also `mk install` could not copy the binary**: `mkone`'s rule is +`cp $prereq $BIN/$TARG` and nothing set `BIN`, which in rc is a null +list in a concatenation rather than an empty string -- every other +APE command mkfile assigns it, and nothing could notice until a link +succeeded. +**ALL 515 TRANSLATION UNITS COMPILE**, measured on the HOST. 8.5.12, vendored at `sys/src/external/php`; `--compile` answers `compiled 515, errors 0, missing 0` and **`--undefined` says the program wants 283 symbols from outside php, every one of which libap has** (273 the sweep diff --git a/docs/notes/php.md b/docs/notes/php.md index 0b3a364dc..84ab9d04a 100644 --- a/docs/notes/php.md +++ b/docs/notes/php.md @@ -746,6 +746,78 @@ general form is worth keeping**: a header-only helper that calls into a file the port does not compile is a link error here and silence on gcc. +### It links, it runs, and the first fault was opcache's + +`6.out --version` prints + + PHP 8.5.12 (cli) (built: Nov 5 2026 16:19:35) (NTS) + Copyright (c) The PHP Group + Zend Engine v4.5.12, Copyright (c) Zend Technologies + +and then dies: `suicide: sys: trap: fault read addr=0x4 pc=0x7ef0f7`. + +**Two things were wrong and they are unrelated.** `mk install` could +not copy the binary at all -- `null list in concatenation`, because +nothing set `BIN` and `mkone`'s rule is `cp $prereq $BIN/$TARG`; the +mkfile now assigns it like every other APE command's. The fault is +below. + +***THE DIAGNOSIS COST ONE COMMAND AND NO REBUILD, BECAUSE THE CLI HAS +AN EXIT PATH THAT SKIPS THE SHUTDOWNS.*** `php_cli.c:1318`'s `-h` +arm does `php_cli_usage(); goto out;` while `module_started` and +`sapi_started` are still 0 -- they are set at `:1337` and `:1359`, +after `exit_loop` -- so `-h` runs `exit()` and neither +`php_module_shutdown()` nor `sapi_shutdown()`. **`./6.out -h` exits +clean**, which cleared libap's `exit`, the stdio teardown and +`php_ini_builder_deinit` in one line and left two functions standing. +*Two outcomes, one command, no acid and no rebuild* -- and the +alternative was nine candidate calls to read. + +Then `acid 6.out` and `src(0x7ef0f7)`: + + Zend/zend_string.h:401 + >401 if (!ZSTR_IS_INTERNED(s)) { + +`zend_string_free(NULL)`. **The address agrees with the source**: +`zend_refcounted_h` is `{uint32_t refcount; union {uint32_t +type_info;} u;}`, so `GC_FLAGS` reads **offset 4**, and `addr=0x4` is +a null `zend_string *`. *`zend_string_free` is `zend_always_inline` +and kencc does not inline*, so it is a real static function in every +TU that uses it -- which is why there was a pc to resolve at all. + +**The caller is `accel_globals_dtor`** (`ZendAccelerator.c:2974`), +found by listing every `zend_string_free` call site in php -- 91 of +them -- and keeping the ones whose enclosing function name looks like +teardown. Exactly one did. + +***AND THE MECHANISM IS A PLATFORM CONSEQUENCE, NOT AN ACCIDENT.*** +`accel_globals->key` is allocated by `accel_globals_ctor`, whose only +caller is `accel_startup` -- the **zend_extension's** startup. And +`zend_extensions.c:194`'s `zend_extension_startup` has its entire +body inside `#if ZEND_EXTENSIONS_SUPPORT`, which +`zend_portability.h:191-200` sets to 1 for dlopen and for Windows and +**0 otherwise**. Plan 9 has no dlopen, so no zend_extension's startup +ever runs here. opcache's **MSHUTDOWN is an ordinary MODULE +shutdown**, gated by nothing, and calls `accel_shutdown`, which takes +its `!ZCG(enabled) || !accel_startup_ok` arm and destroys the globals +anyway. *A platform that constructs nothing destroys regardless.* + +Guarded, which is what the **next statement in the same function** +already does for `preloaded_internal_run_time_cache` and what +`zend_accel_blacklist_shutdown` opens with four lines earlier in the +same call. **Upstream's, and the second upstream php bug this +campaign** after the `zend_atomic.h` typo; both are worth reporting. + +*The rest of that path was checked for the same shape rather than +left*: `zend_jit_shutdown` is `#ifdef HAVE_JIT` and we answer no, the +blacklist has its own guard, and `UNREGISTER_INI_ENTRIES` over +entries that were never registered walks an empty set. + +**Prediction, written down before the run**: `php -v` now exits 0. +**What would refute it**: a fault at a different address or line -- +which would be the next member of the same family, since every +zend_extension on this platform is constructed by nobody. + ### The eight, and what each turned out to be Three were kencc's, three were APE headers', one was php's and one was diff --git a/sys/src/external/php/ext/opcache/ZendAccelerator.c b/sys/src/external/php/ext/opcache/ZendAccelerator.c index 5f38f3532..71a9aa3ca 100644 --- a/sys/src/external/php/ext/opcache/ZendAccelerator.c +++ b/sys/src/external/php/ext/opcache/ZendAccelerator.c @@ -2971,7 +2971,34 @@ static void accel_globals_ctor(zend_accel_globals *accel_globals) static void accel_globals_dtor(zend_accel_globals *accel_globals) { - zend_string_free(accel_globals->key); + /* + * APExp: `key' is allocated by `accel_globals_ctor', which is + * called ONLY from `accel_startup' -- the zend_extension's + * startup -- and `zend_extension_startup' in + * `Zend/zend_extensions.c:194' has its whole body inside + * `#if ZEND_EXTENSIONS_SUPPORT'. That macro is 1 for dlopen and + * for Windows and **0 otherwise** (`zend_portability.h:200'), and + * Plan 9 has no dlopen -- so `accel_startup' never runs. + * + * opcache's MSHUTDOWN is an ordinary MODULE shutdown and is not + * gated by anything, so it still calls `accel_shutdown', which + * takes its `!ZCG(enabled) || !accel_startup_ok' arm and calls + * this function. **A platform that constructs nothing destroys + * anyway**, and `zend_string_free' requires a non-NULL argument: + * + * zend_string.h:401 if (!ZSTR_IS_INTERNED(s)) + * 6.out: suicide: sys: trap: fault read addr=0x4 + * + * -- `GC_FLAGS' reading `gc.u.type_info', which is at offset 4. + * Every `php -v' died this way, after printing the version. + * + * The guard is the one the next statement already has, and the + * one `zend_accel_blacklist_shutdown' opens with four lines + * earlier in the same call. Upstream. + */ + if (accel_globals->key) { + zend_string_free(accel_globals->key); + } if (accel_globals->preloaded_internal_run_time_cache) { pefree(accel_globals->preloaded_internal_run_time_cache, 1); }