Skip to content

Fix ARMv6 fiber asm: gate Cortex-M0 branch on __ARM_ARCH_6M__#22751

Open
andypost wants to merge 1 commit into
php:masterfrom
skilld-labs:fix-armv6-fiber-textrel
Open

Fix ARMv6 fiber asm: gate Cortex-M0 branch on __ARM_ARCH_6M__#22751
andypost wants to merge 1 commit into
php:masterfrom
skilld-labs:fix-armv6-fiber-textrel

Conversation

@andypost

Copy link
Copy Markdown
Contributor

Description

Problem

Since the Boost.Context 1.91.0 sync (9cefeea, 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, 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:

    #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 itselfmap_library()
mprotects the mapping RWX before relocating (ldso/dynlink.c, added 2011 in
commit 9f17413c "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:

there's no compelling reason to support textrels in the main program

Szabolcs Nagy, same thread:

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 strs and ten
ldrs, on every fiber switch, on every ARMv6 build.

Fix

Gate on __ARM_ARCH_6M__ — precisely the target #325 names:

-    #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 -vSegmentation 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, =finishadr 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
    Add schema default/fixed value support in DOM #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).

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:

    # 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 ../../../.)

The Boost.Context 1.91.0 sync (phpGH-22584) pulled in boostorg/context#325,
"fix cortex-m0 support to aapcs_elf_gas target", which adds a fallback
branch to make_fcontext/jump_fcontext for the Thumb-1 encoding limits of
ARMv6-M. The branch is correct for Cortex-M0 but is gated on
__ARM_ARCH >= 7, which is also false for classic ARMv6 in ARM state
(Alpine armhf, Raspberry Pi 1/Zero) -- targets that can encode every
instruction in the >= 7 branch and need none of the workaround.

Two consequences. make_fcontext took `ldr a2, =finish', materialising the
absolute address of finish through a literal pool in .text and 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. musl supports DT_TEXTREL only for DSOs it maps itself; the
main executable is kernel-mapped and handled by kernel_mapped_dso(),
which never inspects it, so ldso writes to a read-only page and dies with
SIGSEGV in do_relocs() before main(). Every SAPI, every invocation, no
output. glibc masks this by mprotecting the segment writable, which is
why only musl builds noticed.

Separately, jump_fcontext -- the hot path on every fiber switch -- grew
from 13 to 43 instructions on all ARMv6 builds, glibc included.

Gate on __ARM_ARCH_6M__ instead, exactly the target php#325 names. The
result emits byte-identical .text to 8.6.0alpha1 on every ARM target
except Cortex-M0, where it emits byte-identical .text to current master:
each target gets back the code it had before the regression, and M0 keeps
what php#325 added. armv7 and arm64 are unaffected either way.

Zend/asm is vendored and verified by the Verify Bundled Files workflow,
so carry the same diff in .github/scripts/download-bundled and re-apply
it from the sync script, as uriparser already does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@andypost

Copy link
Copy Markdown
Contributor Author

Hit it when upgrading to alpha2 for Alpinelinux https://gitlab.alpinelinux.org/alpine/aports/-/merge_requests/105341

@LamentXU123

Copy link
Copy Markdown
Member

Also cc @kn1g78 as the author of #22584 for opinions.

@kn1g78

kn1g78 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Thanks for tracking this down.

As the author of GH-22584, I confirmed the root cause. The Cortex-M0 fallback must be selected specifically with
__ARM_ARCH_6M__, rather than treating every architecture below ARMv7 as Cortex-M0.

The required changes are:

  • Replace all seven #if __ARM_ARCH >= 7 checks in the ARM fcontext assembly with #if !defined(__ARM_ARCH_6M__).
  • Keep the same changes in boost-context.patch, because Zend/asm is a verified bundled directory.
  • Apply that patch from boost-context.sh after regenerating the bundled files.

Your current patch implements these changes correctly.

I also verified that:

  • ARMv6KZ make_fcontext / jump_fcontext return from 15/43 instructions to 11/13.
  • The R_ARM_ABS32 text relocation and PIE DT_TEXTREL are removed.
  • Classic ARMv5/ARMv6 output matches the pre-1.91 implementation.
  • Cortex-M0 output remains unchanged.
  • The bundled-files regeneration check passes.

My commit is here:

kn1g78@801dd8c

From my side, GH-22751 is correct and ready for review.

git restore save_xmm_x86_64_ms_masm.asm # added in GH-18352, not an upstream boost.context file

# patch customized files
git apply -v ../../.github/scripts/download-bundled/boost-context.patch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to name the patch more precisely or even use sed here to make sure __ARM_ARCH >= 7 is correctly replaced even if more #if is added in future upstream sync.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants