diff --git a/boot/m1n1/README.md b/boot/m1n1/README.md index bd78f93b..0a7c6e1c 100644 --- a/boot/m1n1/README.md +++ b/boot/m1n1/README.md @@ -28,3 +28,16 @@ gives the same tree as Hoolock's tip before updating `sources.toml`. Scratch stays in ignored `boot/m1n1/.work//`. `--resume` reuses it only if the recorded patch set is unchanged. + +## Resident PSCI on A7-A9 + +Orchard patch 0015 adds the resident EL3 PSCI monitor (CPU_ON, CPU_OFF, +AFFINITY_INFO); 0016-0019 make it the default whenever m1n1 is entered at EL3 +and add SYSTEM_RESET (warm reboot back into m1n1, which boots the default BLS +entry with the last launch's DTB) and SYSTEM_OFF (halt). Any PSCI-aware +kernel reboots, powers off and kexecs through its ordinary paths; m1n1 takes +IPI-stopped CPUs back by trapping EL1 WFI/WFE to EL3. EL1/EL2 entries keep +the spin table. Payload variables (append to `m1n1.bin`, e.g. with +`psci-loader`): `psci=0` turns PSCI off, `psci_reset=0` keeps only the CPU +lifecycle (no WFI/WFE traps), `psci_reset=proxy` stops in the proxy after a +reset. 0020 marks retained hardware in the device tree after a re-entry. diff --git a/boot/m1n1/patches/orchard/0015-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch b/boot/m1n1/patches/orchard/0015-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch new file mode 100644 index 00000000..4eb87533 --- /dev/null +++ b/boot/m1n1/patches/orchard/0015-psci-Add-opt-in-resident-EL3-CPU-lifecycle-for-A7-A9.patch @@ -0,0 +1,880 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sat, 7 Nov 2026 15:00:00 +0100 +Subject: [PATCH] psci: Add opt-in resident EL3 CPU lifecycle for A7-A9 + +On A7-A9 m1n1 is entered at EL3 and stays resident: its image and the +per-CPU EL3 stacks are memreserved, and VBAR_EL3 points at its vectors +while Linux runs at EL1. Use that to give Linux PSCI 1.0 CPU_ON, CPU_OFF +and AFFINITY_INFO (plus VERSION, FEATURES and MIGRATE_INFO_TYPE). The +spin table has no cpu_die, so without this the kernel refuses kexec on +an SMP system, can't offline CPUs, and a crash kernel inherits CPUs +stuck in the old kernel. + +CPU_OFF does not power the core down. It cleans and invalidates its L1D, +stops its timers, and waits in WFE at EL3 (MMU off) on a one-line +mailbox. CPU_ON posts an entry point; the core leaves with a clean +I-cache and TLB, SCTLR_EL1 at its reset value, DAIF masked and SP_EL3 on +a static per-CPU stack. SYSTEM_OFF/SYSTEM_RESET, CPU_SUSPEND and +SMCCC_VERSION return NOT_SUPPORTED, so callers fall back to their own +methods and assume SMCCC 1.0. + +Opt-in with the payload variable psci=1, and only used when m1n1 itself +was entered at EL3. kboot then parks the secondaries in the pen (a core +that doesn't park is pruned), sets enable-method = "psci" on every CPU, +adds /psci (method = "smc") and records the re-entry address and the +original boot_args in /chosen. + +Re-entry: _reentry can be jumped to from a kexec'd trampoline (EL1, MMU +off, x0 = boot_args copy). It drops this image's lines from the caches, +flags the re-entry and restarts m1n1, which then skips the (stale) +payload area and runs the proxy. smp_start_secondaries() takes cores +back from the pen with CPU_ON, and the mailboxes and the psci=1 setting +live in .data so they survive the .bss clear. + +Rebase the resident boot shim onto Orchard's complete m1n1 series +without changing its runtime behavior. The payload variable is not a +kernel command-line option; a bare proxy loader also needs it appended. + +Assisted-by: LLM +--- + Makefile | 1 + + src/kboot.c | 56 +++++++++++++- + src/main.c | 8 +- + src/payload.c | 3 + + src/psci.c | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + src/psci.h | 78 ++++++++++++++++++++ + src/psci_asm.S | 170 ++++++++++++++++++++++++++++++++++++++++++ + src/smp.c | 73 ++++++++++++++++++ + src/smp.h | 1 + + src/start.S | 6 +- + src/startup.c | 6 +- + 11 files changed, 626 insertions(+), 5 deletions(-) + +diff --git a/Makefile b/Makefile +index bb72266..4aefb37 100644 +--- a/Makefile ++++ b/Makefile +@@ -166,6 +166,7 @@ OBJECTS := \ + pcie.o \ + pmgr.o \ + proxy.o \ ++ psci.o psci_asm.o \ + ringbuffer.o \ + rtkit.o \ + sart.o \ +diff --git a/src/kboot.c b/src/kboot.c +index b75bb45..2316709 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -21,6 +21,7 @@ + #include "memory.h" + #include "pcie.h" + #include "pmgr.h" ++#include "psci.h" + #include "sep.h" + #include "sio.h" + #include "smc.h" +@@ -574,6 +575,53 @@ static int dt_set_smbios(void) + return 0; + } + ++/* ++ * With psci=1 and m1n1 owning EL3 (A7-A9), CPUs boot through m1n1's PSCI ++ * (see psci.c) instead of the spin table, so the kernel can offline them ++ * for kexec and CPU hotplug. Also tells Linux how to re-enter m1n1. ++ */ ++static int dt_set_psci(void) ++{ ++ static const char compat[] = "arm,psci-1.0\0arm,psci-0.2"; ++ int node; ++ ++ if (!psci_active()) { ++ printf("PSCI: off (needs psci=1 and m1n1 entered at EL3)\n"); ++ return 0; ++ } ++ ++ int cpus = fdt_path_offset(dt, "/cpus"); ++ if (cpus < 0) ++ bail("FDT: /cpus node not found in devtree\n"); ++ ++ fdt_for_each_subnode(node, dt, cpus) ++ { ++ if (strncmp(fdt_get_name(dt, node, NULL), "cpu@", 4)) ++ continue; ++ if (fdt_setprop_string(dt, node, "enable-method", "psci")) ++ bail("FDT: couldn't set enable-method\n"); ++ } ++ ++ node = fdt_path_offset(dt, "/psci"); ++ if (node < 0) ++ node = fdt_add_subnode(dt, 0, "psci"); ++ if (node < 0) ++ bail("FDT: couldn't add /psci\n"); ++ if (fdt_setprop(dt, node, "compatible", compat, sizeof(compat)) || ++ fdt_setprop_string(dt, node, "method", "smc")) ++ bail("FDT: couldn't fill /psci\n"); ++ ++ node = fdt_path_offset(dt, "/chosen"); ++ if (node < 0) ++ bail("FDT: /chosen node not found in devtree\n"); ++ if (fdt_setprop_u64(dt, node, "linux-a9,m1n1-reentry", (u64)_reentry) || ++ fdt_setprop(dt, node, "linux-a9,m1n1-boot-args", &cur_boot_args, sizeof(cur_boot_args))) ++ bail("FDT: couldn't set the m1n1 re-entry properties\n"); ++ ++ printf("PSCI: on, enable-method = \"psci\", m1n1 re-entry at %p\n", _reentry); ++ return 0; ++} ++ + static int dt_set_cpus(void) + { + int ret = 0; +@@ -587,6 +635,10 @@ static int dt_set_cpus(void) + if (!pruned_phandles) + bail("FDT: out of memory\n"); + ++ /* Secondaries wait in the PSCI pen; one that can't get there is pruned below */ ++ if (psci_active()) ++ printf("PSCI: parked %d secondary CPU(s)\n", smp_park_secondaries_psci()); ++ + /* Prune CPU nodes */ + int node, cpu = 0; + for (node = fdt_first_subnode(dt, cpus); node >= 0;) { +@@ -618,7 +670,7 @@ static int dt_set_cpus(void) + } else { + printf("FDT: Reserving stack for CPU %d 0x%lx\n", cpu, (uint64_t)secondary_stacks[cpu]); + fdt_add_mem_rsv(dt, (uint64_t)secondary_stacks[cpu], SECONDARY_STACK_SIZE); +- if (has_el3()) { ++ if (has_el3() && secondary_stacks_el3[cpu]) { + printf("FDT: Reserving EL3 stack for CPU %d 0x%lx\n", cpu, + (uint64_t)secondary_stacks_el3[cpu]); + fdt_add_mem_rsv(dt, (uint64_t)secondary_stacks_el3[cpu], SECONDARY_STACK_SIZE); +@@ -3206,6 +3258,8 @@ int kboot_prepare_dt(void *fdt) + return -1; + if (dt_set_cpus()) + return -1; ++ if (dt_set_psci()) ++ return -1; + if (dt_set_mac_addresses()) + return -1; + if (dt_set_wifi()) +diff --git a/src/main.c b/src/main.c +index d4e828c..3f32380 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -21,6 +21,7 @@ + #include "pcie.h" + #include "pmgr.h" + #include "sep.h" ++#include "psci.h" + #include "smp.h" + #include "string.h" + #include "tps6598x.h" +@@ -117,9 +118,12 @@ void run_actions(void) + #endif + #endif + +- printf("Checking for payloads...\n"); ++ if (psci_reentered()) ++ printf("Re-entered from Linux: whatever follows m1n1 is stale, not loading payloads\n"); ++ else ++ printf("Checking for payloads...\n"); + +- if (payload_run() == 0) { ++ if (!psci_reentered() && payload_run() == 0) { + printf("Valid payload found\n"); + return; + } +diff --git a/src/payload.c b/src/payload.c +index 5b1547f..4ffc9e2 100644 +--- a/src/payload.c ++++ b/src/payload.c +@@ -12,6 +12,7 @@ + #include "heapblock.h" + #include "kboot.h" + #include "mitigations.h" ++#include "psci.h" + #include "smp.h" + #include "utils.h" + +@@ -205,6 +206,8 @@ static bool check_var(u8 **p) + mitigations_configure(val); + } else if (IS_VAR("tso=")) { + enable_tso = val[0] == '1'; ++ } else if (IS_VAR("psci=")) { ++ psci_configure(val); + } else { + printf("Unknown variable %s\n", *p); + } +diff --git a/src/psci.c b/src/psci.c +new file mode 100644 +index 0000000..2b8ca0d +--- /dev/null ++++ b/src/psci.c +@@ -0,0 +1,229 @@ ++/* SPDX-License-Identifier: MIT */ ++ ++#include "psci.h" ++#include "smp.h" ++#include "types.h" ++#include "utils.h" ++ ++struct psci_cpu { ++ volatile u64 state; ++ volatile u64 entry; ++ volatile u64 context; ++ u64 pad[PSCI_CPU_SIZE / 8 - 3]; ++}; ++ ++_Static_assert(sizeof(struct psci_cpu) == PSCI_CPU_SIZE, "psci_cpu layout"); ++ ++/* ++ * Mailboxes, one cache line each, only ever accessed at EL3 with the MMU off. ++ * They live in .data rather than .bss so that a parked CPU's state survives ++ * m1n1 being re-entered from Linux (which clears .bss). ++ */ ++struct psci_cpu psci_cpus[PSCI_MAX_CPUS] ++ __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ ++/* Written with the MMU off only (_reentry sets reentry_flag, the first member) */ ++struct { ++ u64 reentry_flag; ++ u64 boot_el3; ++} psci_persist __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ ++/* psci=1 from the payload, kept across a re-entry */ ++static u32 psci_mode __attribute__((section(".data"))); ++ ++/* EL3 stacks a CPU gets when it leaves the pen */ ++static u8 psci_stacks[PSCI_MAX_CPUS][PSCI_STACK_SIZE] __attribute__((aligned(16))); ++ ++static bool psci_have_el3; ++static bool reentered; ++ ++/* EL3 side */ ++ ++static inline void psci_line_sync(volatile void *p) ++{ ++ __asm__ volatile("dc civac, %0\n\tdsb sy" ::"r"(p) : "memory"); ++} ++ ++static int psci_mpidr_index(u64 mpidr) ++{ ++ mpidr &= 0xff00ffffffUL; ++ ++ /* A7-A9: one cluster, Aff0 is the core */ ++ if (mpidr >= PSCI_MAX_CPUS) ++ return -1; ++ ++ return mpidr; ++} ++ ++static s64 psci_cpu_off(void) ++{ ++ int idx = psci_mpidr_index(mrs(MPIDR_EL1)); ++ ++ if (idx < 0) ++ return PSCI_RET_DENIED; ++ ++ psci_pen(&psci_cpus[idx], (u64)&psci_stacks[idx][PSCI_STACK_SIZE]); ++} ++ ++static s64 psci_cpu_on(u64 mpidr, u64 entry, u64 context) ++{ ++ int idx = psci_mpidr_index(mpidr); ++ ++ if (idx < 0 || !entry) ++ return PSCI_RET_INVALID_PARAMS; ++ ++ struct psci_cpu *c = &psci_cpus[idx]; ++ ++ psci_line_sync(c); ++ switch (c->state) { ++ case PSCI_AFF_OFF: ++ break; ++ case PSCI_AFF_ON_PENDING: ++ return PSCI_RET_ON_PENDING; ++ default: ++ /* Running, or never seen by us (still in m1n1 or never started) */ ++ return PSCI_RET_ALREADY_ON; ++ } ++ ++ /* State before entry: the pen sets ON as soon as it sees the entry */ ++ c->context = context; ++ c->state = PSCI_AFF_ON_PENDING; ++ psci_line_sync(c); ++ c->entry = entry; ++ psci_line_sync(c); ++ sysop("sev"); ++ ++ return PSCI_RET_SUCCESS; ++} ++ ++static s64 psci_affinity(u64 mpidr, u64 level) ++{ ++ int idx = psci_mpidr_index(mpidr); ++ ++ if (idx < 0 || level) ++ return PSCI_RET_INVALID_PARAMS; ++ ++ psci_line_sync(&psci_cpus[idx]); ++ return psci_cpus[idx].state; ++} ++ ++static s64 psci_features(u32 fid) ++{ ++ switch (fid) { ++ case PSCI_0_2_FN_PSCI_VERSION: ++ case PSCI_0_2_FN_CPU_OFF: ++ case PSCI_0_2_FN_CPU_ON: ++ case PSCI_0_2_FN64_CPU_ON: ++ case PSCI_0_2_FN_AFFINITY_INFO: ++ case PSCI_0_2_FN64_AFFINITY_INFO: ++ case PSCI_0_2_FN_MIGRATE_INFO_TYPE: ++ case PSCI_1_0_FN_PSCI_FEATURES: ++ return PSCI_RET_SUCCESS; ++ default: ++ /* ++ * Includes SMCCC_VERSION (so callers assume SMCCC 1.0 and make ++ * no other SMCs), CPU_SUSPEND, SYSTEM_OFF and SYSTEM_RESET. ++ */ ++ return PSCI_RET_NOT_SUPPORTED; ++ } ++} ++ ++u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) ++{ ++ u32 fn = fid; ++ ++ /* SMC32 calls carry 32-bit arguments */ ++ if (!(fn & 0x40000000)) { ++ a1 = (u32)a1; ++ a2 = (u32)a2; ++ a3 = (u32)a3; ++ } ++ ++ switch (fn) { ++ case PSCI_0_2_FN_PSCI_VERSION: ++ return 0x10000; /* 1.0 */ ++ case PSCI_0_2_FN_CPU_OFF: ++ return psci_cpu_off(); ++ case PSCI_0_2_FN_CPU_ON: ++ case PSCI_0_2_FN64_CPU_ON: ++ return psci_cpu_on(a1, a2, a3); ++ case PSCI_0_2_FN_AFFINITY_INFO: ++ case PSCI_0_2_FN64_AFFINITY_INFO: ++ return psci_affinity(a1, a2); ++ case PSCI_0_2_FN_MIGRATE_INFO_TYPE: ++ return 2; /* no trusted OS, nothing to migrate */ ++ case PSCI_1_0_FN_PSCI_FEATURES: ++ return psci_features(a1); ++ default: ++ /* SYSTEM_RESET/SYSTEM_OFF too: the caller falls back to its own */ ++ return PSCI_RET_NOT_SUPPORTED; ++ } ++} ++ ++/* EL1 side */ ++ ++void psci_note_boot_el(u64 el) ++{ ++ /* MMU still off: these .data accesses are uncached, like the writers' */ ++ if (psci_persist.reentry_flag) { ++ reentered = true; ++ psci_persist.reentry_flag = 0; ++ } ++ if (el == 3) ++ psci_persist.boot_el3 = 1; ++ ++ psci_have_el3 = psci_persist.boot_el3 && has_el3(); ++} ++ ++void psci_configure(const char *val) ++{ ++ psci_mode = val[0] == '1'; ++} ++ ++bool psci_active(void) ++{ ++ return psci_mode && psci_have_el3; ++} ++ ++bool psci_reentered(void) ++{ ++ return reentered; ++} ++ ++int psci_affinity_info(u64 mpidr) ++{ ++ return psci_smc(PSCI_0_2_FN64_AFFINITY_INFO, mpidr, 0, 0); ++} ++ ++static void psci_self_off(void) ++{ ++ /* Only returns on failure; the core then stays in m1n1's loop */ ++ psci_smc(PSCI_0_2_FN_CPU_OFF, 0, 0, 0); ++} ++ ++int psci_park_cpu(int cpu, u64 mpidr) ++{ ++ if (psci_affinity_info(mpidr) == PSCI_AFF_OFF) ++ return 0; ++ ++ smp_call0(cpu, psci_self_off); ++ ++ for (int i = 0; i < 100; i++) { ++ if (psci_affinity_info(mpidr) == PSCI_AFF_OFF) ++ return 0; ++ udelay(1000); ++ } ++ ++ return -1; ++} ++ ++int psci_unpark_cpu(int index, u64 mpidr, void *stack_top) ++{ ++ if (psci_affinity_info(mpidr) != PSCI_AFF_OFF) ++ return -1; ++ ++ s64 ret = psci_smc(PSCI_0_2_FN64_CPU_ON, mpidr, (u64)psci_secondary_entry, ++ (u64)stack_top | (u64)index); ++ ++ return ret == PSCI_RET_SUCCESS ? 0 : -1; ++} +diff --git a/src/psci.h b/src/psci.h +new file mode 100644 +index 0000000..6769ebe +--- /dev/null ++++ b/src/psci.h +@@ -0,0 +1,78 @@ ++/* SPDX-License-Identifier: MIT */ ++ ++#ifndef PSCI_H ++#define PSCI_H ++ ++#ifndef __ASSEMBLER__ ++#include "types.h" ++#endif ++ ++/* ++ * Minimal PSCI 1.0 for SoCs where m1n1 owns EL3 (A7-A9 entered at EL3). ++ * Only CPU_ON/CPU_OFF/AFFINITY_INFO and the discovery calls are implemented. ++ * A CPU that calls CPU_OFF is not powered down: it waits at EL3, MMU and ++ * caches off, until CPU_ON hands it a new EL1 entry point. That is what the ++ * kernel needs to offline CPUs for kexec and CPU hotplug. ++ */ ++ ++#define PSCI_0_2_FN_PSCI_VERSION 0x84000000 ++#define PSCI_0_2_FN_CPU_SUSPEND 0x84000001 ++#define PSCI_0_2_FN_CPU_OFF 0x84000002 ++#define PSCI_0_2_FN_CPU_ON 0x84000003 ++#define PSCI_0_2_FN_AFFINITY_INFO 0x84000004 ++#define PSCI_0_2_FN_MIGRATE_INFO_TYPE 0x84000006 ++#define PSCI_0_2_FN_SYSTEM_OFF 0x84000008 ++#define PSCI_0_2_FN_SYSTEM_RESET 0x84000009 ++#define PSCI_1_0_FN_PSCI_FEATURES 0x8400000a ++#define PSCI_0_2_FN64_CPU_ON 0xc4000003 ++#define PSCI_0_2_FN64_AFFINITY_INFO 0xc4000004 ++ ++#define PSCI_RET_SUCCESS 0 ++#define PSCI_RET_NOT_SUPPORTED -1 ++#define PSCI_RET_INVALID_PARAMS -2 ++#define PSCI_RET_DENIED -3 ++#define PSCI_RET_ALREADY_ON -4 ++#define PSCI_RET_ON_PENDING -5 ++#define PSCI_RET_INTERNAL_FAILURE -6 ++ ++#define PSCI_AFF_ON 0 ++#define PSCI_AFF_OFF 1 ++#define PSCI_AFF_ON_PENDING 2 ++ ++#define PSCI_MAX_CPUS 4 ++#define PSCI_STACK_SIZE 0x4000 ++ ++/* Layout shared with psci_asm.S */ ++#define PSCI_CPU_STATE 0x00 ++#define PSCI_CPU_ENTRY 0x08 ++#define PSCI_CPU_CONTEXT 0x10 ++#define PSCI_CPU_SIZE 0x80 ++ ++#ifndef __ASSEMBLER__ ++ ++/* Payload variable psci=1 */ ++void psci_configure(const char *val); ++/* m1n1 entered at EL3 and psci=1 given: the kernel may use PSCI */ ++bool psci_active(void); ++/* Called once from _start_c with the EL m1n1 was entered at */ ++void psci_note_boot_el(u64 el); ++/* Set when m1n1 was re-entered from Linux (see psci_asm.S _reentry) */ ++bool psci_reentered(void); ++ ++/* EL1 side: SMC into our own EL3 */ ++s64 psci_smc(u64 fid, u64 a1, u64 a2, u64 a3); ++int psci_affinity_info(u64 mpidr); ++/* Park a secondary that is spinning in smp_secondary_entry() at EL3 */ ++int psci_park_cpu(int cpu, u64 mpidr); ++/* Bring a parked CPU back into m1n1 (re-entry); returns 0 if it was parked */ ++int psci_unpark_cpu(int index, u64 mpidr, void *stack_top); ++ ++/* EL3 side */ ++u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3); ++void psci_pen(void *cpu, u64 stack_top) __attribute__((noreturn)); ++void psci_secondary_entry(void); ++void _reentry(void); ++ ++#endif ++ ++#endif +diff --git a/src/psci_asm.S b/src/psci_asm.S +new file mode 100644 +index 0000000..14f842f +--- /dev/null ++++ b/src/psci_asm.S +@@ -0,0 +1,170 @@ ++/* SPDX-License-Identifier: MIT */ ++ ++#include "psci.h" ++ ++#define DCSW_OP_DCCISW 0x1 // memory.h ++ ++.extern _v_sp0_sync ++.extern _start ++.extern _cpu_reset_c ++.extern psci_handle ++.extern psci_persist ++ ++.text ++ ++/* ++ * Lower-EL synchronous exceptions. At EL3, an SMCCC call (smc #0 from EL1) ++ * goes to the PSCI dispatcher; everything else, and every exception taken ++ * at EL1/EL2, takes the usual path. ++ */ ++.globl _v_lower_sync ++.type _v_lower_sync, @function ++_v_lower_sync: ++ stp x16, x17, [sp, #-16]! ++ mrs x16, CurrentEL ++ cmp x16, #0xc ++ b.ne 1f ++ mrs x16, esr_el3 ++ lsr x17, x16, #26 ++ cmp x17, #0x17 // EC: SMC from AArch64 ++ b.ne 1f ++ tst x16, #0xffff // imm16 == 0: SMCCC ++ b.ne 1f ++ ldp x16, x17, [sp], #16 ++ b psci_smc_entry ++1: ++ ldp x16, x17, [sp], #16 ++ b _v_sp0_sync ++ ++/* ++ * SMCCC 1.0: x0-x17 may be clobbered, x18-x30 and SP_EL1 belong to the ++ * caller. x0-x3 go straight to psci_handle(). ELR/SPSR are saved in case ++ * anything below takes an exception at EL3. ++ */ ++.type psci_smc_entry, @function ++psci_smc_entry: ++ stp x29, x30, [sp, #-16]! ++ mrs x29, elr_el3 ++ mrs x30, spsr_el3 ++ stp x29, x30, [sp, #-16]! ++ str x18, [sp, #-16]! ++ bl psci_handle ++ ldr x18, [sp], #16 ++ ldp x29, x30, [sp], #16 ++ msr elr_el3, x29 ++ msr spsr_el3, x30 ++ ldp x29, x30, [sp], #16 ++ eret ++ ++/* ++ * psci_pen(struct psci_cpu *me, u64 stack_top): CPU_OFF. Never returns. ++ * ++ * The core is not powered down. It writes its L1D back, marks itself OFF and ++ * waits in WFE at EL3 (MMU off, so every access here is non-cacheable) for ++ * CPU_ON to post an entry point. It then enters EL1 there with the MMU off, ++ * a clean I-cache and TLB, and x0 = context id, like a core coming out of ++ * reset. SP_EL3 is reset to this core's own static stack. ++ */ ++.globl psci_pen ++.type psci_pen, @function ++psci_pen: ++ mov x19, x0 ++ mov x20, x1 ++ ++ // A pending timer interrupt must not keep waking WFE. ++ msr cntp_ctl_el0, xzr ++ msr cntv_ctl_el0, xzr ++ isb ++ ++ // Clean and invalidate this core's L1D (L2 is shared and coherent). ++ mov x0, #DCSW_OP_DCCISW ++ bl dcsw_op_level1 ++ dsb sy ++ ++ str xzr, [x19, #PSCI_CPU_ENTRY] ++ mov x1, #PSCI_AFF_OFF ++ str x1, [x19, #PSCI_CPU_STATE] ++ dc civac, x19 ++ dsb sy ++ sev ++ ++1: ++ wfe ++ dc civac, x19 ++ dsb sy ++ ldr x2, [x19, #PSCI_CPU_ENTRY] ++ cbz x2, 1b ++ ++ ldr x0, [x19, #PSCI_CPU_CONTEXT] ++ str xzr, [x19, #PSCI_CPU_ENTRY] ++ mov x1, #PSCI_AFF_ON ++ str x1, [x19, #PSCI_CPU_STATE] ++ dc civac, x19 ++ dsb sy ++ ++ // Nothing of the previous kernel's EL1 context may survive. ++ ic iallu ++ tlbi alle1 ++ dsb sy ++ mov x1, #0x0800 // SCTLR_EL1: RES1 bits only, MMU and caches off ++ movk x1, #0x30d0, lsl #16 ++ msr sctlr_el1, x1 ++ mov x1, #0x3c5 // EL1h, DAIF masked ++ msr spsr_el3, x1 ++ msr elr_el3, x2 ++ mov sp, x20 ++ isb ++ eret ++ ++/* EL1 side: s64 psci_smc(u64 fid, u64 a1, u64 a2, u64 a3) */ ++.globl psci_smc ++.type psci_smc, @function ++psci_smc: ++ smc #0 ++ ret ++ ++/* ++ * CPU_ON target when m1n1 takes a parked core back after a re-entry. ++ * x0 = EL1 stack top | m1n1 CPU index (stack tops are 16 KiB aligned). ++ */ ++.globl psci_secondary_entry ++.type psci_secondary_entry, @function ++psci_secondary_entry: ++ and x1, x0, #0xf ++ bic x0, x0, #0xf ++ msr tpidr_el1, x1 ++ mov sp, x0 ++ bl _cpu_reset_c ++ b . ++ ++/* ++ * Re-entry from Linux (a kexec'd trampoline, see docs/boot/warm-reboot.md): ++ * EL1, MMU and caches off, x0 = a copy of the original boot_args. ++ * Linux may hold clean lines of this image in its linear map, so drop them ++ * before m1n1 writes its .bss uncached and then reads it with caches on. ++ */ ++.globl _reentry ++.type _reentry, @function ++_reentry: ++ mov x19, x0 ++ adrp x1, _base ++ adrp x2, _end ++1: ++ dc civac, x1 ++ add x1, x1, #64 ++ cmp x1, x2 ++ b.lo 1b ++ dsb sy ++ ic iallu ++ dsb sy ++ isb ++ ++ adrp x1, psci_persist // .reentry_flag ++ add x1, x1, :lo12:psci_persist ++ mov x2, #1 ++ str x2, [x1] ++ dc civac, x1 ++ dsb sy ++ ++ mov x0, x19 ++ b _start +diff --git a/src/smp.c b/src/smp.c +index 83eadff..7f5b336 100644 +--- a/src/smp.c ++++ b/src/smp.c +@@ -8,6 +8,7 @@ + #include "malloc.h" + #include "memory.h" + #include "pmgr.h" ++#include "psci.h" + #include "soc.h" + #include "string.h" + #include "types.h" +@@ -227,6 +228,74 @@ static void smp_stop_cpu(int index, int die, int cluster, int core, u64 impl, u6 + } + } + ++/* ++ * After a re-entry from Linux, a secondary that Linux offlined waits in the ++ * PSCI pen at EL3 rather than in smp_secondary_entry(). It can't be reset ++ * through the PMGR (it is running), so hand it an EL1 entry with CPU_ON. ++ */ ++static bool smp_reclaim_cpu(int index, u64 mpidr) ++{ ++ int i; ++ ++ if (index >= MAX_CPUS || spin_table[index].flag) ++ return false; ++ ++ if (psci_affinity_info(mpidr) != PSCI_AFF_OFF) ++ return false; ++ ++ printf("Taking CPU %d back from the PSCI pen... ", index); ++ ++ memset(&spin_table[index], 0, sizeof(struct spin_table)); ++ target_cpu = index; ++ secondary_stacks[index] = memalign(0x4000, SECONDARY_STACK_SIZE); ++ ++ /* The core starts with its MMU off */ ++ dc_civac_range(&spin_table[index], sizeof(struct spin_table)); ++ dc_civac_range(&target_cpu, sizeof(target_cpu)); ++ dc_civac_range(&boot_cpu_idx, sizeof(boot_cpu_idx)); ++ dc_civac_range(&boot_cpu_mpidr, sizeof(boot_cpu_mpidr)); ++ ++ if (psci_unpark_cpu(index, mpidr, secondary_stacks[index] + SECONDARY_STACK_SIZE) < 0) { ++ printf("Failed (CPU_ON)\n"); ++ return true; ++ } ++ ++ for (i = 0; i < 100; i++) { ++ sysop("dmb ld"); ++ if (spin_table[index].flag) ++ break; ++ udelay(1000); ++ } ++ ++ printf("%s\n", i >= 100 ? "Failed!" : " Back."); ++ return true; ++} ++ ++/* ++ * Put every live secondary into the PSCI pen, for a kernel that boots them ++ * with enable-method = "psci". A CPU that doesn't make it is dropped (and ++ * then pruned from the DT by the caller). ++ */ ++int smp_park_secondaries_psci(void) ++{ ++ int parked = 0; ++ ++ for (int i = 0; i < MAX_CPUS; i++) { ++ if (i == boot_cpu_idx || !smp_is_alive(i)) ++ continue; ++ ++ u64 mpidr = spin_table[i].mpidr; ++ if (psci_park_cpu(i, mpidr) < 0) { ++ printf("PSCI: CPU %d (MPIDR 0x%lx) did not park, dropping it\n", i, mpidr); ++ memset(&spin_table[i], 0, sizeof(struct spin_table)); ++ continue; ++ } ++ parked++; ++ } ++ ++ return parked; ++} ++ + static u64 smp_get_iorvbar(int node, int cpu) + { + u64 cpu_impl_reg[2]; +@@ -405,6 +474,10 @@ void smp_start_secondaries(void) + u8 cluster = FIELD_GET(CPU_REG_CLUSTER, reg); + u8 die = FIELD_GET(CPU_REG_DIE, reg); + ++ if (psci_active() && psci_reentered() && ++ smp_reclaim_cpu(i, core | (cluster << 8) | (die << 16))) ++ continue; ++ + smp_start_cpu(i, die, cluster, core, iorvbar, pmgr_reg + cpu_start_off); + } + } +diff --git a/src/smp.h b/src/smp.h +index 0296978..6c90557 100644 +--- a/src/smp.h ++++ b/src/smp.h +@@ -18,6 +18,7 @@ void smp_secondary_prep_el3(void); + + void smp_start_secondaries(void); + void smp_stop_secondaries(bool deep_sleep); ++int smp_park_secondaries_psci(void); + + #define smp_call0(i, f) smp_call4(i, f, 0, 0, 0, 0) + #define smp_call1(i, f, a) smp_call4(i, f, a, 0, 0, 0) +diff --git a/src/start.S b/src/start.S +index aa11e18..a3f35ae 100644 +--- a/src/start.S ++++ b/src/start.S +@@ -11,6 +11,7 @@ + .extern _v_sp0_irq + .extern _v_sp0_fiq + .extern _v_sp0_serr ++.extern _v_lower_sync + .extern _reset_stack + .extern _cpu_reset_c + .extern wdt_reboot +@@ -41,7 +42,7 @@ _vectors_start: + .align 7 + b _v_sp0_serr + .align 7 +- b _v_sp0_sync ++ b _v_lower_sync + .align 7 + b _v_sp0_irq + .align 7 +@@ -103,6 +104,8 @@ _start: + + mov x0, x19 + mov x1, x20 ++ mrs x2, CurrentEL ++ lsr x2, x2, #2 + bl _start_c + b . + +@@ -115,6 +118,7 @@ start_el3: + msr tpidr_el3, xzr + adrp x6, _stack_bot + adr x7, _start_c ++ mov x2, #3 // _start_c: entered at EL3 + b el3_eret_to_el1 + + .globl exc_unk +diff --git a/src/startup.c b/src/startup.c +index 61afe16..167fae8 100644 +--- a/src/startup.c ++++ b/src/startup.c +@@ -5,6 +5,7 @@ + #include "dockchannel_uart.h" + #include "exception.h" + #include "firmware.h" ++#include "psci.h" + #include "smp.h" + #include "string.h" + #include "types.h" +@@ -159,12 +160,15 @@ void dump_boot_args(struct boot_args *ba) + } + + extern void get_device_info(void); +-void _start_c(void *boot_args, void *base) ++void _start_c(void *boot_args, void *base, u64 boot_el) + { + UNUSED(base); + u32 cpu_id = 0; + + memset64(_bss_start, 0, _bss_end - _bss_start); ++ psci_note_boot_el(boot_el); ++ if (psci_reentered()) ++ boot_cpu_idx = -1; // .data keeps the last run's value; find it again + boot_args_addr = (u64)boot_args; + memcpy(&cur_boot_args, boot_args, sizeof(cur_boot_args)); + diff --git a/boot/m1n1/patches/orchard/0016-psci-Turn-the-resident-EL3-PSCI-on-by-default.patch b/boot/m1n1/patches/orchard/0016-psci-Turn-the-resident-EL3-PSCI-on-by-default.patch new file mode 100644 index 00000000..edfc7592 --- /dev/null +++ b/boot/m1n1/patches/orchard/0016-psci-Turn-the-resident-EL3-PSCI-on-by-default.patch @@ -0,0 +1,84 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 15:56:40 +0100 +Subject: [PATCH] psci: Turn the resident EL3 PSCI on by default + +When m1n1 was entered at EL3 (A7-A9 booted by Pongo) it stays resident +and already implements CPU_ON/CPU_OFF/AFFINITY_INFO for the kernel, but +only when the payload asked for it with psci=1. Without it the kernel +gets the spin table and can neither offline CPUs nor kexec on SMP, so +every loader had to be built with the variable appended. + +Make it the default. psci=0 turns it off again (spin table, no /psci +node), psci=1 is still accepted. Nothing changes where m1n1 is entered +at EL1 or EL2: psci_active() still requires the EL3 entry. + +Assisted-by: LLM +--- + src/kboot.c | 4 ++-- + src/psci.c | 6 +++--- + src/psci.h | 4 ++-- + 3 files changed, 7 insertions(+), 7 deletions(-) + +diff --git a/src/kboot.c b/src/kboot.c +index 7123d60..a5f4238 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -576,7 +576,7 @@ static int dt_set_smbios(void) + } + + /* +- * With psci=1 and m1n1 owning EL3 (A7-A9), CPUs boot through m1n1's PSCI ++ * When m1n1 owns EL3 (A7-A9), CPUs boot through m1n1's PSCI + * (see psci.c) instead of the spin table, so the kernel can offline them + * for kexec and CPU hotplug. Also tells Linux how to re-enter m1n1. + */ +@@ -586,7 +586,7 @@ static int dt_set_psci(void) + int node; + + if (!psci_active()) { +- printf("PSCI: off (needs psci=1 and m1n1 entered at EL3)\n"); ++ printf("PSCI: off (m1n1 not entered at EL3, or psci=0)\n"); + return 0; + } + +diff --git a/src/psci.c b/src/psci.c +index 2b8ca0d..25e2a4f 100644 +--- a/src/psci.c ++++ b/src/psci.c +@@ -28,8 +28,8 @@ struct { + u64 boot_el3; + } psci_persist __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); + +-/* psci=1 from the payload, kept across a re-entry */ +-static u32 psci_mode __attribute__((section(".data"))); ++/* On unless the payload says psci=0; kept across a re-entry */ ++static u32 psci_mode __attribute__((section(".data"))) = 1; + + /* EL3 stacks a CPU gets when it leaves the pen */ + static u8 psci_stacks[PSCI_MAX_CPUS][PSCI_STACK_SIZE] __attribute__((aligned(16))); +@@ -177,7 +177,7 @@ void psci_note_boot_el(u64 el) + + void psci_configure(const char *val) + { +- psci_mode = val[0] == '1'; ++ psci_mode = val[0] != '0'; + } + + bool psci_active(void) +diff --git a/src/psci.h b/src/psci.h +index 6769ebe..9a213af 100644 +--- a/src/psci.h ++++ b/src/psci.h +@@ -50,9 +50,9 @@ + + #ifndef __ASSEMBLER__ + +-/* Payload variable psci=1 */ ++/* Payload variable psci=0 turns PSCI off; anything else (psci=1) leaves it on */ + void psci_configure(const char *val); +-/* m1n1 entered at EL3 and psci=1 given: the kernel may use PSCI */ ++/* m1n1 entered at EL3 and not psci=0: the kernel uses PSCI */ + bool psci_active(void); + /* Called once from _start_c with the EL m1n1 was entered at */ + void psci_note_boot_el(u64 el); diff --git a/boot/m1n1/patches/orchard/0017-psci-Trap-EL1-WFI-WFE-to-EL3-for-a-firmware-CPU-rend.patch b/boot/m1n1/patches/orchard/0017-psci-Trap-EL1-WFI-WFE-to-EL3-for-a-firmware-CPU-rend.patch new file mode 100644 index 00000000..a1c26412 --- /dev/null +++ b/boot/m1n1/patches/orchard/0017-psci-Trap-EL1-WFI-WFE-to-EL3-for-a-firmware-CPU-rend.patch @@ -0,0 +1,286 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 15:58:57 +0100 +Subject: [PATCH] psci: Trap EL1 WFI/WFE to EL3 for a firmware CPU rendezvous + +An ordinary arm64 reboot or power-off does not offline the secondaries +with CPU_OFF: smp_send_stop() leaves them spinning in "wfe; wfi" at EL1 +with interrupts masked, still ON as far as the monitor knows. A9 has no +way to reset a core from another one (Apple and m1n1 power cores down +from the core itself), so SYSTEM_RESET and SYSTEM_OFF need those CPUs to +come to EL3 on their own. + +Give the monitor that hook: a private SiP call, made by m1n1 on the boot +CPU right before it starts a kernel, sets SCR_EL3.TWI and TWE there and +on every CPU that later leaves the pen, and records the AIC and watchdog +bases for later EL3 use. A trapped WFE returns at once (a spurious +wake-up is architectural, and the exception return may have cleared the +exclusive monitor whose event the caller waits for); a trapped WFI waits +at EL3 with IRQ/FIQ routed there only around the wfi, so an interrupt +meant for EL1 still wakes it. While a rendezvous is pending, a trapped +CPU instead parks itself in the pen exactly as CPU_OFF would, cleaning +its L1D on the way. + +The trap enable has no user yet. The CPUs that have been in the pen are +recorded, so a rendezvous knows which ones the kernel was given. + +Assisted-by: LLM +--- + src/psci.c | 56 ++++++++++++++++++++++++++++++++++++++ + src/psci.h | 20 ++++++++++++++ + src/psci_asm.S | 73 ++++++++++++++++++++++++++++++++++++++++++++++++-- + 3 files changed, 147 insertions(+), 2 deletions(-) + +diff --git a/src/psci.c b/src/psci.c +index 25e2a4f..af4f325 100644 +--- a/src/psci.c ++++ b/src/psci.c +@@ -28,6 +28,24 @@ struct { + u64 boot_el3; + } psci_persist __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); + ++/* ++ * Rendezvous state, one line, only accessed at EL3 with the MMU off. A CPU ++ * that wants all others in the pen (SYSTEM_RESET/SYSTEM_OFF) sets pending; ++ * every other CPU then parks itself at its next trapped WFI/WFE. ++ */ ++struct psci_rdv { ++ volatile u64 pending; ++ volatile u64 traps; /* SCR_EL3.TWI|TWE for CPUs leaving the pen */ ++ volatile u64 cpus; /* MPIDR indices that have been in the pen: handed to the kernel */ ++ u64 aic; ++ u64 wdt; ++ u64 pad[PSCI_CPU_SIZE / 8 - 5]; ++} psci_rdv __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ ++_Static_assert(sizeof(struct psci_rdv) == PSCI_CPU_SIZE, "psci_rdv layout"); ++_Static_assert(offsetof(struct psci_rdv, pending) == PSCI_RDV_PENDING, "psci_rdv layout"); ++_Static_assert(offsetof(struct psci_rdv, traps) == PSCI_RDV_TRAPS, "psci_rdv layout"); ++ + /* On unless the payload says psci=0; kept across a re-entry */ + static u32 psci_mode __attribute__((section(".data"))) = 1; + +@@ -62,6 +80,10 @@ static s64 psci_cpu_off(void) + if (idx < 0) + return PSCI_RET_DENIED; + ++ psci_line_sync(&psci_rdv); ++ psci_rdv.cpus |= BIT(idx); ++ psci_line_sync(&psci_rdv); ++ + psci_pen(&psci_cpus[idx], (u64)&psci_stacks[idx][PSCI_STACK_SIZE]); + } + +@@ -128,6 +150,32 @@ static s64 psci_features(u32 fid) + } + } + ++/* Arms (or disarms) the WFx traps on this CPU and on every CPU leaving the pen */ ++static s64 psci_m1n1_handoff(u64 traps, u64 aic, u64 wdt) ++{ ++ u64 bits = traps ? (SCR_EL3_TWI | SCR_EL3_TWE) : 0; ++ ++ psci_rdv.pending = 0; ++ psci_rdv.traps = bits; ++ psci_rdv.aic = aic; ++ psci_rdv.wdt = wdt; ++ psci_line_sync(&psci_rdv); ++ ++ msr(SCR_EL3, (mrs(SCR_EL3) & ~(u64)(SCR_EL3_TWI | SCR_EL3_TWE)) | bits); ++ sysop("isb"); ++ ++ return PSCI_RET_SUCCESS; ++} ++ ++/* A trapped WFI/WFE saw the rendezvous flag: become CPU_OFF right here */ ++void psci_rendezvous_off(void) ++{ ++ psci_cpu_off(); ++ /* Not a CPU we know (can't happen after the pen saw it): stay out of the way */ ++ for (;;) ++ sysop("wfe"); ++} ++ + u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) + { + u32 fn = fid; +@@ -154,6 +202,8 @@ u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) + return 2; /* no trusted OS, nothing to migrate */ + case PSCI_1_0_FN_PSCI_FEATURES: + return psci_features(a1); ++ case PSCI_M1N1_SIP_HANDOFF: ++ return psci_m1n1_handoff(a1, a2, a3); + default: + /* SYSTEM_RESET/SYSTEM_OFF too: the caller falls back to its own */ + return PSCI_RET_NOT_SUPPORTED; +@@ -190,6 +240,12 @@ bool psci_reentered(void) + return reentered; + } + ++void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base) ++{ ++ if (psci_active()) ++ psci_smc(PSCI_M1N1_SIP_HANDOFF, wfx_traps, aic_base, wdt_base); ++} ++ + int psci_affinity_info(u64 mpidr) + { + return psci_smc(PSCI_0_2_FN64_AFFINITY_INFO, mpidr, 0, 0); +diff --git a/src/psci.h b/src/psci.h +index 9a213af..1d3a88e 100644 +--- a/src/psci.h ++++ b/src/psci.h +@@ -27,6 +27,13 @@ + #define PSCI_0_2_FN64_CPU_ON 0xc4000003 + #define PSCI_0_2_FN64_AFFINITY_INFO 0xc4000004 + ++/* ++ * m1n1-private SiP call, made by m1n1 itself on the boot CPU right before it ++ * starts a kernel: x1 = 1 to trap EL1 WFI/WFE to EL3 on this and every CPU ++ * that later leaves the pen, x2 = AIC base, x3 = primary watchdog base. ++ */ ++#define PSCI_M1N1_SIP_HANDOFF 0xc200ff00 ++ + #define PSCI_RET_SUCCESS 0 + #define PSCI_RET_NOT_SUPPORTED -1 + #define PSCI_RET_INVALID_PARAMS -2 +@@ -48,6 +55,15 @@ + #define PSCI_CPU_CONTEXT 0x10 + #define PSCI_CPU_SIZE 0x80 + ++/* struct psci_rdv, one line, EL3 only (MMU off) */ ++#define PSCI_RDV_PENDING 0x00 ++#define PSCI_RDV_TRAPS 0x08 ++ ++#define SCR_EL3_IRQ (1 << 1) ++#define SCR_EL3_FIQ (1 << 2) ++#define SCR_EL3_TWI (1 << 12) ++#define SCR_EL3_TWE (1 << 13) ++ + #ifndef __ASSEMBLER__ + + /* Payload variable psci=0 turns PSCI off; anything else (psci=1) leaves it on */ +@@ -67,9 +83,13 @@ int psci_park_cpu(int cpu, u64 mpidr); + /* Bring a parked CPU back into m1n1 (re-entry); returns 0 if it was parked */ + int psci_unpark_cpu(int index, u64 mpidr, void *stack_top); + ++/* EL1, boot CPU, last thing before the kernel: see PSCI_M1N1_SIP_HANDOFF */ ++void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base); ++ + /* EL3 side */ + u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3); + void psci_pen(void *cpu, u64 stack_top) __attribute__((noreturn)); ++void psci_rendezvous_off(void) __attribute__((noreturn)); + void psci_secondary_entry(void); + void _reentry(void); + +diff --git a/src/psci_asm.S b/src/psci_asm.S +index 14f842f..ff31f3a 100644 +--- a/src/psci_asm.S ++++ b/src/psci_asm.S +@@ -9,13 +9,16 @@ + .extern _cpu_reset_c + .extern psci_handle + .extern psci_persist ++.extern psci_rdv ++.extern psci_rendezvous_off + + .text + + /* + * Lower-EL synchronous exceptions. At EL3, an SMCCC call (smc #0 from EL1) +- * goes to the PSCI dispatcher; everything else, and every exception taken +- * at EL1/EL2, takes the usual path. ++ * goes to the PSCI dispatcher and a trapped WFI/WFE to psci_wfx_entry; ++ * everything else, and every exception taken at EL1/EL2, takes the usual ++ * path. + */ + .globl _v_lower_sync + .type _v_lower_sync, @function +@@ -26,6 +29,8 @@ _v_lower_sync: + b.ne 1f + mrs x16, esr_el3 + lsr x17, x16, #26 ++ cmp x17, #0x01 // EC: trapped WFI/WFE ++ b.eq 2f + cmp x17, #0x17 // EC: SMC from AArch64 + b.ne 1f + tst x16, #0xffff // imm16 == 0: SMCCC +@@ -35,6 +40,59 @@ _v_lower_sync: + 1: + ldp x16, x17, [sp], #16 + b _v_sp0_sync ++2: ++ ldp x16, x17, [sp], #16 ++ b psci_wfx_entry ++ ++/* ++ * WFI/WFE from EL1/EL0, trapped by SCR_EL3.TWI/TWE (see PSCI_M1N1_SIP_HANDOFF). ++ * This is how firmware gets hold of a CPU that the kernel stopped with an ++ * IPI and left spinning in "wfe; wfi" with interrupts masked (an ordinary ++ * reboot does that; only kexec and hotplug use CPU_OFF): while a ++ * SYSTEM_RESET/SYSTEM_OFF rendezvous is pending, the CPU parks itself as if ++ * it had called CPU_OFF. Otherwise: ++ * - WFE completes at once. A spurious WFE wake-up is architectural, and ++ * waiting here could miss the event the caller waits for (the exception ++ * return may clear its exclusive monitor). ++ * - WFI waits here, with IRQ/FIQ routed to EL3 only around the wfi so that ++ * an interrupt for EL1 wakes it (DAIF is masked at EL3, nothing is taken). ++ * Both then return past the instruction. Only x0-x3 are used. ++ */ ++.type psci_wfx_entry, @function ++psci_wfx_entry: ++ stp x0, x1, [sp, #-16]! ++ stp x2, x3, [sp, #-16]! ++ adrp x0, psci_rdv ++ add x0, x0, :lo12:psci_rdv ++ dc civac, x0 ++ dsb sy ++ ldr x1, [x0, #PSCI_RDV_PENDING] ++ cbnz x1, 3f ++ mrs x1, esr_el3 ++ tbnz x1, #0, 2f // ISS.TI bit 0 set: WFE (or WFET) ++ mrs x2, scr_el3 ++ orr x3, x2, #(SCR_EL3_IRQ | SCR_EL3_FIQ) ++ msr scr_el3, x3 ++ isb ++ dsb sy ++ wfi ++ msr scr_el3, x2 ++ isb ++ dc civac, x0 ++ dsb sy ++ ldr x1, [x0, #PSCI_RDV_PENDING] ++ cbnz x1, 3f ++2: ++ mrs x1, elr_el3 ++ add x1, x1, #4 ++ msr elr_el3, x1 ++ ldp x2, x3, [sp], #16 ++ ldp x0, x1, [sp], #16 ++ eret ++3: ++ add sp, sp, #32 ++ bl psci_rendezvous_off ++ b . + + /* + * SMCCC 1.0: x0-x17 may be clobbered, x18-x30 and SP_EL1 belong to the +@@ -106,6 +164,17 @@ psci_pen: + ic iallu + tlbi alle1 + dsb sy ++ ++ // WFI/WFE traps as the boot CPU set them for the kernel (psci_rdv.traps) ++ adrp x3, psci_rdv ++ add x3, x3, :lo12:psci_rdv ++ dc civac, x3 ++ dsb sy ++ ldr x4, [x3, #PSCI_RDV_TRAPS] ++ mrs x1, scr_el3 ++ bic x1, x1, #(SCR_EL3_TWI | SCR_EL3_TWE) ++ orr x1, x1, x4 ++ msr scr_el3, x1 + mov x1, #0x0800 // SCTLR_EL1: RES1 bits only, MMU and caches off + movk x1, #0x30d0, lsl #16 + msr sctlr_el1, x1 diff --git a/boot/m1n1/patches/orchard/0018-psci-Implement-SYSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch b/boot/m1n1/patches/orchard/0018-psci-Implement-SYSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch new file mode 100644 index 00000000..e5dd449e --- /dev/null +++ b/boot/m1n1/patches/orchard/0018-psci-Implement-SYSTEM_RESET-and-SYSTEM_OFF-with-a-CP.patch @@ -0,0 +1,497 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 16:01:53 +0100 +Subject: [PATCH] psci: Implement SYSTEM_RESET and SYSTEM_OFF with a CPU + rendezvous + +Give the kernel's ordinary reboot and power-off paths somewhere to go. +On SYSTEM_RESET or SYSTEM_OFF the calling CPU sets the rendezvous flag, +sends an event and an AIC IPI to every other CPU the kernel was given, +and waits up to a second for all of them to park in the pen through the +WFI/WFE traps. If one does not arrive, it clears the flag and returns +without having changed anything, so the kernel goes on to its next +restart or power-off method as it does today. + +Once they are parked it disarms the primary watchdog (the kernel, or +systemd's reboot watchdog, may have armed it), stops its timers and +drops the traps. SYSTEM_RESET then turns the EL1 MMU and caches off, +writes back every data cache level by set/way, invalidates the EL1 TLB +and I-cache and restarts m1n1 at EL1h with the original iBoot boot_args, +saved in .data on the first cold start before top_of_memory_alloc() +changes them. psci_reentered() and the new psci_reset_reentered() tell +the restarted m1n1 where it came from. SYSTEM_OFF parks the caller too: +there is no PMU driver here, so this halts rather than powers off. + +The payload variable psci_reset selects what SYSTEM_RESET leads to: boot +(the default, used by the next change), proxy, or 0 for NOT_SUPPORTED +with no WFI/WFE traps. It is reported by PSCI_FEATURES and armed by +kboot_boot() through the private handoff call, which also records the +boot CPU and needs an AIC v1 and a watchdog in the ADT. + +Assisted-by: LLM +--- + src/kboot.c | 1 + + src/payload.c | 2 + + src/psci.c | 220 +++++++++++++++++++++++++++++++++++++++++++++++-- + src/psci.h | 25 +++++- + src/psci_asm.S | 44 +++++++++- + src/startup.c | 1 + + 6 files changed, 280 insertions(+), 13 deletions(-) + +diff --git a/src/kboot.c b/src/kboot.c +index a5f4238..299e388 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -3301,6 +3301,7 @@ int kboot_boot(void *kernel) + + printf("Setting SMP mode to WFE...\n"); + smp_set_wfe_mode(true); ++ psci_handoff(); + printf("Preparing to boot kernel at %p with fdt at %p\n", kernel, dt); + + next_stage.entry = kernel; +diff --git a/src/payload.c b/src/payload.c +index 4ffc9e2..0b7dc14 100644 +--- a/src/payload.c ++++ b/src/payload.c +@@ -208,6 +208,8 @@ static bool check_var(u8 **p) + enable_tso = val[0] == '1'; + } else if (IS_VAR("psci=")) { + psci_configure(val); ++ } else if (IS_VAR("psci_reset=")) { ++ psci_reset_configure(val); + } else { + printf("Unknown variable %s\n", *p); + } +diff --git a/src/psci.c b/src/psci.c +index af4f325..1821ebc 100644 +--- a/src/psci.c ++++ b/src/psci.c +@@ -1,9 +1,19 @@ + /* SPDX-License-Identifier: MIT */ + + #include "psci.h" ++#include "adt.h" ++#include "aic_regs.h" ++#include "memory.h" + #include "smp.h" ++#include "string.h" + #include "types.h" + #include "utils.h" ++#include "xnuboot.h" ++ ++#define WDT_CTL 0x1c ++ ++/* SYSTEM_RESET/SYSTEM_OFF wait this long for the other CPUs to park */ ++#define RENDEZVOUS_TIMEOUT_US 1000000 + + struct psci_cpu { + volatile u64 state; +@@ -39,7 +49,8 @@ struct psci_rdv { + volatile u64 cpus; /* MPIDR indices that have been in the pen: handed to the kernel */ + u64 aic; + u64 wdt; +- u64 pad[PSCI_CPU_SIZE / 8 - 5]; ++ u64 reset; /* enum psci_reset_mode the kernel was started with */ ++ u64 pad[PSCI_CPU_SIZE / 8 - 6]; + } psci_rdv __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); + + _Static_assert(sizeof(struct psci_rdv) == PSCI_CPU_SIZE, "psci_rdv layout"); +@@ -49,11 +60,18 @@ _Static_assert(offsetof(struct psci_rdv, traps) == PSCI_RDV_TRAPS, "psci_rdv lay + /* On unless the payload says psci=0; kept across a re-entry */ + static u32 psci_mode __attribute__((section(".data"))) = 1; + ++/* psci_reset=0|proxy|boot, kept across a re-entry */ ++static u32 psci_reset_mode __attribute__((section(".data"))) = PSCI_RESET_BOOT; ++ ++/* The iBoot boot_args m1n1 was first started with; SYSTEM_RESET starts m1n1 with them again */ ++static struct boot_args reset_boot_args __attribute__((section(".data"), aligned(PSCI_CPU_SIZE))); ++ + /* EL3 stacks a CPU gets when it leaves the pen */ + static u8 psci_stacks[PSCI_MAX_CPUS][PSCI_STACK_SIZE] __attribute__((aligned(16))); + + static bool psci_have_el3; + static bool reentered; ++static bool reset_reentered; + + /* EL3 side */ + +@@ -129,9 +147,113 @@ static s64 psci_affinity(u64 mpidr, u64 level) + return psci_cpus[idx].state; + } + ++/* ++ * Gets every other CPU the kernel was given into the pen: they park at their ++ * next trapped WFI/WFE (see psci_wfx_entry). A CPU waiting in WFI is woken ++ * with an IPI, one in WFE with an event. If one doesn't arrive in time, ++ * nothing has been changed that the kernel cares about. ++ */ ++static bool psci_rendezvous(int self) ++{ ++ psci_line_sync(&psci_rdv); ++ u64 others = psci_rdv.cpus & ~BIT(self); ++ ++ psci_rdv.pending = 1; ++ psci_line_sync(&psci_rdv); ++ sysop("sev"); ++ ++ for (int i = 0; i < PSCI_MAX_CPUS; i++) { ++ if (!(others & BIT(i))) ++ continue; ++ psci_line_sync(&psci_cpus[i]); ++ if (psci_cpus[i].state != PSCI_AFF_OFF) ++ write32(psci_rdv.aic + AIC_IPI_SEND, AIC_IPI_SEND_CPU(i)); ++ } ++ ++ u64 timeout = timeout_calculate(RENDEZVOUS_TIMEOUT_US); ++ do { ++ bool all = true; ++ ++ for (int i = 0; i < PSCI_MAX_CPUS; i++) { ++ if (!(others & BIT(i))) ++ continue; ++ psci_line_sync(&psci_cpus[i]); ++ if (psci_cpus[i].state != PSCI_AFF_OFF) ++ all = false; ++ } ++ if (all) { ++ psci_rdv.pending = 0; ++ psci_line_sync(&psci_rdv); ++ return true; ++ } ++ sysop("sev"); ++ udelay(100); ++ } while (!timeout_expired(timeout)); ++ ++ psci_rdv.pending = 0; ++ psci_line_sync(&psci_rdv); ++ return false; ++} ++ ++/* Everything that must happen before this CPU leaves the kernel for good */ ++static s64 psci_system_stop(void) ++{ ++ int self = psci_mpidr_index(mrs(MPIDR_EL1)); ++ ++ psci_line_sync(&psci_rdv); ++ if (psci_rdv.reset == PSCI_RESET_OFF) ++ return PSCI_RET_NOT_SUPPORTED; ++ if (self < 0 || !psci_rendezvous(self)) ++ return PSCI_RET_INTERNAL_FAILURE; ++ ++ msr(CNTP_CTL_EL0, 0); ++ msr(CNTV_CTL_EL0, 0); ++ /* The kernel (or systemd's reboot watchdog) may have armed it */ ++ write32(psci_rdv.wdt + WDT_CTL, 0); ++ read32(psci_rdv.wdt + WDT_CTL); ++ ++ /* ++ * The WFx traps go away with the kernel; the next handoff sets them ++ * again, and the next kboot records which CPUs the next kernel gets. ++ */ ++ psci_rdv.traps = 0; ++ psci_rdv.cpus = 0; ++ psci_line_sync(&psci_rdv); ++ msr(SCR_EL3, mrs(SCR_EL3) & ~(u64)(SCR_EL3_TWI | SCR_EL3_TWE)); ++ sysop("isb"); ++ return PSCI_RET_SUCCESS; ++} ++ ++/* Returns only if the other CPUs could not be stopped: the kernel tries its next method */ ++static s64 psci_system_reset(void) ++{ ++ s64 ret = psci_system_stop(); ++ ++ if (ret != PSCI_RET_SUCCESS) ++ return ret; ++ ++ psci_system_reset_entry(&reset_boot_args); ++} ++ ++/* No PMU driver here: this halts the SoC with every CPU parked, it does not power off */ ++static s64 psci_system_off(void) ++{ ++ s64 ret = psci_system_stop(); ++ ++ if (ret != PSCI_RET_SUCCESS) ++ return ret; ++ ++ for (;;) ++ sysop("wfi"); ++} ++ + static s64 psci_features(u32 fid) + { + switch (fid) { ++ case PSCI_0_2_FN_SYSTEM_OFF: ++ case PSCI_0_2_FN_SYSTEM_RESET: ++ psci_line_sync(&psci_rdv); ++ return psci_rdv.reset != PSCI_RESET_OFF ? PSCI_RET_SUCCESS : PSCI_RET_NOT_SUPPORTED; + case PSCI_0_2_FN_PSCI_VERSION: + case PSCI_0_2_FN_CPU_OFF: + case PSCI_0_2_FN_CPU_ON: +@@ -144,21 +266,35 @@ static s64 psci_features(u32 fid) + default: + /* + * Includes SMCCC_VERSION (so callers assume SMCCC 1.0 and make +- * no other SMCs), CPU_SUSPEND, SYSTEM_OFF and SYSTEM_RESET. ++ * no other SMCs), CPU_SUSPEND and SYSTEM_RESET2. + */ + return PSCI_RET_NOT_SUPPORTED; + } + } + +-/* Arms (or disarms) the WFx traps on this CPU and on every CPU leaving the pen */ +-static s64 psci_m1n1_handoff(u64 traps, u64 aic, u64 wdt) ++/* ++ * Arms (or disarms) the WFx traps on this CPU and on every CPU leaving the ++ * pen, and with them SYSTEM_RESET/SYSTEM_OFF (reset = enum psci_reset_mode). ++ */ ++static s64 psci_m1n1_handoff(u64 reset, u64 aic, u64 wdt) + { +- u64 bits = traps ? (SCR_EL3_TWI | SCR_EL3_TWE) : 0; ++ if (reset > PSCI_RESET_PROXY || (reset && (!aic || !wdt))) ++ return PSCI_RET_INVALID_PARAMS; ++ ++ u64 bits = reset ? (SCR_EL3_TWI | SCR_EL3_TWE) : 0; ++ int self = psci_mpidr_index(mrs(MPIDR_EL1)); ++ ++ if (self < 0) ++ return PSCI_RET_DENIED; + ++ psci_line_sync(&psci_rdv); ++ /* The secondaries were added when kboot parked them */ ++ psci_rdv.cpus |= BIT(self); + psci_rdv.pending = 0; + psci_rdv.traps = bits; + psci_rdv.aic = aic; + psci_rdv.wdt = wdt; ++ psci_rdv.reset = reset; + psci_line_sync(&psci_rdv); + + msr(SCR_EL3, (mrs(SCR_EL3) & ~(u64)(SCR_EL3_TWI | SCR_EL3_TWE)) | bits); +@@ -202,10 +338,13 @@ u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3) + return 2; /* no trusted OS, nothing to migrate */ + case PSCI_1_0_FN_PSCI_FEATURES: + return psci_features(a1); ++ case PSCI_0_2_FN_SYSTEM_RESET: ++ return psci_system_reset(); ++ case PSCI_0_2_FN_SYSTEM_OFF: ++ return psci_system_off(); + case PSCI_M1N1_SIP_HANDOFF: + return psci_m1n1_handoff(a1, a2, a3); + default: +- /* SYSTEM_RESET/SYSTEM_OFF too: the caller falls back to its own */ + return PSCI_RET_NOT_SUPPORTED; + } + } +@@ -216,6 +355,7 @@ void psci_note_boot_el(u64 el) + { + /* MMU still off: these .data accesses are uncached, like the writers' */ + if (psci_persist.reentry_flag) { ++ reset_reentered = psci_persist.reentry_flag == PSCI_REENTRY_RESET; + reentered = true; + psci_persist.reentry_flag = 0; + } +@@ -240,10 +380,72 @@ bool psci_reentered(void) + return reentered; + } + +-void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base) ++void psci_reset_configure(const char *val) ++{ ++ if (val[0] == '0') ++ psci_reset_mode = PSCI_RESET_OFF; ++ else if (!strncmp(val, "proxy", 5)) ++ psci_reset_mode = PSCI_RESET_PROXY; ++ else ++ psci_reset_mode = PSCI_RESET_BOOT; ++} ++ ++enum psci_reset_mode psci_reset_mode_get(void) + { +- if (psci_active()) +- psci_smc(PSCI_M1N1_SIP_HANDOFF, wfx_traps, aic_base, wdt_base); ++ return psci_reset_mode; ++} ++ ++bool psci_reset_reentered(void) ++{ ++ return reset_reentered; ++} ++ ++void psci_reset_save_boot_args(void) ++{ ++ if (reentered) ++ return; ++ memcpy(&reset_boot_args, &cur_boot_args, sizeof(reset_boot_args)); ++ dc_civac_range(&reset_boot_args, sizeof(reset_boot_args)); ++} ++ ++/* ++ * The CPU rendezvous needs the AIC v1 IPI (A7-A9) and the primary watchdog ++ * (wdt-version 1 there, nothing else to stop). Without them SYSTEM_RESET and ++ * SYSTEM_OFF stay NOT_SUPPORTED and the kernel uses its own methods. ++ */ ++void psci_handoff(void) ++{ ++ enum psci_reset_mode reset = psci_reset_mode; ++ u64 aic = 0, wdt = 0; ++ int path[8]; ++ ++ if (!psci_active()) ++ return; ++ ++ if (reset != PSCI_RESET_OFF) { ++ int node = adt_path_offset_trace(adt, "/arm-io/aic", path); ++ if (node < 0 || !adt_is_compatible(adt, node, "aic,1") || ++ adt_get_reg(adt, path, "reg", 0, &aic, NULL) < 0) { ++ printf("PSCI: SYSTEM_RESET/SYSTEM_OFF unavailable: no AIC v1\n"); ++ reset = PSCI_RESET_OFF; ++ } ++ node = adt_path_offset_trace(adt, "/arm-io/wdt", path); ++ if (node < 0 || adt_get_reg(adt, path, "reg", 0, &wdt, NULL) < 0) { ++ printf("PSCI: SYSTEM_RESET/SYSTEM_OFF unavailable: no watchdog\n"); ++ reset = PSCI_RESET_OFF; ++ } ++ } ++ ++ if (psci_smc(PSCI_M1N1_SIP_HANDOFF, reset, aic, wdt) != PSCI_RET_SUCCESS) { ++ printf("PSCI: handoff refused, SYSTEM_RESET/SYSTEM_OFF unavailable\n"); ++ psci_smc(PSCI_M1N1_SIP_HANDOFF, PSCI_RESET_OFF, 0, 0); ++ return; ++ } ++ ++ static const char *const names[] = {"off", "warm reboot into the default entry", ++ "warm reboot into the proxy"}; ++ printf("PSCI: SYSTEM_RESET: %s%s\n", names[reset], ++ reset ? "; EL1 WFI/WFE trap to EL3" : ""); + } + + int psci_affinity_info(u64 mpidr) +diff --git a/src/psci.h b/src/psci.h +index 1d3a88e..7c81af7 100644 +--- a/src/psci.h ++++ b/src/psci.h +@@ -29,8 +29,9 @@ + + /* + * m1n1-private SiP call, made by m1n1 itself on the boot CPU right before it +- * starts a kernel: x1 = 1 to trap EL1 WFI/WFE to EL3 on this and every CPU +- * that later leaves the pen, x2 = AIC base, x3 = primary watchdog base. ++ * starts a kernel: x1 = enum psci_reset_mode (nonzero also traps EL1 WFI/WFE ++ * to EL3 on this and every CPU that later leaves the pen), x2 = AIC base, ++ * x3 = primary watchdog base. + */ + #define PSCI_M1N1_SIP_HANDOFF 0xc200ff00 + +@@ -59,6 +60,10 @@ + #define PSCI_RDV_PENDING 0x00 + #define PSCI_RDV_TRAPS 0x08 + ++/* psci_persist.reentry_flag */ ++#define PSCI_REENTRY_KEXEC 1 ++#define PSCI_REENTRY_RESET 2 ++ + #define SCR_EL3_IRQ (1 << 1) + #define SCR_EL3_FIQ (1 << 2) + #define SCR_EL3_TWI (1 << 12) +@@ -83,13 +88,27 @@ int psci_park_cpu(int cpu, u64 mpidr); + /* Bring a parked CPU back into m1n1 (re-entry); returns 0 if it was parked */ + int psci_unpark_cpu(int index, u64 mpidr, void *stack_top); + ++/* What SYSTEM_RESET does: payload variable psci_reset=0|proxy|boot (default boot) */ ++enum psci_reset_mode { ++ PSCI_RESET_OFF = 0, /* NOT_SUPPORTED: the kernel resets the SoC its own way */ ++ PSCI_RESET_BOOT = 1, /* back into m1n1, which boots the default boot entry */ ++ PSCI_RESET_PROXY = 2, /* back into m1n1, which only runs the proxy */ ++}; ++void psci_reset_configure(const char *val); ++enum psci_reset_mode psci_reset_mode_get(void); ++/* Set when m1n1 was re-entered by SYSTEM_RESET (psci_reentered() is too) */ ++bool psci_reset_reentered(void); ++/* _start_c: keep the first boot_args, SYSTEM_RESET starts m1n1 with them */ ++void psci_reset_save_boot_args(void); ++ + /* EL1, boot CPU, last thing before the kernel: see PSCI_M1N1_SIP_HANDOFF */ +-void psci_handoff(bool wfx_traps, u64 aic_base, u64 wdt_base); ++void psci_handoff(void); + + /* EL3 side */ + u64 psci_handle(u64 fid, u64 a1, u64 a2, u64 a3); + void psci_pen(void *cpu, u64 stack_top) __attribute__((noreturn)); + void psci_rendezvous_off(void) __attribute__((noreturn)); ++void psci_system_reset_entry(void *boot_args) __attribute__((noreturn)); + void psci_secondary_entry(void); + void _reentry(void); + +diff --git a/src/psci_asm.S b/src/psci_asm.S +index ff31f3a..61a6a02 100644 +--- a/src/psci_asm.S ++++ b/src/psci_asm.S +@@ -230,10 +230,52 @@ _reentry: + + adrp x1, psci_persist // .reentry_flag + add x1, x1, :lo12:psci_persist +- mov x2, #1 ++ mov x2, #PSCI_REENTRY_KEXEC + str x2, [x1] + dc civac, x1 + dsb sy + + mov x0, x19 + b _start ++ ++/* ++ * psci_system_reset_entry(boot_args): the end of SYSTEM_RESET, at EL3 on the ++ * calling CPU once every other CPU is in the pen (L1D clean) and nothing ++ * else runs. The kernel's EL1 MMU and caches may still be on; its stack and ++ * memory are never used again. Turn EL1 translation and caching off, write ++ * all data caches back, flag the reset and restart m1n1 at EL1h with the ++ * original boot_args, like a cold boot. _start resets the stack and reapplies ++ * the relocations; SP_EL3 goes back to the boot stack. ++ */ ++.globl psci_system_reset_entry ++.type psci_system_reset_entry, @function ++psci_system_reset_entry: ++ msr daifset, #0xf ++ mov x19, x0 ++ mov x1, #0x0800 // SCTLR_EL1: RES1 bits only, MMU and caches off ++ movk x1, #0x30d0, lsl #16 ++ msr sctlr_el1, x1 ++ isb ++ mov x0, #DCSW_OP_DCCISW ++ bl dcsw_op_all ++ tlbi alle1 ++ ic iallu ++ dsb sy ++ isb ++ ++ adrp x1, psci_persist // .reentry_flag ++ add x1, x1, :lo12:psci_persist ++ mov x2, #PSCI_REENTRY_RESET ++ str x2, [x1] ++ dc civac, x1 ++ dsb sy ++ ++ adrp x1, _start ++ add x1, x1, :lo12:_start ++ msr elr_el3, x1 ++ mov x1, #0x3c5 // EL1h, DAIF masked ++ msr spsr_el3, x1 ++ adrp x1, _stack_bot_el3 ++ mov sp, x1 ++ mov x0, x19 ++ eret +diff --git a/src/startup.c b/src/startup.c +index 167fae8..4d9ac39 100644 +--- a/src/startup.c ++++ b/src/startup.c +@@ -171,6 +171,7 @@ void _start_c(void *boot_args, void *base, u64 boot_el) + boot_cpu_idx = -1; // .data keeps the last run's value; find it again + boot_args_addr = (u64)boot_args; + memcpy(&cur_boot_args, boot_args, sizeof(cur_boot_args)); ++ psci_reset_save_boot_args(); + + adt = + (void *)(((u64)cur_boot_args.devtree) - cur_boot_args.virt_base + cur_boot_args.phys_base); diff --git a/boot/m1n1/patches/orchard/0019-bootmgr-Boot-the-default-entry-after-a-PSCI-SYSTEM_R.patch b/boot/m1n1/patches/orchard/0019-bootmgr-Boot-the-default-entry-after-a-PSCI-SYSTEM_R.patch new file mode 100644 index 00000000..a8039549 --- /dev/null +++ b/boot/m1n1/patches/orchard/0019-bootmgr-Boot-the-default-entry-after-a-PSCI-SYSTEM_R.patch @@ -0,0 +1,298 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 9 Nov 2026 16:03:26 +0100 +Subject: [PATCH] bootmgr: Boot the default entry after a PSCI SYSTEM_RESET + +SYSTEM_RESET brings m1n1 back with every other CPU parked and the +original boot_args, so m1n1 can finish the reboot itself the way the +host's orchard launch starts a system: bring main storage up, take the +first bootable entry in BLS order (the same default the host suggests; +there is still no remembered default) and start it with bootmgr_start(). +The board DTB file name comes from the last launch, which m1n1 now keeps +in .data; if no entry was ever launched (a kernel from the payload, say) +m1n1 doesn't guess the board and stays in the proxy. + +A re-entered m1n1 also skips the stale payload area for the logo, keeps +the picture on screen instead of clearing it and drawing the logo (the +entry's splash replaces it), and leaves SEP alone, which was never +reset. A 96x96 square at the top left shows how far it got: cyan back in +m1n1, green loading the entry, magenta no previous launch, red storage +or entry failure. Any failure ends in the USB proxy, as a cold boot with +nothing to do does; m1n1 never turns it into a cold reset. + +With psci_reset=proxy it stops in the proxy right after the re-entry. + +Assisted-by: LLM +--- + rust/src/bls.rs | 48 +++++++++++++++++++++++++++++ + src/bootmgr.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ + src/bootmgr.h | 2 ++ + src/fb.c | 4 ++- + src/main.c | 16 +++++++--- + 5 files changed, 146 insertions(+), 5 deletions(-) + +diff --git a/rust/src/bls.rs b/rust/src/bls.rs +index 35ddc1b..4570d63 100644 +--- a/rust/src/bls.rs ++++ b/rust/src/bls.rs +@@ -629,6 +629,54 @@ pub unsafe extern "C" fn rust_boot_entries( + text.leak().as_mut_ptr() as *mut c_void + } + ++/// The entry a boot without a host starts: the first bootable one in BLS ++/// order, as the host's `orchard launch` suggests (there is no remembered ++/// default). ++pub fn default_entry(listing: &Listing) -> Option<&Entry> { ++ listing.entries.iter().find(|entry| entry.bootable()) ++} ++ ++/// Writes the id of main storage's default entry (see `default_entry`), ++/// NUL-terminated, to `id` (`size` bytes). Main storage must be up; ++/// `devicetree` may be NULL. Returns 0, or -1 with the reason on the console. ++#[no_mangle] ++pub unsafe extern "C" fn rust_boot_default_entry( ++ devicetree: *const c_char, ++ id: *mut c_char, ++ size: c_size_t, ++) -> c_int { ++ let devicetree = match devicetree.is_null() { ++ true => None, ++ false => unsafe { CStr::from_ptr(devicetree) }.to_str().ok(), ++ }; ++ let listing = read_entries(storage::MainStorage::new, devicetree); ++ log(&listing); ++ let entry = match &listing { ++ Err(error) => { ++ println!("bls: {}", error); ++ return -1; ++ } ++ Ok(listing) => match default_entry(listing) { ++ Some(entry) => entry, ++ None => { ++ println!("bls: no bootable entry"); ++ return -1; ++ } ++ }, ++ }; ++ let name = entry.id.as_bytes(); ++ if name.len() >= size || name.contains(&0) { ++ println!("bls: entry id too long"); ++ return -1; ++ } ++ unsafe { ++ core::ptr::copy_nonoverlapping(name.as_ptr(), id as *mut u8, name.len()); ++ *id.add(name.len()) = 0; ++ } ++ println!("bls: default entry {}", entry.id); ++ 0 ++} ++ + #[repr(C)] + pub struct BootFiles { + kernel: *mut c_void, +diff --git a/src/bootmgr.c b/src/bootmgr.c +index b91b6a6..a746673 100644 +--- a/src/bootmgr.c ++++ b/src/bootmgr.c +@@ -8,6 +8,7 @@ + #include "heapblock.h" + #include "kboot.h" + #include "mitigations.h" ++#include "psci.h" + #include "smp.h" + #include "string.h" + #include "utils.h" +@@ -32,6 +33,32 @@ struct boot_files { + + int rust_boot_entry_load(const char *id, const char *devicetree, bool splash, + struct boot_files *files); ++int rust_boot_default_entry(const char *devicetree, char *id, size_t size); ++ ++/* ++ * The board DTB file name of the last launch (the host's orchard launch ++ * passes it), kept across a SYSTEM_RESET so m1n1 can boot an entry without ++ * the host and without guessing the board. ++ */ ++static struct { ++ bool launched; ++ bool have_devicetree; ++ char devicetree[MAX_NAME]; ++} last_launch __attribute__((section(".data"))); ++ ++/* Warm reset progress, top left */ ++#define MARK_REENTERED 0x00b7ff ++#define MARK_LOADING 0x00c853 ++#define MARK_NO_LAUNCH 0xff00ff ++#define MARK_FAILED 0xff1744 ++ ++static void bootmgr_mark(u32 color) ++{ ++ if (!fb.ptr) ++ return; ++ fb_fill(0, 0, min(fb.width, 96U), min(fb.height, 96U), int2rgb(color)); ++ fb_update(); ++} + + /* Called from Rust: the entry's splash, pixels as b, g, r, 0, centred on black. */ + void bootmgr_paint_splash(const void *pixels, u32 width, u32 height) +@@ -104,6 +131,11 @@ int bootmgr_start(const char *id_arg, const char *devicetree_arg, u64 flags) + return -1; + } + ++ last_launch.launched = true; ++ last_launch.have_devicetree = devicetree_arg; ++ if (devicetree_arg) ++ memcpy(last_launch.devicetree, devicetree, sizeof(devicetree)); ++ + /* The kernel, initrd and DT are m1n1's now, past any limit the host set. */ + heapblock_set_limit(NULL); + +@@ -136,8 +168,57 @@ int bootmgr_start(const char *id_arg, const char *devicetree_arg, u64 flags) + return kboot_boot(kernel); + } + ++/* ++ * m1n1 was restarted by a PSCI SYSTEM_RESET from the kernel: every other CPU ++ * is parked, the watchdog is off and memory is as after a cold start. Do ++ * what the host's orchard launch would: bring up main storage and start the ++ * default entry with the last launch's board DTB. Returns 0 when the kernel ++ * is set up to run, -1 (proxy) otherwise. ++ */ ++int bootmgr_warm_reset(void) ++{ ++ char id[MAX_NAME]; ++ const char *devicetree = last_launch.have_devicetree ? last_launch.devicetree : NULL; ++ ++ bootmgr_mark(MARK_REENTERED); ++ if (psci_reset_mode_get() == PSCI_RESET_PROXY) { ++ printf("bootmgr: warm reset: psci_reset=proxy, not booting\n"); ++ return -1; ++ } ++ if (!last_launch.launched) { ++ printf("bootmgr: warm reset: no boot entry was launched before, not guessing the board\n"); ++ bootmgr_mark(MARK_NO_LAUNCH); ++ return -1; ++ } ++ ++ printf("bootmgr: warm reset: starting the default entry (devicetree %s)\n", ++ devicetree ? devicetree : "from the entry"); ++ if (!main_storage_init()) { ++ printf("bootmgr: warm reset: main storage did not come up\n"); ++ bootmgr_mark(MARK_FAILED); ++ return -1; ++ } ++ if (rust_boot_default_entry(devicetree, id, sizeof(id)) < 0) { ++ main_storage_shutdown(); ++ bootmgr_mark(MARK_FAILED); ++ return -1; ++ } ++ bootmgr_mark(MARK_LOADING); ++ if (bootmgr_start(id, devicetree, 0) < 0) { ++ bootmgr_mark(MARK_FAILED); ++ return -1; ++ } ++ return 0; ++} ++ + #else + ++int bootmgr_warm_reset(void) ++{ ++ printf("bootmgr: not supported in this build\n"); ++ return -1; ++} ++ + int bootmgr_start(const char *id, const char *devicetree, u64 flags) + { + UNUSED(id); +diff --git a/src/bootmgr.h b/src/bootmgr.h +index 8a03baa..f3b86ee 100644 +--- a/src/bootmgr.h ++++ b/src/bootmgr.h +@@ -8,5 +8,7 @@ + #define BOOTMGR_NO_SPLASH BIT(0) + + int bootmgr_start(const char *id, const char *devicetree, u64 flags); ++/* After a PSCI SYSTEM_RESET: boot the default entry the way the last launch did */ ++int bootmgr_warm_reset(void); + + #endif +diff --git a/src/fb.c b/src/fb.c +index ba606b2..0aa88b8 100644 +--- a/src/fb.c ++++ b/src/fb.c +@@ -1,6 +1,7 @@ + /* SPDX-License-Identifier: MIT */ + + #include "fb.h" ++#include "psci.h" + #include "assert.h" + #include "iodev.h" + #include "malloc.h" +@@ -464,14 +465,16 @@ void fb_init(bool clear) + &orig_logo); + } + ++ // After a re-entry from Linux the payload area behind m1n1 is stale ++ bool payload_ok = !psci_reentered(); + u32 *splash, splash_w, splash_h; +- if (payload_splash(&splash, &splash_w, &splash_h) && splash_w <= fb.width && ++ if (payload_ok && payload_splash(&splash, &splash_w, &splash_h) && splash_w <= fb.width && + splash_h <= fb.height) { + custom_logo.ptr = splash; + custom_logo.width = splash_w; + custom_logo.height = splash_h; + logo = &custom_logo; +- } else if (payload_logo(&custom_128, &custom_256)) { ++ } else if (payload_ok && payload_logo(&custom_128, &custom_256)) { + custom_logo = *logo; + if (custom_logo.width == 256) { + custom_logo.ptr = custom_256; +diff --git a/src/main.c b/src/main.c +index 3f32380..8966e41 100644 +--- a/src/main.c ++++ b/src/main.c +@@ -7,6 +7,7 @@ + + #include "adt.h" + #include "aic.h" ++#include "bootmgr.h" + #include "cpufreq.h" + #include "display.h" + #include "exception.h" +@@ -127,6 +128,9 @@ void run_actions(void) + printf("Valid payload found\n"); + return; + } ++ ++ if (psci_reset_reentered() && bootmgr_warm_reset() == 0) ++ return; + fb_set_active(true); + + printf("No valid payload found\n"); +@@ -173,9 +177,11 @@ void m1n1_main(void) + // Kick DCP to sleep, so dodgy monitors which cause reconnect cycles don't cause us to lose the + // framebuffer. + display_shutdown(DCP_SLEEP_IF_EXTERNAL); +- // On idevice we need to always clear, because otherwise it looks scuffed on white devices +- fb_init(!is_mac); +- fb_display_logo(); ++ // On idevice we need to always clear, because otherwise it looks scuffed on white devices. ++ // After a warm reset, keep the picture until the next system's splash. ++ fb_init(!is_mac && !psci_reset_reentered()); ++ if (!psci_reset_reentered()) ++ fb_display_logo(); + #ifdef FB_SILENT_MODE + fb_set_active(!cur_boot_args.video.display); + #else +@@ -184,7 +190,9 @@ void m1n1_main(void) + #endif + + cpufreq_fixup(); +- sep_init(); ++ // SEP was never reset; it is still running what it ran at the first boot ++ if (!psci_reset_reentered()) ++ sep_init(); + #endif + + printf("Initialization complete.\n"); diff --git a/boot/m1n1/patches/orchard/0020-kboot-Mark-retained-hardware-after-PSCI-re-entry.patch b/boot/m1n1/patches/orchard/0020-kboot-Mark-retained-hardware-after-PSCI-re-entry.patch new file mode 100644 index 00000000..08935588 --- /dev/null +++ b/boot/m1n1/patches/orchard/0020-kboot-Mark-retained-hardware-after-PSCI-re-entry.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Wed, 11 Nov 2026 12:00:00 +0100 +Subject: [PATCH] kboot: Mark retained hardware after PSCI re-entry + +SYSTEM_RESET re-enters m1n1 without resetting all peripherals. Mark the +next kernel's /chosen with the existing linux,booted-from-kexec boolean +so retained hardware is not mistaken for the cold iBoot baseline. + +Use psci_reentered(), which covers both SYSTEM_RESET and the kexec +trampoline. This also preserves the contract when kboot loads a fresh +BLS devicetree instead of inheriting the outgoing kernel's tree. + +The Orchard kernel's G5P, PMGR, PCIe/ANS and AOP consumers all use this +property to select retained-state handling, with their existing hardware +and certification guards. The only other reader is PowerPC-only. No +kernel consumer needs a separate PSCI-specific property. + +Assisted-by: LLM +--- +diff --git a/src/kboot.c b/src/kboot.c +index 1d4b26f..7dc9874 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -278,6 +278,13 @@ static int dt_set_chosen(void) + printf("FDT: %s = '%s'\n", name, value); + } + ++ /* Re-entry retains peripherals, just like a kernel-to-kernel handoff. */ ++ if (psci_reentered()) { ++ if (fdt_setprop(dt, node, "linux,booted-from-kexec", NULL, 0)) ++ bail("FDT: couldn't mark retained hardware after m1n1 re-entry\n"); ++ printf("FDT: linux,booted-from-kexec (m1n1 re-entry)\n"); ++ } ++ + if (initrd_start && initrd_size) { + if (fdt_setprop_u64(dt, node, "linux,initrd-start", (u64)initrd_start)) + bail("FDT: couldn't set chosen.linux,initrd-start property\n"); diff --git a/boot/m1n1/psci-loader b/boot/m1n1/psci-loader new file mode 100755 index 00000000..c21f1801 --- /dev/null +++ b/boot/m1n1/psci-loader @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Append m1n1 PSCI payload variables to a bare m1n1.bin. + +m1n1 entered at EL3 (A7-A9 from Pongo) provides PSCI by default, including +SYSTEM_RESET as a warm reboot into the default boot entry; a bare m1n1.bin +needs no framing for that. Use this to get the other settings, before +assembling any kernel/DTB/logo payload. The output works with +orchard run/build --loader or launch's loaders/m1n1.bin. m1n1 entered at EL1 +always uses the spin table. +""" +import argparse +from pathlib import Path +import shutil + +RESET = {'boot': b'', 'proxy': b'psci_reset=proxy\n', 'off': b'psci_reset=0\n'} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--device', required=True, choices=('j71t',)) + parser.add_argument('--loader', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + group = parser.add_mutually_exclusive_group() + group.add_argument('--system-reset', choices=tuple(RESET), default='boot', + help='boot: warm reboot into the default entry (default); proxy: stop in ' + 'the m1n1 proxy; off: NOT_SUPPORTED, kernel resets the SoC itself') + group.add_argument('--no-psci', action='store_true', help='psci=0: spin table, no PSCI') + args = parser.parse_args() + if not args.loader.is_file() or not args.loader.stat().st_size: + parser.error('input must be a nonempty bare m1n1.bin') + # Exclusive creation: never replace the original or another boot artifact. + with args.loader.open('rb') as source, args.output.open('xb') as output: + shutil.copyfileobj(source, output) + output.write(b'psci=0\n' if args.no_psci else b'psci=1\n' + RESET[args.system_reset]) + print(args.output) + + +if __name__ == '__main__': + main() diff --git a/kernel/patches/devices/s8000/0013-arm64-dts-apple-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/devices/s8000/0013-arm64-dts-apple-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..dab3a029 --- /dev/null +++ b/kernel/patches/devices/s8000/0013-arm64-dts-apple-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] arm64: dts: apple: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Describe the board-selected retained handoff. + +Assisted-by: LLM +--- + arch/arm64/boot/dts/apple/s8003-j71t.dts | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/arch/arm64/boot/dts/apple/s8003-j71t.dts b/arch/arm64/boot/dts/apple/s8003-j71t.dts +index 733cefc66..0bc19727d 100644 +--- a/arch/arm64/boot/dts/apple/s8003-j71t.dts ++++ b/arch/arm64/boot/dts/apple/s8003-j71t.dts +@@ -11,6 +11,14 @@ + #include "s8003.dtsi" + #include "s800x-ipad5.dtsi" + ++&pcie0 { ++ apple,ans-kexec-handoff; ++}; ++ ++&pcie0_dart0 { ++ apple,ans-dart-handoff; ++}; ++ + / { + compatible = "apple,j71t", "apple,s8003", "apple,arm-platform"; + model = "Apple iPad 5 (Wi-Fi) (TSMC)"; diff --git a/kernel/patches/devices/s800x/0002-arm64-dts-apple-preserve-PMU-and-RTC-interrupt-owner.patch b/kernel/patches/devices/s800x/0002-arm64-dts-apple-preserve-PMU-and-RTC-interrupt-owner.patch new file mode 100644 index 00000000..ff7c75cc --- /dev/null +++ b/kernel/patches/devices/s800x/0002-arm64-dts-apple-preserve-PMU-and-RTC-interrupt-owner.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] arm64: dts: apple: preserve PMU and RTC interrupt ownership + +Shared wake events must have one interrupt owner. Preserve the PMU and +RTC event split so that acknowledgement by one consumer cannot steal the +other consumer's wake event. + +Assisted-by: LLM +--- + arch/arm64/boot/dts/apple/s800x-ipad5.dtsi | 7 +++++-- + 1 file changed, 5 insertions(+), 2 deletions(-) + +diff --git a/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi b/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi +index 2ce40b469..20de6953f 100644 +--- a/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi ++++ b/arch/arm64/boot/dts/apple/s800x-ipad5.dtsi +@@ -191,11 +191,11 @@ + interrupt-parent = <&pmu_gpio>; + /* + * The PMU line, ADC done, then the events the iPadOS +- * charger subscribes to. ++ * charger subscribes to, except RTC event 16: Linux gives that ++ * event exclusively to rtc@500, not to source identification. + */ + interrupts = <240 IRQ_TYPE_EDGE_RISING>, + <176 IRQ_TYPE_EDGE_RISING>, +- <16 IRQ_TYPE_EDGE_RISING>, + <40 IRQ_TYPE_EDGE_RISING>, + <41 IRQ_TYPE_EDGE_RISING>, + <42 IRQ_TYPE_EDGE_RISING>, +@@ -218,6 +218,9 @@ + reg = <0x500 0x10>; + nvmem-cells = <&rtc_offset>; + nvmem-cell-names = "rtc_offset"; ++ interrupt-parent = <&pmu_gpio>; ++ interrupts = <16 IRQ_TYPE_EDGE_RISING>; ++ wakeup-source; + }; + + /* D2365 register 0x31c bit 0 */ diff --git a/kernel/patches/drivers/base/0001-core-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/base/0001-core-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..c3550de8 --- /dev/null +++ b/kernel/patches/drivers/base/0001-core-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] core: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +Assisted-by: LLM +--- + kernel/kexec_core.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/kernel/kexec_core.c b/kernel/kexec_core.c +index dc770b9a6..819650bcd 100644 +--- a/kernel/kexec_core.c ++++ b/kernel/kexec_core.c +@@ -53,6 +53,7 @@ atomic_t __kexec_lock = ATOMIC_INIT(0); + + /* Flag to indicate we are going to kexec a new kernel */ + bool kexec_in_progress = false; ++EXPORT_SYMBOL_GPL(kexec_in_progress); + + bool kexec_file_dbg_print; + diff --git a/kernel/patches/drivers/base/0002-printk-export-kmsg_dump_desc-for-module-cold-reset-d.patch b/kernel/patches/drivers/base/0002-printk-export-kmsg_dump_desc-for-module-cold-reset-d.patch new file mode 100644 index 00000000..054cba10 --- /dev/null +++ b/kernel/patches/drivers/base/0002-printk-export-kmsg_dump_desc-for-module-cold-reset-d.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 00:54:33 +0100 +Subject: [PATCH] printk: export kmsg_dump_desc for module cold-reset + diagnostics + +The VXD shutdown fail-safe is a GPL module. Allow it to dump emergency +evidence before a cold reset, matching the built-in GPU and PCIe paths. +No printk behavior changes. + +Assisted-by: LLM +--- + kernel/printk/printk.c | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c +index fcc51b699..a812ec1e5 100644 +--- a/kernel/printk/printk.c ++++ b/kernel/printk/printk.c +@@ -4882,6 +4882,7 @@ void kmsg_dump_desc(enum kmsg_dump_reason reason, const char *desc) + } + rcu_read_unlock(); + } ++EXPORT_SYMBOL_GPL(kmsg_dump_desc); + + /** + * kmsg_dump_get_line - retrieve one kmsg log line diff --git a/kernel/patches/drivers/iommu/0011-dt-bindings-iommu-keep-the-certified-ANS-kexec-hando.patch b/kernel/patches/drivers/iommu/0011-dt-bindings-iommu-keep-the-certified-ANS-kexec-hando.patch new file mode 100644 index 00000000..e442e50e --- /dev/null +++ b/kernel/patches/drivers/iommu/0011-dt-bindings-iommu-keep-the-certified-ANS-kexec-hando.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] dt-bindings: iommu: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Describe the board-selected retained handoff. + +Assisted-by: LLM +--- + Documentation/devicetree/bindings/iommu/apple,dart.yaml | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/Documentation/devicetree/bindings/iommu/apple,dart.yaml b/Documentation/devicetree/bindings/iommu/apple,dart.yaml +index df72722fc..adcbd21fc 100644 +--- a/Documentation/devicetree/bindings/iommu/apple,dart.yaml ++++ b/Documentation/devicetree/bindings/iommu/apple,dart.yaml +@@ -21,6 +21,10 @@ description: |+ + fault conditions. + + properties: ++ apple,ans-dart-handoff: ++ type: boolean ++ description: Allow the storage endpoint to retire and rebuild this DART stream during kexec. ++ + compatible: + oneOf: + - enum: diff --git a/kernel/patches/drivers/iommu/0012-iommu-apple-dart-keep-the-certified-ANS-kexec-handof.patch b/kernel/patches/drivers/iommu/0012-iommu-apple-dart-keep-the-certified-ANS-kexec-handof.patch new file mode 100644 index 00000000..e5204e73 --- /dev/null +++ b/kernel/patches/drivers/iommu/0012-iommu-apple-dart-keep-the-certified-ANS-kexec-handof.patch @@ -0,0 +1,56 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] iommu/apple-dart: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +Assisted-by: LLM +--- + drivers/iommu/apple-dart.c | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c +index 1ee6add55..5aff4bc3e 100644 +--- a/drivers/iommu/apple-dart.c ++++ b/drivers/iommu/apple-dart.c +@@ -35,6 +35,7 @@ + #include + + #include ++#include + + #include "dma-iommu.h" + +@@ -1009,6 +1010,29 @@ apple_dart_setup_translation(struct apple_dart_domain *domain, + return dart->hw->invalidate_tlb(stream_map); + } + ++int apple_ans_dart_stop(struct device *dev) ++{ ++ struct iommu_domain *domain = iommu_get_domain_for_dev(dev); ++ struct apple_dart_master_cfg *cfg = dev_iommu_priv_get(dev); ++ struct apple_dart_stream_map *map; ++ int i; ++ ++ if (!domain || !cfg || ++ !cfg->stream_maps[0].dart || ++ !of_property_read_bool(cfg->stream_maps[0].dart->dev->of_node, ++ "apple,ans-dart-handoff") || ++ (domain->type != IOMMU_DOMAIN_DMA && domain->type != IOMMU_DOMAIN_DMA_FQ)) ++ return -ENODEV; ++ for_each_stream_map(i, cfg, map) { ++ if (map->dart->hw->type != DART_S5L8960X) ++ return -EOPNOTSUPP; ++ apple_dart_hw_disable_dma(map); ++ } ++ dev_info(dev, "Apple ANS: DART stopped for handoff\n"); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_dart_stop); ++ + static int apple_dart_finalize_domain(struct apple_dart_domain *dart_domain, + struct apple_dart_master_cfg *cfg) + { diff --git a/kernel/patches/drivers/pci/0033-dt-bindings-pci-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/pci/0033-dt-bindings-pci-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..c4b7b0ee --- /dev/null +++ b/kernel/patches/drivers/pci/0033-dt-bindings-pci-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] dt-bindings: pci: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Describe the board-selected retained handoff. + +Assisted-by: LLM +--- + Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml b/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml +index f6497a4e1..f9b72ce26 100644 +--- a/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml ++++ b/Documentation/devicetree/bindings/pci/apple,t8010-pcie.yaml +@@ -140,6 +140,10 @@ allOf: + - apple,nvme-hmb + + properties: ++ apple,ans-kexec-handoff: ++ type: boolean ++ description: Retain storage port power across a certified stopped-DMA kexec handoff. ++ + compatible: + oneOf: + - enum: diff --git a/kernel/patches/drivers/pci/0034-PCI-apple-h9p-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/pci/0034-PCI-apple-h9p-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..e030657f --- /dev/null +++ b/kernel/patches/drivers/pci/0034-PCI-apple-h9p-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,668 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] PCI: apple-h9p: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +The storage port is handed over powered, with its reference clock running, +PERST# asserted and the LTSSM stopped: the state the boot loader leaves it +in, except that the link is down. The incoming kernel brings it up as on a +cold boot, with the full port power cycle, NVMe mode and the port enable +sequence before the link is trained. iOS trains this link only after that +sequence and never re-trains a powered port by toggling PERST# alone; +doing so after a kexec leaves the root port in Detect.Quiet with the LTSSM +enable not latched. + +Assisted-by: LLM +--- + drivers/pci/controller/pcie-apple-h9p.c | 459 +++++++++++++++++++++--- + include/linux/apple-ans.h | 23 ++ + 2 files changed, 432 insertions(+), 50 deletions(-) + +diff --git a/drivers/pci/controller/pcie-apple-h9p.c b/drivers/pci/controller/pcie-apple-h9p.c +index 15f7a0cbe..f0095394a 100644 +--- a/drivers/pci/controller/pcie-apple-h9p.c ++++ b/drivers/pci/controller/pcie-apple-h9p.c +@@ -15,6 +15,8 @@ + + #include + #include ++#include ++#include + #include + #include + #include +@@ -38,12 +40,15 @@ + #include + #include + #include ++#include + #include + #include + #include ++#include + + #include + #include ++#include + + #include "pci-host-common.h" + +@@ -248,7 +253,7 @@ + #define S8003_PORT_LINKCDMSTS_LTSSM GENMASK(14, 9) + #define S8003_LTSSM_L0 0x11 + +-/* ADT perst-to-refclk-delay-us and pci-bridge t-refclk-to-perst (j71t) */ ++/* ADT perst-to-refclk-delay-us and pci-bridge t-refclk-to-perst (S8003) */ + #define S8003_PERST_TO_REFCLK_US 60 + #define S8003_REFCLK_TO_PERST_US 100 + #define S8003_PERST_HOLD_MS 100 +@@ -269,7 +274,7 @@ + + /* + * NVMMU invalidate FIFO status, in the port 0 block (iBoot sets the live ADT +- * nvme-mmu0 invalidate-fifo-workaround = 1 on j71t). iOS (AppleNVMeMMU, ++ * nvme-mmu0 invalidate-fifo-workaround = 1 on S8003). iOS (AppleNVMeMMU, + * 0xfffffff005eebc9c) waits up to 20 ms for it to drain before every + * invalidate and never reads the invalidate register back. + */ +@@ -429,7 +434,7 @@ static const struct apple_h9p_tunable t7000_port_tunables[] = { + }; + + /* +- * S8003 tunables, which iBoot puts in the live ADT; copied from the j71t ++ * S8003 tunables, which iBoot puts in the live ADT; copied from the S8003 + * live ADT (iOS 16.7.10 iBoot), where both bridges carry the same values. + * apcie-phy-tunables and apcie-common-tunables apply after the common init, + * in that order. +@@ -582,6 +587,7 @@ struct apple_h9p_tunables { + struct apple_h9p_nvmmu { + struct apple_h9p_pcie *pcie; + void __iomem *base; ++ bool poisoned; + /* Programmed: the ANS glue may map through it */ + bool ready; + /* Serializes the invalidate FIFO wait and the invalidate write */ +@@ -711,6 +717,7 @@ struct apple_h9p_pcie { + u32 late_ports; + unsigned int late_tries; + struct delayed_work late_work; ++ bool ans_handoff_stopped; + + struct apple_h9p_nvmmu nvmmu; + +@@ -1554,6 +1561,64 @@ static irqreturn_t apple_h9p_nvmmu_irq(int irq, void *data) + return IRQ_HANDLED; + } + ++static int apple_h9p_nvmmu_program(struct apple_h9p_pcie *pcie) ++{ ++ struct apple_h9p_nvmmu *nvmmu = &pcie->nvmmu; ++ u32 val; ++ int ret; ++ ++ if (pcie->hw->nvmmu_inv_fifo) { ++ /* ++ * iOS's S8003 order: the invalidate FIFO drained, the old SART ++ * aperture off before its address changes, the zeroed scratch ++ * page and TCBs visible, then scratch page, TCB base, init, SART. ++ */ ++ ret = readl_poll_timeout(pcie->base_port[0] + ++ S8003_PORT_NVMMU_INV_FIFO, val, ++ val & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ if (ret) ++ return ret; ++ h9p_writel_flush(0, nvmmu->base + H9P_NVMMU_SART_CTRL); ++ dma_wmb(); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_HI); ++ } else { ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), ++ nvmmu->base + H9P_NVMMU_TCB_BASE_HI); ++ h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); ++ h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), ++ nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); ++ } ++ h9p_writel_flush(H9P_NVMMU_TCB_CTRL_INIT, ++ nvmmu->base + H9P_NVMMU_TCB_CTRL); ++ ++ ret = apple_h9p_wait(nvmmu->base + H9P_NVMMU_TCB_CTRL, ++ pcie->hw->nvmmu_init_busy, 0, 0, 250000); ++ if (ret) ++ return ret; ++ ++ h9p_writel_flush(nvmmu->va_base - 0x80000000U, ++ nvmmu->base + H9P_NVMMU_SART_VA_BASE); ++ h9p_writel_flush(round_up(nvmmu->va_base + nvmmu->size, ++ H9P_NVMMU_SART_ALIGNMENT) - 0x80100000U, ++ nvmmu->base + H9P_NVMMU_SART_VA_END); ++ h9p_writel_flush(nvmmu->pa_base >> 20, ++ nvmmu->base + H9P_NVMMU_SART_PA_BASE); ++ h9p_writel_flush(1, nvmmu->base + H9P_NVMMU_SART_CTRL); ++ ++ return 0; ++} ++ + static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + { + struct apple_h9p_nvmmu *nvmmu = &pcie->nvmmu; +@@ -1561,7 +1626,7 @@ static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + struct device_node *mem_np; + struct resource res; + resource_size_t size; +- u32 iova, val; ++ u32 iova; + int irq; + int ret; + +@@ -1630,55 +1695,10 @@ static int apple_h9p_setup_nvmmu(struct apple_h9p_pcie *pcie) + } + + spin_lock_init(&nvmmu->inv_lock); +- if (pcie->hw->nvmmu_inv_fifo) { +- /* +- * iOS's S8003 order: the invalidate FIFO drained, the old SART +- * aperture off before its address changes, the zeroed scratch +- * page and TCBs visible, then scratch page, TCB base, init, SART. +- */ +- ret = readl_poll_timeout(pcie->base_port[0] + +- S8003_PORT_NVMMU_INV_FIFO, val, +- val & S8003_PORT_NVMMU_INV_FIFO_EMPTY, +- 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); +- if (ret) +- goto out_put_node; +- h9p_writel_flush(0, nvmmu->base + H9P_NVMMU_SART_CTRL); +- dma_wmb(); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_HI); +- } else { +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_dma), +- nvmmu->base + H9P_NVMMU_TCB_BASE_HI); +- h9p_writel_flush(lower_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_LO); +- h9p_writel_flush(upper_32_bits(nvmmu->tcb_table_dma), +- nvmmu->base + H9P_NVMMU_TCB_TABLE_HI); +- } +- h9p_writel_flush(H9P_NVMMU_TCB_CTRL_INIT, +- nvmmu->base + H9P_NVMMU_TCB_CTRL); +- +- ret = apple_h9p_wait(nvmmu->base + H9P_NVMMU_TCB_CTRL, +- pcie->hw->nvmmu_init_busy, 0, 0, 250000); ++ ret = apple_h9p_nvmmu_program(pcie); + if (ret) + goto out_put_node; + +- h9p_writel_flush(nvmmu->va_base - 0x80000000U, +- nvmmu->base + H9P_NVMMU_SART_VA_BASE); +- h9p_writel_flush(round_up(nvmmu->va_base + nvmmu->size, +- H9P_NVMMU_SART_ALIGNMENT) - 0x80100000U, +- nvmmu->base + H9P_NVMMU_SART_VA_END); +- h9p_writel_flush(nvmmu->pa_base >> 20, +- nvmmu->base + H9P_NVMMU_SART_PA_BASE); +- h9p_writel_flush(1, nvmmu->base + H9P_NVMMU_SART_CTRL); +- + irq = platform_get_irq_byname_optional(pcie->pdev, "nvmmu"); + if (irq > 0) { + ret = devm_request_irq(dev, irq, apple_h9p_nvmmu_irq, 0, +@@ -1726,6 +1746,7 @@ static int apple_h9p_nvmmu_invalidate(struct apple_h9p_pcie *pcie, + spin_unlock_irqrestore(&nvmmu->inv_lock, flags); + + if (ret) { ++ WRITE_ONCE(nvmmu->poisoned, true); + dev_err_ratelimited(pcie->dev, + "NVMMU invalidate FIFO not empty (%#x), tag %u\n", + val, tag); +@@ -1781,6 +1802,8 @@ int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + nvmmu = &pcie->nvmmu; + if (!nvmmu->ready) + return -EOPNOTSUPP; ++ if (READ_ONCE(nvmmu->poisoned)) ++ return -EIO; + + tcb = (u32 *)nvmmu->tcb + tag * H9P_NVMMU_TCB_DWORDS; + sgl = (u32 *)nvmmu->tcb_sgl + tag * H9P_NVMMU_SGL_WORDS; +@@ -2705,6 +2728,8 @@ static int apple_s8003_power_cycle_port(struct apple_h9p_pcie *pcie, + return ret; + } + ++ if (!port && pcie->nvmmu.base) ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); + apple_s8003_port_off(pcie, port); + usleep_range(S8003_POWER_OFF_SETTLE_US, S8003_POWER_OFF_SETTLE_US + 1000); + ret = apple_h9p_wait(rc + S8003_RC_PORT_CTL(port), +@@ -2887,6 +2912,18 @@ static int apple_s8003_start_link(struct apple_h9p_pcie *pcie, + return ret; + } + ++static bool apple_s8003_port_powered(struct apple_h9p_pcie *pcie) ++{ ++ void __iomem *rc = pcie->base_rc; ++ ++ return (readl(rc + S8003_RC_PORT_CTL(0)) & S8003_RC_PORT_CTL_ENABLE) && ++ (readl(rc + S8003_RC_PORT_PWR(0)) & S8003_RC_PORT_PWR_ON) && ++ (readl(rc + S8003_RC_PORT_REFCLK(0)) & S8003_RC_PORT_REFCLK_ON) && ++ !(readl(rc + S8003_RC_PORT_CTL(0)) & S8003_RC_PORT_CTL_RESET) && ++ (readl(rc + S8003_RC_PORT_PWR(0)) & S8003_RC_PORT_PWR_REFCLK_NOGATE) && ++ (readl(rc + S8003_RC_PORT_READY(0)) & S8003_RC_PORT_READY_DONE); ++} ++ + /* Power the port up as validated for its kind, and train its link once */ + static int apple_s8003_setup_port_once(struct apple_h9p_pcie *pcie, + unsigned int port) +@@ -3059,6 +3096,11 @@ static void apple_s8003_late_ports_work(struct work_struct *work) + unsigned int port; + int ret; + ++ /* Lab 0019: no secondary bus publication without storage bus 1. */ ++ if (of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie") && ++ !(READ_ONCE(pcie->enabled_ports) & BIT(0))) ++ return; ++ + for_each_set_bit(port, &pending, H9P_NUM_PORTS) { + ret = apple_s8003_setup_endpoint_port(pcie, port); + if (ret == -EPROBE_DEFER) { +@@ -3418,6 +3460,314 @@ static void apple_h9p_pcie_pwrctrl_cleanup(void *data) + pci_pwrctrl_destroy_devices(pcie->dev); + } + ++/* SPDX-License-Identifier: GPL-2.0-only */ ++static struct apple_h9p_pcie *apple_ans_endpoint_host(struct device *dev) ++{ ++ struct device *host = dev; ++ struct apple_h9p_pcie *pcie; ++ ++ while (host && host->bus == dev->bus) ++ host = host->parent; ++ if (!host || !host->parent) ++ return NULL; ++ pcie = apple_h9p_pcie_lookup(host->parent); ++ if (!pcie || !of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie") || ++ apple_h9p_pcie_device_port(pcie, dev) != 0) ++ return NULL; ++ return pcie; ++} ++ ++ ++/* release6: caller froze and synchronized I/O, before SHN. */ ++/* Lab 0018: caller synchronized queues and stopped CC/RDY before this. */ ++int apple_ans_nvmmu_reset(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ struct apple_h9p_nvmmu *nvmmu; ++ int ret; ++ ++ if (!pcie || !(pcie->enabled_ports & BIT(0))) ++ return -ENODEV; ++ nvmmu = &pcie->nvmmu; ++ if (!nvmmu->ready) ++ return -ENODEV; ++ memset(nvmmu->tcb, 0, nvmmu->tcb_size); ++ memset(nvmmu->tcb_table, 0, nvmmu->tcb_table_size); ++ memset(nvmmu->tcb_sgl, 0, nvmmu->tcb_sgl_size); ++ ret = apple_h9p_nvmmu_program(pcie); ++ WRITE_ONCE(nvmmu->poisoned, !!ret); ++ if (ret) ++ return ret; ++ bitmap_zero(nvmmu->mapped, APPLE_ANS_NVMMU_MAX_REQS); ++ dev_info(dev, "NVMMU: stopped-controller cache/SART rebuilt\n"); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_nvmmu_reset); ++ ++bool apple_ans_is_s8003(struct device *dev) ++{ ++ return apple_ans_endpoint_host(dev) != NULL; ++} ++EXPORT_SYMBOL_GPL(apple_ans_is_s8003); ++ ++int apple_ans_link_up(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return -ENODEV; ++ if (!(READ_ONCE(pcie->enabled_ports) & BIT(0))) ++ return -ENOLINK; ++ return !!(readl(pcie->base_port[0] + S8003_PORT_LINKSTS) & ++ S8003_PORT_LINKSTS_UP); ++} ++EXPORT_SYMBOL_GPL(apple_ans_link_up); ++ ++/* Host-only evidence, no endpoint config/BAR reads or invalidate register. */ ++void apple_ans_host_dump(struct device *dev, const char *reason) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return; ++ dev_info(dev, "ANS host %s: ports %#x link %#x LTSSM %#x FIFO %#x poisoned %u mapped %*pb\n", ++ reason, pcie->enabled_ports, ++ readl(pcie->base_port[0] + H9P_PORT_LINKSTS), ++ readl(pcie->base_port[0] + H9P_PORT_LTSSMCTL), ++ readl(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO), ++ READ_ONCE(pcie->nvmmu.poisoned), APPLE_ANS_NVMMU_MAX_REQS, ++ pcie->nvmmu.mapped); ++} ++EXPORT_SYMBOL_GPL(apple_ans_host_dump); ++ ++int apple_ans_nvmmu_drain(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ u32 fifo; ++ int ret; ++ ++ if (!pcie || !pcie->nvmmu.ready) ++ return -ENODEV; ++ if (!bitmap_empty(pcie->nvmmu.mapped, APPLE_ANS_NVMMU_MAX_REQS)) ++ return -EBUSY; ++ ret = readl_poll_timeout(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO, ++ fifo, fifo & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 10, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ dev_info(dev, "Apple ANS: outgoing NVMMU FIFO empty check (20ms) result %d\n", ret); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_ans_nvmmu_drain); ++ ++int apple_ans_handoff_stopped(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ unsigned int tag; ++ u32 fifo; ++ int ret; ++ ++ if (!pcie) ++ return -ENODEV; ++ /* All queues are drained and BME is clear before retiring translations. */ ++ for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { ++ ret = apple_ans_nvmmu_map(dev, tag, NULL, 0, false, NULL); ++ if (ret) ++ return ret; ++ } ++ /* The last invalidate also has to leave the FIFO before certification. */ ++ ret = readl_poll_timeout_atomic(pcie->base_port[0] + S8003_PORT_NVMMU_INV_FIFO, ++ fifo, fifo & S8003_PORT_NVMMU_INV_FIFO_EMPTY, ++ 1, S8003_NVMMU_INV_FIFO_TIMEOUT_US); ++ if (ret) ++ return ret; ++ ret = apple_ans_dart_stop(dev); ++ if (!ret) ++ WRITE_ONCE(pcie->ans_handoff_stopped, true); ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_ans_handoff_stopped); ++ ++int apple_ans_handoff_begin(struct device *dev) ++{ ++ struct apple_h9p_pcie *pcie = apple_ans_endpoint_host(dev); ++ ++ if (!pcie) ++ return -EBUSY; ++ WRITE_ONCE(pcie->ans_handoff_stopped, false); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_ans_handoff_begin); ++ ++static void apple_h9p_s8003_shutdown(struct platform_device *pdev) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(&pdev->dev); ++ unsigned int port; ++ ++ if (!kexec_in_progress || !pcie || ++ !of_device_is_compatible(pdev->dev.of_node, "apple,s8003-pcie")) ++ return; ++ cancel_delayed_work_sync(&pcie->late_work); ++ if (!READ_ONCE(pcie->ans_handoff_stopped) || ++ (of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && ++ !apple_s8003_port_powered(pcie))) ++ panic("Apple ANS: refusing uncertified powered handoff"); ++ for (port = 0; port < H9P_NUM_PORTS; port++) { ++ void __iomem *cfg = pcie->base_config + port * H9P_CFG_PORT_STRIDE; ++ ++ if (!(pcie->enabled_ports & BIT(port))) ++ continue; ++ /* release6: block root routing, retain refclk, then isolate. */ ++ writew(0, cfg + PCI_COMMAND); ++ h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); ++ h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(port), 0, ++ S8003_RC_PORT_PWR_REFCLK_NOGATE); ++ if (gpiod_direction_output(pcie->perst[port], 1)) ++ panic("Apple PCIe: PERST failed during handoff"); ++ if (apple_s8003_port_stop(pcie, port)) ++ panic("Apple PCIe: LTSSM stop failed during handoff"); ++ udelay(S8003_PERST_TO_REFCLK_US); ++ if (!port && pcie->nvmmu.ready) { ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); ++ pcie->nvmmu.ready = false; ++ } ++ if (!port && !of_property_read_bool(pcie->dev->of_node, ++ "apple,ans-kexec-handoff")) ++ apple_s8003_port_off(pcie, port); ++ if (!port && of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && ++ (!apple_s8003_port_powered(pcie) || ++ gpiod_get_value_cansleep(pcie->perst[0]) != 1)) ++ panic("Apple ANS: outgoing powered port certificate failed"); ++ pcie->enabled_ports &= ~BIT(port); ++ } ++ dev_info(pcie->dev, "kexec: PCIe PERST asserted, LTSSM stopped, ports powered; NVMMU SART disabled\n"); ++} ++ ++static void apple_h9p_capture_park(struct apple_h9p_pcie *pcie) ++{ ++ void __iomem *cfg = pcie->base_config; ++ ++ cancel_delayed_work_sync(&pcie->late_work); ++ if (!(pcie->enabled_ports & BIT(0))) ++ return; ++ writew(0, cfg + PCI_COMMAND); ++ h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); ++ h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(0), 0, ++ S8003_RC_PORT_PWR_REFCLK_NOGATE); ++ if (gpiod_direction_output(pcie->perst[0], 1) || ++ apple_s8003_port_stop(pcie, 0)) ++ panic("ANS capture isolation failed"); ++ udelay(S8003_PERST_TO_REFCLK_US); ++ if (pcie->nvmmu.base) ++ h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); ++ pcie->nvmmu.ready = false; ++ pcie->enabled_ports &= ~BIT(0); ++} ++ ++struct apple_h9p_crash_state { ++ struct apple_h9p_pcie *pcie; ++ int result; ++ bool busy; ++ bool found; ++}; ++ ++/* No NVMe driver, queues, SHN, or old DMA buffers are needed in phoenix. */ ++static int apple_h9p_crash_stop_ans(struct pci_dev *pdev, void *data) ++{ ++ struct apple_h9p_crash_state *state = data; ++ struct apple_h9p_pcie *pcie = state->pcie; ++ void __iomem *bar; ++ u32 csts = 0; ++ int i, ret; ++ ++ if (pdev->vendor != PCI_VENDOR_ID_APPLE || ++ ((pdev->class >> 8) != PCI_CLASS_STORAGE_EXPRESS && ++ (pdev->class >> 8) != PCI_CLASS_STORAGE_OTHER)) ++ return 0; ++ state->found = true; ++ if (pdev->driver) { ++ state->busy = true; ++ return 1; ++ } ++ if (apple_ans_link_up(&pdev->dev) <= 0) { ++ state->result = -ENOLINK; ++ return 0; ++ } ++ ret = pci_enable_device_mem(pdev); ++ if (ret) { ++ state->result = ret; ++ return 0; ++ } ++ bar = pci_iomap_range(pdev, 0, 0, SZ_8K); ++ if (!bar) { ++ ret = -ENOMEM; ++ goto disable; ++ } ++ /* An unknown queue must never be rung or sent a shutdown notification. */ ++ dev_info(pcie->dev, "phoenix ANS: CC %#x CSTS %#x, clearing EN without SHN\n", ++ readl(bar + NVME_REG_CC), readl(bar + NVME_REG_CSTS)); ++ writel(0, bar + NVME_REG_CC); ++ ret = -ETIMEDOUT; ++ for (i = 0; i < 2000; i++) { ++ if (apple_ans_link_up(&pdev->dev) <= 0) { ++ ret = -ENOLINK; ++ break; ++ } ++ csts = readl(bar + NVME_REG_CSTS); ++ if (csts == U32_MAX) { ++ ret = -EIO; ++ break; ++ } ++ if (!(csts & NVME_CSTS_RDY)) { ++ ret = 0; ++ break; ++ } ++ usleep_range(1000, 1100); ++ } ++ dev_info(pcie->dev, "phoenix ANS: disable result %d CSTS %#x; blocking bus master\n", ++ ret, csts); ++ pci_iounmap(pdev, bar); ++disable: ++ pci_clear_master(pdev); ++ pci_disable_device(pdev); ++ state->result = ret; ++ return 0; ++} ++ ++static ssize_t crash_quiesce_store(struct device *dev, ++ struct device_attribute *attr, ++ const char *buf, size_t count) ++{ ++ struct apple_h9p_pcie *pcie = apple_h9p_pcie_lookup(dev); ++ struct apple_h9p_crash_state state = { .pcie = pcie }; ++ ++ if (!pcie || !is_kdump_kernel() || ++ !of_device_is_compatible(dev->of_node, "apple,s8003-pcie") || ++ !sysfs_streq(buf, "1")) ++ return -EPERM; ++ /* The endpoint gate must stay closed; don't park under a live driver. */ ++ if (pcie->late_ports || work_busy(&pcie->late_work.work)) ++ return -EBUSY; ++ pci_walk_bus(pcie->bridge->bus, apple_h9p_crash_stop_ans, &state); ++ if (state.busy) ++ return -EBUSY; ++ /* Isolation is still required after a bounded RDY timeout. */ ++ /* Wi-Fi stays linked until its own shutdown callback has run. */ ++ apple_h9p_capture_park(pcie); ++ dev_info(dev, "phoenix: ANS/PCIe quiesced (controller found %d result %d)\n", ++ state.found, state.result); ++ return count; ++} ++static DEVICE_ATTR_WO(crash_quiesce); ++ ++static struct attribute *apple_h9p_crash_attrs[] = { ++ &dev_attr_crash_quiesce.attr, ++ NULL, ++}; ++ ++static const struct attribute_group apple_h9p_crash_group = { ++ .attrs = apple_h9p_crash_attrs, ++}; ++ ++ + static int apple_h9p_pcie_probe(struct platform_device *pdev) + { + struct device *dev = &pdev->dev; +@@ -3426,6 +3776,7 @@ static int apple_h9p_pcie_probe(struct platform_device *pdev) + u32 doorbell; + int ret; + ++ + bridge = devm_pci_alloc_host_bridge(dev, sizeof(*pcie)); + if (!bridge) + return -ENOMEM; +@@ -3578,6 +3929,13 @@ static int apple_h9p_pcie_probe(struct platform_device *pdev) + if (ret) + return ret; + ++ if (is_kdump_kernel() && of_device_is_compatible(pcie->dev->of_node, "apple,s8003-pcie")) { ++ ret = devm_device_add_group(dev, &apple_h9p_crash_group); ++ if (ret) ++ return ret; ++ } ++ ++ + if (pcie->late_ports) + schedule_delayed_work(&pcie->late_work, + msecs_to_jiffies(S8003_LATE_PORT_RETRY_MS)); +@@ -3660,6 +4018,7 @@ MODULE_DEVICE_TABLE(of, apple_h9p_pcie_of_match); + + static struct platform_driver apple_h9p_pcie_driver = { + .probe = apple_h9p_pcie_probe, ++ .shutdown = apple_h9p_s8003_shutdown, + .driver = { + .name = "pcie-apple-h9p", + .of_match_table = apple_h9p_pcie_of_match, +diff --git a/include/linux/apple-ans.h b/include/linux/apple-ans.h +index a59861010..10be3ddef 100644 +--- a/include/linux/apple-ans.h ++++ b/include/linux/apple-ans.h +@@ -17,11 +17,34 @@ struct device; + * Map @npages pages for FlatDMA request @tag, or retire the tag if @npages is + * zero. @to_memory: the controller writes the pages (an NVMe read). + */ ++#if IS_REACHABLE(CONFIG_APPLE_DART) ++int apple_ans_dart_stop(struct device *dev); ++#else ++static inline int apple_ans_dart_stop(struct device *dev) ++{ ++ return -EOPNOTSUPP; ++} ++#endif ++ + #if IS_REACHABLE(CONFIG_PCIE_APPLE_H9P) ++int apple_ans_handoff_begin(struct device *dev); ++int apple_ans_handoff_stopped(struct device *dev); ++int apple_ans_nvmmu_drain(struct device *dev); ++int apple_ans_nvmmu_reset(struct device *dev); ++int apple_ans_link_up(struct device *dev); ++bool apple_ans_is_s8003(struct device *dev); ++void apple_ans_host_dump(struct device *dev, const char *reason); + int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + const u64 *pages, unsigned int npages, + bool to_memory, dma_addr_t *iova); + #else ++static inline int apple_ans_handoff_begin(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_handoff_stopped(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_nvmmu_drain(struct device *dev) { return -EBUSY; } ++static inline int apple_ans_nvmmu_reset(struct device *dev) { return -EOPNOTSUPP; } ++static inline int apple_ans_link_up(struct device *dev) { return 1; } ++static inline bool apple_ans_is_s8003(struct device *dev) { return false; } ++static inline void apple_ans_host_dump(struct device *dev, const char *reason) {} + static inline int apple_ans_nvmmu_map(struct device *dev, unsigned int tag, + const u64 *pages, unsigned int npages, + bool to_memory, dma_addr_t *iova) diff --git a/kernel/patches/drivers/pci/0035-nvme-pci-keep-the-certified-ANS-kexec-handoff.patch b/kernel/patches/drivers/pci/0035-nvme-pci-keep-the-certified-ANS-kexec-handoff.patch new file mode 100644 index 00000000..62d5bda6 --- /dev/null +++ b/kernel/patches/drivers/pci/0035-nvme-pci-keep-the-certified-ANS-kexec-handoff.patch @@ -0,0 +1,337 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] nvme-pci: keep the certified ANS kexec handoff + +Resetting the ANS host or its translations destroys the state required +by the next kernel. Retain the stopped-DMA handoff only where the device +tree requests it. + +Assisted-by: LLM +--- + drivers/nvme/host/pci-apple-ans.c | 33 +++++++- + drivers/nvme/host/pci-internal.h | 2 + + drivers/nvme/host/pci.c | 128 ++++++++++++++++++++++++++++-- + 3 files changed, 155 insertions(+), 8 deletions(-) + +diff --git a/drivers/nvme/host/pci-apple-ans.c b/drivers/nvme/host/pci-apple-ans.c +index 5ba239e47..fdd9fa091 100644 +--- a/drivers/nvme/host/pci-apple-ans.c ++++ b/drivers/nvme/host/pci-apple-ans.c +@@ -84,6 +84,7 @@ struct apple_ans_nvme { + u32 cc; + unsigned long cc_jiffies; + bool cc_written; ++ bool host_fault; + dma_addr_t hmb_dma; + u32 hmb_size; + struct apple_ans_nvme_req req[APPLE_ANS_NVMMU_MAX_REQS]; +@@ -218,13 +219,20 @@ static int nvme_pci_apple_ans_reset_dma(struct nvme_dev *dev) + { + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + unsigned long flags; +- unsigned int tag; + int ret; + +- for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { +- ret = apple_ans_nvmmu_map(dev->dev, tag, NULL, 0, false, NULL); ++ if (ans->host == &apple_ans_host_s8003) { ++ ret = apple_ans_nvmmu_reset(dev->dev); + if (ret) + return ret; ++ } else { ++ unsigned int tag; ++ ++ for (tag = 0; tag < APPLE_ANS_NVMMU_MAX_REQS; tag++) { ++ ret = apple_ans_nvmmu_map(dev->dev, tag, NULL, 0, false, NULL); ++ if (ret) ++ return ret; ++ } + } + + spin_lock_irqsave(&ans->req_lock, flags); +@@ -326,10 +334,24 @@ static int nvme_pci_apple_ans_prepare_enable(struct nvme_dev *dev) + return 0; + } + ++static int nvme_pci_apple_ans_check_link(struct nvme_dev *dev) ++{ ++ struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); ++ ++ if (ans && ans->host == &apple_ans_host_s8003 && ++ (ans->host_fault || apple_ans_link_up(dev->dev) <= 0)) { ++ ans->host_fault = true; ++ return -EIO; ++ } ++ return 0; ++} ++ + static int nvme_pci_apple_ans_write_cc(struct nvme_dev *dev, u32 cc) + { + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + ++ if (nvme_pci_apple_ans_check_link(dev)) ++ return -EIO; + writel(cc, dev->bar + NVME_REG_CC); + if (!ans) + return 0; +@@ -349,6 +371,8 @@ static int nvme_pci_apple_ans_read_csts(struct nvme_dev *dev, u32 *csts) + struct apple_ans_nvme *ans = nvme_pci_apple_ans(dev); + unsigned long deadline; + ++ if (nvme_pci_apple_ans_check_link(dev)) ++ return -EIO; + *csts = readl(dev->bar + NVME_REG_CSTS); + if (!ans || !ans->cc_written || !ans->host->rdy_timeout_ms || + (ans->cc & NVME_CC_SHN_MASK)) +@@ -360,6 +384,8 @@ static int nvme_pci_apple_ans_read_csts(struct nvme_dev *dev, u32 *csts) + time_after(jiffies, deadline)) { + dev_err(dev->dev, "ANS RDY timeout (CC %#x, CSTS %#x)\n", + ans->cc, *csts); ++ ans->host_fault = true; ++ apple_ans_host_dump(dev->dev, "RDY deadline"); + return -ETIMEDOUT; + } + +@@ -419,6 +445,7 @@ static int nvme_pci_apple_ans_free_req(struct nvme_dev *dev, + dev_err_ratelimited(dev->dev, + "failed to release ANS DMA slot %u: %d\n", + tag, ret); ++ nvme_reset_ctrl(&dev->ctrl); + return ret; + } + +diff --git a/drivers/nvme/host/pci-internal.h b/drivers/nvme/host/pci-internal.h +index a62777abd..d3fe17867 100644 +--- a/drivers/nvme/host/pci-internal.h ++++ b/drivers/nvme/host/pci-internal.h +@@ -74,6 +74,8 @@ struct nvme_dev { + struct nvme_ctrl ctrl; + u32 last_ps; + bool hmb; ++ unsigned long apple_ans_flags; ++ atomic_t apple_ans_irq_count; + struct sg_table *hmb_sgt; + mempool_t *dmavec_mempool; + const struct nvme_pci_dma_ops *dma_ops; +diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c +index 9ffe2fb74..40b2448d1 100644 +--- a/drivers/nvme/host/pci.c ++++ b/drivers/nvme/host/pci.c +@@ -5,6 +5,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -17,6 +18,10 @@ + #include + #include + #include ++#include ++#include ++#include ++#include + #include + #include + #include +@@ -1559,6 +1564,9 @@ static irqreturn_t nvme_irq(int irq, void *data) + DEFINE_IO_COMP_BATCH(iob); + bool found; + ++ if (!nvmeq->qid && dev->dma_ops == &nvme_pci_apple_ans_ops) ++ atomic_inc(&dev->apple_ans_irq_count); ++ + if (nvme_apple_ans_shared_vector(dev)) + found = nvme_apple_ans_irq_vector(dev, nvmeq->cq_vector, &iob); + else +@@ -1836,6 +1844,40 @@ static void nvme_warn_reset(struct nvme_dev *dev, u32 csts) + "Try \"nvme_core.default_ps_max_latency_us=0 pcie_aspm=off pcie_port_pm=off\" and report a bug\n"); + } + ++/* No endpoint MMIO/config or doorbell reads: safe first evidence for ANS. */ ++static void nvme_apple_ans_dump(struct nvme_dev *dev, const char *reason) ++{ ++ unsigned int i; ++ ++ dev_info(dev->dev, "Apple ANS: snapshot %s state %d online %u allocated %u vectors %u recovery_used %u\n", ++ reason, nvme_ctrl_state(&dev->ctrl), dev->online_queues, ++ dev->ctrl.queue_count, dev->num_vecs, ++ test_bit(0, &dev->apple_ans_flags)); ++ apple_ans_host_dump(dev->dev, reason); ++ for (i = 0; i < dev->ctrl.queue_count; i++) { ++ struct nvme_queue *q = &dev->queues[i]; ++ u16 head = READ_ONCE(q->cq_head); ++ struct nvme_completion *cqe; ++ int irq = -1; ++ ++ if (!q->cqes || head >= q->q_depth) ++ continue; ++ cqe = &q->cqes[head]; ++ if (test_bit(NVMEQ_ENABLED, &q->flags) && ++ !test_bit(NVMEQ_POLLED, &q->flags)) ++ irq = pci_irq_vector(to_pci_dev(dev->dev), q->cq_vector); ++ dev_info(dev->dev, "Apple ANS: q%u flags %#lx depth %u SQ tail %u/%u CQ head %u phase %u next status %#x cid %#x sqhead %u vector %u irq %d ANS admin IRQ actions %u SQ %pad CQ %pad\n", ++ i, READ_ONCE(q->flags), q->q_depth, ++ READ_ONCE(q->sq_tail), READ_ONCE(q->last_sq_tail), ++ head, READ_ONCE(q->cq_phase), ++ le16_to_cpu(READ_ONCE(cqe->status)), ++ le16_to_cpu(READ_ONCE(cqe->command_id)), ++ le16_to_cpu(READ_ONCE(cqe->sq_head)), q->cq_vector, ++ irq, atomic_read(&dev->apple_ans_irq_count), ++ &q->sq_dma_addr, &q->cq_dma_addr); ++ } ++} ++ + static enum blk_eh_timer_return nvme_timeout(struct request *req) + { + struct nvme_iod *iod = blk_mq_rq_to_pdu(req); +@@ -1844,8 +1886,18 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) + struct request *abort_req; + struct nvme_command cmd = { }; + struct pci_dev *pdev = to_pci_dev(dev->dev); +- u32 csts = readl(dev->bar + NVME_REG_CSTS); ++ u32 csts = ~0U; + u8 opcode; ++ bool ans = dev->dma_ops == &nvme_pci_apple_ans_ops && ++ apple_ans_is_s8003(dev->dev); ++ ++ if (ans) { ++ nvme_apple_ans_dump(dev, "timeout before CSTS"); ++ if (dev->dma_ops->read_csts(dev, &csts)) ++ goto disable; ++ } else { ++ csts = readl(dev->bar + NVME_REG_CSTS); ++ } + + /* + * Shutdown the device immediately if we see it is disconnected. This +@@ -1921,6 +1973,11 @@ static enum blk_eh_timer_return nvme_timeout(struct request *req) + * returned to the driver, or if this is the admin queue. + */ + opcode = nvme_req(req)->cmd->common.opcode; ++ if (ans) { ++ if (test_and_set_bit(0, &dev->apple_ans_flags)) ++ nvme_change_ctrl_state(&dev->ctrl, NVME_CTRL_DELETING); ++ goto disable; ++ } + if (!nvmeq->qid || (iod->flags & IOD_ABORTED)) { + dev_warn(dev->ctrl.device, + "I/O tag %d (%04x) opcode %#x (%s) QID %d timeout, reset controller\n", +@@ -2804,6 +2861,7 @@ static umode_t nvme_pci_attrs_are_visible(struct kobject *kobj, + return a->mode; + } + ++ + static struct attribute *nvme_pci_attrs[] = { + &dev_attr_cmb.attr, + &dev_attr_cmbloc.attr, +@@ -3283,11 +3341,46 @@ static bool nvme_pci_ctrl_is_dead(struct nvme_dev *dev) + return (csts & NVME_CSTS_CFS) || !(csts & NVME_CSTS_RDY); + } + +-static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) ++/* Certify stopped DMA before the kexec PCIe handoff. */ ++static int nvme_apple_ans_handoff_disable(struct nvme_dev *dev, int shutdown_ret) ++{ ++ struct pci_dev *pdev = to_pci_dev(dev->dev); ++ u32 csts = ~0U, cc, expected; ++ int ret, err; ++ ++ ret = dev->dma_ops->read_csts(dev, &csts); ++ if (shutdown_ret || ret || (csts & NVME_CSTS_CFS) || ++ (csts & NVME_CSTS_SHST_MASK) != NVME_CSTS_SHST_CMPLT) { ++ pci_clear_master(pdev); ++ return -EBUSY; ++ } ++ dev->ctrl.ctrl_config &= ~(NVME_CC_ENABLE | NVME_CC_SHN_MASK); ++ expected = dev->ctrl.ctrl_config; ++ ret = dev->dma_ops->write_cc(dev, expected); ++ if (!ret) { ++ ret = read_poll_timeout(dev->dma_ops->read_csts, err, ++ err || !(csts & NVME_CSTS_RDY), ++ 1000, 2000000, false, dev, &csts); ++ if (!ret) ++ ret = err; ++ } ++ if (ret) ++ goto block_dma; ++ cc = readl(dev->bar + NVME_REG_CC); ++ if ((csts & NVME_CSTS_CFS) || ++ (cc & (NVME_CC_ENABLE | NVME_CC_SHN_MASK))) ++ ret = -EBUSY; ++block_dma: ++ pci_clear_master(pdev); ++ return ret; ++} ++ ++static int nvme_dev_disable_paired(struct nvme_dev *dev, bool shutdown, bool paired) + { + enum nvme_ctrl_state state = nvme_ctrl_state(&dev->ctrl); + struct pci_dev *pdev = to_pci_dev(dev->dev); + bool dead; ++ int ret = paired ? -EBUSY : 0; + + mutex_lock(&dev->shutdown_lock); + dead = nvme_pci_ctrl_is_dead(dev); +@@ -3298,15 +3391,26 @@ static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) + * Give the controller a chance to complete all entered requests + * if doing a safe shutdown. + */ +- if (!dead && shutdown) +- nvme_wait_freeze_timeout(&dev->ctrl); ++ if (!dead && shutdown && nvme_wait_freeze_timeout(&dev->ctrl) <= 0 && paired) ++ panic("Apple ANS: outgoing I/O did not drain; refusing kexec"); + } + + nvme_quiesce_io_queues(&dev->ctrl); + ++ if (paired && (dead || !dev->ctrl.queue_count)) ++ panic("Apple ANS: dead outgoing controller; refusing kexec"); + if (!dead && dev->ctrl.queue_count > 0) { ++ if (paired) { ++ nvme_sync_io_queues(&dev->ctrl); ++ ret = apple_ans_nvmmu_drain(dev->dev); ++ if (ret) ++ panic("Apple ANS: outgoing drain failed %d; refusing kexec", ret); ++ } + nvme_delete_io_queues(dev); +- nvme_disable_ctrl(&dev->ctrl, shutdown); ++ dev_info(dev->dev, "Apple ANS: shutdown starting NVMe SHN paired=%u\n", paired); ++ ret = nvme_disable_ctrl(&dev->ctrl, shutdown); ++ if (paired) ++ ret = nvme_apple_ans_handoff_disable(dev, ret); + nvme_poll_irqdisable(&dev->queues[0]); + } + nvme_suspend_io_queues(dev); +@@ -3329,7 +3433,15 @@ static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) + if (dev->ctrl.admin_q && !blk_queue_dying(dev->ctrl.admin_q)) + nvme_unquiesce_admin_queue(&dev->ctrl); + } ++ if (paired && !ret) ++ ret = apple_ans_handoff_stopped(dev->dev); + mutex_unlock(&dev->shutdown_lock); ++ return ret; ++} ++ ++static void nvme_dev_disable(struct nvme_dev *dev, bool shutdown) ++{ ++ nvme_dev_disable_paired(dev, shutdown, false); + } + + static int nvme_disable_prepare_reset(struct nvme_dev *dev, bool shutdown) +@@ -3920,6 +4032,12 @@ static void nvme_shutdown(struct pci_dev *pdev) + { + struct nvme_dev *dev = pci_get_drvdata(pdev); + ++ if (kexec_in_progress && apple_ans_is_s8003(dev->dev) && ++ dev->dma_ops == &nvme_pci_apple_ans_ops && !apple_ans_handoff_begin(dev->dev)) { ++ if (!nvme_wait_reset(&dev->ctrl) || nvme_dev_disable_paired(dev, true, true)) ++ panic("Apple ANS: refusing unsafe powered kexec handoff"); ++ return; ++ } + nvme_disable_prepare_reset(dev, true); + } + diff --git a/kernel/patches/drivers/pci/0036-PCI-apple-cold-reset-after-an-uncertified-kexec-hand.patch b/kernel/patches/drivers/pci/0036-PCI-apple-cold-reset-after-an-uncertified-kexec-hand.patch new file mode 100644 index 00000000..f82f0350 --- /dev/null +++ b/kernel/patches/drivers/pci/0036-PCI-apple-cold-reset-after-an-uncertified-kexec-hand.patch @@ -0,0 +1,85 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 00:43:07 +0100 +Subject: [PATCH] PCI: apple: cold-reset after an uncertified kexec handoff + +The warm guard sets panic_timeout to zero. Neither an uncertified ANS +stop nor failed PERST, LTSSM stop or powered-port certificate can safely +continue into the incoming kernel. Flush the reason and request a cold +watchdog reset instead of hanging in panic. Return without further port +writes if restart unexpectedly returns. + +Assisted-by: LLM +--- + drivers/pci/controller/pcie-apple-h9p.c | 9 +++++---- + include/linux/soc/apple/cold-reset.h | 19 +++++++++++++++++++ + 2 files changed, 24 insertions(+), 4 deletions(-) + create mode 100644 include/linux/soc/apple/cold-reset.h + +diff --git a/drivers/pci/controller/pcie-apple-h9p.c b/drivers/pci/controller/pcie-apple-h9p.c +index f0095394a..28305b0ee 100644 +--- a/drivers/pci/controller/pcie-apple-h9p.c ++++ b/drivers/pci/controller/pcie-apple-h9p.c +@@ -49,6 +49,7 @@ + #include + #include + #include ++#include + + #include "pci-host-common.h" + +@@ -3609,7 +3610,7 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + if (!READ_ONCE(pcie->ans_handoff_stopped) || + (of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && + !apple_s8003_port_powered(pcie))) +- panic("Apple ANS: refusing uncertified powered handoff"); ++ return apple_cold_reset("Apple ANS: refusing uncertified powered handoff"); + for (port = 0; port < H9P_NUM_PORTS; port++) { + void __iomem *cfg = pcie->base_config + port * H9P_CFG_PORT_STRIDE; + +@@ -3621,9 +3622,9 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(port), 0, + S8003_RC_PORT_PWR_REFCLK_NOGATE); + if (gpiod_direction_output(pcie->perst[port], 1)) +- panic("Apple PCIe: PERST failed during handoff"); ++ return apple_cold_reset("Apple PCIe: PERST failed during handoff"); + if (apple_s8003_port_stop(pcie, port)) +- panic("Apple PCIe: LTSSM stop failed during handoff"); ++ return apple_cold_reset("Apple PCIe: LTSSM stop failed during handoff"); + udelay(S8003_PERST_TO_REFCLK_US); + if (!port && pcie->nvmmu.ready) { + h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); +@@ -3635,7 +3636,7 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + if (!port && of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && + (!apple_s8003_port_powered(pcie) || + gpiod_get_value_cansleep(pcie->perst[0]) != 1)) +- panic("Apple ANS: outgoing powered port certificate failed"); ++ return apple_cold_reset("Apple ANS: outgoing powered port certificate failed"); + pcie->enabled_ports &= ~BIT(port); + } + dev_info(pcie->dev, "kexec: PCIe PERST asserted, LTSSM stopped, ports powered; NVMMU SART disabled\n"); +diff --git a/include/linux/soc/apple/cold-reset.h b/include/linux/soc/apple/cold-reset.h +new file mode 100644 +index 000000000..34467be50 +--- /dev/null ++++ b/include/linux/soc/apple/cold-reset.h +@@ -0,0 +1,19 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#ifndef _LINUX_SOC_APPLE_COLD_RESET_H ++#define _LINUX_SOC_APPLE_COLD_RESET_H ++ ++#include ++#include ++#include ++ ++/* Shutdown has no safe return to a warm kernel after an unproven DMA stop. */ ++static inline void apple_cold_reset(const char *reason) ++{ ++ pr_emerg("Apple unsafe handover: %s; forcing cold watchdog reset\n", reason); ++ pr_flush(1000, true); ++ kmsg_dump_desc(KMSG_DUMP_PANIC, reason); ++ WRITE_ONCE(reboot_mode, REBOOT_COLD); ++ emergency_restart(); ++} ++ ++#endif diff --git a/kernel/patches/drivers/pci/0037-nvme-pci-cold-reset-after-unsafe-ANS-kexec-shutdown.patch b/kernel/patches/drivers/pci/0037-nvme-pci-cold-reset-after-unsafe-ANS-kexec-shutdown.patch new file mode 100644 index 00000000..5075181b --- /dev/null +++ b/kernel/patches/drivers/pci/0037-nvme-pci-cold-reset-after-unsafe-ANS-kexec-shutdown.patch @@ -0,0 +1,76 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 02:08:51 +0100 +Subject: [PATCH] nvme-pci: cold-reset after unsafe ANS kexec shutdown + +An unsafe ANS shutdown cannot panic under a zero panic timeout. Use the +shared logged and flushed cold-reset path for each failed certificate. +If reset unexpectedly returns, leave without further device operations. + +Assisted-by: LLM +--- + drivers/nvme/host/pci.c | 29 +++++++++++++++++++++-------- + 1 file changed, 21 insertions(+), 8 deletions(-) + +diff --git a/drivers/nvme/host/pci.c b/drivers/nvme/host/pci.c +index 40b2448d1..2f356a8ce 100644 +--- a/drivers/nvme/host/pci.c ++++ b/drivers/nvme/host/pci.c +@@ -22,6 +22,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -3391,20 +3392,30 @@ static int nvme_dev_disable_paired(struct nvme_dev *dev, bool shutdown, bool pai + * Give the controller a chance to complete all entered requests + * if doing a safe shutdown. + */ +- if (!dead && shutdown && nvme_wait_freeze_timeout(&dev->ctrl) <= 0 && paired) +- panic("Apple ANS: outgoing I/O did not drain; refusing kexec"); ++ if (!dead && shutdown && nvme_wait_freeze_timeout(&dev->ctrl) <= 0 && paired) { ++ apple_cold_reset("Apple ANS: outgoing I/O did not drain"); ++ mutex_unlock(&dev->shutdown_lock); ++ return -EIO; ++ } + } + + nvme_quiesce_io_queues(&dev->ctrl); + +- if (paired && (dead || !dev->ctrl.queue_count)) +- panic("Apple ANS: dead outgoing controller; refusing kexec"); ++ if (paired && (dead || !dev->ctrl.queue_count)) { ++ apple_cold_reset("Apple ANS: dead outgoing controller"); ++ mutex_unlock(&dev->shutdown_lock); ++ return -EIO; ++ } + if (!dead && dev->ctrl.queue_count > 0) { + if (paired) { + nvme_sync_io_queues(&dev->ctrl); + ret = apple_ans_nvmmu_drain(dev->dev); +- if (ret) +- panic("Apple ANS: outgoing drain failed %d; refusing kexec", ret); ++ if (ret) { ++ dev_err(dev->dev, "Apple ANS: outgoing drain failed %d\n", ret); ++ apple_cold_reset("Apple ANS: outgoing drain failed"); ++ mutex_unlock(&dev->shutdown_lock); ++ return -EIO; ++ } + } + nvme_delete_io_queues(dev); + dev_info(dev->dev, "Apple ANS: shutdown starting NVMe SHN paired=%u\n", paired); +@@ -4034,8 +4045,10 @@ static void nvme_shutdown(struct pci_dev *pdev) + + if (kexec_in_progress && apple_ans_is_s8003(dev->dev) && + dev->dma_ops == &nvme_pci_apple_ans_ops && !apple_ans_handoff_begin(dev->dev)) { +- if (!nvme_wait_reset(&dev->ctrl) || nvme_dev_disable_paired(dev, true, true)) +- panic("Apple ANS: refusing unsafe powered kexec handoff"); ++ if (!nvme_wait_reset(&dev->ctrl) || nvme_dev_disable_paired(dev, true, true)) { ++ apple_cold_reset("Apple ANS: refusing unsafe powered kexec handoff"); ++ return; ++ } + return; + } + nvme_disable_prepare_reset(dev, true); diff --git a/kernel/patches/drivers/pci/0038-PCI-apple-use-explicit-returns-after-cold-reset.patch b/kernel/patches/drivers/pci/0038-PCI-apple-use-explicit-returns-after-cold-reset.patch new file mode 100644 index 00000000..5b49b55d --- /dev/null +++ b/kernel/patches/drivers/pci/0038-PCI-apple-use-explicit-returns-after-cold-reset.patch @@ -0,0 +1,62 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 02:08:51 +0100 +Subject: [PATCH] PCI: apple: use explicit returns after cold reset + +Use standard C void returns while retaining the early exit if the reset +helper unexpectedly returns. Successful handoff ordering is unchanged. + +Assisted-by: LLM +--- + drivers/pci/controller/pcie-apple-h9p.c | 24 ++++++++++++++++-------- + 1 file changed, 16 insertions(+), 8 deletions(-) + +diff --git a/drivers/pci/controller/pcie-apple-h9p.c b/drivers/pci/controller/pcie-apple-h9p.c +index 28305b0ee..5890d632a 100644 +--- a/drivers/pci/controller/pcie-apple-h9p.c ++++ b/drivers/pci/controller/pcie-apple-h9p.c +@@ -3609,8 +3609,10 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + cancel_delayed_work_sync(&pcie->late_work); + if (!READ_ONCE(pcie->ans_handoff_stopped) || + (of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && +- !apple_s8003_port_powered(pcie))) +- return apple_cold_reset("Apple ANS: refusing uncertified powered handoff"); ++ !apple_s8003_port_powered(pcie))) { ++ apple_cold_reset("Apple ANS: refusing uncertified powered handoff"); ++ return; ++ } + for (port = 0; port < H9P_NUM_PORTS; port++) { + void __iomem *cfg = pcie->base_config + port * H9P_CFG_PORT_STRIDE; + +@@ -3621,10 +3623,14 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + h9p_rmw(cfg + PCI_PRIMARY_BUS, GENMASK(23, 8), 0); + h9p_rmw(pcie->base_rc + S8003_RC_PORT_PWR(port), 0, + S8003_RC_PORT_PWR_REFCLK_NOGATE); +- if (gpiod_direction_output(pcie->perst[port], 1)) +- return apple_cold_reset("Apple PCIe: PERST failed during handoff"); +- if (apple_s8003_port_stop(pcie, port)) +- return apple_cold_reset("Apple PCIe: LTSSM stop failed during handoff"); ++ if (gpiod_direction_output(pcie->perst[port], 1)) { ++ apple_cold_reset("Apple PCIe: PERST failed during handoff"); ++ return; ++ } ++ if (apple_s8003_port_stop(pcie, port)) { ++ apple_cold_reset("Apple PCIe: LTSSM stop failed during handoff"); ++ return; ++ } + udelay(S8003_PERST_TO_REFCLK_US); + if (!port && pcie->nvmmu.ready) { + h9p_writel_flush(0, pcie->nvmmu.base + H9P_NVMMU_SART_CTRL); +@@ -3635,8 +3641,10 @@ static void apple_h9p_s8003_shutdown(struct platform_device *pdev) + apple_s8003_port_off(pcie, port); + if (!port && of_property_read_bool(pcie->dev->of_node, "apple,ans-kexec-handoff") && + (!apple_s8003_port_powered(pcie) || +- gpiod_get_value_cansleep(pcie->perst[0]) != 1)) +- return apple_cold_reset("Apple ANS: outgoing powered port certificate failed"); ++ gpiod_get_value_cansleep(pcie->perst[0]) != 1)) { ++ apple_cold_reset("Apple ANS: outgoing powered port certificate failed"); ++ return; ++ } + pcie->enabled_ports &= ~BIT(port); + } + dev_info(pcie->dev, "kexec: PCIe PERST asserted, LTSSM stopped, ports powered; NVMMU SART disabled\n"); diff --git a/kernel/patches/drivers/rtc/0001-rtc-preserve-PMU-and-RTC-interrupt-ownership.patch b/kernel/patches/drivers/rtc/0001-rtc-preserve-PMU-and-RTC-interrupt-ownership.patch new file mode 100644 index 00000000..e1ea9505 --- /dev/null +++ b/kernel/patches/drivers/rtc/0001-rtc-preserve-PMU-and-RTC-interrupt-ownership.patch @@ -0,0 +1,206 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] rtc: preserve PMU and RTC interrupt ownership + +Shared wake events must have one interrupt owner. Preserve the PMU and +RTC event split so that acknowledgement by one consumer cannot steal the +other consumer's wake event. + +Assisted-by: LLM +--- + drivers/rtc/rtc-apple-pmic.c | 127 ++++++++++++++++++++++++++++++++++- + 1 file changed, 126 insertions(+), 1 deletion(-) + +diff --git a/drivers/rtc/rtc-apple-pmic.c b/drivers/rtc/rtc-apple-pmic.c +index ee56618c8..d31d4c716 100644 +--- a/drivers/rtc/rtc-apple-pmic.c ++++ b/drivers/rtc/rtc-apple-pmic.c +@@ -12,6 +12,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -24,22 +25,30 @@ + #include + + #define RTC_COUNTER_SIZE 6 ++#define RTC_ALARM_SIZE 4 ++#define RTC_CONTROL_RUN BIT(0) ++#define RTC_CONTROL_ALARM BIT(6) + + /** + * struct apple_pmic_rtc_regs - RTC Hardware registers + * @control: RTC Control + * @counter: Time Counter ++ * @alarm: Alarm seconds counter ++ * @has_alarm: Alarm registers are present + */ + + struct apple_pmic_rtc_regs { + u32 control; + u32 counter; ++ u32 alarm; ++ bool has_alarm; + }; + + /** + * struct apple_pmic_rtc - RTC driver internal structure + * @rtc: RTC device + * @regmap: regmap used to access registers ++ * @dev: device used for logging and wake setup + * @nvmem_cell: nvmem cell for offset + * @regs: hardware registers + * @base: RTC base address +@@ -183,17 +192,107 @@ static int apple_pmic_rtc_read_time(struct device *dev, struct rtc_time *tm) + return 0; + } + ++static int apple_pmic_rtc_alarm_irq_enable(struct device *dev, ++ unsigned int enabled) ++{ ++ struct apple_pmic_rtc *ap_rtc = dev_get_drvdata(dev); ++ ++ /* AppleD2365PMU 0xfffffff006bbbc1c writes these exact values. */ ++ return regmap_write(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->control, ++ RTC_CONTROL_RUN | ++ (enabled ? RTC_CONTROL_ALARM : 0)); ++} ++ ++static int apple_pmic_rtc_read_alarm(struct device *dev, ++ struct rtc_wkalrm *alarm) ++{ ++ struct apple_pmic_rtc *ap_rtc = dev_get_drvdata(dev); ++ u8 buf[RTC_ALARM_SIZE]; ++ unsigned int control; ++ time64_t secs; ++ int rc; ++ ++ rc = regmap_bulk_read(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->alarm, ++ buf, sizeof(buf)); ++ if (rc) ++ return rc; ++ rc = regmap_read(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->control, &control); ++ if (rc) ++ return rc; ++ ++ secs = (time64_t)get_unaligned_le32(buf) + ap_rtc->offset; ++ rtc_time64_to_tm(secs, &alarm->time); ++ alarm->enabled = !!(control & RTC_CONTROL_ALARM); ++ alarm->pending = 0; ++ return 0; ++} ++ ++static int apple_pmic_rtc_set_alarm(struct device *dev, ++ struct rtc_wkalrm *alarm) ++{ ++ struct apple_pmic_rtc *ap_rtc = dev_get_drvdata(dev); ++ u8 buf[RTC_ALARM_SIZE]; ++ time64_t secs; ++ int rc; ++ ++ if (!alarm->enabled) ++ return apple_pmic_rtc_alarm_irq_enable(dev, 0); ++ ++ secs = rtc_tm_to_time64(&alarm->time) - ap_rtc->offset; ++ if (secs <= 0 || secs > U32_MAX) ++ return -ERANGE; ++ put_unaligned_le32(secs, buf); ++ ++ /* iOS programs 0x508 first, then enables the one-shot at 0x500. */ ++ rc = regmap_bulk_write(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->alarm, ++ buf, sizeof(buf)); ++ if (rc) ++ return rc; ++ return apple_pmic_rtc_alarm_irq_enable(dev, 1); ++} ++ ++static irqreturn_t apple_pmic_rtc_alarm_irq(int irq, void *data) ++{ ++ struct apple_pmic_rtc *ap_rtc = data; ++ int rc; ++ ++ /* The PMU event read is the acknowledge; iOS then clears alarm bit 6. */ ++ rc = regmap_update_bits(ap_rtc->regmap, ++ ap_rtc->base + ap_rtc->regs->control, ++ RTC_CONTROL_ALARM, 0); ++ if (rc) ++ dev_warn_ratelimited(ap_rtc->dev, ++ "failed to disable fired alarm: %d\n", rc); ++ rtc_update_irq(ap_rtc->rtc, 1, RTC_IRQF | RTC_AF); ++ return IRQ_HANDLED; ++} ++ + static const struct rtc_class_ops apple_pmic_rtc_ops = { + .read_time = apple_pmic_rtc_read_time, + .set_time = apple_pmic_rtc_set_time, + }; + ++static const struct rtc_class_ops apple_pmic_rtc_alarm_ops = { ++ .read_time = apple_pmic_rtc_read_time, ++ .set_time = apple_pmic_rtc_set_time, ++ .read_alarm = apple_pmic_rtc_read_alarm, ++ .set_alarm = apple_pmic_rtc_set_alarm, ++ .alarm_irq_enable = apple_pmic_rtc_alarm_irq_enable, ++}; ++ + static const struct apple_pmic_rtc_regs amber_regs = { + .counter = 0x6 + }; + + static const struct apple_pmic_rtc_regs antigua_regs = { +- .counter = 0x2 ++ .control = 0x0, ++ .counter = 0x2, ++ .alarm = 0x8, ++ .has_alarm = true, + }; + + static const struct apple_pmic_rtc_regs calpe_regs = { +@@ -230,6 +329,7 @@ static int apple_pmic_rtc_probe(struct platform_device *pdev) + struct device_node *node = pdev->dev.of_node; + const struct of_device_id *match; + struct apple_pmic_rtc *ap_rtc; ++ int irq; + int rc; + + match = of_match_node(apple_pmic_id_table, node); +@@ -264,6 +364,31 @@ static int apple_pmic_rtc_probe(struct platform_device *pdev) + ap_rtc->rtc->ops = &apple_pmic_rtc_ops; + ap_rtc->rtc->range_min = INT_MIN; + ap_rtc->rtc->range_max = INT_MAX; ++ if (ap_rtc->regs->has_alarm) { ++ irq = platform_get_irq_optional(pdev, 0); ++ if (irq == -EPROBE_DEFER) ++ return irq; ++ if (irq > 0) { ++ rc = devm_request_threaded_irq(&pdev->dev, irq, NULL, ++ apple_pmic_rtc_alarm_irq, ++ IRQF_ONESHOT, ++ "apple-pmic-rtc", ap_rtc); ++ if (rc) ++ return dev_err_probe(&pdev->dev, rc, ++ "cannot request alarm IRQ\n"); ++ device_init_wakeup(&pdev->dev, true); ++ rc = devm_pm_set_wake_irq(&pdev->dev, irq); ++ if (rc) ++ return dev_err_probe(&pdev->dev, rc, ++ "cannot set alarm wake IRQ\n"); ++ ap_rtc->rtc->ops = &apple_pmic_rtc_alarm_ops; ++ dev_info(&pdev->dev, ++ "D2365 alarm at 0x%03x on PMU event IRQ %d\n", ++ ap_rtc->base + ap_rtc->regs->alarm, irq); ++ } else if (irq != -ENXIO) { ++ return irq; ++ } ++ } + + return devm_rtc_register_device(ap_rtc->rtc); + } diff --git a/kernel/patches/drivers/usb/0014-usb-preserve-PMU-and-RTC-interrupt-ownership.patch b/kernel/patches/drivers/usb/0014-usb-preserve-PMU-and-RTC-interrupt-ownership.patch new file mode 100644 index 00000000..58875bd3 --- /dev/null +++ b/kernel/patches/drivers/usb/0014-usb-preserve-PMU-and-RTC-interrupt-ownership.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 17:15:00 +0100 +Subject: [PATCH] usb: preserve PMU and RTC interrupt ownership + +Shared wake events must have one interrupt owner. Preserve the PMU and +RTC event split so that acknowledgement by one consumer cannot steal the +other consumer's wake event. + +Assisted-by: LLM +--- + drivers/usb/gadget/composite.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/drivers/usb/gadget/composite.c b/drivers/usb/gadget/composite.c +index 1da3effb2..14e87fbc8 100644 +--- a/drivers/usb/gadget/composite.c ++++ b/drivers/usb/gadget/composite.c +@@ -968,7 +968,7 @@ static int composite_apple_current_limit(struct usb_composite_dev *cdev, + u16 w_value, u16 w_index) + { + cdev->apple_extra_ma = (s16)w_index; +- DBG(cdev, "Apple host current grant: extra %d mA, suspend %d mA\n", ++ dev_info(&cdev->gadget->dev, "Apple host current grant: extra %d mA, suspend %d mA\n", + (s16)w_index, (s16)w_value); + composite_vbus_draw(cdev, cdev->vbus_draw_ma); + return 0; +@@ -2221,7 +2221,7 @@ unknown: + goto check_value; + } + +- if (ctrl->bRequestType == (USB_DIR_OUT | USB_TYPE_VENDOR | ++ if ((ctrl->bRequestType & ~USB_DIR_IN) == (USB_TYPE_VENDOR | + USB_RECIP_DEVICE) && + ctrl->bRequest == USB_APPLE_REQ_CURRENT_LIMIT && !w_length && + le16_to_cpu(cdev->desc.idVendor) == USB_APPLE_VENDOR_ID) { diff --git a/tests/test_m1n1_psci.py b/tests/test_m1n1_psci.py new file mode 100644 index 00000000..85091696 --- /dev/null +++ b/tests/test_m1n1_psci.py @@ -0,0 +1,81 @@ +"""Host-only framing checks for the J71t PSCI loader variables.""" +from pathlib import Path +import json +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +TOOL = ROOT / 'boot/m1n1/psci-loader' + + +class PSCILoader(unittest.TestCase): + def test_payload_and_no_overwrite(self): + with tempfile.TemporaryDirectory() as tmp: + loader, output = Path(tmp) / 'm1n1.bin', Path(tmp) / 'psci.bin' + original = b'bare loader\0' + loader.write_bytes(original) + cmd = [sys.executable, str(TOOL), '--device', 'j71t', '--loader', str(loader), + '--output', str(output)] + result = subprocess.run(cmd, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.read_bytes(), original + b'psci=1\n') + self.assertEqual(loader.read_bytes(), original) + self.assertNotEqual(subprocess.run(cmd, capture_output=True).returncode, 0) + self.assertEqual(output.read_bytes(), original + b'psci=1\n') + + def test_reset_and_off_variables(self): + cases = {('--system-reset', 'proxy'): b'psci=1\npsci_reset=proxy\n', + ('--system-reset', 'off'): b'psci=1\npsci_reset=0\n', + ('--system-reset', 'boot'): b'psci=1\n', + ('--no-psci',): b'psci=0\n'} + with tempfile.TemporaryDirectory() as tmp: + loader = Path(tmp) / 'm1n1.bin' + loader.write_bytes(b'bare') + for n, (extra, records) in enumerate(cases.items()): + output = Path(tmp) / f'out{n}.bin' + result = subprocess.run([sys.executable, str(TOOL), '--device', 'j71t', '--loader', + str(loader), '--output', str(output), *extra], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.read_bytes(), b'bare' + records) + both = subprocess.run([sys.executable, str(TOOL), '--device', 'j71t', '--loader', str(loader), + '--output', str(Path(tmp) / 'x.bin'), '--no-psci', '--system-reset', + 'proxy'], capture_output=True) + self.assertNotEqual(both.returncode, 0) + + def test_series_has_reset_patches(self): + names = sorted(p.name for p in (ROOT / 'boot/m1n1/patches/orchard').glob('*.patch')) + for prefix in ('0015-psci-', '0016-psci-Turn-the-resident-EL3-PSCI-on-by-default', + '0017-psci-Trap-EL1-WFI-WFE', '0018-psci-Implement-SYSTEM_RESET', + '0019-bootmgr-Boot-the-default-entry'): + self.assertTrue(any(n.startswith(prefix) for n in names), prefix) + + def test_manifest_psci_record_keeps_logo_first(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for name, data in {'loader': b'M1N1', 'dtb': b'DTB', 'kernel': b'KERNEL', + 'initrd': b'INITRD'}.items(): + (root / name).write_bytes(data) + logo = b'm1n1_logo_256128' + bytes(4 * (256 * 256 + 128 * 128)) + (root / 'logo').write_bytes(logo) + records = b'chosen.bootargs=console=tty0\npsci=1\n' + (root / 'profile.json').write_text(json.dumps({'bootargs': records.decode()})) + result = subprocess.run([sys.executable, '-I', str(ROOT / 'src/orchard/image/assemble.py'), + '--manifest', str(root / 'profile.json'), '--loader', str(root / 'loader'), + '--logo', str(root / 'logo'), '--dtb', str(root / 'dtb'), + '--kernel', str(root / 'kernel'), '--initramfs', str(root / 'initrd'), + '--output', str(root / 'payload')], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue((root / 'payload').read_bytes().startswith(b'M1N1' + logo + records + b'DTB')) + + def test_board_requires_explicit_opt_in(self): + for device in ([], ['--device', 'j71b']): + result = subprocess.run([sys.executable, str(TOOL), *device, '--loader', 'unused', + '--output', 'unused'], capture_output=True) + self.assertNotEqual(result.returncode, 0) + + +if __name__ == '__main__': + unittest.main()