Skip to content

aarch64: a kernel-STACK resume PC is reached (dispatch-path unproven, R41) — [INSTRUCTION_ABORT] FAR==ELR==x30==x29, ESR=0x8600000e IFSC=0xe, 3/200 clean SS-gate boots on both CPU profiles #635

Description

@ryanbreen

Face

An EL1 instruction fetch at a kernel stack addressfrom a ret-based kernel dispatch [struck 2026-08-22, coordinator ruling R43: this specific-consumer attribution is superseded by coordinator ruling R41 (see the R41 comment on this issue), which found this whole field-keyed family also reaches the fault via the ERET epilogue and is not attributable to the ret-dispatch consumer by register shape alone]:

[INSTRUCTION_ABORT] FAR=0xffff000054243f00 ELR=0xffff000054243f00 ESR=0x8600000e IFSC=0xe TTBR0=0x1000044137000 from_el0=0
[FATAL_REGS] label=INSTRUCTION_ABORT cpu=0 spsr=0x20000305 esr=0x8600000e far=0xffff000054243f00 elr=0xffff000054243f00 sp=0xffff000054243c40
  x19=0xffff000054243ef0 x20=0xffff000054243f08 x21=0xffff000054243f18
  x26=0xffff000054243ef8 x27=0xffff000054243f10 x29=0xffff000054243f00 x30=0xffff000054243f00

Field-exact signature for classification: ESR=0x8600000e IFSC=0xe, FAR == ELR == x30 == x29, the
value a kernel-stack VA, from_el0=0. This is not #576 (FAR=0x0 ELR=0x0 ESR=0x86000005) and
not #626 (FAR=0x0 ELR=0x0 ESR=0x8600000d) — same consumer, different garbage resume PC.

Rate

Measured on branch fix/607-576-zero-pc-family @ c9c75c3b (PR #634), service-sequence gate, idle
host, throttling.iops-total=2000:

leg boots occurrences
clean, -cpu cortex-a72, 100 boots 100 2 (boots 3, 37)
clean, -cpu max, 100 boots 100 1 (boot 30)
starved (14 yes hogs at nice -n 19), 100 boots 100 0 of this single-record form

3/200 clean (1.5%), byte-identical across two CPU profiles, which argues against a VM hiccup.
Whether this rate is pre-existing on main or was raised by that branch is being measured
separately; PR #634 discloses the attribution either way.
[struck 2026-08-22, coordinator ruling R43: this has since been measured, not left open — see the R41 comment on this issue. Verdict: BRANCH-CAUSED SURFACING, with both honest qualifiers disclosed there (the field family itself is not branch-new; the branch's ~1% observed rate against 0/300 main boots is suggestive at ≈5% chance, not conclusive).]

Register-file analysis

Verbatim from the PR #634 round-1 review, which is the analysis this issue is filed on:

  • IFSC=0xe is a permission fault, level 2 — an EL1 instruction fetch at a PXN kernel stack
    page. ELR == FAR == x30 == x29, and sp is 0x2c0 below it on the same stack.
  • x19/x26/x29/x30/x20/x27/x21 hold the consecutive stack slots …ef0, …ef8, …f00, …f08, …f10, …f18 — a callee-saved register file restored from a garbage/shifted region, exactly the
    "dispatch with an incoherent record" shape the RCA names in §3.
  • spsr=0x20000305 decodes to EL1h with I and F clear, D and A set — bit-for-bit the PSTATE
    produced by msr daifclr, #3 at context_switch.rs:627, i.e. the state
    aarch64_ret_to_kernel_context is in four instructions before br x1. And x29 == x30 is what
    ldp x29, x30, [x0, #232] at :624 produces from a context whose two saved slots hold the same
    garbage pointer.
  • No [RET_DISPATCH_REFUSED: line in any of the three serials, and DISPATCH_TRACE's last entry is
    an ERET with different NZCV — which per RCA §2a is precisely what a later ret dispatch looks
    like, since ret dispatches are never recorded.
    [struck 2026-08-22, coordinator ruling R43: this
    inference is inverted by coordinator ruling R41 — the round-2 confirm battery observed zero
    [RET_DISPATCH_REFUSED: lines across 50 boots of a gate this family's predicates never touch, so
    the absence of that line proves nothing about a later ret dispatch; see the R41 comment on this
    issue.]

One correction to that text, from re-reading the serials for this filing: the -cpu max capture
carries spsr=0x20002305, not 0x20000305. The extra bit is 13 (PSTATE.ALLINT, FEAT_NMI, which
the max CPU model implements and cortex-a72 does not). DAIF and mode are identical in all three:
D=1 A=1 I=0 F=0, M=EL1h — the post-daifclr #3 state either way.

Further shared detail across all three captures, not in the review: cpu=0 every time, victim
tid=1212 (the clonevm_exec child) in the deferred-cleanup record, the same stack page
0xffff000054243xxx, and the same x28=0xffff00004120a830, x1=0x400, x3=0x400. The last
DISPATCH_TRACE entry is I old=1212->tid=0 elr=0xffff00004049dafc spsr=0x5 (an ERET to idle) —
the fatal transfer itself is absent from the trace, which is what a ret dispatch looks like.
[struck 2026-08-22, coordinator ruling R43: same inversion as above — an absent trace entry is not
proof of a ret dispatch under R41; see the R41 comment on this issue.]

Serials

All four are preserved in-repo under docs/planning/teardown-unification/607-576-serials/:

file what it is
gate-clean100-cortexa72-boot3-stackpc-8600000e.txt this face, single self-consistent record (abort at :684)
gate-clean100-cortexa72-boot37-stackpc-8600000e.txt this face, byte-identical (abort at :686)
gate-clean100-max-boot30-stackpc-8600000e.txt this face on -cpu max, spsr=0x20002305 (abort at :685)
gate-starved100-max-boot3-disagreeing-pair-613.txt #613, not this face — a disagreeing record pair in one boot; included because its second record FAR=0xffff000040800008 ELR=0xffff00004040b064 ESR=0x8600000e shares this ESR/IFSC at different addresses

The fourth is cross-referenced to #613 rather than claimed here: #613 is the filed
disagreeing-record-pair face (~2/400) and its own second exemplar is
0xffff000054242320 0xffff000054242320 0x8600000e. That serial also carries a [PC_ALIGN] ELR=0x4b1 FAR=0x7 from_el0=0 cpu=2 record at :614, appended to #633.

Mechanism, and what PR #634 does about it

The RCA behind #576 found that five of six aarch64 resume-PC consumers validate a stored PC before
transferring to it and one — the ret-based kernel dispatch, tried first at both dispatch sites —
did not. PR #634's round-1 fix gave that consumer a validation, but the predicate it shared was an
address-space check (>= 0xFFFF_0000_0000_0000, or the identity-mapped RAM window) with an
assembly floor of #0x1000. A kernel stack address passes both. So the round-1 fix contained the
PC == 0 face and left this one fully fail-open.

PR #634 round 2 narrows the predicate to the kernel text window using the linker-exported
__kernel_text_start / __kernel_text_end, and raises the assembly floor to the same lower bound,
which refuses this face by construction and records
[RET_DISPATCH_REFUSED:tid=..:pc=0xffff0000....:...] naming the producer instead of dying.

[struck 2026-08-22, coordinator ruling R43: "by construction" is true only of the ret-dispatch
consumer this predicate guards; it is falsified for the family by coordinator ruling R41's
guard-live zero-refusal recurrence — the round-2 confirm battery observed this face recur with the
guard in place and zero [RET_DISPATCH_REFUSED: lines, so the guard does not refuse this face by
construction across the family. See the R41 comment on this issue.]
A forced
oracle constructs this exact face (injects a kernel-stack resume PC at a ret dispatch) and is proven
red before the fix and green after.

Containment is not the producer. This issue stays open for the producer: whatever writes a
consecutive run of stack slots into a saved callee-saved register file such that x29 == x30 == a
stack address. Related producer trails: #560, #605, #633, and the incoherent-record shape in the
#576 RCA §3.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions