Build and Patching Controls
The controls on how the interpreter is built and how patches reach a site.
The Build
The shipping interpreter is built by phasm, which drives seanmorris/php-wasm's Makefile with an
rc file per variant. The Makefile sets TOTAL_STACK=32MB. The extension list is short. soap,
phar, intl, gd, openssl, exif and sqlite are not compiled in (WITH_* settings in
long64.rc; the binary contains no SoapServer string, Measured with grep -a -c). Every extension
left out is an extension its bugs cannot reach.
phasm now pins the PHP patch release (src/php-versions.lock, 8.5.12) and the php-wasm commit.
php-wasm's Makefile hardcodes 8.5.2, and an rc cannot move it, so the pinned release arrives as make
variables, build-variant.sh refuses a php-src tree that disagrees with the pin, and
inspect-build.sh fails a binary that reports a PHP below the pin. The worker's interpreter.yml
now runs gh attestation verify on the fetched files before it prices or pins them, against the
phasm build workflow's identity. That verification was written and tested for its position in the
workflow. The workflow step itself has not run against a real attestation; the one real artifact checked with the command is under Patching.
Stack protection is not in the shipping build. A rebuilt baseline and two candidate flags were measured on paisley-park (results/84-s12-builds.md). -fstack-protector-strong added 0.545% to the wasm and left a 4-byte guard that is a constant zero: every access to it is a load, no data segment writes it, so it catches a non-zero overwrite of that slot and nothing else. -sSTACK_OVERFLOW_CHECK=2 cost 6.3% of the blended median (range 4.9% to 9.7% over eight processes) and added 3.4% to the wasm. -sSTACK_OVERFLOW_CHECK=1 changes only the glue and cost nothing the harness could resolve. Neither check was adopted. Unmeasured: whether a real PHP stack overflow reaches either trap. The open objective for v1.1 is a canary that is not constant. Nothing in the build writes the guard slot before the first PHP call, so a randomised canary needs a writer for that slot and its own cost measurement; that is an inference from the wasm dump, not a tested design.
Patching
Cloudflare patches V8 and the platform. Drupal core and the PHP interpreter are the project's to
patch, and that path has a manual step. A new interpreter build travels from phasm to a pull
request in this repository and ends there. The workflow has no deploy step, and a person merges,
releases and deploys (docs/operating/php-update-delivery/index.md). The pin records .interp/php8.5.wasm with its
sha256.
The interpreter in the tree reports PHP 8.5.12, the first 8.5 release that carries the PHP2Shell
fixes (Measured: interpreter-lifecycle.spec.ts asserts PHP_VERSION >= 8.5.12). The advisory was
published 2026-10-21 13:34 UTC. A local rig build that passes the park, heap and static-sweep specs
finished 31 hours 39 minutes after it, and the attested CI artifact (phasm run 39876497434, artifact
12463245201, gh attestation verify exit 0) was created 32 hours 35 minutes after it. (Corrected 2026-11-02: this paragraph first cited run 39861586180. The pin record in DRUPFLARE_ROADMAP.md S11 and commit 0ee10e9db name 39876497434. The 32 hours 35 minutes figure was not re-derived from the run record; it ends at 22:09 UTC, which is the pin commit's 22:09 EDT clock time read as UTC, so treat it as unverified.) The advisory
time is read from the PHP announcement; the build times come from the rig log and the workflow run
record. That is one interval on one build, not a rate. Deployed sites move only when someone
publishes the artifact, merges the pin and deploys. The root composer.lock pins Drupal core 11.4.11, which includes the 11.4.10 fix for the 11.4 line of SA-CORE-2026-014.
Rebuilding found a defect in phasm's own park extension. When a parked chain completes after a
resume, the root frame's return_value still pointed at a C local of zend_eval_stringl that the
longjmp had destroyed, so the final ZEND_RETURN wrote a 16-byte zval onto whatever the resuming
stack held. On 8.5.2 the bytes were dead and nothing showed. On 8.5.12 the write corrupted the
dispatch state and the next call trapped. The extension now points the root at a slot it owns
(park_pin_root() in cfwpark.c). The fix is covered natively (22 pass with the fix; 21 pass and
1 fail without it) and by park-interpreter.spec.ts. It matters here because it is a memory-safety bug in
code the project wrote, reachable only on a site that arms the park (REDIS_URL set, or an OIDC or
other outbound fetch that parks).