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/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 +# `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); }