ARM64: SP_EL1 is never set, the boot thread's interrupt frames land on the bootloader's stack
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 18/100
- Tipo di issue
- Bug
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Ferma
- Ambito
- operating-systems
Direzione di ricerca
The stack setup belongs in the native ARM64 assembly under src/Cosmos.Kernel.Native.ARM64/CPU/Interrupts.s, with the vector install in _native_arm64_init_exception_vectors and the boot-thread bounds in BootStack and RhGetCurrentThreadStackBounds. Start by reading the EL1t special case at Interrupts.s:161-168 and the GC stack scan in GarbageCollector.Mark.cs. Done means SP_EL1 points at a kernel-owned interrupt stack before vbar_el1 is written, and the boot thread's frames land in a range the kernel reports. A linked pull request (#3368) is already open, so check it before starting.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Found on #3323 while reviewing the ARM64 fix in c1bb68ec1 (Interrupts.s: the IRQ stub saves SP_EL0 as the interrupted stack pointer when the saved SPSR says EL1t). That fix is right as far as it goes, and it exposes a boot-state gap of its own: nothing in the kernel ever writes SP_EL1, so every exception the boot thread takes pushes its 832-byte frame on whatever stack the bootloader left in SP_EL1, inside bootloader-reclaimable memory the kernel does not own.
Problem
Limine enters the kernel in EL1t: enter_in_el1 in common/lib/spinup.asm_aarch64 of the v10.x tree does msr spsel, #0; mov sp, x1 and enters with spsr_el1 = 0x3c4 (M[3:0] = 0100, EL1t), and the protocol's machine state at entry gives SP as the top of a stack in bootloader-reclaimable memory with every other PSTATE field 0. On that path the kernel's stack pointer is SP_EL0, and SP_EL1 keeps the value it had while Limine ran: AArch64 resets with PSTATE.SP = 1, so a firmware and a loader that never select SP_EL0 run on SP_EL1, and what is left there is the UEFI stack Limine executed on, memory its pmm.s2.c converts from EfiBootServicesData/EfiLoaderData into the bootloader-reclaimable type the kernel sees. The vector table installed by _native_arm64_init_exception_vectors (Interrupts.s:359) covers both the "Current EL with SP0" and the "Current EL with SPx" vectors with the same stub, and the architecture selects SP_EL1 on every exception taken to EL1 (PSTATE.SP becomes 1 on entry regardless of its value before). So:
- Every scheduler thread runs EL1h:
ThreadContext.InitializesetsSpsr = 0x3C5(ThreadContext.ARM64.cs:82), the first run doesmov sp, x12withSPSel= 1, soSP_EL1is the thread's own stack while it runs and the frame lands on it. Correct. - The boot thread (thread 0, CPU 0's idle thread, the one
Mainruns on) runs EL1t, onSP_EL0. On its exceptions the frame lands onSP_EL1, which holds whatever Limine last had there. The kernel sets it nowhere:grep -rn 'sp_el1\|spsel' src/finds only the comment atInterrupts.s:162.
What that stack is: [GenericTimer] Tick 1 SP=0x476863A0 is the frame's address on every ARM64 boot of the test kernels (the probe at GenericTimer.cs:214 prints SavedContextStackPointer, the frame's base). The memory map of the same boot puts it in Entry 5: 0x47666000 - 0x48000000 (9 MB) Type: 5 (Bootloader Reclaimable): an identity-mapped low address in Limine's own allocations, next to the kernel's Entry 6: 0x48000000 - ... (Usable). The kernel's stack at that moment is BootStack.Top in the higher half (0xFFFF000059DBDFD0 in the commit message of c1bb68ec1), the 1 MiB Limine stack the Stack Size request asks for, and the boot thread's frames never land on it.
Why it has not bitten:
PageAllocatorhands out onlyUsableentries (PageAllocator.cs:196) and nothing reclaims bootloader memory yet, so the region stays mapped and unused by anyone else: the frames land on memory nobody owns and nobody overwrites.- The stub runs the whole managed handler (
__managed__irq→InterruptManager.Dispatch, the timer tick, the scheduler) with IRQs masked, so no interrupt nests on that stack and its depth is bounded by whatDispatchcalls (a synchronous exception raised inside the handler would nest, on the same unknown stack). Limine's stack has had room for that so far.
What it costs:
- Unowned memory. The first consumer of bootloader-reclaimable memory (a later
PageAllocatorthat reclaims it, which the memory-map type exists for) turns every boot-thread interrupt into a write into someone's pages, with nothing pointing back atSP_EL1. - No bounds for the boot thread's interrupt frames.
BootStackreports[Top - Size, Top)for the boot thread (RhGetCurrentThreadStackBounds,Thread.cs:47), and that range never contains the frames; a stack walk, a panic dump, or the GC'sScanThreadStackhas no bounds for the stack the frames sit on. c1bb68ec1 works around the GC half of that by scanning fromSP_EL0whenSPSR.M[0]is 0 (GarbageCollector.Mark.cs:243-250); the saved context itself, which that scan reads throughGetContext(), still lives on the unowned stack. - An asymmetry per thread kind. The boot thread is the only one whose context is not on its own stack; every special case in the stub (
Interrupts.s:161-168) and in the GC exists for it. - SMP (#3339). Each application processor Limine starts enters the kernel the same way:
msr spsel, #0, its own reclaimable stack inSP_EL0,spsr_el1 = 0x3c4(common/sys/smp_trampoline.asm_aarch64), andSP_EL1as PSCICPU_ONhanded the core over, which that contract leaves undefined: the trampoline never writes it. Without a kernel-ownedSP_EL1per CPU the same gap exists on every CPU, with a different unknown stack each, and on an AP not even a stack that was once valid.
Evidence
From an ARM64 run of the Network suite (same lines on every test kernel):
[KMAIN] - HHDM offset: 0x0xFFFF000000000000
[MemMap] Entry 4: 0x44020000 - 0x47666000 (54 MB) Type: 0 (Usable)
[MemMap] Entry 5: 0x47666000 - 0x48000000 (9 MB) Type: 5 (Bootloader Reclaimable)
[MemMap] Entry 6: 0x48000000 - 0x596A2000 (278 MB) Type: 0 (Usable)
...
[Kernel] Enabling interrupts...
[GenericTimer] Tick 1 SP=0x476863A0
[GenericTimer] Tick 2 SP=0x476863A0
0x476863A0 is inside entry 5; the kernel's boot stack is in the higher half. Before c1bb68ec1 the GC scanned from that address up to BootStack.Top when a worker collected while the boot thread was switched out, which is how the gap surfaced (the GC_CollectFromWorkerThread cell, 35/90 cells and QEMU exit 134 without the fix on #3323).
Suggested fix
Give SP_EL1 a kernel-owned interrupt stack before the first exception can be taken, that is before _native_arm64_init_exception_vectors installs the vectors (ARM64InterruptController.Initialize): a static per-CPU area in the native ARM64 assembly or a page allocation, then
msr spsel, #1
mov sp, x0 // top of the kernel's interrupt stack for this CPU
msr spsel, #0
run early in kmain (or from the controller's Initialize, before msr vbar_el1). With that, BootStack can report two ranges for the boot thread (its call stack and its interrupt stack), the frames have an owner, the GC and the panic dump have bounds, and the AP bring-up of #3339 does the same per CPU with the stack it allocates anyway.
The alternative is to run the boot thread in EL1h from kmain (msr spsel, #1 after copying SP_EL0 into SP_EL1), so its frames land on the boot stack like every other thread's: it removes the EL1t special case from Interrupts.s:161-168 and from GarbageCollector.Mark.cs, at the cost of the boot stack carrying the frames too (1 MiB, the same arrangement as the x64 side, where no TSS is loaded and every frame goes on the interrupted stack). Either way the fix is a few lines of assembly plus the bounds; the choice is whether the kernel wants a separate interrupt stack per CPU, which SMP and a future user mode both argue for.
Not for #3323: the fix there makes the GC right with the stacks as they are; this is the follow-up that gives those frames a home.
- Lingua principale
- C#
- Stelle
- 3.2k
- Fork
- 591
- Merge medio
- 21h 34m
- PR unite (30g)
- 26
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di CosmosOS/Cosmos
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
CosmosOS/Cosmos#3410 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Area: Kernel Bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
Area: Networking Needs Triage
Difficoltà 3/5 1-2 giorni Idoneità per principianti 55/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
CosmosOS/Cosmos#3407 · 8 commenti ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di CosmosOS/Cosmos
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
nopSolutions/nopCommerce#8459 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 64/100
criticalmanufacturing/cli#909 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
RadicalCSG/com.chisel#312 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
eyalgolan/statecraft-israel#3093 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno