[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 `../../../`.)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.