[GIT-PULLS] [php-src] PR #22751: Fix ARMv6 fiber asm: gate Cortex-M0 branch on __ARM_ARCH_6M__
[email protected] (andypost)
| Newsgroups | php.git-pulls |
|---|---|
| Message-ID | <[email protected]> |
Pull Request: https://github.com/php/php-src/pull/22751
Author: andypost
## Description
### Problem
Since the Boost.Context 1.91.0 sync (9cefeea7b10, GH-22584), every PHP binary
built for ARMv6 as PIE on musl segfaults **before `main()`** — every invocation,
every SAPI, no output. First shipped in 8.6.0alpha2; alpha1 is fine. Found on
Alpine Linux armhf (`armv6-alpine-linux-musleabihf`, `-march=armv6kz`).
A second, silent regression rides along: `jump_fcontext` — the hot path on every
fiber switch — grew from 13 to 43 instructions on **all** ARMv6 builds, glibc
included.
### Root cause
The 1.91.0 sync pulled in [boostorg/context#325][pr325], titled *"fix cortex-m0
support to aapcs_elf_gas target"*. Its author states the motivation plainly:
> some instruction used here is not available on Cortex-M0 [...] I'm using
> boost_fcontext on some M0 based MCU for cooperative multi tasking.
The branch works around **Thumb-1 encoding limits on ARMv6-M**, where
`push {r8-r11}` and `bic` with an immediate do not exist. That is legitimate.
The bug is the gate:
```asm
#if __ARM_ARCH >= 7
adr a2, finish
#else
ldr a2, =finish @ Cortex-M0 fallback
#endif
```
`__ARM_ARCH >= 7` is false for Cortex-M0 — but it is *also* false for **classic
ARMv6 in ARM state**: Alpine armhf, Raspberry Pi 1/Zero. Those targets can
encode every instruction in the `>= 7` branch (verified down to ARMv5TE) and
were never the intended audience. They now get Thumb-1-shaped code they cannot
benefit from. Two consequences follow.
**1. DT_TEXTREL → startup SIGSEGV on musl.**
`ldr a2, =finish` materialises the absolute address of `finish` through a
literal pool in `.text`, emitting `R_ARM_ABS32` against `.text`. In a PIE link
that becomes an `R_ARM_RELATIVE` dynamic relocation inside the read-only text
segment, tagging the binary `DT_TEXTREL`:
```
$ readelf -d sapi/cli/php | grep TEXTREL
0x00000016 (TEXTREL) 0x0
$ readelf -r sapi/cli/php | grep -w R_ARM_RELATIVE | head -1
002e1d00 00000017 R_ARM_RELATIVE # inside the R-X text LOAD segment
```
musl supports `DT_TEXTREL` **only for DSOs it maps itself** — `map_library()`
mprotects the mapping RWX before relocating (`ldso/dynlink.c`, added 2011 in
[commit 9f17413c][musl-textrel] "textrel support, cheap and ugly"). The main
executable is mapped by the kernel and handled by `kernel_mapped_dso()`, which
never inspects `DT_TEXTREL`. So ldso writes into a read-only page and dies:
```
SIGSEGV (SEGV_ACCERR) at pc 0x40869c04 in do_relocs, /lib/ld-musl-armhf.so.1
=> str r3, [r7, r8] ; r7=load base, r8=0x2e1d00 (literal pool word in .text)
#1 reloc_all #2 __dls3 #3 __dls2 #4 _dlstart
```
No fiber ever runs — the loader crashes relocating the binary. glibc masks this
by mprotecting the segment writable and restoring it (`elf/dl-reloc.c`), which
is why Raspbian/Debian armv6 never noticed.
Upstream musl considers such binaries defective. Rich Felker, [2020][felker]:
> there's no compelling reason to support textrels in the main program
Szabolcs Nagy, [same thread][nagy]:
> it is considered to be a bug to create binaries with textrels
Worth noting the non-crashing case is arguably worse: where musl *does* handle a
textrel (a shared `libphp.so` via `--enable-embed=shared`, apache2handler), it
leaves the mapping **permanently RWX and never restores it**, unlike glibc.
So the same defect silently disables W^X there instead of failing loudly.
**2. `jump_fcontext` 3.3× instruction blowup.**
Same mis-gate, no literal pool, so no crash — just one burst
`push {r0,r4-r11,lr}` / `pop {r3,r4-r11,lr}` replaced by eleven `str`s and ten
`ldr`s, on every fiber switch, on every ARMv6 build.
### Fix
Gate on `__ARM_ARCH_6M__` — precisely the target [#325][pr325] names:
```diff
- #if __ARM_ARCH >= 7
+ #if !defined(__ARM_ARCH_6M__)
```
Five occurrences in `make_arm_aapcs_elf_gas.S`, two in `jump_arm_aapcs_elf_gas.S`.
Cortex-M0 keeps its fallback branch verbatim — including `ldr a2, =finish` — so
no `.align 2` workaround is needed. Classic ARMv6 takes `adr a2, finish`, as
ARMv7 does and as every pre-1.91 Boost.Context release did.
### Verification
**This change introduces no new codegen on any target.** Comparing `.text` bytes
across three trees — alpha1 (pre-1.91, pre-regression), current master, and this
fix — cross-assembled with clang:
| target | alpha1 vs fix | current vs fix | |
|---|---|---|---|
| armv7-a (Alpine armv7, thumb) | **IDENTICAL** | **IDENTICAL** | pure no-op |
| armv6kz (Alpine armhf) | **IDENTICAL** | differs | restored |
| armv5te (Debian armel) | **IDENTICAL** | differs | restored |
| armv6-m (Cortex-M0) | differs | **IDENTICAL** | #325 preserved |
In one sentence: **byte-identical to 8.6.0alpha1 on every ARM target except
Cortex-M0, where it is byte-identical to current master.** Every affected target
gets back exactly the code it shipped before the regression; Cortex-M0 keeps
exactly what #325 added. Nothing new is invented anywhere.
Instruction counts at armv6kz (`make_fcontext` / `jump_fcontext`):
| | make | jump |
|---|---|---|
| alpha1 (pre-regression) | 11 | 13 |
| current master | 15 | **43** |
| this fix | 11 | **13** |
**Other architectures are untouched.** Scanning every `Zend/asm` ELF file with
clang for x86_64, i386, aarch64, riscv64, ppc64le, s390x and loongarch64 finds
no absolute relocation in `.text` anywhere. `make_arm_aapcs_elf_gas.S` on ARMv6
is the **only** DT_TEXTREL source in the whole tree; aarch64 uses
`{make,jump}_arm64_aapcs_elf_gas.S`, which this patch does not touch.
On Alpine edge armhf (musl, PIE):
- Before: `./sapi/cli/php -v` → `Segmentation fault` (exit 139, no output);
`readelf -d` shows `TEXTREL` and one `R_ARM_RELATIVE` inside the text segment
pointing at the `make_fcontext` literal pool.
- After: `TEXTREL` gone, `php -v` prints the banner, and a `Fiber`
start/suspend/resume/getReturn smoke test passes, confirming both
`make_fcontext` and `jump_fcontext` still work.
### Alternatives considered
Two smaller changes exist. Neither is more conservative:
1. **`ldr a2, =finish` → `adr a2, finish`** clears the TEXTREL, but **fails to
assemble on Cortex-M0** (`error: misaligned pc-relative fixup value` —
`finish` lands 2-byte aligned in Thumb-1), i.e. it breaks the exact platform
#325 exists for. `adr` plus `.align 2` before `finish:` is the smallest safe
form, but still leaves the `jump_fcontext` regression.
2. **Flipping only the single gate around `adr`/`ldr`** (one line) fixes the
crash and keeps Cortex-M0 byte-identical, but leaves `jump_fcontext` at 43
instructions and produces a `make_fcontext` matching neither alpha1 nor
master — a hybrid that has never shipped in any PHP or Boost release.
The seven hunks here are seven copies of one mechanical substitution, and they
are what restores already-shipped code on every affected target. A smaller diff
here buys a larger behavioural delta.
### Affected
- Any ARMv6 PIE on a loader that doesn't relocate text in the main executable
(musl → Alpine armhf and other musl arm32 distros). **Crash.**
- Any ARMv6 build at all, glibc included (Raspberry Pi 1/Zero). **Perf.**
- Boost.Context `develop`/`master` and the 1.91.0 release carry the same defect;
no upstream issue exists for it yet (searched: textrel, musl, alpine, PIE,
armhf, armv6).
[pr325]: https://github.com/boostorg/context/pull/325
[musl-textrel]: https://git.musl-libc.org/cgit/musl/commit/?id=9f17413c753030811761d576fdb95f200bd818d7
[felker]: https://www.openwall.com/lists/musl/2020/09/25/4
[nagy]: https://www.openwall.com/lists/musl/2020/09/25/2
---
## php-src PR: required extra files
`Zend/asm/**` is vendored and **CI-enforced**. `.github/workflows/verify-bundled-files.yml`
re-downloads Boost 1.91.0 and runs `.github/scripts/test-directory-unchanged.sh Zend/asm`,
which ends in `git diff -a --exit-code HEAD Zend/asm`. **Any** local asm edit turns that
job red — on the PR itself (its paths filter includes `Zend/asm/**`) and on the 01:00
nightly cron. The next sync would also silently revert the fix.
The established pattern is uriparser's: a checked-in patch re-applied by the download
script. So the PR needs **three** things, not one:
1. `Zend/asm/{make,jump}_arm_aapcs_elf_gas.S` — the gate fix.
2. `.github/scripts/download-bundled/boost-context.patch` — same diff, repo-root-relative
paths (`a/Zend/asm/...`), mirroring `uriparser.config.patch`.
3. `.github/scripts/download-bundled/boost-context.sh` — append, after its `cd Zend/asm`:
```sh
# patch customized files
git apply -v ../../.github/scripts/download-bundled/boost-context.patch
```
(`git apply` resolves repo-root-relative paths from a subdirectory — verified;
`uriparser.sh` relies on the same behaviour with `../../../`.)