Repository navigation
Preserve dormant SME ZA across the AArch64 stack switch. - #25
Draft
kristjanvalur wants to merge 7 commits into
Draft
kristjanvalur wants to merge 7 commits into
kristjanvalur wants to merge 7 commits into
Conversation
Bump STACKMAN_VERSION and the README banner, and record the changes since 1.2.5 in the changelog. The release tag is not created here.
The x64 test.exe run exits 1 with no assertion text and no test names. stdout is fully buffered on that pipe, and PowerShell turns any native failure into exit code 1. The test now prints each name unbuffered and logs the exception code. The Windows step runs under cmd and prints that process exit code.
The stack enlargement itself is on master. This records it with the other changes since 1.2.5.
The note covers where FEAT_SME shows up, the off, active, and dormant modes, and the helper sequence for aarch64 and darwin_arm64.
stackman_sme_suspend commits a dormant ZA before the save callback and parks the TPIDR2 block pointer next to FPCR. stackman_sme_resume puts that dormant state back after the new stack is installed. Off stays off. The helpers are weak, and Windows ARM64 is unchanged.
stackman_sme_resume is a normal call, so it can destroy x2. The restore callback takes the installed stack pointer in that register. The null path skipped the call and kept the pointer. The dormant path did not.
kristjanvalur
marked this pull request as draft
October 15, 2026 22:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status: open, dormant. Off the main line for the foreseeable future. See "Backwards compatibility" below.
aarch64anddarwin_arm64keep a dormant or off SME ZA state acrossstackman_switch.win_arm64does not.stackman_calldoes not touch ZA.On entry, ZA is off or dormant.
stackman_sme_suspendruns before the save callback. It asks__arm_sme_statefor the TPIDR2 block pointer and does not readTPIDR2_EL0itself. A null pointer stays null. A dormant pointer is committed with__arm_za_disable, and the pointer is stored in the stack image next toFPCRandFPSR. After the new stack is installed,stackman_sme_resumeturns ZA back on, copies the rows with__arm_tpidr2_restore, and pointsTPIDR2_EL0at the same block. The helpers are weak. A toolchain without them stores a null pointer and skips both calls.The published stack pointer is still the
FPCRword. The slot is 32 bytes:FPCR,FPSR, the block pointer, and padding. The unwind CFA issp+192until that slot is popped.When this thread is not using ZA, the added work is the suspend call, a null pointer stored in the slot, and a load-and-branch that skips the resume. The suspend call returns immediately if the helpers are missing. If they are present, it calls
__arm_sme_state, which reads a startup flag and returns zeros when SME is unavailable. The 16 extra bytes sit below the frame the callback already receives.ZT0is caller-saved and is not part of this state. A callback that copies a stack to a new base has to relocate the saved block pointer andza_save_buffer.The checked-in assembly reloads
x2fromspafter the resume call. That call can destroyx2, and the restore callback takes the installed stack pointer in that register. Clang assembled the Linux and Darwin objects. Neither was executed. There is no SME test here: the stockqemu-aarch64model does not provide ZA.Where the hardware actually is
FEAT_SMEis optional. Arm published it in 2021 with Armv9.2-A. It is not part of baseline AArch64, and plenty of Armv9-A cores leave it out.Machines that have it are mostly recent clients. Apple M4 was the first widely shipped SME core; M1, M2, and M3 do not have it. Later Apple phone and desktop chips do (A18 / iPhone 16 and M4 onward). A few 2025–2026 phone SoCs do too. The server cores this library is usually built for do not. Graviton2 through Graviton5, Grace, Cobalt 100, and AmpereOne do not advertise SME. A Graviton5 (Neoverse V3)
/proc/cpuinfofeature list has SVE2 and does not havesme. The stockqemu-aarch64CPU does not have ZA. GitHub'smacos-latestarm64 runners are M1-class and do not have it either. Windows on ARM is a different runtime and is left out of this change.So the extension matters for a program that was built with an SME-aware compiler and enters
stackman_switchwith dormant ZA, on one of those client CPUs. Current cloud AArch64, qemu, and the machines CI runs on are outside that set.What CI showed
CI on this branch is red in two jobs, both on machines with no SME:
build-linux-gnu (aarch64):bin/testandbin/test_ccpass underqemu-aarch64.bin/test_staticprintstest_01and dies withSIGSEGV. That binary is the GCC inline copy. The asm pushes a 32-byte slot withstp [sp, #-32]!, which GCC does not account for, and then reloads the callback pointer from[sp, #160]. After the push that address is the savedd12/d13slot, and the indirect call faults. Helpers are null on that toolchain and ZA is off. The crash is the compiler's stack model, on the path that has to stay a no-op.build-macos (macos-latest):bin/testprintstest_01and dies withSIGILL. That binary uses the checked-in.S, which is the default build. The.Sitself has no SME opcode. The illegal instruction is instackman_sme_suspend/stackman_sme_resumeor in the helper they call. The runner has no SME, somrs TPIDR2_EL0,mrs SVCR, orsmstarton the way into the helper is fatal.macos-15-intelpasses. Every other Linux ABI, the BSD jobs, and Windows x86/x64 pass. Windows arm64 is build-only.Backwards compatibility
Backwards compatible means the
aarch64anddarwin_arm64archives this repo already ships. The same binary keeps running on M1, on Graviton, under qemu, and with a toolchain that has never heard of SME. ZA is preserved only when the CPU and the compiler runtime actually have it.That version is possible, and it is complex. The source diff stays small. The constraints do not.
The untaken path has to be plain Armv8-A in both copies of the switch. The checked-in
.Sis the default build. The inline body is whattests/test_static.ccompiles, and CI runs it as its own program. The old 16-byteFPCR/FPSRpush worked because nothing called out before it, so GCC held the callback in a callee-saved register. Callingstackman_sme_suspendbefore the push made GCC spill that pointer and reload it at the oldspoffset, which is the qemu segfault. The inline restore has the same shape as thex2bug already fixed in the.S:bl stackman_sme_resumekills caller-saved registers, and the compiler still thinks its copy of the new stack pointer survived. Both copies have to be kept honest by hand. A later call, a memory clobber, or a wider slot brings the spill back.The instructions that implement the feature are illegal on the machines the binary must still run on.
mrs TPIDR2_EL0,mrs SVCR, andsmstart zaare UNDEFINED withoutFEAT_SME. They have to sit behind a branch, including the prologue of whatever function contains them. A C helper is a bad place for that. Apple Clang and GCC 14 know the SME procedure call, and a call to__arm_sme_stateor__arm_za_disablecan make them emitmrs TPIDR2_EL0before the null check.target("arch=armv8-a")is on the forward declaration of these helpers. Clang takes that attribute from the definition. Ubuntu Clang 18, aimed atarm64-apple-macos11, emitted no such prologue; the macOS 26 runner's Clang is newer, andbin/testdied withSIGILLon the first switch. A compatible helper is checked-in assembly,.arch armv8-a, with the SME encodings only on the taken branch.Finding out whether SME exists has the same trap. EL0 code must not read
ID_AA64PFR1_EL1, and it must not readTPIDR2_EL0to see whether the read faults.__arm_sme_stateis the supported probe: a correct copy loads a startup flag and only then touches an SME register. The symbol is weak, and a missing symbol means "no SME", which is the compatible answer. A present symbol is.variant_pcs. A compiler or linker thunk for that convention readsTPIDR2_EL0before the helper reaches its own flag check, and that thunk is illegal on M1. Qemu leaves the weak symbol null, so it never takes the call and cannot catch the thunk. Showing that a particular call sequence stays clean takes a run on M1 and a run on M4. This CI has the M1, which crashed, and it has no M4.The half that needs the feature has no test. Stock qemu has no ZA, and the GitHub arm64 runners have no SME. A restore that uses the wrong register, a missed
smstart, or a block pointer written onto the wrong stack all come out ofmake testgreen.A compatible change is therefore a second assembler implementation of the helpers, a matching fix to the inline stack picture, a call sequence that cannot grow a ZA prologue or a variant-PCS thunk, and a dormant path that CI will never execute. That remains a constraint on every later edit of the AArch64 switch, while the compilers grow more SME-aware. It is too much to put on master for the hardware that exists today.
This stays off the main line for the foreseeable future. The PR stays open and dormant. If a program on those CPUs needs ZA preserved across a switch, that belongs in a separate library those users build and link, with SME as a requirement of that library. It does not become another ABI in this tree, and it does not go into the default
libstackman.