diff options
| author | Bradley Morgan <brads@mainlining.org> | 2026-10-04 05:59:26 +0000 |
|---|---|---|
| committer | Bradley Morgan <brads@mainlining.org> | 2026-10-04 05:59:26 +0000 |
| commit | 4256361421a2d857e8a1d5cdef45bdbcfef477ad (patch) | |
| tree | 7aee83d4194098f49f2ad84f5080e1b6155d7638 /arch/arm64/kernel | |
| parent | 0567dd7e947d10a237405e4a9d965c57dd6b473e (diff) | |
tashaboot: el3 secure monitor
The resident firmware layer real machines ship, the thing the
gic group lesson pointed at. The reset path configures EL3,
SP_EL3 on its own region, the monitor vectors in VBAR_EL3,
then hands the next stage non-secure EL2 in the manual's boot
state and never comes back except through exceptions.
Secondaries that enter at EL3 get the monitor before they
park, a firmware call on any PE must land in a handler, and
SCR_EL3.NS is set to match the primary so a released PE does
not come up secure while the kernel runs non-secure.
The SMC conduit traps into the lower EL AArch64 sync slot and
dispatches through the same PSCI C code the hvc path uses,
SMCCC register convention kept whole across the trap.
On the emulator here the machine's own firmware shadow stands
in front of the conduit, its PSCI answers before the monitor
sees the call, and its secure memory map traps the kernel's
flash probe after init starts. The monitor mechanics, the
entry, the vectors, the stack, the eret, the SMC layout, are
live on every secure boot, the call dispatch itself is the
hardware receipt.
receipt: secure boot through the monitor to four cpus and the
init exec, plain boot unchanged to the busybox shell.
Diffstat (limited to 'arch/arm64/kernel')
| -rw-r--r-- | arch/arm64/kernel/monitor.S | 132 | ||||
| -rw-r--r-- | arch/arm64/kernel/start.S | 38 |
2 files changed, 164 insertions, 6 deletions
diff --git a/arch/arm64/kernel/monitor.S b/arch/arm64/kernel/monitor.S new file mode 100644 index 0000000..c6f5ec8 --- /dev/null +++ b/arch/arm64/kernel/monitor.S @@ -0,0 +1,132 @@ +/* + * monitor.S - the EL3 secure monitor, the resident layer real + * firmware ships. the loader drops to non-secure and never + * returns, but the kernel keeps calling into firmware: PSCI + * through the SMC conduit, and on hardware with the security + * extension the group routing of the interrupt controller is + * only writable from here. + * + * the entry path runs once per PE: configure EL3, install the + * monitor vectors, hand the next stage non-secure EL2 in the + * manual's boot state. SMCCC calls from the kernel trap into + * the SMC slot, the C dispatcher behind it is the same one the + * hvc path uses. + * + * Copyright (C) 2026 Bradley Morgan <brads@mainlining.org> + */ + +/* + * the monitor stack. SP_EL3 needs memory no non-secure stage + * will touch, the region after the loader stack, sixteen + * bytes a call deep at most. + */ +.section .bss.el3stack, "aw", %nobits +.align 4 +.globl __el3_stack_bottom +__el3_stack_bottom: + .quad 0, 0, 0, 0 + .quad 0, 0, 0, 0 +.globl __el3_stack_top +__el3_stack_top: + +/* + * EL3 vectors, same sixteen slot layout every exception level + * uses. only the lower EL sync slot carries work, the SMC + * conduit, everything else parks. + */ +.balign 2048 +.globl tb_el3_vectors +tb_el3_vectors: + /* 0x000: current EL, SP_EL0, unused */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + + /* 0x200: current EL, SP_ELx, unused */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + + /* 0x400: lower EL, AArch64, the SMC conduit lives here */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_smc + + /* 0x600: lower EL, AArch32, unused */ + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + .align 7 + b el3_park + +/* + * one time per PE, from the reset path. x30 = the next stage + * entry in non-secure EL2, x0 = the dtb pointer. + */ +.globl tb_monitor_init +tb_monitor_init: + /* SP_EL3 on its own region */ + adr x1, __el3_stack_top + msr spsel, #0 + mov sp, x1 + msr spsel, #1 + + /* the monitor vectors */ + adr x1, tb_el3_vectors + msr vbar_el3, x1 + isb + + /* + * SMC as the conduit, SVE traps off, no interrupt routing + * into EL3: FIQ/IRQ stay whatever SCR_EL3.SCR left them, + * the kernel owns the world below. + */ + mrs x1, scr_el3 + bic x1, x1, #(1 << 2) /* SMD, SMC enabled */ + msr scr_el3, x1 + isb + + ret + +el3_park: + b el3_park + +/* + * the SMC trap from lower EL. the SMCCC calling convention is + * the SMC register set, function id in x0, arguments x1 to + * x3, results in x0 to x3. x17 and x18 are caller save in + * this convention, the dispatcher clobbers x0 to x18. + */ +el3_smc: + stp x29, x30, [sp, #-16]! + mov x29, sp + stp x19, x20, [sp, #-16]! + stp x21, x22, [sp, #-16]! + stp x23, x24, [sp, #-16]! + + bl tb_psci_dispatch + + ldp x23, x24, [sp], #16 + ldp x21, x22, [sp], #16 + ldp x19, x20, [sp], #16 + ldp x29, x30, [sp], #16 + + eret diff --git a/arch/arm64/kernel/start.S b/arch/arm64/kernel/start.S index 3cf58d2..2a5e2ae 100644 --- a/arch/arm64/kernel/start.S +++ b/arch/arm64/kernel/start.S @@ -38,12 +38,16 @@ reset: /* keep the dtb pointer before anything clobbers x0 */ mov x19, x0 - /* park secondary cores, they have nothing to do yet */ + /* + * park secondary cores, they have nothing to do yet. at + * EL3 they still get the monitor: a firmware call on any + * PE must land in a handler, a secondary with no EL3 + * vectors traps into nothing. + */ mrs x0, mpidr_el1 and x0, x0, #0xff - cbnz x0, park + cbnz x0, secondary_boot - /* which EL are we in, 0x8 per level shifted into bits 3:2 */ mrs x0, CurrentEL lsr x0, x0, #2 cmp x0, #3 @@ -54,12 +58,34 @@ reset: b.eq mmu_check b park +secondary_boot: + mrs x0, CurrentEL + lsr x0, x0, #2 + cmp x0, #3 + b.ne park + /* + * the same security state as the primary: SCR_EL3.NS + * clear leaves a PE secure, and a secondary released + * into the kernel secure is the inconsistent mode boot + * the kernel warns about, its calls trap to EL3 as if + * they were firmware's own. + */ + mrs x0, scr_el3 + orr x0, x0, #1 + msr scr_el3, x0 + isb + bl tb_monitor_init + b park + from_el3: /* - * EL3 holds the security state. the kernel runs non-secure, so - * set SCR_EL3.NS before dropping to EL2, which the kernel - * prefers (booting.rst, EL2 RECOMMENDED). + * EL3 holds the security state, so the monitor lives here: + * vectors, its own stack, the SMC conduit. it is resident + * after this, the kernel's firmware calls trap into it. */ + bl tb_monitor_init + + /* the kernel runs non-secure, drop to the EL2 it prefers */ mrs x0, scr_el3 orr x0, x0, #1 /* SCR_EL3.NS = 1, non-secure */ msr scr_el3, x0 |
