diff --git a/boot/initramfs/root/init b/boot/initramfs/root/init index 16c7913a..86cc9c65 100755 --- a/boot/initramfs/root/init +++ b/boot/initramfs/root/init @@ -89,6 +89,32 @@ mkdir -p /newroot mount ${fstype:+-t "$fstype"} -o "$mode${flags:+,$flags}" "$device" /newroot || stop "could not mount $device" [ -x "/newroot$init" ] || [ -L "/newroot$init" ] || stop "the root has no $init" +# Preserve the running boot's *pristine* tree before any package update can +# replace /boot or same-release module assets. The matching initrd embeds cold +# DTBs; warm boots name their retained stage instead. Never infer it by uname. +mkdir -p /run /newroot/run +mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run +mkdir -p /run/orchard-kexec +warm_pristine= +for argument in $(cat /proc/cmdline); do + case "$argument" in orchard_warm.dtb=*) warm_pristine=${argument#orchard_warm.dtb=} ;; esac +done +if [ -n "$warm_pristine" ]; then + case "$warm_pristine" in + /var/tmp/orchard-warm.*/pristine.dtb) + case "$warm_pristine" in *..*) stop 'invalid warm DT provenance' ;; esac + cp "/newroot$warm_pristine" /run/orchard-kexec/pristine.dtb || stop 'missing warm pristine DT' ;; + *) stop 'invalid warm DT provenance' ;; + esac +elif [ -f /etc/orchard/pristine-map ]; then + board=$(tr '\000' '\n' < /proc/device-tree/compatible | head -1) + while read compatible file; do + [ "$compatible" != "$board" ] || cp "/etc/orchard/dtbs/$file" /run/orchard-kexec/pristine.dtb +done < /etc/orchard/pristine-map +fi +cat /proc/sys/kernel/random/boot_id > /run/orchard-kexec/boot-id +mount --move /run /newroot/run || stop 'cannot retain boot provenance' + # USB for the host: network (usb0) and a serial console (ttyGS0), named after # the system, so `orchard devices` sees it. What runs on them is the system's. usb_gadget() { 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/patches/orchard/0021-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch b/boot/m1n1/patches/orchard/0021-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch new file mode 100644 index 00000000..6a30fc37 --- /dev/null +++ b/boot/m1n1/patches/orchard/0021-kboot-Pass-the-A9-AOP-s-boot-arguments-and-sensor-ca.patch @@ -0,0 +1,116 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Mon, 2 Nov 2026 15:07:08 +0100 +Subject: [PATCH] kboot: Pass the A9 AOP's boot arguments and sensor + calibration + +The A9's AOP firmware takes more than the ECID from its loader: the AOP +target number (iop-aop-nub aop-target), the chip's ID (/chosen chip-id) +and revision (/arm-io chip-revision). Its accelerometer, gyroscope and +pressure sensor also send no reports until the driver has given them the +unit's factory calibration, which iBoot resolves from syscfg into the +sensors' nodes below iop-aop-nub. + +Copy the three numbers into the AOP's node (alias aop) as +apple,aop-target, apple,chip-id and apple,chip-revision, and each +sensor's calibration properties into the AOP's child node of the same +name, prefixed with "apple,". Unprovisioned "syscfg/" references and +nodes the device tree doesn't have are skipped. The orientation +properties are not copied: the A9 firmware rejects them and the board's +mount-matrix describes the mounting. + +The values are the unit's own and never leave the device: the kernel +sends them to the AOP and nothing else. + +Assisted-by: LLM +--- + src/kboot.c | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++- + 1 file changed, 68 insertions(+), 1 deletion(-) + +diff --git a/src/kboot.c b/src/kboot.c +index ebd6282..7ce9d72 100644 +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -1005,6 +1005,73 @@ static int dt_set_als(void) + return 0; + } + ++/* ++ * The A9's AOP firmware also takes the AOP target number and the chip's ID and ++ * revision in its boot arguments, and each motion sensor's factory ++ * calibration, which iBoot resolves from syscfg into the sensor's node below ++ * iop-aop-nub. Copy them into the AOP's node and its sensor child nodes of the ++ * same name; the calibration keeps its name with an "apple," prefix. ++ */ ++static const char *const aop_sensor_calibrations[] = { ++ "low-temp-accel-offset", ++ "accel-sensitivity-calibration", ++ "accel-range-sensitivity-cal", ++ "accel-interrupt-calibration", ++ "gyro-sensitivity-calibration", ++ "gyro-range-sensitivity-cal", ++ "gyro-interrupt-calibration", ++ "gyro-trim-calibration", ++ "pressure-offset-calibration", ++ "pressure-accel-sensitivity", ++ "pressure-heater-resistance", ++}; ++ ++static int dt_set_aop_sensors(int node) ++{ ++ int nub = adt_path_offset(adt, "/arm-io/aop/iop-aop-nub"); ++ if (nub < 0) ++ return 0; ++ ++ u32 val; ++ if (ADT_GETPROP(adt, nub, "aop-target", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,aop-target", val)) ++ bail("FDT: failed to set apple,aop-target\n"); ++ ++ int chosen = adt_path_offset(adt, "/chosen"); ++ if (chosen >= 0 && ADT_GETPROP(adt, chosen, "chip-id", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,chip-id", val)) ++ bail("FDT: failed to set apple,chip-id\n"); ++ ++ int arm_io = adt_path_offset(adt, "/arm-io"); ++ if (arm_io >= 0 && ADT_GETPROP(adt, arm_io, "chip-revision", &val) >= 0 && ++ fdt_setprop_u32(dt, node, "apple,chip-revision", val)) ++ bail("FDT: failed to set apple,chip-revision\n"); ++ ++ int sensor = nub; ++ ADT_FOREACH_CHILD(adt, sensor) ++ { ++ const char *name = adt_get_name(adt, sensor); ++ ++ for (size_t i = 0; i < ARRAY_SIZE(aop_sensor_calibrations); i++) { ++ u32 len; ++ const u8 *blob = adt_getprop(adt, sensor, aop_sensor_calibrations[i], &len); ++ if (!blob || !len || (len >= 7 && !memcmp(blob, "syscfg/", 7))) ++ continue; ++ ++ /* Earlier properties may have moved the sensor's node. */ ++ int child = fdt_subnode_offset(dt, node, name); ++ if (child < 0) ++ break; ++ ++ char prop[64]; ++ snprintf(prop, sizeof(prop), "apple,%s", aop_sensor_calibrations[i]); ++ if (fdt_setprop(dt, child, prop, blob, len)) ++ bail("FDT: failed to set %s/%s\n", name, prop); ++ } ++ } ++ return 0; ++} ++ + /* + * The AOP's firmware carries the chip's ECID in its boot arguments. Pass it + * only to the AOP's node; the driver removes it from the tree once read. +@@ -1028,7 +1095,7 @@ static int dt_set_aop(void) + + if (fdt_setprop_u64(dt, node, "apple,ecid", ecid)) + bail("FDT: failed to set apple,ecid\n"); +- return 0; ++ return dt_set_aop_sensors(node); + } + + static int dt_set_multitouch(void) 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/s8000/0014-arm64-dts-apple-retain-the-tethered-warm-boot-state.patch b/kernel/patches/devices/s8000/0014-arm64-dts-apple-retain-the-tethered-warm-boot-state.patch new file mode 100644 index 00000000..36c9879f --- /dev/null +++ b/kernel/patches/devices/s8000/0014-arm64-dts-apple-retain-the-tethered-warm-boot-state.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] arm64: dts: apple: retain the tethered warm boot state + +A tethered kernel inherits initialized hardware rather than power-on +state. Retain the board-selected warm handoff state so probe and +shutdown do not discard resources needed by the incoming kernel. + +Assisted-by: LLM +--- + arch/arm64/boot/dts/apple/s800-0-3.dtsi | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/arch/arm64/boot/dts/apple/s800-0-3.dtsi b/arch/arm64/boot/dts/apple/s800-0-3.dtsi +index 3277a73bb..f6449a7e6 100644 +--- a/arch/arm64/boot/dts/apple/s800-0-3.dtsi ++++ b/arch/arm64/boot/dts/apple/s800-0-3.dtsi +@@ -443,3 +443,7 @@ pmu { + * the S8003 (APL1022) on 16nm. There are some minor differences + * such as timing in cpufreq state transistions. + */ ++ ++/ { ++ apple,reserve-pstore; ++}; diff --git a/kernel/patches/devices/s8000/0015-arm64-dts-apple-select-certified-AOP-reload-through-.patch b/kernel/patches/devices/s8000/0015-arm64-dts-apple-select-certified-AOP-reload-through-.patch new file mode 100644 index 00000000..46d27545 --- /dev/null +++ b/kernel/patches/devices/s8000/0015-arm64-dts-apple-select-certified-AOP-reload-through-.patch @@ -0,0 +1,49 @@ +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: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. The AOP itself, its firmware and +mailbox are enabled for every iPad 5 in the shared file; J71t adds the +reserved shutdown page and the power domains the reload observes. + +Assisted-by: LLM +--- + arch/arm64/boot/dts/apple/s8003-j71t.dts | 21 ++++++++++++++++++++ + 1 file changed, 21 insertions(+) + +diff --git a/arch/arm64/boot/dts/apple/s8003-j71t.dts b/arch/arm64/boot/dts/apple/s8003-j71t.dts +--- a/arch/arm64/boot/dts/apple/s8003-j71t.dts ++++ b/arch/arm64/boot/dts/apple/s8003-j71t.dts +@@ -16,6 +16,27 @@ + model = "Apple iPad 5 (Wi-Fi) (TSMC)"; + }; + ++/ { ++ reserved-memory { ++ #address-cells = <2>; ++ #size-cells = <2>; ++ ranges; ++ /* A fixed tail page, never a firmware DMA grant. */ ++ aop_shutdown: aop-shutdown@8583fc000 { ++ reg = <0x8 0x583fc000 0 0x4000>; ++ no-map; ++ }; ++ }; ++}; ++ ++&aop { ++ apple,certified-reload; ++ memory-region = <&aop_shutdown>; ++ apple,observe-domains = <&ps_aop_gpio &ps_aop &ps_aop_busif ++ &ps_aop_cpu &ps_aop_filter>; ++ apple,spi0-domain = <&ps_spi0>; ++}; ++ + &touchscreen { + firmware-name = "apple/j71t-fwstart.z2fw"; + }; +-- +2.54.0 diff --git a/kernel/patches/devices/s8003/0001-arm64-dts-apple-describe-J71t-tethered-boot.patch b/kernel/patches/devices/s8003/0001-arm64-dts-apple-describe-J71t-tethered-boot.patch new file mode 100644 index 00000000..70a0484d --- /dev/null +++ b/kernel/patches/devices/s8003/0001-arm64-dts-apple-describe-J71t-tethered-boot.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Tue, 10 Nov 2026 16:20:28 +0100 +Subject: [PATCH] arm64: dts: apple: describe J71t tethered boot + +Record the board fact for the droppable watchdog reset guard. + +Assisted-by: LLM +--- + arch/arm64/boot/dts/apple/s8003-j71t.dts | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/arch/arm64/boot/dts/apple/s8003-j71t.dts b/arch/arm64/boot/dts/apple/s8003-j71t.dts +index 733cefc66..0df58a56f 100644 +--- a/arch/arm64/boot/dts/apple/s8003-j71t.dts ++++ b/arch/arm64/boot/dts/apple/s8003-j71t.dts +@@ -24,3 +24,7 @@ &touchscreen { + &wifi { + brcm,board-type = "apple,feara"; + }; ++ ++&wdt { ++ apple,tethered-boot; ++}; 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/base/0003-core-select-certified-AOP-reload-through-DT.patch b/kernel/patches/drivers/base/0003-core-select-certified-AOP-reload-through-DT.patch new file mode 100644 index 00000000..6fd6514b --- /dev/null +++ b/kernel/patches/drivers/base/0003-core-select-certified-AOP-reload-through-DT.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: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +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 3fcdf4b4e..fcc51b699 100644 +--- a/kernel/printk/printk.c ++++ b/kernel/printk/printk.c +@@ -4589,6 +4589,7 @@ bool pr_flush(int timeout_ms, bool reset_on_progress) + { + return __pr_flush(NULL, timeout_ms, reset_on_progress); + } ++EXPORT_SYMBOL_GPL(pr_flush); + + /* + * Delayed printk version, for scheduler-internal messages: diff --git a/kernel/patches/drivers/drm/0008-drm-retain-the-tethered-warm-boot-state.patch b/kernel/patches/drivers/drm/0008-drm-retain-the-tethered-warm-boot-state.patch new file mode 100644 index 00000000..2865725b --- /dev/null +++ b/kernel/patches/drivers/drm/0008-drm-retain-the-tethered-warm-boot-state.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] drm: retain the tethered warm boot state + +A tethered kernel inherits initialized hardware rather than power-on +state. Retain the board-selected warm handoff state so probe and +shutdown do not discard resources needed by the incoming kernel. + +Assisted-by: LLM +--- + drivers/gpu/drm/adbe/adbe_drv.c | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/drivers/gpu/drm/adbe/adbe_drv.c b/drivers/gpu/drm/adbe/adbe_drv.c +index 2508e6531..7ed2de3d7 100644 +--- a/drivers/gpu/drm/adbe/adbe_drv.c ++++ b/drivers/gpu/drm/adbe/adbe_drv.c +@@ -1065,6 +1065,9 @@ static void adbe_shutdown(struct platform_device *pdev) + struct adbe *adbe = platform_get_drvdata(pdev); + + drm_atomic_helper_shutdown(&adbe->drm); ++ timer_delete_sync(&adbe->flip_timer); ++ /* release9: restore the boot buffer even if the CRTC was never enabled. */ ++ adbe_park(adbe); + writel(adbe->saved_inten, adbe->regs + ADP_INTEN); + } + diff --git a/kernel/patches/drivers/input/0030-Input-apple-z2-stop-the-touch-controller-on-shutdown.patch b/kernel/patches/drivers/input/0030-Input-apple-z2-stop-the-touch-controller-on-shutdown.patch new file mode 100644 index 00000000..88cd9ce1 --- /dev/null +++ b/kernel/patches/drivers/input/0030-Input-apple-z2-stop-the-touch-controller-on-shutdown.patch @@ -0,0 +1,48 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] Input: apple-z2: stop the touch controller on shutdown + +A kexec hands the next kernel an S8000 SPI bus that the touch controller +may still be using: its interrupt thread can run and the SPI core can +still hold its messages. + +Add a shutdown callback that stops the controller and waits for its +interrupt, also when the threaded handler has already masked itself. SPI +children shut down before their controller, so the SPI driver can then +drain its queue. + +Assisted-by: LLM +--- + drivers/input/touchscreen/apple_z2.c | 11 +++++++++++ + 1 file changed, 11 insertions(+) + +diff --git a/drivers/input/touchscreen/apple_z2.c b/drivers/input/touchscreen/apple_z2.c +index 92a04edcd..5dcf3579d 100644 +--- a/drivers/input/touchscreen/apple_z2.c ++++ b/drivers/input/touchscreen/apple_z2.c +@@ -4599,6 +4599,16 @@ static struct spi_device_id apple_z2_of_id[] = { + }; + MODULE_DEVICE_TABLE(spi, apple_z2_of_id); + ++/* Children stop before the SPI controller drains its core queue. */ ++static void apple_z2_shutdown(struct spi_device *spi) ++{ ++ struct apple_z2 *z2 = spi_get_drvdata(spi); ++ ++ apple_z2_stop(z2); ++ /* Also wait when the IRQ thread already masked itself. */ ++ synchronize_irq(spi->irq); ++} ++ + static struct spi_driver apple_z2_driver = { + .driver = { + .name = "apple-z2", +@@ -4609,6 +4619,7 @@ static struct spi_driver apple_z2_driver = { + .id_table = apple_z2_of_id, + .probe = apple_z2_probe, + .remove = apple_z2_remove, ++ .shutdown = apple_z2_shutdown, + }; + + module_spi_driver(apple_z2_driver); 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/of/0001-of-retain-the-tethered-warm-boot-state.patch b/kernel/patches/drivers/of/0001-of-retain-the-tethered-warm-boot-state.patch new file mode 100644 index 00000000..64a11814 --- /dev/null +++ b/kernel/patches/drivers/of/0001-of-retain-the-tethered-warm-boot-state.patch @@ -0,0 +1,35 @@ +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] of: retain the tethered warm boot state + +A tethered kernel inherits initialized hardware rather than power-on +state. Retain the board-selected warm handoff state so probe and +shutdown do not discard resources needed by the incoming kernel. + +Assisted-by: LLM +--- + drivers/of/of_reserved_mem.c | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/drivers/of/of_reserved_mem.c b/drivers/of/of_reserved_mem.c +index 8c9d6395d..76cf3e5a4 100644 +--- a/drivers/of/of_reserved_mem.c ++++ b/drivers/of/of_reserved_mem.c +@@ -275,6 +275,16 @@ void __init fdt_scan_reserved_mem_late(void) + return; + } + ++ /* Early architecture fixups may have added static reservations. */ ++ total_reserved_mem_cnt = reserved_mem_count; ++ fdt_for_each_subnode(child, fdt, node) { ++ int len; ++ ++ if (of_fdt_device_is_available(fdt, child) && ++ fdt_getprop(fdt, child, "reg", &len) && len) ++ total_reserved_mem_cnt++; ++ } ++ + /* Attempt dynamic allocation of a new reserved_mem array */ + if (alloc_reserved_mem_array()) + return; 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/pinctrl/0004-pinctrl-apple-gpio-retire-inherited-GPIO-interrupt-m.patch b/kernel/patches/drivers/pinctrl/0004-pinctrl-apple-gpio-retire-inherited-GPIO-interrupt-m.patch new file mode 100644 index 00000000..dc5b66a6 --- /dev/null +++ b/kernel/patches/drivers/pinctrl/0004-pinctrl-apple-gpio-retire-inherited-GPIO-interrupt-m.patch @@ -0,0 +1,130 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] pinctrl: apple-gpio: retire inherited GPIO interrupt modes + after kexec + +After a kexec, or after a panic that ran no shutdown callbacks, GPIO +pins can still be in an interrupt mode set up by the previous kernel. +Once the chained parent interrupts are enabled, such a pin raises an +interrupt that nobody maps or acknowledges, and it keeps firing. + +On apple,s8000-pinctrl, switch every GPIO interrupt mode off at probe, +before the parent interrupts are enabled, and keep outputs, mux, pulls, +data and grouping. Consumers arm their own pins again later. In the +handler, a pending pin without a mapping is masked and its group latch +cleared, with a rate-limited warning. + +Assisted-by: LLM +--- + drivers/pinctrl/pinctrl-apple-gpio.c | 62 +++++++++++++++++++++++++++- + 1 file changed, 60 insertions(+), 2 deletions(-) + +diff --git a/drivers/pinctrl/pinctrl-apple-gpio.c b/drivers/pinctrl/pinctrl-apple-gpio.c +index b97023c97..766fcf5bc 100644 +--- a/drivers/pinctrl/pinctrl-apple-gpio.c ++++ b/drivers/pinctrl/pinctrl-apple-gpio.c +@@ -19,6 +19,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -46,6 +47,7 @@ struct apple_gpio_pinctrl { + + void __iomem *base; + struct regmap *map; ++ bool retire_irqs; + + struct pinctrl_desc pinctrl_desc; + struct gpio_chip gpio_chip; +@@ -379,8 +381,33 @@ static void apple_gpio_irq_handler(struct irq_desc *desc) + chained_irq_enter(chip, desc); + for (pinh = 0; pinh < gc->ngpio; pinh += 32) { + pending = readl_relaxed(pctl->base + REG_IRQ(*grpp, pinh)); +- for_each_set_bit(pinl, &pending, 32) +- generic_handle_domain_irq(gc->irq.domain, pinh + pinl); ++ for_each_set_bit(pinl, &pending, 32) { ++ unsigned int pin = pinh + pinl; ++ int ret; ++ ++ ret = pin < gc->ngpio ? ++ generic_handle_domain_irq(gc->irq.domain, pin) : -EINVAL; ++ if (ret && pctl->retire_irqs) { ++ /* An inherited, unmapped source has nobody to ack it. ++ * Mask only this GPIO, then clear this group's latch. ++ */ ++ if (pin < gc->ngpio) { ++ u32 reg = apple_gpio_get_reg(pctl, pin); ++ u32 mode = FIELD_GET(REG_GPIOx_MODE, reg); ++ u32 off = FIELD_PREP(REG_GPIOx_MODE, ++ REG_GPIOx_IN_IRQ_OFF); ++ ++ if (!(reg & REG_GPIOx_PERIPH) && ++ mode >= REG_GPIOx_IN_IRQ_HI && ++ mode <= REG_GPIOx_IN_IRQ_ANY) ++ apple_gpio_set_reg(pctl, pin, REG_GPIOx_MODE, off); ++ } ++ writel(BIT(pinl), pctl->base + REG_IRQ(*grpp, pinh)); ++ dev_warn_ratelimited(pctl->dev, ++ "masked orphan inherited GPIO IRQ group=%u pin=%u\n", ++ *grpp, pin); ++ } ++ } + } + chained_irq_exit(chip, desc); + } +@@ -403,6 +430,35 @@ static const struct irq_chip apple_gpio_irqchip = { + + /* Probe & register */ + ++static void apple_gpio_quiesce_irqs(struct apple_gpio_pinctrl *pctl) ++{ ++ struct gpio_chip *gc = &pctl->gpio_chip; ++ unsigned int pin, group, mode, masked = 0; ++ u32 reg; ++ ++ if (!pctl->retire_irqs) ++ return; ++ /* Before chained parent IRQs are enabled, retire old GPIO IRQ modes. ++ * Preserve outputs, mux, pulls, data and grouping. Consumers rearm their ++ * own pins later; panic entry need not have run any shutdown callbacks. ++ */ ++ for (pin = 0; pin < gc->ngpio; pin++) { ++ reg = apple_gpio_get_reg(pctl, pin); ++ mode = FIELD_GET(REG_GPIOx_MODE, reg); ++ if (!(reg & REG_GPIOx_PERIPH) && mode >= REG_GPIOx_IN_IRQ_HI && ++ mode <= REG_GPIOx_IN_IRQ_ANY) { ++ apple_gpio_set_reg(pctl, pin, REG_GPIOx_MODE, ++ FIELD_PREP(REG_GPIOx_MODE, REG_GPIOx_IN_IRQ_OFF)); ++ masked++; ++ } ++ } ++ for (group = 0; group < gc->irq.num_parents; group++) ++ for (pin = 0; pin < gc->ngpio; pin += 32) ++ writel(GENMASK(min(32U, gc->ngpio - pin) - 1, 0), ++ pctl->base + REG_IRQ(group, pin)); ++ dev_info(pctl->dev, "inherited GPIO IRQ baseline: masked %u inherited inputs\n", masked); ++} ++ + static int apple_gpio_register(struct apple_gpio_pinctrl *pctl) + { + struct gpio_irq_chip *girq = &pctl->gpio_chip.irq; +@@ -452,6 +508,7 @@ static int apple_gpio_register(struct apple_gpio_pinctrl *pctl) + girq->handler = handle_level_irq; + } + ++ apple_gpio_quiesce_irqs(pctl); + ret = devm_gpiochip_add_data(pctl->dev, &pctl->gpio_chip, pctl); + + out_free_irq_data: +@@ -485,6 +542,7 @@ static int apple_gpio_pinctrl_probe(struct platform_device *pdev) + if (!pctl) + return -ENOMEM; + pctl->dev = &pdev->dev; ++ pctl->retire_irqs = of_device_is_compatible(pdev->dev.of_node, "apple,s8000-pinctrl"); + pctl->soc = of_device_get_match_data(&pdev->dev); + pctl->gpio_chip.irq.num_parents = nirqs; + dev_set_drvdata(&pdev->dev, pctl); diff --git a/kernel/patches/drivers/pmdomain/0001-pmdomain-apple-retain-the-tethered-warm-boot-state.patch b/kernel/patches/drivers/pmdomain/0001-pmdomain-apple-retain-the-tethered-warm-boot-state.patch new file mode 100644 index 00000000..0ae20f3c --- /dev/null +++ b/kernel/patches/drivers/pmdomain/0001-pmdomain-apple-retain-the-tethered-warm-boot-state.patch @@ -0,0 +1,113 @@ +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] pmdomain: apple: retain the tethered warm boot state + +A tethered kernel inherits initialized hardware rather than power-on +state. Retain the board-selected warm handoff state so probe and +shutdown do not discard resources needed by the incoming kernel. + +Assisted-by: LLM +--- + drivers/pmdomain/apple/pmgr-pwrstate.c | 34 +++++++++++++++++++++----- + 1 file changed, 28 insertions(+), 6 deletions(-) + +diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c +index 65dc32d30..7995d3b71 100644 +--- a/drivers/pmdomain/apple/pmgr-pwrstate.c ++++ b/drivers/pmdomain/apple/pmgr-pwrstate.c +@@ -58,6 +58,8 @@ struct apple_pmgr_ps { + u32 min_state; + bool has_genpd; + bool manual_pm; ++ bool warm; ++ bool keep_on; + }; + + #define genpd_to_apple_pmgr_ps(_genpd) container_of(_genpd, struct apple_pmgr_ps, genpd) +@@ -83,17 +85,21 @@ static int apple_pmgr_ps_set(struct apple_pmgr_ps *ps, u32 pstate, bool auto_ena + + dev_dbg(ps->dev, "PS %s: pwrstate = 0x%x: 0x%x\n", ps->name, pstate, reg); + +- regmap_write(ps->regmap, ps->offset, reg); ++ ret = regmap_write(ps->regmap, ps->offset, reg); ++ if (ret) ++ return ret; + + ret = regmap_read_poll_timeout_atomic( + ps->regmap, ps->offset, reg, + (FIELD_GET(APPLE_PMGR_PS_ACTUAL, reg) == pstate), 1, + APPLE_PMGR_PS_SET_TIMEOUT); +- if (ret < 0) ++ if (ret < 0) { + dev_err(ps->dev, "PS %s: Failed to reach power state 0x%x (now: 0x%x)\n", + ps->name, pstate, reg); ++ return ret; ++ } + +- if (auto_enable) { ++ if (auto_enable && !ps->keep_on) { + /* Not all devices implement this; this is a no-op where not implemented. */ + reg &= ~APPLE_PMGR_FLAGS; + reg |= APPLE_PMGR_AUTO_ENABLE; +@@ -136,6 +142,9 @@ static int apple_pmgr_ps_power_off(struct generic_pm_domain *genpd) + unsigned long flags; + int ret; + ++ if (ps->keep_on) ++ return -EBUSY; ++ + spin_lock_irqsave(&ps->lock, flags); + ret = apple_pmgr_ps_set(ps, APPLE_PMGR_PS_PWRGATE, false); + spin_unlock_irqrestore(&ps->lock, flags); +@@ -168,6 +177,10 @@ static void apple_pmgr_clk_disable(struct clk_hw *hw) + unsigned long flags; + int ret; + ++ /* The lab has no separate clk gate sharing this power-state word. */ ++ if (ps->keep_on) ++ return; ++ + spin_lock_irqsave(&ps->lock, flags); + ret = apple_pmgr_ps_set(ps, gate->off_state, false); + spin_unlock_irqrestore(&ps->lock, flags); +@@ -306,6 +319,10 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) + return ret; + } + ++ ps->warm = of_device_is_compatible(node, "apple,s8000-pmgr-pwrstate") && ++ of_property_read_bool(of_chosen, "linux,booted-from-kexec"); ++ ps->keep_on = ps->warm; ++ ps->manual_pm = of_property_read_bool(node, "apple,manual-pm"); + clock_only = of_property_read_bool(node, "apple,clock-gate-only"); + if (!clock_only) { + ps->genpd.flags |= GENPD_FLAG_IRQ_SAFE; +@@ -315,12 +332,17 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) + } + + ret = of_property_read_u32(node, "apple,min-state", &ps->min_state); +- if (ret == 0 && ps->min_state <= APPLE_PMGR_PS_ACTIVE) ++ if (ret == 0 && ps->min_state <= APPLE_PMGR_PS_ACTIVE && ++ !(ps->warm && ps->manual_pm)) + regmap_update_bits(regmap, ps->offset, APPLE_PMGR_FLAGS | APPLE_PMGR_PS_MIN, + FIELD_PREP(APPLE_PMGR_PS_MIN, ps->min_state)); + + active = apple_pmgr_ps_is_active(ps); +- ps->manual_pm = of_property_read_bool(node, "apple,manual-pm"); ++ if (ps->warm && active && !ps->manual_pm) { ++ ret = apple_pmgr_ps_set(ps, APPLE_PMGR_PS_ACTIVE, false); ++ if (ret) ++ return ret; ++ } + if (!clock_only && of_property_read_bool(node, "apple,always-on")) { + ps->genpd.flags |= GENPD_FLAG_ALWAYS_ON; + if (!active) { +@@ -331,7 +353,7 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) + } + + /* Turn on auto-PM if the domain is already on */ +- if (active) ++ if (active && !ps->warm) + regmap_update_bits(regmap, ps->offset, APPLE_PMGR_FLAGS | APPLE_PMGR_AUTO_ENABLE, + ps->manual_pm ? 0 : APPLE_PMGR_AUTO_ENABLE); + diff --git a/kernel/patches/drivers/pmdomain/0002-pmdomain-apple-select-certified-AOP-reload-through-D.patch b/kernel/patches/drivers/pmdomain/0002-pmdomain-apple-select-certified-AOP-reload-through-D.patch new file mode 100644 index 00000000..d9a8480b --- /dev/null +++ b/kernel/patches/drivers/pmdomain/0002-pmdomain-apple-select-certified-AOP-reload-through-D.patch @@ -0,0 +1,127 @@ +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] pmdomain: apple: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +Assisted-by: LLM +--- + drivers/pmdomain/apple/pmgr-pwrstate.c | 84 ++++++++++++++++++++++++++ + 1 file changed, 84 insertions(+) + +diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c +index 65dc32d30..0ae0eda2d 100644 +--- a/drivers/pmdomain/apple/pmgr-pwrstate.c ++++ b/drivers/pmdomain/apple/pmgr-pwrstate.c +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -17,6 +18,7 @@ + #include + #include + #include ++#include + + #define APPLE_PMGR_RESET BIT(31) + #define APPLE_PMGR_AUTO_ENABLE BIT(28) +@@ -417,6 +419,7 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) + goto err_remove; + } + ++ platform_set_drvdata(pdev, ps); + return 0; + err_remove: + if (ps->has_genpd) { +@@ -446,3 +449,84 @@ MODULE_AUTHOR("Hector Martin "); + MODULE_DESCRIPTION("PMGR power state driver for Apple SoCs"); + + module_platform_driver(apple_pmgr_ps_driver); ++ ++/* ++ * Read-only observer for bring-up. The caller's nodes follow the same ++ * child -> parent order as genpd power transitions. No domain is attached, ++ * resumed, pinned on, reset, or written by this function. ++ */ ++int apple_pmgr_observe(struct device_node **nodes, unsigned int count, ++ struct apple_pmgr_observation *states, ++ void (*read)(void *), void *data) ++{ ++ struct platform_device *providers[5]; ++ struct apple_pmgr_ps *ps[5]; ++ unsigned long flags; ++ unsigned int i, j, found = 0; ++ int ret = 0; ++ bool accessible = true; ++ ++ if (!count || count > ARRAY_SIZE(providers) || !read) ++ return -EINVAL; ++ for (i = 0; i < count; i++) { ++ for (j = 0; j < i; j++) ++ if (nodes[i] == nodes[j]) { ++ ret = -EINVAL; ++ goto put; ++ } ++ providers[i] = of_find_device_by_node(nodes[i]); ++ if (!providers[i]) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ device_lock(&providers[i]->dev); ++ found++; ++ if (!providers[i]->dev.driver || ++ strcmp(providers[i]->dev.driver->name, "apple-pmgr-pwrstate")) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ ps[i] = platform_get_drvdata(providers[i]); ++ if (!ps[i]) { ++ ret = -EPROBE_DEFER; ++ goto put; ++ } ++ } ++ ++ /* PMGR domains use GENPD_FLAG_IRQ_SAFE, i.e. genpd.slock. */ ++ local_irq_save(flags); ++ for (i = 0; i < count; i++) ++ spin_lock_nested(&ps[i]->genpd.slock, i); ++ for (i = 0; i < count; i++) ++ spin_lock_nested(&ps[i]->lock, i); ++ for (i = 0; i < count; i++) { ++ states[i].genpd_status = ps[i]->genpd.status; ++ ret = regmap_read(ps[i]->regmap, ps[i]->offset, &states[i].raw); ++ if (ret) ++ goto unlock; ++ if (states[i].genpd_status != GENPD_STATE_ON || ++ FIELD_GET(APPLE_PMGR_PS_ACTUAL, states[i].raw) != APPLE_PMGR_PS_ACTIVE || ++ FIELD_GET(APPLE_PMGR_PS_TARGET, states[i].raw) != APPLE_PMGR_PS_ACTIVE || ++ states[i].raw & (APPLE_PMGR_RESET | APPLE_PMGR_DEV_DISABLE | ++ APPLE_PMGR_PARENT_OFF)) ++ accessible = false; ++ } ++ if (accessible) ++ read(data); ++ else ++ ret = -EHOSTDOWN; ++unlock: ++ for (i = count; i > 0; i--) ++ spin_unlock(&ps[i - 1]->lock); ++ for (i = count; i > 0; i--) ++ spin_unlock(&ps[i - 1]->genpd.slock); ++ local_irq_restore(flags); ++put: ++ while (found) { ++ found--; ++ device_unlock(&providers[found]->dev); ++ put_device(&providers[found]->dev); ++ } ++ return ret; ++} ++EXPORT_SYMBOL_GPL(apple_pmgr_observe); diff --git a/kernel/patches/drivers/pstore/0001-dt-bindings-pstore-retain-the-tethered-warm-boot-sta.patch b/kernel/patches/drivers/pstore/0001-dt-bindings-pstore-retain-the-tethered-warm-boot-sta.patch new file mode 100644 index 00000000..af7dc5ff --- /dev/null +++ b/kernel/patches/drivers/pstore/0001-dt-bindings-pstore-retain-the-tethered-warm-boot-sta.patch @@ -0,0 +1,68 @@ +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: pstore: retain the tethered warm boot state + +A tethered kernel inherits initialized hardware rather than power-on +state. Retain the board-selected warm handoff state so probe and +shutdown do not discard resources needed by the incoming kernel. + +Assisted-by: LLM +--- + .../devicetree/bindings/arm/apple,pstore.yaml | 31 +++++++++++++++++++ + .../bindings/reserved-memory/ramoops.yaml | 4 +++ + 2 files changed, 35 insertions(+) + create mode 100644 Documentation/devicetree/bindings/arm/apple,pstore.yaml + +diff --git a/Documentation/devicetree/bindings/arm/apple,pstore.yaml b/Documentation/devicetree/bindings/arm/apple,pstore.yaml +new file mode 100644 +index 000000000..241dee447 +--- /dev/null ++++ b/Documentation/devicetree/bindings/arm/apple,pstore.yaml +@@ -0,0 +1,31 @@ ++# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) ++%YAML 1.2 ++--- ++$id: http://devicetree.org/schemas/arm/apple,pstore.yaml# ++$schema: http://devicetree.org/meta-schemas/core.yaml# ++ ++title: Apple retained pstore allocation ++ ++maintainers: ++ - brkosebay ++ ++description: | ++ Select early allocation on the root node. The generated ramoops node ++ carries apple,preserve-in-capture so a capture kernel leaves evidence intact. ++ Its address is carried in the kexec FDT, never reallocated on capture boots. ++ ++select: ++ properties: ++ compatible: ++ contains: ++ enum: ++ - apple,s8000 ++ - apple,s8003 ++ required: ++ - compatible ++properties: ++ apple,reserve-pstore: ++ type: boolean ++ description: Reserve an aligned 2 MiB no-map ramoops area before paging. ++ ++additionalProperties: true +diff --git a/Documentation/devicetree/bindings/reserved-memory/ramoops.yaml b/Documentation/devicetree/bindings/reserved-memory/ramoops.yaml +index 45cc39ecc..0b349c0ba 100644 +--- a/Documentation/devicetree/bindings/reserved-memory/ramoops.yaml ++++ b/Documentation/devicetree/bindings/reserved-memory/ramoops.yaml +@@ -30,6 +30,10 @@ allOf: + - $ref: reserved-memory.yaml + + properties: ++ apple,preserve-in-capture: ++ type: boolean ++ description: Preserve the previous kernel records by refusing capture-kernel probe. ++ + compatible: + const: ramoops + diff --git a/kernel/patches/drivers/pstore/0002-pstore-retain-the-tethered-warm-boot-state.patch b/kernel/patches/drivers/pstore/0002-pstore-retain-the-tethered-warm-boot-state.patch new file mode 100644 index 00000000..1bac3a8f --- /dev/null +++ b/kernel/patches/drivers/pstore/0002-pstore-retain-the-tethered-warm-boot-state.patch @@ -0,0 +1,180 @@ +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] pstore: retain the tethered warm boot state + +A tethered kernel inherits initialized hardware rather than power-on +state. Retain the board-selected warm handoff state so probe and +shutdown do not discard resources needed by the incoming kernel. + +Assisted-by: LLM +--- + arch/arm64/include/asm/mmu.h | 1 + + arch/arm64/kernel/setup.c | 1 + + arch/arm64/mm/init.c | 88 ++++++++++++++++++++++++++++++++++++ + fs/pstore/ram.c | 8 ++++ + 4 files changed, 98 insertions(+) + +diff --git a/arch/arm64/include/asm/mmu.h b/arch/arm64/include/asm/mmu.h +index 5e1211c54..bd22a913d 100644 +--- a/arch/arm64/include/asm/mmu.h ++++ b/arch/arm64/include/asm/mmu.h +@@ -62,6 +62,7 @@ static inline bool arm64_kernel_unmapped_at_el0(void) + + extern void arm64_memblock_init(void); + extern void paging_init(void); ++extern void apple_soc_publish_pstore(void); + extern void bootmem_init(void); + extern void create_mapping_noalloc(phys_addr_t phys, unsigned long virt, + phys_addr_t size, pgprot_t prot); +diff --git a/arch/arm64/kernel/setup.c b/arch/arm64/kernel/setup.c +index 29c6100f0..45c23e3d9 100644 +--- a/arch/arm64/kernel/setup.c ++++ b/arch/arm64/kernel/setup.c +@@ -332,6 +332,7 @@ void __init __no_sanitize_address setup_arch(char **cmdline_p) + arm64_memblock_init(); + + paging_init(); ++ apple_soc_publish_pstore(); + + acpi_table_upgrade(); + +diff --git a/arch/arm64/mm/init.c b/arch/arm64/mm/init.c +index fbf215ecc..3e458f58e 100644 +--- a/arch/arm64/mm/init.c ++++ b/arch/arm64/mm/init.c +@@ -22,6 +22,8 @@ + #include + #include + #include ++#include ++#include + #include + #include + #include +@@ -193,6 +195,91 @@ static int __init early_mem(char *p) + } + early_param("mem", early_mem); + ++static phys_addr_t apple_soc_pstore_fdt __initdata; ++ ++/* ++ * Reserve before paging/CMA/crashkernel, without touching the linear map: ++ * paging_init() has not built it yet. Prepare the replacement FDT through a ++ * temporary early mapping and publish its linear alias only after paging. ++ * Never choose a fresh address in kdump; reuse the first kernel's region. ++ */ ++static void __init apple_soc_reserve_pstore(void) ++{ ++ void *fdt = initial_boot_params, *copy; ++ phys_addr_t base, copy_pa; ++ fdt64_t reg[2]; ++ char name[40]; ++ int parent, node, size, ret; ++ ++ if (!IS_BUILTIN(CONFIG_PSTORE_RAM) || !fdt || ++ !fdt_getprop(fdt, 0, "apple,reserve-pstore", NULL)) ++ return; ++ if (fdt_node_offset_by_compatible(fdt, -1, "ramoops") >= 0) ++ return; ++ if (is_kdump_kernel()) { ++ pr_warn("Apple pstore: no inherited region in capture kernel\n"); ++ return; ++ } ++ parent = fdt_path_offset(fdt, "/reserved-memory"); ++ if (parent < 0 || fdt_address_cells(fdt, parent) != 2 || ++ fdt_size_cells(fdt, parent) != 2) ++ return; ++ ++ /* Existing firmware, kernel and initrd reservations are already known. */ ++ base = memblock_phys_alloc(SZ_2M, SZ_2M); ++ if (!base) ++ return; ++ size = fdt_totalsize(fdt) + SZ_4K; ++ copy_pa = memblock_phys_alloc(size, SMP_CACHE_BYTES); ++ if (!copy_pa) ++ goto free_region; ++ copy = early_memremap(copy_pa, size); ++ if (!copy) ++ goto free_copy; ++ ret = fdt_open_into(fdt, copy, size); ++ if (ret) ++ goto unmap_copy; ++ parent = fdt_path_offset(copy, "/reserved-memory"); ++ snprintf(name, sizeof(name), "ramoops@%llx", (u64)base); ++ node = fdt_add_subnode(copy, parent, name); ++ if (node < 0) ++ goto unmap_copy; ++ reg[0] = cpu_to_fdt64(base); ++ reg[1] = cpu_to_fdt64(SZ_2M); ++ if (fdt_setprop(copy, node, "compatible", "ramoops", sizeof("ramoops")) || ++ fdt_setprop(copy, node, "reg", reg, sizeof(reg)) || ++ fdt_setprop(copy, node, "no-map", NULL, 0) || ++ fdt_setprop_u32(copy, node, "record-size", SZ_1M) || ++ fdt_setprop_u32(copy, node, "console-size", SZ_1M) || ++ fdt_setprop_u32(copy, node, "max-reason", 2) || ++ fdt_setprop(copy, node, "apple,preserve-in-capture", NULL, 0) || ++ fdt_add_mem_rsv(copy, base, SZ_2M)) ++ goto unmap_copy; ++ /* No cached linear alias of ramoops' write-combined persistent mapping. */ ++ if (memblock_mark_nomap(base, SZ_2M)) ++ goto unmap_copy; ++ early_memunmap(copy, size); ++ apple_soc_pstore_fdt = copy_pa; ++ pr_info("Apple pstore: reserved 2 MiB at %pa, carried in kexec FDT\n", &base); ++ return; ++ ++unmap_copy: ++ early_memunmap(copy, size); ++free_copy: ++ memblock_phys_free(copy_pa, size); ++free_region: ++ memblock_phys_free(base, SZ_2M); ++ pr_warn("Apple pstore: reservation failed\n"); ++} ++ ++/* The linear map now exists; unflattening must see the prepared FDT. */ ++void __init apple_soc_publish_pstore(void) ++{ ++ if (apple_soc_pstore_fdt) ++ early_init_dt_verify(phys_to_virt(apple_soc_pstore_fdt), ++ apple_soc_pstore_fdt); ++} ++ + void __init arm64_memblock_init(void) + { + s64 linear_region_size = PAGE_END - _PAGE_OFFSET(vabits_actual); +@@ -298,6 +385,7 @@ void __init arm64_memblock_init(void) + } + + early_init_fdt_scan_reserved_mem(); ++ apple_soc_reserve_pstore(); + } + + void __init bootmem_init(void) +diff --git a/fs/pstore/ram.c b/fs/pstore/ram.c +index 2eb0d4fa2..b26a3a0d5 100644 +--- a/fs/pstore/ram.c ++++ b/fs/pstore/ram.c +@@ -9,6 +9,7 @@ + #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + + #include ++#include + #include + #include + #include +@@ -729,6 +730,13 @@ static int ramoops_probe(struct platform_device *pdev) + phys_addr_t paddr; + int err = -EINVAL; + ++ /* Leave the original panic/console records intact across phoenix. */ ++ if (is_kdump_kernel() && of_property_read_bool(pdev->dev.of_node, ++ "apple,preserve-in-capture")) { ++ pr_info("capture kernel: preserving Apple records for the full boot\n"); ++ return -ENODEV; ++ } ++ + /* + * Only a single ramoops area allowed at a time, so fail extra + * probes. 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/soc-apple/0022-dt-bindings-soc-apple-select-certified-AOP-reload-th.patch b/kernel/patches/drivers/soc-apple/0022-dt-bindings-soc-apple-select-certified-AOP-reload-th.patch new file mode 100644 index 00000000..7b738fe7 --- /dev/null +++ b/kernel/patches/drivers/soc-apple/0022-dt-bindings-soc-apple-select-certified-AOP-reload-th.patch @@ -0,0 +1,52 @@ +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: soc-apple: select certified AOP reload through + DT + +A retained AOP must not be reset as though it were a fresh boot. +Describe the retained-reload properties: the certified-reload flag, the +reserved shutdown page and the power domains the reload observes. + +Assisted-by: LLM +--- +--- + .../bindings/soc/apple/apple,t8010-aop.yaml | 18 ++++++++++++++++++ + 1 file changed, 18 insertions(+) + +diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml +index 32788d1c7..74586c6e0 100644 +--- a/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml ++++ b/Documentation/devicetree/bindings/soc/apple/apple,t8010-aop.yaml +@@ -18,6 +18,10 @@ description: + board-specific and is loaded by the operating system. + + properties: ++ apple,certified-reload: ++ type: boolean ++ description: Firmware supports the retained shutdown certificate for warm reload. ++ + compatible: + enum: + - apple,s8000-aop +@@ -70,6 +74,20 @@ properties: + The SoC's chip revision, filled in by the bootloader. The S8000 + firmware's boot arguments carry it. + ++ memory-region: ++ maxItems: 1 ++ description: Reserved 16 KiB shutdown transcript page for J71t warm reload. ++ ++ apple,observe-domains: ++ $ref: /schemas/types.yaml#/definitions/phandle-array ++ minItems: 5 ++ maxItems: 5 ++ description: AOP GPIO, AOP, bus interface, CPU and filter in lock order. ++ ++ apple,spi0-domain: ++ $ref: /schemas/types.yaml#/definitions/phandle ++ description: Read-only SPI0 power-state provider observation. ++ + patternProperties: + "^(accel|gyro|compass|pressure)$": + type: object diff --git a/kernel/patches/drivers/soc-apple/0023-soc-apple-aop-select-certified-AOP-reload-through-DT.patch b/kernel/patches/drivers/soc-apple/0023-soc-apple-aop-select-certified-AOP-reload-through-DT.patch new file mode 100644 index 00000000..a73acf3a --- /dev/null +++ b/kernel/patches/drivers/soc-apple/0023-soc-apple-aop-select-certified-AOP-reload-through-DT.patch @@ -0,0 +1,2777 @@ +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] soc: apple: aop: select certified AOP reload through DT + +A retained AOP must not be reset as though it were a fresh boot. Select +retained reload through device-tree data, preserving ordinary +initialization for other hardware. + +Assisted-by: LLM +--- + drivers/soc/apple/aop-warm-record.h | 63 ++ + drivers/soc/apple/aop-warm-spi0.c.h | 1339 ++++++++++++++++++++++++ + drivers/soc/apple/aop-warm-spi0.h | 543 ++++++++++ + drivers/soc/apple/aop.c | 528 +++++++++- + drivers/soc/apple/rtkit.c | 50 + + include/linux/soc/apple/pmgr-observe.h | 27 + + include/linux/soc/apple/rtkit.h | 2 + + 7 files changed, 2543 insertions(+), 9 deletions(-) + create mode 100644 drivers/soc/apple/aop-warm-record.h + create mode 100644 drivers/soc/apple/aop-warm-spi0.c.h + create mode 100644 drivers/soc/apple/aop-warm-spi0.h + create mode 100644 include/linux/soc/apple/pmgr-observe.h + +diff --git a/drivers/soc/apple/aop-warm-record.h b/drivers/soc/apple/aop-warm-record.h +new file mode 100644 +index 000000000..237d78d17 +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-record.h +@@ -0,0 +1,63 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#include ++#include ++#define A9_HO_PAGE 0x4000U ++#define A9_HO_LOG_OFFSET 0x800U ++#define A9_SL_MAGIC 0x4c533941U /* "A9SL" */ ++#define A9_SL_VERSION 1U ++ ++enum a9_ho_step { ++ A9_HO_STEP_BEGIN, ++ A9_HO_STEP_RATES, ++ A9_HO_STEP_AFK_SENT, ++ A9_HO_STEP_AFK_ACKED, ++ A9_HO_STEP_DISABLED, ++ A9_HO_STEP_AP_SENT, ++ A9_HO_STEP_AP_ACKED, ++ A9_HO_STEP_IOP_SENT, ++ A9_HO_STEP_IOP_ACKED, ++ A9_HO_STEP_RAM_STABLE, ++ A9_HO_STEP_MAILBOX_IDLE, ++ A9_HO_STEP_HALTED, ++ A9_HO_STEP_DRAINED, ++ A9_HO_STEPS, ++}; ++ ++static const char * const a9_ho_step_names[A9_HO_STEPS] = { ++ "begin", "rates-zero", "afk-sent", "afk-acked", "endpoint-disable", ++ "ap-sent", "ap-ack", "iop-sent", "iop-ack", "ram-stable", "mailbox-idle", ++ "halt", "drain", ++}; ++ ++struct a9_sl_header { ++ u32 magic, version, capacity, length; ++ u32 lines, truncated, checksum, mode; ++ u64 begin_ns; ++ u8 writer[16]; ++ char entry[64]; ++}; ++ ++_Static_assert(sizeof(struct a9_sl_header) == 120, "shutdown log ABI"); ++ ++#define A9_SL_CAPACITY (A9_HO_PAGE - A9_HO_LOG_OFFSET - sizeof(struct a9_sl_header)) ++ ++/* FNV-1a; incremental over appended text. */ ++static inline u32 a9_sl_hash(u32 hash, const char *text, size_t length) ++{ ++ size_t i; ++ ++ for (i = 0; i < length; i++) ++ hash = (hash ^ (u8)text[i]) * 16777619U; ++ return hash; ++} ++ ++#define A9_SL_HASH_SEED 2166136261U ++ ++/* A replayable previous transcript: complete header, bounded, intact. */ ++static inline bool a9_sl_valid(const struct a9_sl_header *h, const char *text) ++{ ++ return h->magic == A9_SL_MAGIC && h->version == A9_SL_VERSION && ++ h->capacity == A9_SL_CAPACITY && h->length <= h->capacity && ++ !h->entry[sizeof(h->entry) - 1] && ++ a9_sl_hash(A9_SL_HASH_SEED, text, h->length) == h->checksum; ++} +diff --git a/drivers/soc/apple/aop-warm-spi0.c.h b/drivers/soc/apple/aop-warm-spi0.c.h +new file mode 100644 +index 000000000..e9f608abb +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-spi0.c.h +@@ -0,0 +1,1339 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* Ported from a9/aop-release: wake16, release6-d254a9f790. */ ++static void a9_aop_spi0_snapshot(void __iomem *base, u32 *values) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_REGS; i++) ++ values[i] = readl(base + a9_spi0_offsets[i]); ++} ++ ++/* ++ * AOPSPIMaster_reset (0102f2f4) under the AOP-domain PMGR guard with IRQs ++ * off and the AOP CPU halted. Its provider power transition 3 is an ++ * AOP-local call with no AP register equivalent and is not replayed. A poll ++ * timeout still restores the saved control word. ++ */ ++static void a9_aop_spi0_reset_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ void __iomem *control = s->base + A9_SPI0_CONTROL; ++ u32 value = 0; ++ int ret; ++ ++ a9_aop_spi0_snapshot(s->base, s->before); ++ s->saved = s->before[A9_SPI0_I_CONTROL]; ++ s->asserted = a9_spi0_base_word(s->saved) | A9_SPI0_RESET; ++ writel(s->asserted, control); ++ ret = readl_poll_timeout_atomic(control, value, value & A9_SPI0_RESET, ++ 1, A9_SPI0_POLL_US); ++ if (!ret) ++ ret = readl_poll_timeout_atomic(control, value, ++ !(value & A9_SPI0_ENABLE), 1, ++ A9_SPI0_POLL_US); ++ s->polled = value; ++ if (!ret) { ++ udelay(A9_SPI0_DELAY_US); ++ writel(a9_spi0_base_word(s->saved), control); ++ udelay(A9_SPI0_DELAY_US); ++ } ++ writel(s->saved, control); ++ s->reset_done_ns = ktime_get_ns(); ++ a9_aop_spi0_snapshot(s->base, s->after); ++ if (!ret && (s->after[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)) ++ ret = -EIO; ++ s->error = ret; ++} ++ ++/* ++ * Wake8 device evidence: SCM was 0 before/after the first controller reset, ++ * then became 3 at the channel-3 stop store. There is no firmware-derived ++ * SCM status write. Replay AOPSPIMaster_reset after the teardown so the ++ * controller reset, rather than an invented SCM write, is the one variable. ++ */ ++static void a9_aop_spi0_post_reset_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ struct a9_aop_spi0_post_reset *r = &s->post_reset; ++ void __iomem *control = s->base + A9_SPI0_CONTROL; ++ u32 value = 0; ++ int ret; ++ ++ a9_aop_spi0_snapshot(s->base, r->before); ++ r->saved = r->before[A9_SPI0_I_CONTROL]; ++ r->asserted = a9_spi0_base_word(r->saved) | A9_SPI0_RESET; ++ writel(r->asserted, control); ++ ret = readl_poll_timeout_atomic(control, value, value & A9_SPI0_RESET, ++ 1, A9_SPI0_POLL_US); ++ if (!ret) ++ ret = readl_poll_timeout_atomic(control, value, ++ !(value & A9_SPI0_ENABLE), 1, ++ A9_SPI0_POLL_US); ++ r->polled = value; ++ if (!ret) { ++ udelay(A9_SPI0_DELAY_US); ++ writel(a9_spi0_base_word(r->saved), control); ++ udelay(A9_SPI0_DELAY_US); ++ } ++ writel(r->saved, control); ++ a9_aop_spi0_snapshot(s->base, r->after); ++ if (!ret && (r->after[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)) ++ ret = -EIO; ++ r->error = ret; ++} ++ ++static void a9_aop_spi0_ch3_snapshot(void __iomem *base, u32 *values) ++{ ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_CH3_REGS; i++) ++ values[i] = readl(base + a9_spi0_ch3_offsets[i]); ++} ++ ++static void a9_aop_spi0_trace_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ unsigned int channel, i; ++ ++ s->trace.control = readl(s->base + A9_SPI0_CONTROL); ++ s->trace.clock = readl(s->base + A9_SPI0_CLOCK); ++ for (i = 0; i < A9_SPI0_SCM_REGS; i++) ++ s->trace.scm[i] = readl(s->base + a9_spi0_scm_offsets[i]); ++ a9_aop_spi0_ch3_snapshot(s->base, s->trace.ch3); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) ++ for (i = 0; i < A9_SPI0_CHANNEL_REGS; i++) ++ s->trace.channel[channel][i] = ++ readl(s->base + A9_SPI0_CHANNEL_BASE + ++ A9_SPI0_CHANNEL_STRIDE * channel + ++ a9_spi0_channel_offsets[i]); ++} ++ ++/* Warm path only, CPU halted. Log outside the PMGR guard; no SCM writes. */ ++static int a9_aop_spi0_trace(struct apple_aop *aop, const char *stage) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *c = s->trace.ch3, *m = s->trace.scm; ++ const u32 *v; ++ unsigned int channel; ++ int ret; ++ ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_trace_read, s); ++ if (ret) { ++ s->intc.failed = "failed-spi0-trace-guard"; ++ dev_info(aop->dev, "SPI0 SCM stage=%s errno=%d: guard refused\n", stage, ret); ++ return ret; ++ } ++ dev_info(aop->dev, "SPI0 SCM stage=%s errno=0 ctl=%08x clk=%08x scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x cpu=halted\n", ++ stage, s->trace.control, s->trace.clock, m[0], m[1], m[2], m[3], ++ c[0], c[1], c[2], c[3], c[4], c[5], c[6]); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ v = s->trace.channel[channel]; ++ dev_info(aop->dev, "SPI0 SCM channel stage=%s ch=%02u ctl=%08x route=%08x rate=%08x cfg=%08x interval=%08x cursor=%08x desc=%08x pending0=%u pending1=%u\n", ++ stage, channel, v[0], v[1], v[2], v[3], v[4], v[5], v[6], ++ !!(c[A9_CH3_I_8D0] & BIT(2 * channel)), ++ !!(c[A9_CH3_I_8D4] & BIT(2 * channel))); ++ } ++ return 0; ++} ++ ++static const char *a9_aop_spi0_selected_verify(struct a9_aop_spi0 *s); ++ ++/* ++ * Wake10 safety gate, strengthened in wake11 to retain every selected-channel ++ * teardown invariant. A fault observed by a copy sample remains terminal even ++ * if it is transient and the final read happens to clear. ++ */ ++static int a9_aop_spi0_gate(struct apple_aop *aop, const char *stage, ++ const char *check, bool include_copy) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *m, *c; ++ int ret; ++ ++ ret = a9_aop_spi0_trace(aop, stage); ++ if (ret) ++ return ret; ++ m = s->trace.scm; ++ c = s->trace.ch3; ++ if (m[0] || m[1]) ++ s->gate_failed = "failed-spi0-scm"; ++ else if (c[A9_CH3_I_8D0] || c[A9_CH3_I_8D4]) ++ s->gate_failed = "failed-spi0-fault"; ++ else if (include_copy && s->copy_faulted) ++ s->gate_failed = "failed-spi0-copy-sample"; ++ else { ++ s->gate_failed = a9_spi0_ch3_verify(c, s->teardown_stable & BIT(3)); ++ if (!s->gate_failed) ++ s->gate_failed = a9_aop_spi0_selected_verify(s); ++ } ++ ret = s->gate_failed ? -EIO : 0; ++ dev_info(aop->dev, "SPI0 %s check errno=%d scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x copy_faulted=%u result=%s\n", ++ check, ret, m[0], m[1], c[A9_CH3_I_8D0], c[A9_CH3_I_8D4], ++ s->copy_faulted, s->gate_failed ?: "pass"); ++ return ret; ++} ++ ++#define A9_SPI0_COPY_CHUNK 256U ++#define A9_SPI0_COPY_SAMPLE_US 5000U ++ ++struct a9_aop_spi0_copy_probe { ++ void __iomem *base; ++ u32 scm_lo, scm_hi, fault, fault_8d4; ++}; ++ ++static void a9_aop_spi0_copy_probe_read(void *data) ++{ ++ struct a9_aop_spi0_copy_probe *p = data; ++ ++ p->scm_lo = readl(p->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(p->base + A9_SPI0_SCM_HI); ++ p->fault = readl(p->base + A9_SPI0_FAULT); ++ p->fault_8d4 = readl(p->base + A9_SPI0_FAULT_8D4); ++} ++ ++/* ++ * Read-only samples spaced by at least 5 ms across both halves of the SRAM ++ * copy. Once a nonzero state is seen it remains a terminal fact, but the copy ++ * completes so later samples and the full after-copy channel snapshot expose ++ * whether the source is level, transient or moves between channels. ++ */ ++static int a9_aop_spi0_copy_sample(struct apple_aop *aop, const char *phase, ++ size_t offset, size_t size, u64 start_ns, ++ u64 *last_us, unsigned int *sample, ++ bool force) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_copy_probe p = { .base = s->base }; ++ u64 elapsed_us = div_u64(ktime_get_boottime_ns() - start_ns, ++ NSEC_PER_USEC); ++ bool first_fault, initial = !*sample; ++ int ret; ++ ++ if (!force && *sample && elapsed_us - *last_us < A9_SPI0_COPY_SAMPLE_US) ++ return 0; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_copy_probe_read, &p); ++ first_fault = !s->copy_faulted && ++ (p.scm_lo || p.scm_hi || p.fault || p.fault_8d4); ++ if (!ret && first_fault) ++ s->copy_faulted = true; ++ dev_info(aop->dev, "SPI0 COPY sample=%u phase=%s elapsed_us=%llu offset=0x%zx size=0x%zx scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x result=%s\n", ++ *sample, phase, elapsed_us, offset, size, p.scm_lo, p.scm_hi, ++ p.fault, p.fault_8d4, ret ? "guard" : s->copy_faulted ? "fault" : "pass"); ++ *last_us = elapsed_us; ++ (*sample)++; ++ if (ret) ++ return ret; ++ if (first_fault) { ++ ret = a9_aop_spi0_trace(aop, "copy-fault"); ++ if (ret) ++ return ret; ++ } ++ /* The offset-zero sample is the last no-write gate before the copy. */ ++ return !initial || !s->copy_faulted ? 0 : -EIO; ++} ++ ++static int a9_aop_copy_verify_wake10(struct apple_aop *aop, const u8 *src, ++ size_t size) ++{ ++ u32 chunk[A9_SPI0_COPY_CHUNK / 4]; ++ u64 start_ns = ktime_get_boottime_ns(), last_us = 0; ++ unsigned int sample = 0; ++ size_t done, n; ++ int ret; ++ ++ ret = a9_aop_spi0_copy_sample(aop, "write", 0, size, start_ns, &last_us, &sample, true); ++ if (ret) ++ return ret; ++ for (done = 0; done < size; done += n) { ++ n = min_t(size_t, size - done, sizeof(chunk)); ++ __iowrite32_copy(aop->sram + done, src + done, n / 4); ++ ret = a9_aop_spi0_copy_sample(aop, "write", done + n, size, start_ns, ++ &last_us, &sample, done + n == size); ++ if (ret) ++ return ret; ++ } ++ ret = a9_aop_spi0_copy_sample(aop, "verify", 0, size, start_ns, &last_us, &sample, true); ++ if (ret) ++ return ret; ++ for (done = 0; done < size; done += n) { ++ n = min_t(size_t, size - done, sizeof(chunk)); ++ __ioread32_copy(chunk, aop->sram + done, n / 4); ++ if (memcmp(chunk, src + done, n)) ++ return -EIO; ++ ret = a9_aop_spi0_copy_sample(aop, "verify", done + n, size, start_ns, ++ &last_us, &sample, done + n == size); ++ if (ret) ++ return ret; ++ } ++ return 0; ++} ++ ++static void __iomem *a9_aop_spi0_channel_base(struct a9_aop_spi0 *s, ++ unsigned int channel) ++{ ++ return s->base + A9_SPI0_CHANNEL_BASE + A9_SPI0_CHANNEL_STRIDE * channel; ++} ++ ++/* ++ * Wake15: which other channels were RUN with a counting interval when this ++ * stop was issued, and which of them advanced across the stop. Read only; it ++ * separates a stop that had a ticking peer from the stop of the last live ++ * channel in the log. It never gates anything in-kernel. ++ */ ++static void a9_aop_spi0_peers_begin(struct a9_aop_spi0 *s, unsigned int channel, ++ u32 *interval, u32 *run) ++{ ++ unsigned int i; ++ u32 ctl; ++ ++ *run = 0; ++ for (i = 0; i < A9_SPI0_CHANNELS; i++) { ++ void __iomem *base = a9_aop_spi0_channel_base(s, i); ++ ++ interval[i] = 0; ++ if (i == channel) ++ continue; ++ ctl = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]); ++ interval[i] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ if ((ctl & A9_SPI0_CH3_RUN) && interval[i]) ++ *run |= BIT(i); ++ } ++} ++ ++static void a9_aop_spi0_stop_timing(struct a9_aop_spi0 *s, ++ struct a9_aop_spi0_ch3 *c, ++ const u32 *interval) ++{ ++ unsigned int i; ++ ++ c->peers_ticking = 0; ++ for (i = 0; i < A9_SPI0_CHANNELS; i++) { ++ void __iomem *base = a9_aop_spi0_channel_base(s, i); ++ ++ if ((c->peers_run & BIT(i)) && ++ readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]) != ++ interval[i]) ++ c->peers_ticking |= BIT(i); ++ } ++ c->reset_to_stop_us = c->stop_ns > s->reset_done_ns ? ++ div_u64(c->stop_ns - s->reset_done_ns, NSEC_PER_USEC) : 0; ++} ++ ++/* ++ * Wake12/13 captured channel 7, and wake13 channel 3, stopped but never idle: ++ * for 20 complete slices the control/progress word, config, interval, cursor, ++ * SCM and both fault banks were unchanged. This predicate is intentionally ++ * narrower than "RUN clear and no progress": the channel must be one of the ++ * two device-observed periodic clients, must carry that client's route, rate ++ * and config before the stop, and every sample must be byte-identical with SCM ++ * 3/0 (every earlier non-idle stop, which followed a channel-3 stop that had ++ * latched it) or 0/0 (a first stop that latches nothing), no fault, no cursor ++ * movement and a retained nonzero interval. ++ * It only permits the callback masks, descriptor clear, config acknowledge and ++ * the established controller reset; every later SCM, fault and copy gate stays. ++ */ ++static const char *a9_aop_spi0_stable_why(struct a9_aop_spi0_ch3 *c, ++ const struct a9_aop_channel_diag_slice *diag, ++ unsigned int channel) ++{ ++ const struct a9_aop_ch3_slice *first; ++ unsigned int i; ++ u32 route; ++ ++ if (channel == A9_SPI0_PHOSPHORUS_CHANNEL) ++ route = A9_SPI0_PHOSPHORUS_ROUTE; ++ else if (channel == A9_SPI0_MPU_ASYNC_CHANNEL) ++ route = A9_SPI0_MPU_ASYNC_ROUTE; ++ else ++ return "channel"; ++ if (c->poll != -ETIMEDOUT || c->slices != A9_SPI0_CH3_SLICES) ++ return "no-timeout"; ++ if (!(c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN) || ++ c->before[A9_CH3_I_ROUTE] != route || ++ c->before[A9_CH3_I_RATE] != A9_SPI0_PHOSPHORUS_RATE || ++ c->before[A9_CH3_I_C3C] != A9_SPI0_PHOSPHORUS_CFG || ++ c->before[A9_CH3_I_DESC] || (c->stop & A9_SPI0_CH3_RUN)) ++ return "before-identity"; ++ if (c->before[A9_CH3_I_8D4] || (c->before[A9_CH3_I_8D0] & ~BIT(2 * channel))) ++ return "before-fault"; ++ ++ first = &c->slice[0]; ++ if ((first->c3 & A9_SPI0_STABLE_CTL_MASK) != A9_SPI0_STABLE_CTL_STOPPED || ++ (first->c3 & (A9_SPI0_CH3_RUN | A9_SPI0_CH3_IDLE | A9_SPI0_CH3_IRQ1))) ++ return "stopped-control"; ++ if ((first->scm_lo != 0 && first->scm_lo != 3) || first->scm_hi) ++ return "scm"; ++ if (first->fault || diag[0].fault_8d4) ++ return "sample-fault"; ++ if (diag[0].cfg != A9_SPI0_PHOSPHORUS_CFG || diag[0].cursor || !diag[0].interval) ++ return "cfg-cursor-interval"; ++ ++ for (i = 1; i < A9_SPI0_CH3_SLICES; i++) { ++ const struct a9_aop_ch3_slice *p = &c->slice[i]; ++ ++ if (p->c3 != first->c3 || p->scm_lo != first->scm_lo || ++ p->scm_hi != first->scm_hi || p->fault != first->fault || ++ diag[i].cfg != diag[0].cfg || ++ diag[i].interval != diag[0].interval || ++ diag[i].cursor != diag[0].cursor || ++ diag[i].fault_8d4 != diag[0].fault_8d4) ++ return "progress"; ++ } ++ if (c->slice[A9_SPI0_CH3_SLICES - 1].ns < ++ (u64)A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US * NSEC_PER_USEC) ++ return "short"; ++ return NULL; ++} ++ ++/* ++ * AOPSPIMaster_teardownChannel (0102e4f8) for channel 3, then the fault ++ * handler's acknowledge (0102f20a), under the same guard as the reset with ++ * the CPU halted. Every store is a read-modify-write except the descriptor, ++ * which the firmware zeroes. The idle wait runs in 1 ms slices, up to twice ++ * the firmware's own 10 ms SCMto bound; each slice ends with read-only ++ * samples of C3, the SCM state and +0x8d0 (wake15: and config, interval, ++ * cursor and +0x8d4). An idle timeout stops before the masks unless the ++ * stable-stop predicate holds. The acknowledge bit self-clears; +0x8d0 bit 6 ++ * shows whether it took effect. ++ */ ++static void a9_aop_spi0_ch3_locked(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ void __iomem *c3 = s->base + A9_SPI0_CH3; ++ void __iomem *c3c = s->base + A9_SPI0_CH3_0C; ++ void __iomem *base = a9_aop_spi0_channel_base(s, 3); ++ struct a9_aop_ch3_slice *p; ++ struct a9_aop_channel_diag_slice *d; ++ u32 interval[A9_SPI0_CHANNELS]; ++ u32 value = 0; ++ u64 start; ++ ++ a9_aop_spi0_ch3_snapshot(s->base, c->before); ++ c->run_before = c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN; ++ c->stop = readl(c3) & ~A9_SPI0_CH3_STOP; ++ c->stop_ns = ktime_get_ns(); ++ writel(c->stop, c3); ++ start = ktime_get_ns(); ++ a9_aop_spi0_peers_begin(s, 3, interval, &c->peers_run); ++ c->poll = -ETIMEDOUT; ++ for (c->slices = 0; c->poll && c->slices < A9_SPI0_CH3_SLICES; c->slices++) { ++ p = &c->slice[c->slices]; ++ c->poll = readl_poll_timeout_atomic(c3, value, value & A9_SPI0_CH3_IDLE, ++ 1, A9_SPI0_CH3_SLICE_US); ++ p->ns = ktime_get_ns() - start; ++ p->c3 = value; ++ p->scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ p->fault = readl(s->base + A9_SPI0_FAULT); ++ d = &s->ch3_diag[c->slices]; ++ d->cfg = readl(c3c); ++ d->interval = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ d->cursor = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CURSOR]); ++ d->fault_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ } ++ c->idle = value; ++ a9_aop_spi0_stop_timing(s, c, interval); ++ c->stable_why = a9_aop_spi0_stable_why(c, s->ch3_diag, 3); ++ c->stable = !c->stable_why; ++ if (c->stable) ++ s->teardown_stable |= BIT(3); ++ if (!c->poll || c->stable) { ++ /* A normal idle or the captured stopped state: mask callbacks. */ ++ c->idle_us = div_u64(c->slice[c->slices - 1].ns, NSEC_PER_USEC); ++ c->class0 = readl(c3) & ~A9_SPI0_CH3_CLASS0; ++ writel(c->class0, c3); ++ c->class1 = readl(c3) & ~A9_SPI0_CH3_CLASS1; ++ writel(c->class1, c3); ++ writel(0, s->base + A9_SPI0_CH3_DESC); ++ c->ack = readl(c3c) | A9_SPI0_CH3_ACK; ++ writel(c->ack, c3c); ++ } ++ a9_aop_spi0_ch3_snapshot(s->base, c->after); ++ c->failed = c->poll && !c->stable ? "failed-ch3-teardown-idle" : ++ a9_spi0_ch3_verify(c->after, c->stable); ++ if (!c->failed) ++ c->error = 0; ++ else if (c->stable) ++ c->error = -EIO; ++ else ++ c->error = c->poll ?: -EIO; ++} ++ ++ ++static void a9_aop_spi0_channel_snapshot(struct a9_aop_spi0 *s, ++ unsigned int channel, u32 *values) ++{ ++ void __iomem *base = a9_aop_spi0_channel_base(s, channel); ++ ++ values[A9_CH3_I_C3] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]); ++ values[A9_CH3_I_ROUTE] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_ROUTE]); ++ values[A9_CH3_I_RATE] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_RATE]); ++ values[A9_CH3_I_C3C] = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CFG]); ++ values[A9_CH3_I_DESC] = readl(base + A9_SPI0_CHANNEL_DESC); ++ values[A9_CH3_I_8D0] = readl(s->base + A9_SPI0_FAULT); ++ values[A9_CH3_I_8D4] = readl(s->base + A9_SPI0_FAULT_8D4); ++} ++ ++static const char *a9_spi0_channel_state_verify(u32 control, u32 desc, ++ u32 fault, u32 fault_8d4, ++ unsigned int channel, ++ bool stable_stop) ++{ ++ u32 pending = BIT(2 * channel); ++ ++ if (control & A9_SPI0_CH3_RUN) ++ return "failed-channel-teardown-run"; ++ if (control & A9_SPI0_CH3_IRQ0) ++ return "failed-channel-teardown-class0"; ++ if (control & A9_SPI0_CH3_IRQ1) ++ return "failed-channel-teardown-class1"; ++ if (!(control & A9_SPI0_CH3_IDLE) && !stable_stop) ++ return "failed-channel-teardown-idle"; ++ if (desc) ++ return "failed-channel-teardown-desc"; ++ if ((fault | fault_8d4) & pending) ++ return "failed-channel-teardown-pending"; ++ return NULL; ++} ++ ++static const char *a9_spi0_channel_verify(const u32 *after, ++ unsigned int channel, ++ bool stable_stop) ++{ ++ return a9_spi0_channel_state_verify(after[A9_CH3_I_C3], ++ after[A9_CH3_I_DESC], ++ after[A9_CH3_I_8D0], ++ after[A9_CH3_I_8D4], channel, ++ stable_stop); ++} ++ ++/* ++ * Wake11 applies the channel-3 firmware sequence unchanged to another slot. ++ * Wake12 only appends read-only scheduler samples to that established poll. ++ */ ++static void a9_aop_spi0_channel_locked(void *data) ++{ ++ struct a9_aop_spi0_channel_op *op = data; ++ struct a9_aop_spi0 *s = op->spi0; ++ struct a9_aop_spi0_ch3 *c = op->state; ++ void __iomem *base = a9_aop_spi0_channel_base(s, op->channel); ++ void __iomem *control = base + a9_spi0_channel_offsets[A9_CHANNEL_I_CTL]; ++ void __iomem *config = base + a9_spi0_channel_offsets[A9_CHANNEL_I_CFG]; ++ struct a9_aop_ch3_slice *p; ++ struct a9_aop_channel_diag_slice *d; ++ u32 interval[A9_SPI0_CHANNELS]; ++ u32 value = 0; ++ u64 start; ++ ++ a9_aop_spi0_channel_snapshot(s, op->channel, c->before); ++ c->run_before = c->before[A9_CH3_I_C3] & A9_SPI0_CH3_RUN; ++ c->stop = readl(control) & ~A9_SPI0_CH3_STOP; ++ c->stop_ns = ktime_get_ns(); ++ writel(c->stop, control); ++ start = ktime_get_ns(); ++ a9_aop_spi0_peers_begin(s, op->channel, interval, &c->peers_run); ++ c->poll = -ETIMEDOUT; ++ for (c->slices = 0; c->poll && c->slices < A9_SPI0_CH3_SLICES; c->slices++) { ++ p = &c->slice[c->slices]; ++ c->poll = readl_poll_timeout_atomic(control, value, ++ value & A9_SPI0_CH3_IDLE, 1, ++ A9_SPI0_CH3_SLICE_US); ++ p->ns = ktime_get_ns() - start; ++ p->c3 = value; ++ p->scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ p->scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ p->fault = readl(s->base + A9_SPI0_FAULT); ++ d = &s->channel_diag[c->slices]; ++ d->cfg = readl(config); ++ d->interval = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_INTERVAL]); ++ d->cursor = readl(base + a9_spi0_channel_offsets[A9_CHANNEL_I_CURSOR]); ++ d->fault_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ } ++ c->idle = value; ++ a9_aop_spi0_stop_timing(s, c, interval); ++ c->stable_why = a9_aop_spi0_stable_why(c, s->channel_diag, op->channel); ++ c->stable = !c->stable_why; ++ if (c->stable) ++ s->teardown_stable |= BIT(op->channel); ++ if (!c->poll || c->stable) { ++ /* A normal idle or the captured stopped state: mask callbacks. */ ++ c->idle_us = div_u64(c->slice[c->slices - 1].ns, NSEC_PER_USEC); ++ c->class0 = readl(control) & ~A9_SPI0_CH3_CLASS0; ++ writel(c->class0, control); ++ c->class1 = readl(control) & ~A9_SPI0_CH3_CLASS1; ++ writel(c->class1, control); ++ writel(0, base + A9_SPI0_CHANNEL_DESC); ++ c->ack = readl(config) | A9_SPI0_CH3_ACK; ++ writel(c->ack, config); ++ } ++ a9_aop_spi0_channel_snapshot(s, op->channel, c->after); ++ c->failed = c->poll && !c->stable ? "failed-channel-teardown-idle" : ++ a9_spi0_channel_verify(c->after, op->channel, ++ c->stable); ++ if (!c->failed) ++ c->error = 0; ++ else if (c->stable) ++ c->error = -EIO; ++ else ++ c->error = c->poll ?: -EIO; ++} ++ ++/* Called only if ps_spi0 is fully on; reads nothing. */ ++static void a9_aop_spi0_pmgr_on(void *data) ++{ ++ *(bool *)data = true; ++} ++ ++/* AP PMGR ps_spi0, read-only: never requested, resumed or written. ++ * 0 = on, -EHOSTDOWN = off (raw still filled), -ENODEV = no node. ++ */ ++static int a9_aop_spi0_pmgr(struct apple_aop *aop, u32 *raw) ++{ ++ struct apple_pmgr_observation state = {}; ++ bool on = false; ++ int ret; ++ ++ if (!aop->spi0_pmgr) ++ return -ENODEV; ++ ret = apple_pmgr_observe(&aop->spi0_pmgr, 1, &state, ++ a9_aop_spi0_pmgr_on, &on); ++ *raw = state.raw; ++ return ret ?: on ? 0 : -EIO; ++} ++ ++/* ++ * After a reset that cleared +0x8d0 bit 6, before the image copy: CPU halted, ++ * AP notifications masked. A refusal sets ch3.failed and nothing is loaded. ++ */ ++static int a9_aop_spi0_ch3_teardown(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ const u32 *b = c->before, *a = c->after; ++ const struct a9_aop_ch3_slice *p; ++ const struct a9_aop_channel_diag_slice *d; ++ unsigned int i; ++ bool source_progress = false; ++ int ret; ++ ++ aop->experiment_step = "spi0-ch3-teardown"; ++ dev_info(aop->dev, "SPI0 CH3TD begin pa=%llx cpu=halted: first channel-3 store follows\n", ++ A9_SPI0_PA + A9_SPI0_CH3); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint_noflush(aop, "spi0-ch3-teardown"); ++ if (ret) { ++ c->failed = "failed-ch3-teardown-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_ch3_locked, s); ++ if (ret) { ++ c->failed = "failed-ch3-teardown-guard"; ++ dev_info(aop->dev, "SPI0 CH3TD errno=%d ran=0: guard refused, channel 3 not touched\n", ++ ret); ++ return ret; ++ } ++ c->ran = true; ++ dev_info(aop->dev, "SPI0 CH3TD before c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ b[0], b[1], b[2], b[3], b[4], b[5], b[6]); ++ for (i = 0; i < c->slices; i++) { ++ p = &c->slice[i]; ++ dev_info(aop->dev, "SPI0 CH3TD idle-sample slice=%u at_us=%llu c3=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x idle=%u\n", ++ i, div_u64(p->ns, NSEC_PER_USEC), p->c3, p->scm_lo, ++ p->scm_hi, p->fault, !!(p->c3 & A9_SPI0_CH3_IDLE)); ++ if (c->poll) { ++ d = &s->ch3_diag[i]; ++ dev_info(aop->dev, "SPI0 CH3TD diagnostic slice=%u ctl=%08x cfg=%08x interval=%08x cursor=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x\n", ++ i, p->c3, d->cfg, d->interval, d->cursor, p->scm_lo, ++ p->scm_hi, p->fault, d->fault_8d4); ++ if (i && (p->c3 != c->slice[0].c3 || ++ d->interval != s->ch3_diag[0].interval || ++ d->cursor != s->ch3_diag[0].cursor)) ++ source_progress = true; ++ } ++ } ++ if (c->poll && c->slices) ++ dev_info(aop->dev, "SPI0 CH3TD diagnostic mode=scheduler-read-only samples=%u ctl_hi=%02x->%02x interval=%08x->%08x cursor=%08x->%08x source_progress=%u result=%s\n", ++ c->slices, ++ c->slice[0].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ c->slice[c->slices - 1].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ s->ch3_diag[0].interval, ++ s->ch3_diag[c->slices - 1].interval, ++ s->ch3_diag[0].cursor, ++ s->ch3_diag[c->slices - 1].cursor, source_progress, ++ c->stable ? "stable-stop-reset-only" : "refused"); ++ /* beyond_fw_bound=1: idle, but later than the firmware would wait. */ ++ dev_info(aop->dev, "SPI0 CH3TD idle-poll run_before=%u slices=%u/%u idle_us=%llu bound_us=%u fw_bound_us=%u beyond_fw_bound=%u result=%s\n", ++ c->run_before, c->slices, A9_SPI0_CH3_SLICES, c->idle_us, ++ A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US, A9_SPI0_CH3_IDLE_FW_US, ++ !c->poll && c->idle_us > A9_SPI0_CH3_IDLE_FW_US, ++ c->poll ? "timeout" : "idle"); ++ if (c->poll && !c->stable) ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=%d masks=skipped: idle bit 1 never set, no further store\n", ++ c->stop, c->idle, c->poll); ++ else if (c->stable) ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=%d stable_stop=1 class0=%08x class1=%08x desc=00000000 c3c=%08x\n", ++ c->stop, c->idle, c->poll, c->class0, c->class1, c->ack); ++ else ++ dev_info(aop->dev, "SPI0 CH3TD writes stop=%08x idle_poll=%08x poll_errno=0 class0=%08x class1=%08x desc=00000000 c3c=%08x\n", ++ c->stop, c->idle, c->class0, c->class1, c->ack); ++ dev_info(aop->dev, "SPI0 CH3TD after c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], a[6]); ++ dev_info(aop->dev, "SPI0 CH3TD timing reset_to_stop_us=%llu late=%u peers_run=%04x peers_ticking=%04x stable=%u checkpoint=noflush stable_why=%s\n", ++ c->reset_to_stop_us, c->reset_to_stop_us > A9_SPI0_LATE_STOP_US, ++ c->peers_run, c->peers_ticking, c->stable, ++ c->stable ? "stable" : c->poll ? c->stable_why : "idle"); ++ /* c3c_bit5 is the pulse's readback, logged only (0 at 19:50). */ ++ dev_info(aop->dev, "SPI0 CH3TD check errno=%d run=%u class0=%u class1=%u idle=%u desc=%08x c3c_bit5=%u ch3_pending=%u result=%s\n", ++ c->error, !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_RUN), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1), !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE), ++ a[A9_CH3_I_DESC], !!(a[A9_CH3_I_C3C] & A9_SPI0_CH3_ACK), ++ !!(a[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING), c->failed ?: ++ c->stable ? "pass-stable-reset-only" : "pass"); ++ return c->error; ++} ++ ++static int a9_aop_spi0_channel_teardown(struct apple_aop *aop, ++ unsigned int channel) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->channel; ++ struct a9_aop_spi0_channel_op op = { ++ .spi0 = s, ++ .state = c, ++ .channel = channel, ++ }; ++ const u32 *b, *a; ++ const struct a9_aop_ch3_slice *p; ++ const struct a9_aop_channel_diag_slice *d; ++ char checkpoint[32]; ++ unsigned int i; ++ bool source_progress = false; ++ bool stable_stop; ++ int ret; ++ ++ memset(c, 0, sizeof(*c)); ++ memset(s->channel_diag, 0, sizeof(s->channel_diag)); ++ s->teardown_failed_channel = channel; ++ aop->experiment_step = "spi0-channel-teardown"; ++ snprintf(checkpoint, sizeof(checkpoint), "spi0-ch%u-teardown", channel); ++ dev_info(aop->dev, "SPI0 CH%uTD begin pa=%llx cpu=halted: first channel-%u store follows\n", ++ channel, A9_SPI0_PA + A9_SPI0_CHANNEL_BASE + ++ A9_SPI0_CHANNEL_STRIDE * channel, channel); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint_noflush(aop, checkpoint); ++ if (ret) { ++ c->failed = "failed-channel-teardown-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_channel_locked, &op); ++ if (ret) { ++ c->failed = "failed-channel-teardown-guard"; ++ dev_info(aop->dev, "SPI0 CH%uTD errno=%d ran=0: guard refused, channel %u not touched\n", ++ channel, ret, channel); ++ return ret; ++ } ++ c->ran = true; ++ stable_stop = s->teardown_stable & BIT(channel); ++ b = c->before; ++ a = c->after; ++ dev_info(aop->dev, "SPI0 CH%uTD before ctl=%08x route=%08x rate=%08x cfg=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ channel, b[0], b[1], b[2], b[3], b[4], b[5], b[6]); ++ for (i = 0; i < c->slices; i++) { ++ p = &c->slice[i]; ++ dev_info(aop->dev, "SPI0 CH%uTD idle-sample slice=%u at_us=%llu ctl=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x idle=%u\n", ++ channel, i, div_u64(p->ns, NSEC_PER_USEC), p->c3, ++ p->scm_lo, p->scm_hi, p->fault, ++ !!(p->c3 & A9_SPI0_CH3_IDLE)); ++ if (c->poll) { ++ d = &s->channel_diag[i]; ++ dev_info(aop->dev, "SPI0 CH%uTD diagnostic slice=%u ctl=%08x cfg=%08x interval=%08x cursor=%08x scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x\n", ++ channel, i, p->c3, d->cfg, d->interval, d->cursor, ++ p->scm_lo, p->scm_hi, p->fault, d->fault_8d4); ++ if (i && (p->c3 != c->slice[0].c3 || ++ d->interval != s->channel_diag[0].interval || ++ d->cursor != s->channel_diag[0].cursor)) ++ source_progress = true; ++ } ++ } ++ if (c->poll && c->slices) ++ dev_info(aop->dev, "SPI0 CH%uTD diagnostic mode=scheduler-read-only samples=%u ctl_hi=%02x->%02x interval=%08x->%08x cursor=%08x->%08x source_progress=%u result=%s\n", ++ channel, c->slices, ++ c->slice[0].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ c->slice[c->slices - 1].c3 >> A9_SPI0_CHANNEL_PROGRESS_SHIFT, ++ s->channel_diag[0].interval, ++ s->channel_diag[c->slices - 1].interval, ++ s->channel_diag[0].cursor, ++ s->channel_diag[c->slices - 1].cursor, ++ source_progress, ++ stable_stop ? "stable-stop-reset-only" : "refused"); ++ dev_info(aop->dev, "SPI0 CH%uTD idle-poll run_before=%u slices=%u/%u idle_us=%llu bound_us=%u fw_bound_us=%u beyond_fw_bound=%u result=%s\n", ++ channel, c->run_before, c->slices, A9_SPI0_CH3_SLICES, c->idle_us, ++ A9_SPI0_CH3_SLICES * A9_SPI0_CH3_SLICE_US, A9_SPI0_CH3_IDLE_FW_US, ++ !c->poll && c->idle_us > A9_SPI0_CH3_IDLE_FW_US, ++ c->poll ? "timeout" : "idle"); ++ if (c->poll && !stable_stop) ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=%d masks=skipped: idle bit 1 never set, no further store\n", ++ channel, c->stop, c->idle, c->poll); ++ else if (stable_stop) ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=%d stable_stop=1 class0=%08x class1=%08x desc=00000000 cfg=%08x\n", ++ channel, c->stop, c->idle, c->poll, c->class0, c->class1, ++ c->ack); ++ else ++ dev_info(aop->dev, "SPI0 CH%uTD writes stop=%08x idle_poll=%08x poll_errno=0 class0=%08x class1=%08x desc=00000000 cfg=%08x\n", ++ channel, c->stop, c->idle, c->class0, c->class1, c->ack); ++ dev_info(aop->dev, "SPI0 CH%uTD after ctl=%08x route=%08x rate=%08x cfg=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ channel, a[0], a[1], a[2], a[3], a[4], a[5], a[6]); ++ dev_info(aop->dev, "SPI0 CH%uTD timing reset_to_stop_us=%llu late=%u peers_run=%04x peers_ticking=%04x stable=%u checkpoint=noflush stable_why=%s\n", ++ channel, c->reset_to_stop_us, c->reset_to_stop_us > A9_SPI0_LATE_STOP_US, ++ c->peers_run, c->peers_ticking, c->stable, ++ c->stable ? "stable" : c->poll ? c->stable_why : "idle"); ++ dev_info(aop->dev, "SPI0 CH%uTD check errno=%d run=%u class0=%u class1=%u idle=%u desc=%08x cfg_bit5=%u pending0=%u pending1=%u result=%s\n", ++ channel, c->error, !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_RUN), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1), ++ !!(a[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE), a[A9_CH3_I_DESC], ++ !!(a[A9_CH3_I_C3C] & A9_SPI0_CH3_ACK), ++ !!(a[A9_CH3_I_8D0] & BIT(2 * channel)), ++ !!(a[A9_CH3_I_8D4] & BIT(2 * channel)), c->failed ?: ++ stable_stop ? "pass-stable-reset-only" : "pass"); ++ return c->error; ++} ++ ++static const char *a9_aop_spi0_selected_verify(struct a9_aop_spi0 *s) ++{ ++ const u32 *v; ++ const char *failed = NULL; ++ unsigned int channel; ++ ++ if (s->teardown_selected != s->teardown_done) ++ return "failed-channel-teardown-incomplete"; ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3 || !(s->teardown_selected & BIT(channel))) ++ continue; ++ v = s->trace.channel[channel]; ++ failed = a9_spi0_channel_state_verify(v[A9_CHANNEL_I_CTL], ++ v[A9_CHANNEL_I_DESC], ++ s->trace.ch3[A9_CH3_I_8D0], ++ s->trace.ch3[A9_CH3_I_8D4], channel, ++ s->teardown_stable & BIT(channel)); ++ if (failed) { ++ s->teardown_failed_channel = channel; ++ return failed; ++ } ++ } ++ return NULL; ++} ++ ++/* ++ * Channel 3 remains mandatory and executes first. Wake11 additionally selects ++ * every snapshot slot with run bit 0 (RUN) set and a nonzero interval, which ++ * excludes configured-but-idle slots while selecting any periodic client still ++ * owned by the old firmware. Wake16 returns to wake11's order after wake15's ++ * channel-7-first stop failed: all ten device stops fit "channel 3 idles when ++ * stopped within ~2 ms of the reset (5/5; 0/2 at 11 and 33 ms) and channel 7 ++ * never idles (0/3, at 0.3 ms, 1.9 ms and 15 ms)". Channel 3 goes first and ++ * early; channel 7 is then the stopped-without-idle channel the stable-stop ++ * fingerprint was captured from (wake11/12). ++ */ ++static int a9_aop_spi0_teardown_running(struct apple_aop *aop) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *v; ++ unsigned int channel; ++ int ret; ++ ++ s->teardown_selected = BIT(3); ++ s->teardown_done = 0; ++ s->teardown_stable = 0; ++ s->teardown_failed_channel = A9_SPI0_CHANNELS; ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3) ++ continue; ++ v = s->trace.channel[channel]; ++ if ((v[A9_CHANNEL_I_CTL] & A9_SPI0_CH3_RUN) && ++ v[A9_CHANNEL_I_INTERVAL]) ++ s->teardown_selected |= BIT(channel); ++ } ++ dev_info(aop->dev, "SPI0 teardown select stage=after-controller-reset selected=%04x mandatory=0008 rule=ch3-or-run-and-interval order=ch3-then-others\n", ++ s->teardown_selected); ++ ret = a9_aop_spi0_ch3_teardown(aop); ++ if (ret) { ++ s->teardown_failed_channel = 3; ++ dev_info(aop->dev, "SPI0 teardown refused selected=%04x done=%04x failed_channel=3 errno=%d result=%s\n", ++ s->teardown_selected, s->teardown_done, ret, ++ s->ch3.failed ?: "failed-ch3-teardown"); ++ return ret; ++ } ++ s->teardown_done |= BIT(3); ++ for (channel = 0; channel < A9_SPI0_CHANNELS; channel++) { ++ if (channel == 3 || !(s->teardown_selected & BIT(channel))) ++ continue; ++ ret = a9_aop_spi0_channel_teardown(aop, channel); ++ if (ret) { ++ dev_info(aop->dev, "SPI0 teardown refused selected=%04x done=%04x failed_channel=%u errno=%d result=%s\n", ++ s->teardown_selected, s->teardown_done, channel, ret, ++ s->channel.failed ?: "failed-channel-teardown"); ++ return ret; ++ } ++ s->teardown_done |= BIT(channel); ++ } ++ s->teardown_failed_channel = A9_SPI0_CHANNELS; ++ dev_info(aop->dev, "SPI0 teardown complete selected=%04x done=%04x stable=%04x result=pass\n", ++ s->teardown_selected, s->teardown_done, s->teardown_stable); ++ return 0; ++} ++ ++/* ++ * Second copy of the firmware controller reset, after the stop has completed. ++ * The preceding after-ch3-teardown trace is the read-only before snapshot; ++ * after-post-teardown-reset is the matching read-only after snapshot. Refuse ++ * the image copy unless SCM is zero and every channel quiet-state gate holds. ++ */ ++static int a9_aop_spi0_post_reset(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_post_reset *r = &s->post_reset; ++ const u32 *b = r->before, *a = r->after; ++ int ret; ++ ++ if (!aop->force_halted || !aop->force_drained || !s->ch3.ran || ++ s->ch3.error || s->teardown_selected != s->teardown_done || r->ran) { ++ r->failed = "failed-post-reset-state"; ++ return -EPERM; ++ } ++ aop->experiment_step = "spi0-post-teardown-reset"; ++ dev_info(aop->dev, "SPI0 POSTRST begin pa=%llx reason=ch3-stop-latched-scm cpu=halted\n", ++ A9_SPI0_PA); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "spi0-post-teardown-reset"); ++ if (ret) { ++ r->failed = "failed-post-reset-checkpoint"; ++ return ret; ++ } ++ } ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_post_reset_locked, s); ++ r->ran = !ret; ++ r->error = ret ?: r->error; ++ dev_info(aop->dev, "SPI0 POSTRST errno=%d ran=%d before ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x\n", ++ r->error, r->ran, b[0], b[1], b[2], b[3], b[4], b[5]); ++ dev_info(aop->dev, "SPI0 POSTRST after ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x saved=%08x asserted=%08x polled=%08x ch3_pending=%u->%u\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], r->saved, r->asserted, ++ r->polled, !!(b[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING), ++ !!(a[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)); ++ if (r->error) { ++ r->failed = "failed-post-reset-controller"; ++ return r->error; ++ } ++ ret = a9_aop_spi0_trace(aop, "after-post-teardown-reset"); ++ if (ret) ++ return ret; ++ if (s->trace.scm[0] || s->trace.scm[1]) ++ r->failed = "failed-post-reset-scm"; ++ else { ++ r->failed = a9_spi0_ch3_verify(s->trace.ch3, ++ s->teardown_stable & BIT(3)); ++ if (!r->failed) ++ r->failed = a9_aop_spi0_selected_verify(s); ++ } ++ ret = r->failed ? -EIO : 0; ++ dev_info(aop->dev, "SPI0 POSTRST check errno=%d scm_lo=%08x scm_hi=%08x ch3_pending=%u result=%s\n", ++ ret, s->trace.scm[0], s->trace.scm[1], ++ !!(s->trace.ch3[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING), ++ r->failed ?: "pass"); ++ dev_info(aop->dev, "SPI0 POSTRST channels selected=%04x done=%04x stable=%04x result=%s\n", ++ s->teardown_selected, s->teardown_done, s->teardown_stable, ++ r->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * The ten INTC words of a9_intc_offsets into state[@at], read only. The ++ * INTC is the AOP wrapper block (0xc1800000 in the firmware, space 2 as ++ * SPI0's verified 0xc1100000 = 0x210100000), which this driver reads ++ * unguarded for the mailbox and CPU control; no separate PMGR domain. ++ */ ++static void a9_aop_intc_snapshot(struct apple_aop *aop, unsigned int at) ++{ ++ struct a9_aop_intc *n = &aop->spi0.intc; ++ u32 *v = n->state[at]; ++ unsigned int i; ++ ++ for (i = 0; i < A9_INTC_REGS; i++) ++ v[i] = aop_rd(aop, a9_intc_offsets[i]); ++ n->states |= BIT(at); ++ dev_info(aop->dev, "AOP INTC state at=%s reset808=%08x current81c=%08x raise900=%08x ack980=%08x mask_a00=%08x mask_a04=%08x mask_a08=%08x mask_a80=%08x g7_c020=%08x g7_c024=%08x src_masked=%u view=%s\n", ++ a9_intc_at_name(at), v[0], v[1], v[2], v[3], v[4], v[5], v[6], ++ v[7], v[8], v[9], !!(v[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM), ++ a9_intc_view(v)); ++} ++ ++/* ++ * Round 4 (ds-aop-spi0-ch3-round4.md): the old session's SPI0 global IRQ ++ * 0x10014 stays latched in the AOP interrupt controller, and fresh firmware ++ * takes it as soon as SPI lifecycle event 1 enables it, before its own SPI0 ++ * reset (SCMErr:0x3,3). With the CPU halted and before the first SPI0 ++ * access, disable that source (group 1 disable bank, 0102c9ce). The bank ++ * reads back the mask, as the firmware's own reads of it show: bit 20 must ++ * read 1 after the store. ++ */ ++static int a9_aop_intc_disable(struct apple_aop *aop) ++{ ++ struct a9_aop_intc *n = &aop->spi0.intc; ++ const u32 *b = n->state[A9_INTC_AT_PRE_DISABLE]; ++ const u32 *a = n->state[A9_INTC_AT_POST_DISABLE]; ++ int ret; ++ ++ aop->experiment_step = "intc-disable"; ++ dev_info(aop->dev, "AOP INTC disable begin irq=0x10014 pa=%llx value=%08x cpu=halted: first INTC store follows\n", ++ A9_SPI0_WRAPPER_PA + A9_INTC_DISABLE, A9_INTC_SPI0_SCM); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "intc-disable"); ++ if (ret) { ++ n->failed = "failed-intc-disable-checkpoint"; ++ return ret; ++ } ++ } ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_PRE_DISABLE); ++ ret = aop_wr(aop, A9_INTC_DISABLE, A9_INTC_SPI0_SCM); ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_POST_DISABLE); ++ if (ret) { ++ n->failed = "failed-intc-disable"; ++ } else if (!(a[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM)) { ++ n->failed = "failed-intc-disable-readback"; ++ ret = -EIO; ++ } ++ n->disabled = !ret; ++ dev_info(aop->dev, "AOP INTC disable errno=%d before=%08x value=%08x after=%08x mask_a80=%08x src_masked=%u view=%s result=%s\n", ++ ret, b[A9_INTC_I_MASK0], A9_INTC_SPI0_SCM, a[A9_INTC_I_MASK0], ++ a[A9_INTC_I_ENABLE0], !!(a[A9_INTC_I_MASK0] & A9_INTC_SPI0_SCM), ++ a9_intc_view(a), n->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * Release policy for aop_spi0_reset=1. The reload is device-proven for exactly ++ * one retained state: the previous kernel quiesced and halted the firmware ++ * (wake16, 2026-10-29; decision prev-quiesce-halted). Auto-boot still set, a ++ * previous kernel that did not halt it, or an older kernel in the chain are ++ * different hardware states that no run has reloaded with the final teardown. ++ * Refuse them before the first halt or SPI0 access so the old firmware is ++ * left exactly as found; aop_spi0_untested=1 attempts them anyway. A skip ++ * (DFU cold, bad record, nothing ran) must also find the wrapper at its reset ++ * state: a record-less AOP that something already stopped is refused rather ++ * than cold-loaded. A stale record skips as before. ++ */ ++static int a9_aop_spi0_policy(struct apple_aop *aop, u32 auto_boot, u32 outbox) ++{ ++ enum a9_spi0_reason reason = a9_spi0_decide(auto_boot, outbox & AOP_OUTBOX_ENABLE, ++ &aop->spi0.prev); ++ ++ if (a9_spi0_policy_allows(reason, outbox & AOP_OUTBOX_ENABLE, spi0_untested)) ++ return 0; ++ dev_info(aop->dev, "SPI0 policy decision=refuse reason=%s auto_boot=%u out4020=%08x prev=%s prev_hello=%d prev_halted=%d: only prev-quiesce-halted reloads and a skip needs the wrapper at its reset state block not touched\n", ++ a9_spi0_reason_name(reason), auto_boot, outbox, ++ a9_spi0_prev_name(aop->spi0.prev.state), aop->spi0.prev.hello, ++ aop->spi0.prev.halted); ++ return -EPERM; ++} ++ ++/* ++ * After the proven halt/drain, before any SRAM write. Only firmware that ran ++ * since the last SoC reset leaves SPI0 state behind: auto-boot 1, or the ++ * previous kernel's transcript bound by +0x4020 (a9_spi0_decide). Without ++ * that evidence the block is never touched. @auto_boot and @outbox are the ++ * boot snapshot from before the stop. Nonzero means: do not load or ++ * release the AOP. ++ */ ++static int a9_aop_spi0_prereset(struct apple_aop *aop, u32 auto_boot, u32 outbox) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ const u32 *b = s->before, *a = s->after; ++ const char *failed = NULL; ++ bool run; ++ int ret; ++ ++ s->auto_boot = auto_boot; ++ s->outbox = outbox; ++ s->reason = a9_spi0_decide(auto_boot, outbox & AOP_OUTBOX_ENABLE, &s->prev); ++ s->decided = true; ++ run = a9_spi0_reason_runs(s->reason); ++ dev_info(aop->dev, "SPI0 reset decision=%s reason=%s auto_boot=%u out4020=%08x prev=%s prev_hello=%d prev_halted=%d prev_steps=0x%x prev_carry=%d%s\n", ++ run ? "run" : "skip", a9_spi0_reason_name(s->reason), auto_boot, ++ outbox, a9_spi0_prev_name(s->prev.state), s->prev.hello, ++ s->prev.halted, s->prev.steps, s->prev.carried, ++ run ? "" : ": block not touched"); ++ if (!run) { ++ s->skipped = a9_spi0_reason_name(s->reason); ++ return 0; ++ } ++ if (!s->base || !aop->force_halted || !aop->force_drained || ++ !a9_aop_force_stopped(aop)) ++ return -EPERM; ++ ret = a9_aop_intc_disable(aop); ++ if (ret) ++ return ret; ++ aop->experiment_step = "spi0-reset"; ++ s->pmgr_before_errno = a9_aop_spi0_pmgr(aop, &s->pmgr_before); ++ /* Reaches ramoops before the first AP access to the block. */ ++ dev_info(aop->dev, "SPI0 reset begin pa=%llx cpu=halted ps_spi0=%08x errno=%d: first AP access follows\n", ++ A9_SPI0_PA, s->pmgr_before, s->pmgr_before_errno); ++ if (cold_defer) { ++ ret = a9_aop_cold_checkpoint(aop, "spi0-reset"); ++ if (ret) ++ return ret; ++ } ++ ret = a9_aop_spi0_trace(aop, "before-controller-reset"); ++ if (ret) ++ return ret; ++ /* ++ * Wake15: from the controller reset to the last channel stop nothing may ++ * drain a console in this task. Holding the console lock makes every ++ * dev_info() below only append to the log buffer; the flushed spi0-reset ++ * checkpoint above already put the begin line into ramoops, and the lines ++ * are emitted by console_unlock() once the stops are stored. ++ */ ++ console_lock(); ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_reset_locked, s); ++ s->ran = !ret; ++ s->error = ret ?: s->error; ++ s->pmgr_after_errno = a9_aop_spi0_pmgr(aop, &s->pmgr_after); ++ dev_info(aop->dev, "SPI0 reset errno=%d ran=%d pa=%llx before ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x\n", ++ s->error, s->ran, A9_SPI0_PA, b[0], b[1], b[2], b[3], b[4], b[5]); ++ dev_info(aop->dev, "SPI0 reset after ctl=%08x clk=%08x 8d0=%08x 8d4=%08x ch3=%08x ch3c=%08x saved=%08x asserted=%08x polled=%08x ch3_pending=%u->%u provider3=not-replayed\n", ++ a[0], a[1], a[2], a[3], a[4], a[5], s->saved, s->asserted, ++ s->polled, !!(b[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING), ++ !!(a[A9_SPI0_I_FAULT] & A9_SPI0_CH3_PENDING)); ++ dev_info(aop->dev, "SPI0 pmgr ps_spi0 before=%08x errno=%d after=%08x errno=%d (read-only, never powered)\n", ++ s->pmgr_before, s->pmgr_before_errno, s->pmgr_after, ++ s->pmgr_after_errno); ++ if (s->error) { ++ console_unlock(); ++ return s->error; ++ } ++ ret = a9_aop_spi0_trace(aop, "after-controller-reset"); ++ if (!ret) ++ ret = a9_aop_spi0_teardown_running(aop); ++ console_unlock(); ++ if (!ret) ++ ret = a9_aop_spi0_trace(aop, "after-ch3-teardown"); ++ if (!ret) ++ failed = a9_aop_spi0_selected_verify(s); ++ if (failed) { ++ s->channel.failed = failed; ++ ret = -EIO; ++ } ++ if (!ret) ++ ret = a9_aop_spi0_post_reset(aop); ++ return ret; ++} ++ ++/* Wake10 samples both SCM words and both fault classes, not only channel 3. */ ++static void a9_aop_spi0_fault_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ ++ s->prerun_scm_lo = readl(s->base + A9_SPI0_SCM_LO); ++ s->prerun_scm_hi = readl(s->base + A9_SPI0_SCM_HI); ++ s->prerun = readl(s->base + A9_SPI0_FAULT); ++ s->prerun_8d4 = readl(s->base + A9_SPI0_FAULT_8D4); ++ if (s->prerun & A9_SPI0_CH3_PENDING) ++ a9_aop_spi0_ch3_snapshot(s->base, s->ch3.reassert); ++} ++ ++/* ++ * Last AP look before RUN, CPU still halted, after the image copy: bit 6 ++ * must stay clear in every one of 31 guarded samples spaced >= 5 ms apart, ++ * spanning >= 150 ms. A reasserted channel-3 bit would crash the fresh ++ * image as before, so the first set sample refuses the release; nothing is ++ * retried. Any refusal sets ch3.failed. ++ */ ++static int a9_aop_spi0_recheck(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_spi0_ch3 *c = &s->ch3; ++ const u32 *r = c->reassert; ++ u64 start = ktime_get_boottime_ns(); ++ unsigned int i; ++ int ret = 0; ++ ++ for (i = 0; i < A9_SPI0_WINDOW_SAMPLES; i++) { ++ if (i) ++ usleep_range(A9_SPI0_WINDOW_US, A9_SPI0_WINDOW_US + 1000); ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_fault_read, s); ++ c->samples = i + 1; ++ c->elapsed_ms = div_u64(ktime_get_boottime_ns() - start, NSEC_PER_MSEC); ++ if (ret) { ++ c->failed = "failed-ch3-window-guard"; ++ break; ++ } ++ if (s->prerun_scm_lo || s->prerun_scm_hi || s->prerun || ++ s->prerun_8d4) { ++ s->reasserted = c->reasserted = true; ++ c->reassert_sample = i; ++ c->reassert_ms = c->elapsed_ms; ++ c->failed = "failed-spi0-window-reasserted"; ++ ret = -EIO; ++ dev_info(aop->dev, "SPI0 WINDOW reasserted sample=%u at_ms=%llu scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x cpu=halted\n", ++ i, c->reassert_ms, s->prerun_scm_lo, s->prerun_scm_hi, ++ s->prerun, s->prerun_8d4); ++ if (s->prerun & A9_SPI0_CH3_PENDING) ++ dev_info(aop->dev, "SPI0 WINDOW channel3 c3=%08x route=%08x rate=%08x c3c=%08x desc=%08x 8d0=%08x 8d4=%08x\n", ++ r[0], r[1], r[2], r[3], r[4], r[5], r[6]); ++ break; ++ } ++ } ++ /* usleep_range() cannot return early; this only trips on a bad clock. */ ++ if (!ret && c->elapsed_ms < A9_SPI0_WINDOW_MS) { ++ c->failed = "failed-ch3-window-short"; ++ ret = -EIO; ++ } ++ c->window_done = !ret; ++ dev_info(aop->dev, "SPI0 CH3TD window errno=%d samples=%u/%u elapsed_ms=%llu min_ms=%u result=%s\n", ++ ret, c->samples, A9_SPI0_WINDOW_SAMPLES, c->elapsed_ms, ++ A9_SPI0_WINDOW_MS, ret ? c->failed : "pass"); ++ dev_info(aop->dev, "SPI0 pre-run errno=%d 8d0=%08x ch3_pending=%u cpu=halted\n", ++ ret, s->prerun, !!(s->prerun & A9_SPI0_CH3_PENDING)); ++ dev_info(aop->dev, "SPI0 WINDOW state errno=%d scm_lo=%08x scm_hi=%08x 8d0=%08x 8d4=%08x result=%s\n", ++ ret, s->prerun_scm_lo, s->prerun_scm_hi, s->prerun, ++ s->prerun_8d4, ret ? c->failed : "pass"); ++ if (ret && c->reasserted) ++ a9_aop_spi0_trace(aop, "window-fault"); ++ return ret; ++} ++ ++/* SCM state and the channel-3 interval pair, read-only, into scm[scm_at]. */ ++static void a9_aop_spi0_scm_read(void *data) ++{ ++ struct a9_aop_spi0 *s = data; ++ unsigned int i; ++ ++ for (i = 0; i < A9_SPI0_SCM_REGS; i++) ++ s->intc.scm[s->intc.scm_at][i] = readl(s->base + a9_spi0_scm_offsets[i]); ++} ++ ++/* ++ * After the clean pre-RUN window, CPU still halted: the firmware's group-1 ++ * pre-handler acknowledge for 0x10014 (0102c6de..0102c6f2) discards the ++ * old-session latch now that its SPI0 cause is quiet. Fresh firmware ++ * re-enables the source in event 1. The INTC words, the SCM state and the ++ * C3 interval pair are logged around it and never gated: with 0x10014 ++ * masked, no readable word shows whether it is still pending. ++ */ ++static int a9_aop_intc_ack(struct apple_aop *aop) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS] = {}; ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_intc *n = &s->intc; ++ const u32 *p = n->scm[0], *q = n->scm[1]; ++ const u32 *b = n->state[A9_INTC_AT_PRE_ACK]; ++ const u32 *a = n->state[A9_INTC_AT_POST_ACK]; ++ int ret; ++ ++ aop->experiment_step = "intc-ack"; ++ n->scm_at = 0; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_scm_read, s); ++ if (ret) { ++ n->failed = "failed-intc-ack-guard"; ++ dev_info(aop->dev, "AOP INTC pre-ack errno=%d: guard refused, no acknowledge\n", ++ ret); ++ return ret; ++ } ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_PRE_ACK); ++ dev_info(aop->dev, "AOP INTC pre-ack scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x current=%08x cpu=halted\n", ++ p[0], p[1], p[2], p[3], b[A9_INTC_I_CURRENT]); ++ ret = aop_wr(aop, A9_INTC_ACK, A9_INTC_SPI0_SCM); ++ /* Also the posted-write barrier. */ ++ a9_aop_intc_snapshot(aop, A9_INTC_AT_POST_ACK); ++ n->acked = !ret; ++ dev_info(aop->dev, "AOP INTC ack errno=%d before=%08x value=%08x after=%08x\n", ++ ret, b[A9_INTC_I_ACK], A9_INTC_SPI0_SCM, a[A9_INTC_I_ACK]); ++ if (ret) { ++ n->failed = "failed-intc-ack"; ++ return ret; ++ } ++ n->scm_at = 1; ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, ++ a9_aop_spi0_scm_read, s); ++ if (ret) ++ n->failed = "failed-intc-post-guard"; ++ dev_info(aop->dev, "AOP INTC post-ack errno=%d scm_lo=%08x scm_hi=%08x c3_interval=%08x c3_cursor=%08x current=%08x result=%s\n", ++ ret, q[0], q[1], q[2], q[3], a[A9_INTC_I_CURRENT], n->failed ?: "pass"); ++ return ret; ++} ++ ++/* ++ * Wake7 still took 0x10014 before SPI event 5 despite the targeted ack. ++ * Replay INTC lifecycle event 3 (0102c592/598/59c/5a2): 1/read/0/read at ++ * +0x808. aop_wr includes a posted-write read after each store. This resets ++ * all AOP-private interrupt state; it does not write the SCM status or MPU. ++ * No assumption that SCM must become zero. Only the final reset readback ++ * and the established channel-3 quiet conditions gate RUN. ++ */ ++static int a9_aop_intc_reset(struct apple_aop *aop) ++{ ++ struct a9_aop_spi0 *s = &aop->spi0; ++ struct a9_aop_intc *n = &s->intc; ++ int ret; ++ ++ if (!aop->force_halted || !aop->force_drained || !s->ch3.window_done || ++ !n->acked || n->reset_done || ++ (aop_rd(aop, AOP_CPU_CONTROL) & AOP_CPU_START) || readl(aop->control)) { ++ n->failed = "failed-intc-reset-state"; ++ return -EPERM; ++ } ++ ret = a9_aop_spi0_trace(aop, "pre-intc-reset"); ++ if (ret) ++ return ret; ++ aop->experiment_step = "intc-reset"; ++ dev_info(aop->dev, "AOP INTC reset begin pa=%llx sequence=1/read/0/read cpu=halted\n", ++ A9_SPI0_WRAPPER_PA + A9_INTC_RESET); ++ ret = aop_wr(aop, A9_INTC_RESET, 1); ++ n->reset_asserted = aop_rd(aop, A9_INTC_RESET); ++ if (ret) { ++ n->failed = "failed-intc-reset-assert"; ++ return ret; ++ } ++ ret = aop_wr(aop, A9_INTC_RESET, 0); ++ n->reset_after = aop_rd(aop, A9_INTC_RESET); ++ if (ret) { ++ n->failed = "failed-intc-reset-deassert"; ++ return ret; ++ } ++ if (n->reset_after) { ++ n->failed = "failed-intc-reset-readback"; ++ ret = -EIO; ++ } else { ++ ret = a9_aop_spi0_trace(aop, "post-intc-reset"); ++ if (!ret && a9_spi0_ch3_verify(s->trace.ch3, ++ s->teardown_stable & BIT(3))) { ++ n->failed = "failed-intc-reset-ch3"; ++ ret = -EIO; ++ } else if (!ret && a9_aop_spi0_selected_verify(s)) { ++ n->failed = "failed-intc-reset-channel"; ++ ret = -EIO; ++ } ++ } ++ n->reset_done = !ret; ++ dev_info(aop->dev, "AOP INTC reset errno=%d asserted=%08x after=%08x result=%s\n", ++ ret, n->reset_asserted, n->reset_after, n->failed ?: "pass"); ++ return ret; ++} +diff --git a/drivers/soc/apple/aop-warm-spi0.h b/drivers/soc/apple/aop-warm-spi0.h +new file mode 100644 +index 000000000..2470165ce +--- /dev/null ++++ b/drivers/soc/apple/aop-warm-spi0.h +@@ -0,0 +1,543 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* ++ * J71t AOP reload/wake experiments, both opt-in and default off. Constants ++ * and evidence types only; the MMIO lives in apple-a9-aop-active.c. ++ * Evidence: docs/periph/ds-aop-reload-crash-and-wake.md. ++ * ++ * aop_spi0_reset=1: every halt + SRAM reload of a live AOP crashed in ++ * AOPSPIMaster_channelFaultIRQ (0102f252, DFAR 0x4c): the AOP-private SPI0 ++ * controller kept channel 3's +0x8d0 bit 6 while the fresh firmware's ++ * channel table was still empty. With the CPU halted, the AP replays the ++ * firmware's own controller reset AOPSPIMaster_reset (0102f2f4), then the ++ * firmware's own channel-3 teardown, and samples bit 6 for >= 150 ms after ++ * the image copy before it releases RUN once. Round 4: the old session's ++ * SPI0 global IRQ 0x10014 stays latched in the AOP interrupt controller ++ * (SCMErr:0x3,3 at the accel STARTEP), so the AP disables that source before ++ * the reset and acknowledges it after the clean window, just before RUN. ++ * Wake7: the channel-3 idle wait gets the firmware's own 10 ms bound twice ++ * over, sampled per 1 ms slice, and the disable is judged by the INTC mask ++ * readback, which the firmware itself reads. ++ * Wake8: after the targeted ack, replay INTC event 3 (+0x808: 1/read/0/read). ++ * This resets retained interrupt delivery, not the read-only SCM state. ++ * Wake9: wake8 device evidence showed the channel-3 stop itself changes SCM ++ * from 0 to 3. Replay the same SPI controller reset once more after teardown; ++ * require the post-reset SCM state to be zero and the teardown state to hold. ++ * Wake10: the second reset clears SCM, but a halted CPU then saw SCM 3 and ++ * +0x8d0 bit 14 (channel 7) during the firmware copy. Snapshot all 16 channel ++ * slots, sample global SCM/fault state across the copy, and gate the copy and ++ * final RUN on a completely quiet readback. ++ * Wake11: that snapshot proved channels 3 and 7 were both running. Keep the ++ * channel-3 sequence unchanged, then apply the same firmware teardown to every ++ * other run-bit + nonzero-interval channel before the post-teardown reset. ++ * Wake12: channel 7 is the phosphorus/BMP284 async client. Its firmware has no ++ * owner-specific stop path, so do not invent another store when the generic ++ * stop times out. Record the remaining scheduler state read-only and refuse. ++ * Wake13/14: every device stop of channel 3 that idled (wake8..wake12) came ++ * 1.5-2.2 ms after the controller reset; the two that did not (wake13/14) came ++ * 11/33 ms after it (pr_flush in the checkpoint), with class-0 state identical to ++ * wake12's passes. Wake15 closed that window (no pr_flush between the reset and ++ * the stops, console lock held), recorded the reset-to-stop latency and the peers ++ * per stop, and accepted one identity-pinned stopped-without-idle channel (3 or ++ * 7) as wake13 did for channel 7. Wake15's channel-7-first order then showed ++ * channel 7 never idles even at 0.3 ms (it is 0/3 over wake11/12/15), with ++ * nothing ticking. Wake16 therefore returns to channel 3 first (early, the only ++ * stop that ever idled) and then channel 7, which takes the stable-stop path ++ * (SCM 0/0 or 3/0, constant); every stop logs why the fingerprint did or did not ++ * hold (stable_why=). Wake14's class-0 prelude stays removed. ++ * ++ * aop_wake_wrapper=1: before 0x220, the AppleA7IOP status-3 resume wrapper ++ * writes (b4b854..b4b8b4). No CPU RUN, no auto-boot, no NMI. ++ */ ++#ifndef _A9_AOP_RELOAD_H ++#define _A9_AOP_RELOAD_H ++ ++#include ++#include ++#include "aop-warm-record.h" ++ ++/* Firmware device spi0: IOP 0x10100000 (space 2, 0x1000) via /arm-io ranges. */ ++#define A9_SPI0_PA 0x210100000ULL ++#define A9_SPI0_SIZE 0x1000U ++#define A9_SPI0_WRAPPER_PA 0x210800000ULL /* the only layout this applies to */ ++#define A9_SPI0_CONTROL 0x000U ++#define A9_SPI0_CLOCK 0x008U /* 0102f2aa programs 0x01b71b00 here */ ++#define A9_SPI0_FAULT 0x8d0U /* bit 2n = channel n, read by 0102f20a */ ++#define A9_SPI0_FAULT_8D4 0x8d4U ++#define A9_SPI0_CHANNEL_BASE 0x100U ++#define A9_SPI0_CHANNEL_STRIDE 0x080U ++#define A9_SPI0_CHANNELS 16U ++#define A9_SPI0_CHANNEL_REGS 7U ++#define A9_SPI0_CHANNEL_DESC 0x018U ++#define A9_SPI0_CH3 0x280U /* base + 0x100 + 0x80 * 3 */ ++#define A9_SPI0_CH3_0C 0x28cU ++#define A9_SPI0_RESET 0x100U /* control bit 8 */ ++#define A9_SPI0_ENABLE 0x001U /* control bit 0 */ ++#define A9_SPI0_CH3_PENDING 0x040U ++/* The firmware spins without bound; the AP gives each poll 1 ms. */ ++#define A9_SPI0_POLL_US 1000U ++#define A9_SPI0_DELAY_US 100U /* 0102b3f8(100, 0), twice */ ++#define A9_SPI0_REGS 6 ++ ++static const u32 a9_spi0_offsets[A9_SPI0_REGS] = { ++ A9_SPI0_CONTROL, A9_SPI0_CLOCK, A9_SPI0_FAULT, A9_SPI0_FAULT_8D4, ++ A9_SPI0_CH3, A9_SPI0_CH3_0C, ++}; ++ ++/* Snapshot indices into a9_spi0_offsets. */ ++enum { A9_SPI0_I_CONTROL, A9_SPI0_I_CLOCK, A9_SPI0_I_FAULT }; ++ ++/* ++ * Channel-3 teardown after the reset (ds-aop-spi0-ch3-round3.md section 5). ++ * The reset leaves C3 = 0x00010447: idle but still enabled, async-armed and ++ * with the class-0 fault IRQ on, so a level source re-latched +0x8d0 bit 6 ++ * within ~105 ms with the CPU halted. AOPSPIMaster_teardownChannel ++ * (0102e4f8) stops the channel and masks both IRQ classes; the fault ++ * handler (0102f20a) then acknowledges with C3C bit 5. ++ */ ++#define A9_SPI0_CH3_ROUTE 0x284U ++#define A9_SPI0_CH3_RATE 0x288U ++#define A9_SPI0_CH3_DESC 0x298U /* channel +0x18, zeroed on teardown */ ++#define A9_SPI0_CH3_RUN 0x001U /* C3 bit 0, channel run/enable */ ++#define A9_SPI0_CH3_IDLE 0x002U /* C3 bit 1, polled after the stop */ ++#define A9_SPI0_CH3_IRQ0 0x040U /* C3 bit 6, +0x8d0 class-0/fault enable */ ++#define A9_SPI0_CH3_IRQ1 0x100U /* C3 bit 8, +0x8d4 class-1 enable */ ++#define A9_SPI0_CHANNEL_PROGRESS_SHIFT 24U /* hardware-updated, not an IRQ class */ ++#define A9_SPI0_CH3_STOP 0x411U /* clear run bit 0 and bits 4, 10 */ ++#define A9_SPI0_CH3_CLASS0 0x450U /* mask class 0: bits 4, 6, 10 */ ++#define A9_SPI0_CH3_CLASS1 0x510U /* mask class 1: bits 4, 8, 10 */ ++#define A9_SPI0_CH3_ACK 0x020U /* C3C bit 5, ORed by 0102f20a; self-clearing */ ++#define A9_SPI0_PHOSPHORUS_CHANNEL 7U ++#define A9_SPI0_PHOSPHORUS_CTL_MASK 0x00ffffffU ++#define A9_SPI0_PHOSPHORUS_CTL_RUN 0x00010445U ++#define A9_SPI0_PHOSPHORUS_CTL_STOPPED 0x00010444U ++#define A9_SPI0_PHOSPHORUS_ROUTE 0x00000040U ++#define A9_SPI0_PHOSPHORUS_RATE 0x019000c8U ++#define A9_SPI0_PHOSPHORUS_CFG 0x00200009U ++/* Channel 3 is the MPU6900 async FIFO client (route 0x10, same rate/config). */ ++#define A9_SPI0_MPU_ASYNC_CHANNEL 3U ++#define A9_SPI0_MPU_ASYNC_ROUTE 0x00000010U ++/* Stopped control word, any top byte, class-0 enable optional (0x40). */ ++#define A9_SPI0_STABLE_CTL_STOPPED 0x00010404U ++#define A9_SPI0_STABLE_CTL_MASK 0x00ffffbfU ++/* Peer sampling and the "late stop" mark are reported, never gated, in-kernel. */ ++#define A9_SPI0_LATE_STOP_US 3000U ++/* ++ * AOPSPIMaster_setChannelEnabled(false) (0102ef7c) stores C3 & ~0x411 only ++ * if bit 0 was set, then spins on bit 1 until master +0x30, and panics ++ * "SCMto:%u" (0x010398da) past it. Event 1 sets +0x30 to 0x2710 us when it ++ * is 0 (0102e6fa, as in the live-cold object), through timer +0x18 = ++ * us * Hz / 1000000 (0102ff3c): a 10 ms bound. The AP polls in 1 ms slices, ++ * up to twice that, sampling C3, the SCM state and +0x8d0 after each slice. ++ */ ++#define A9_SPI0_CH3_IDLE_FW_US 10000U ++#define A9_SPI0_CH3_SLICE_US 1000U ++#define A9_SPI0_CH3_SLICES 20U /* 20 ms */ ++/* At least 150 ms (the 16:27 re-latch took ~105 ms) of samples before RUN. */ ++#define A9_SPI0_WINDOW_SAMPLES 31U ++#define A9_SPI0_WINDOW_US 5000U /* 30 sleeps >= 5 ms each */ ++#define A9_SPI0_WINDOW_MS 150U /* required first-to-last sample span */ ++#define A9_SPI0_CH3_REGS 7 ++ ++static const u32 a9_spi0_ch3_offsets[A9_SPI0_CH3_REGS] = { ++ A9_SPI0_CH3, A9_SPI0_CH3_ROUTE, A9_SPI0_CH3_RATE, A9_SPI0_CH3_0C, ++ A9_SPI0_CH3_DESC, A9_SPI0_FAULT, A9_SPI0_FAULT_8D4, ++}; ++ ++/* Every firmware-visible channel slot, read only in wake10 and selected in wake11. */ ++static const u32 a9_spi0_channel_offsets[A9_SPI0_CHANNEL_REGS] = { ++ 0x00U, 0x04U, 0x08U, 0x0cU, 0x10U, 0x14U, 0x18U, ++}; ++ ++enum { ++ A9_CHANNEL_I_CTL, A9_CHANNEL_I_ROUTE, A9_CHANNEL_I_RATE, A9_CHANNEL_I_CFG, ++ A9_CHANNEL_I_INTERVAL, A9_CHANNEL_I_CURSOR, A9_CHANNEL_I_DESC, ++}; ++ ++/* Snapshot indices into a9_spi0_ch3_offsets. */ ++enum { ++ A9_CH3_I_C3, A9_CH3_I_ROUTE, A9_CH3_I_RATE, A9_CH3_I_C3C, A9_CH3_I_DESC, ++ A9_CH3_I_8D0, A9_CH3_I_8D4, ++}; ++ ++/* One idle-poll slice: C3 as the slice ended, then SCM state and +0x8d0. */ ++struct a9_aop_ch3_slice { ++ u64 ns; /* since the stop store */ ++ u32 c3, scm_lo, scm_hi, fault; ++}; ++ ++/* Wake12 read-only scheduler evidence for an additional-channel stop poll. */ ++struct a9_aop_channel_diag_slice { ++ u32 cfg, interval, cursor, fault_8d4; ++}; ++ ++struct a9_aop_spi0_ch3 { ++ u32 before[A9_SPI0_CH3_REGS], after[A9_SPI0_CH3_REGS]; ++ u32 stop, idle, class0, class1, ack; /* words written / polled */ ++ int poll; /* idle-bit poll result */ ++ bool run_before; /* C3 bit 0 before the stop */ ++ unsigned int slices; /* idle-poll slices used */ ++ struct a9_aop_ch3_slice slice[A9_SPI0_CH3_SLICES]; ++ u64 idle_us; /* stop store to the idle read */ ++ int error; ++ bool ran; /* the guarded teardown executed */ ++ const char *failed; /* distinct refusal reason, or NULL */ ++ /* Wake15: stop latency after the controller reset and the peers' state. */ ++ u64 stop_ns, reset_to_stop_us; ++ u32 peers_run, peers_ticking; /* masks of the other 15 channels */ ++ bool stable; /* accepted stopped-without-idle identity */ ++ const char *stable_why; /* first failed fingerprint condition, NULL if stable */ ++ /* Pre-RUN window after the image copy; a refusal also sets failed. */ ++ unsigned int samples; ++ u64 elapsed_ms; ++ bool window_done, reasserted; ++ unsigned int reassert_sample; ++ u64 reassert_ms; ++ u32 reassert[A9_SPI0_CH3_REGS]; ++}; ++ ++struct a9_aop_spi0; ++ ++struct a9_aop_spi0_channel_op { ++ struct a9_aop_spi0 *spi0; ++ struct a9_aop_spi0_ch3 *state; ++ unsigned int channel; ++}; ++ ++/* ++ * AOP interrupt controller, wrapper offsets (ds-aop-spi0-ch3-round4.md ++ * section 3). IRQ 0x10014 is group 1, source 0x14: bank 0, bit 20. The ++ * group-1 disable/enable banks are write-one commands whose reads return ++ * the mask, 1 = disabled: the sleep command saves +0xa00..+0xa08 (0102c5ce) ++ * before masking all (0102c5da), the restore writes saved to +0xa00 and ++ * ~saved to +0xa80 (0102c9e6/0102c9f4), and aop_interrupt_write_enabled ++ * reads the target bank for the previous state (0102c9c0). So the disable ++ * is judged by bit 20 of the +0xa00 readback (wake6: ffffffff before the ++ * store, every source already masked by the quiesce). +0x81c only reports ++ * an unmasked pending source: the sleep path masks all, writes group 7 ++ * +0xc020/+0xc024 = 2 and spins until +0x81c reads 0 (0102c654), so ++ * all-ones there means the AP is not reading the INTC. +0x808 ends 0 after ++ * the event-3 pulse. +0x900 is the group-1 raise (0102c7c6) and +0x980 its ++ * pre-handler clear (0102c6f2); the firmware never reads them, so their ++ * readbacks are logged only. No register shows a masked source pending. ++ */ ++#define A9_INTC_RESET 0x808U /* event-3 pulse 1 then 0 (0102c592) */ ++#define A9_INTC_CURRENT 0x81cU /* current unmasked source */ ++#define A9_INTC_RAISE 0x900U /* group 1 raise, 0102c7c6 */ ++#define A9_INTC_ACK 0x980U /* group 1 pre-handler ack/clear, 0102c6f2 */ ++#define A9_INTC_DISABLE 0xa00U /* group 1 disable, store at 0102c9ce */ ++#define A9_INTC_ENABLE 0xa80U /* group 1 enable; reads the same mask */ ++#define A9_INTC_G7_DISABLE 0xc020U /* group 7 disable, 2 after sleep */ ++#define A9_INTC_SPI0_SCM 0x00100000U /* IRQ 0x10014 */ ++#define A9_INTC_ALL_ONES 0xffffffffU ++#define A9_INTC_REGS 10 ++ ++static const u32 a9_intc_offsets[A9_INTC_REGS] = { ++ A9_INTC_RESET, A9_INTC_CURRENT, A9_INTC_RAISE, A9_INTC_ACK, ++ A9_INTC_DISABLE, A9_INTC_DISABLE + 4, A9_INTC_DISABLE + 8, ++ A9_INTC_ENABLE, A9_INTC_G7_DISABLE, A9_INTC_G7_DISABLE + 4, ++}; ++ ++/* Snapshot indices into a9_intc_offsets. */ ++enum { ++ A9_INTC_I_RESET, A9_INTC_I_CURRENT, A9_INTC_I_RAISE, A9_INTC_I_ACK, ++ A9_INTC_I_MASK0, A9_INTC_I_MASK1, A9_INTC_I_MASK2, A9_INTC_I_ENABLE0, ++ A9_INTC_I_G7_0, A9_INTC_I_G7_1, ++}; ++ ++/* When the AP snapshots the INTC words. */ ++enum { ++ A9_INTC_AT_PRE_DISABLE, A9_INTC_AT_POST_DISABLE, A9_INTC_AT_PRE_ACK, ++ A9_INTC_AT_POST_ACK, A9_INTC_AT_N, ++}; ++ ++static inline const char *a9_intc_at_name(unsigned int at) ++{ ++ static const char * const names[] = { ++ "pre-disable", "post-disable", "pre-ack", "post-ack", ++ }; ++ ++ return at < A9_INTC_AT_N ? names[at] : "unknown"; ++} ++ ++/* "all-ones" if +0x81c reads ffffffff, which a live INTC cannot report. */ ++static inline const char *a9_intc_view(const u32 *state) ++{ ++ return state[A9_INTC_I_CURRENT] == A9_INTC_ALL_ONES ? "all-ones" : "intc"; ++} ++/* ++ * Read only, never written: the SCM state (also per idle-poll slice) and, ++ * around the acknowledge, the C3 interval pair. ++ */ ++#define A9_SPI0_SCM_LO 0x00cU ++#define A9_SPI0_SCM_HI 0x010U ++#define A9_SPI0_CH3_IVAL 0x290U ++#define A9_SPI0_CH3_CURSOR 0x294U ++#define A9_SPI0_SCM_REGS 4 ++ ++static const u32 a9_spi0_scm_offsets[A9_SPI0_SCM_REGS] = { ++ A9_SPI0_SCM_LO, A9_SPI0_SCM_HI, A9_SPI0_CH3_IVAL, A9_SPI0_CH3_CURSOR, ++}; ++ ++struct a9_aop_intc { ++ u32 state[A9_INTC_AT_N][A9_INTC_REGS]; /* around each store */ ++ unsigned int states; /* bit n: state[n] was read */ ++ u32 scm[2][A9_SPI0_SCM_REGS]; /* before / after the ack */ ++ unsigned int scm_at; /* scm[] row the guarded read fills */ ++ bool disabled, acked; ++ bool reset_done; ++ u32 reset_asserted, reset_after; ++ const char *failed; /* distinct refusal reason, or NULL */ ++}; ++ ++/* Read-only milestones. Wake10 gates the global SCM/fault words, not channels. */ ++struct a9_aop_spi0_trace { ++ u32 control, clock, scm[A9_SPI0_SCM_REGS], ch3[A9_SPI0_CH3_REGS]; ++ u32 channel[A9_SPI0_CHANNELS][A9_SPI0_CHANNEL_REGS]; ++}; ++ ++/* Wake9's second, post-channel-stop replay of AOPSPIMaster_reset. */ ++struct a9_aop_spi0_post_reset { ++ u32 before[A9_SPI0_REGS], after[A9_SPI0_REGS]; ++ u32 saved, asserted, polled; ++ int error; ++ bool ran; ++ const char *failed; ++}; ++ ++/* ++ * First failed readback after the teardown, or NULL if all hold. C3C bit 5 ++ * is a pulse: at 19:50 the write of 0x00200029 read back 0x00200009 while ++ * +0x8d0 bit 6 went 1 -> 0, so the acknowledge is judged by bit 6 alone and ++ * the C3C readback is only logged. ++ */ ++static inline const char *a9_spi0_ch3_verify(const u32 *after, bool stable) ++{ ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_RUN) ++ return "failed-ch3-teardown-run"; ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ0) ++ return "failed-ch3-teardown-class0"; ++ if (after[A9_CH3_I_C3] & A9_SPI0_CH3_IRQ1) ++ return "failed-ch3-teardown-class1"; ++ if (!(after[A9_CH3_I_C3] & A9_SPI0_CH3_IDLE) && !stable) ++ return "failed-ch3-teardown-idle"; ++ if (after[A9_CH3_I_DESC]) ++ return "failed-ch3-teardown-desc"; ++ if (after[A9_CH3_I_8D0] & A9_SPI0_CH3_PENDING) ++ return "failed-ch3-teardown-pending"; ++ return NULL; ++} ++ ++/* ++ * Has firmware run since the last SoC reset? auto-boot alone cannot say: ++ * the outgoing aop_quiesce=1 halt clears it, exactly like a DFU cold start. ++ * The previous kernel's reserved-RAM shutdown transcript says whether its ++ * firmware ran. That DRAM can outlive a DFU; wrapper +0x4020 outbox enable ++ * cannot: set after reset (12/12 DFU cold snapshots), cleared by the first ++ * store of every Linux stop. No SPI0 access is needed to decide. ++ */ ++enum a9_spi0_prev_state { ++ A9_SPI0_PREV_MISSING, /* no transcript magic, or none reserved */ ++ A9_SPI0_PREV_INVALID, /* magic present, header/checksum rejected */ ++ A9_SPI0_PREV_VALID, ++}; ++ ++struct a9_spi0_prev { ++ enum a9_spi0_prev_state state; ++ bool hello; /* "SHUTDOWN begin ... hello=1": its firmware ran */ ++ bool halted; /* "QUIESCE step=complete ... halted=1" + steps bit */ ++ bool carried; /* "SPI0-CARRY retained=1": ran earlier in the chain */ ++ u32 steps; /* "QUIESCE step=complete ... steps=0x..." */ ++}; ++ ++enum a9_spi0_reason { ++ /* Run the reset. */ ++ A9_SPI0_LIVE_AUTO_BOOT, /* auto-boot 1 before the stop */ ++ A9_SPI0_PREV_HALTED, /* previous kernel quiesced and halted it */ ++ A9_SPI0_PREV_LIVE, /* previous kernel's firmware ran, no halt */ ++ A9_SPI0_PREV_CARRIED, /* an earlier kernel in the chain ran it */ ++ /* Skip: the block is not touched. */ ++ A9_SPI0_FRESH, /* no transcript, auto-boot 0: DFU cold */ ++ A9_SPI0_PREV_BAD, /* transcript present but corrupt */ ++ A9_SPI0_PREV_NO_RUN, /* transcript: nothing ran in the chain */ ++ A9_SPI0_STALE, /* run evidence, wrapper at reset state */ ++}; ++ ++static inline const char *a9_spi0_reason_name(enum a9_spi0_reason reason) ++{ ++ static const char * const names[] = { ++ "live-auto-boot", "prev-quiesce-halted", "prev-live", "prev-carried", ++ "fresh-instance", "prev-transcript-invalid", "prev-no-run", ++ "stale-transcript", ++ }; ++ ++ return reason <= A9_SPI0_STALE ? names[reason] : "unknown"; ++} ++ ++static inline const char *a9_spi0_prev_name(enum a9_spi0_prev_state state) ++{ ++ return state == A9_SPI0_PREV_VALID ? "valid" : ++ state == A9_SPI0_PREV_INVALID ? "invalid" : "missing"; ++} ++ ++static inline bool a9_spi0_reason_runs(enum a9_spi0_reason reason) ++{ ++ return reason <= A9_SPI0_PREV_CARRIED; ++} ++ ++/* ++ * Release policy: a reason that would run the reload is allowed by default ++ * only for prev-quiesce-halted, the one state device-proven end to end. A ++ * skip (fresh, bad record, nothing ran) is allowed only while the wrapper is ++ * still at its reset state; stale records skip as before. Untested lifts both. ++ */ ++static inline bool a9_spi0_policy_allows(enum a9_spi0_reason reason, bool outbox_enabled, ++ bool untested) ++{ ++ if (untested || reason == A9_SPI0_PREV_HALTED || reason == A9_SPI0_STALE) ++ return true; ++ if (a9_spi0_reason_runs(reason)) ++ return false; ++ /* ++ * A skip means "nothing ran since the SoC reset", which only holds if the ++ * wrapper is still at its reset state (+0x4020 outbox enable set, 12/12 DFU ++ * cold starts). Without it some earlier kernel stopped the firmware and its ++ * record is lost: a cold load would land on a halted, never-torn-down AOP. ++ */ ++ return outbox_enabled; ++} ++ ++/* Token search inside one transcript line, which holds no NUL. */ ++static inline bool a9_spi0_line_has(const char *line, size_t length, ++ const char *token, size_t n, ++ const char **at) ++{ ++ size_t i; ++ ++ for (i = 0; n <= length && i <= length - n; i++) { ++ if (!memcmp(line + i, token, n)) { ++ *at = line + i + n; ++ return true; ++ } ++ } ++ return false; ++} ++ ++#define a9_spi0_has(line, length, token, at) \ ++ a9_spi0_line_has(line, length, token, sizeof(token) - 1, at) ++ ++static inline bool a9_spi0_line_starts(const char *line, size_t length, ++ const char *prefix, size_t n) ++{ ++ return length >= n && !memcmp(line, prefix, n); ++} ++ ++#define a9_spi0_starts(line, length, prefix) \ ++ a9_spi0_line_starts(line, length, prefix, sizeof(prefix) - 1) ++ ++static inline u32 a9_spi0_hex(const char *p, const char *end) ++{ ++ u32 value = 0; ++ ++ for (; p < end; p++) { ++ if (*p >= '0' && *p <= '9') ++ value = value << 4 | (u32)(*p - '0'); ++ else if (*p >= 'a' && *p <= 'f') ++ value = value << 4 | (u32)(*p - 'a' + 10); ++ else ++ break; ++ } ++ return value; ++} ++ ++/* Our own checksummed transcript; only whole-line prefixes count. */ ++static inline void a9_spi0_prev_parse(struct a9_spi0_prev *p, ++ const char *text, size_t length) ++{ ++ const char *end = text + length, *line, *next, *at; ++ size_t n; ++ ++ for (line = text; line < end; line = next < end ? next + 1 : end) { ++ next = memchr(line, '\n', end - line); ++ if (!next) ++ next = end; ++ n = next - line; ++ if (a9_spi0_starts(line, n, "SHUTDOWN begin ") && ++ a9_spi0_has(line, n, " hello=1 ", &at)) ++ p->hello = true; ++ if (a9_spi0_starts(line, n, "QUIESCE step=complete ")) { ++ if (a9_spi0_has(line, n, " steps=0x", &at)) ++ p->steps = a9_spi0_hex(at, next); ++ p->halted = a9_spi0_has(line, n, " halted=1 ", &at) && ++ (p->steps & (1U << A9_HO_STEP_HALTED)); ++ } ++ if (a9_spi0_starts(line, n, "SPI0-CARRY retained=1 ")) ++ p->carried = true; ++ } ++} ++ ++/* ++ * Skips unless there is positive evidence: running the reset on a fresh ++ * block would be the first AP access to SPI0 with nothing retained to ++ * clear (and ps_spi0 may gate it), while a skip on retained state only ++ * repeats the known, non-hanging accel STARTEP crash. ++ */ ++static inline enum a9_spi0_reason a9_spi0_decide(bool auto_boot, bool outbox_enabled, ++ const struct a9_spi0_prev *p) ++{ ++ if (auto_boot) ++ return A9_SPI0_LIVE_AUTO_BOOT; ++ if (p->state == A9_SPI0_PREV_MISSING) ++ return A9_SPI0_FRESH; ++ if (p->state != A9_SPI0_PREV_VALID) ++ return A9_SPI0_PREV_BAD; ++ if (!p->hello && !p->carried) ++ return A9_SPI0_PREV_NO_RUN; ++ if (outbox_enabled) ++ return A9_SPI0_STALE; ++ if (p->hello) ++ return p->halted ? A9_SPI0_PREV_HALTED : A9_SPI0_PREV_LIVE; ++ return A9_SPI0_PREV_CARRIED; ++} ++ ++struct a9_aop_spi0 { ++ void __iomem *base; /* mapped only with aop_spi0_reset=1 */ ++ u32 before[A9_SPI0_REGS], after[A9_SPI0_REGS]; ++ u32 saved; /* control word before the reset */ ++ u32 asserted; /* saved & ~0x101 | 0x100 */ ++ u32 polled; /* control word when the polls ended */ ++ u32 prerun; /* +0x8d0 just before RUN */ ++ u32 prerun_8d4, prerun_scm_lo, prerun_scm_hi; ++ u32 pmgr_before, pmgr_after; /* AP PMGR ps_spi0 raw, read-only */ ++ int pmgr_before_errno, pmgr_after_errno; ++ int error; ++ bool ran, reasserted, copy_faulted; ++ const char *gate_failed; ++ const char *skipped; ++ struct a9_spi0_prev prev; /* parsed at probe from the transcript */ ++ u32 auto_boot, outbox; /* boot snapshot, before the stop */ ++ enum a9_spi0_reason reason; ++ bool decided; ++ struct a9_aop_spi0_ch3 ch3; /* after a clean reset only */ ++ /* Wake11 reuses one bounded scratch record for each additional channel. */ ++ struct a9_aop_spi0_ch3 channel; ++ u32 teardown_selected, teardown_done; ++ unsigned int teardown_failed_channel; ++ struct a9_aop_spi0_post_reset post_reset; /* after the stop latches SCM */ ++ struct a9_aop_intc intc; /* IRQ 0x10014 around the reset */ ++ struct a9_aop_spi0_trace trace; /* latest read-only milestone */ ++ /* Appended so every pre-wake12 field, including ch3, keeps its layout. */ ++ struct a9_aop_channel_diag_slice channel_diag[A9_SPI0_CH3_SLICES]; ++ /* Wake13: exact phosphorus stopped-without-idle exception, by channel. */ ++ u32 teardown_stable; ++ /* Wake15: channel 3 shares the sampled diagnostics; reset completion time. */ ++ struct a9_aop_channel_diag_slice ch3_diag[A9_SPI0_CH3_SLICES]; ++ u64 reset_done_ns; ++}; ++ ++/* 0102f2f4: bits 8 and 0 clear; the reset write adds bit 8. */ ++static inline u32 a9_spi0_base_word(u32 saved) ++{ ++ return saved & ~(A9_SPI0_RESET | A9_SPI0_ENABLE); ++} ++ ++#endif +diff --git a/drivers/soc/apple/aop.c b/drivers/soc/apple/aop.c +index 5c23317d1..542e9b62e 100644 +--- a/drivers/soc/apple/aop.c ++++ b/drivers/soc/apple/aop.c +@@ -12,10 +12,17 @@ + * child platform device; its driver receives the service's reports through + * apple_aop_listen() and sends requests with apple_aop_call(). + * +- * The firmware cannot be restarted once stopped, so the AOP starts once per +- * boot and the driver cannot be unbound or unloaded after that. ++ * J71t can reload after a certified quiesce/halt/drain across kexec. Other ++ * retained states are refused. Unbind/unload after starting remains forbidden. + */ + ++#include ++#include ++#include ++#include ++#include ++#include ++#include "aop-warm-spi0.h" + #include + #include + #include +@@ -95,7 +102,9 @@ + #define AFK_OPC_GET_BUF_ACK 0xa1 + #define AFK_OPC_SEND 0xa2 + #define AFK_OPC_START 0xa3 +-#define AFK_OPC_SHUTDOWN_ACK 0xc1 ++#define AFK_OPC_SHUTDOWN 0xa4 ++#define AFK_OPC_SHUTDOWN_ACK 0x87 ++#define AFK_OPC_HIBERNATE_ACK 0xc3 + + #define AFK_BLOCK_STEP 0x40 + #define AFK_MAX_BUFFER SZ_1M +@@ -183,6 +192,8 @@ struct apple_aop_ep { + + struct mutex lock; /* tx ring, seq and calls */ + u16 seq; ++ bool initialized; ++ struct completion stopped; + struct apple_aop_call *calls[APPLE_AOP_MAX_CALLS]; + }; + +@@ -240,6 +251,16 @@ struct apple_aop { + struct mutex children_lock; /* children, removing */ + struct list_head children; + bool removing; ++ u64 accel_reports; ++ int shutdown_error; ++ /* J71t only; all other boards retain their original start/stop path. */ ++ bool warm, hello, iop_crashed, force_halted, force_drained; ++ void __iomem *warm_wrapper; ++ struct device_node *domains[5], *spi0_pmgr; ++ struct a9_aop_spi0 spi0; ++ const char *experiment_step; ++ struct a9_sl_header *shutdown_log; ++ char *shutdown_text; + }; + + static bool apple_aop_is_started(struct apple_aop *aop) +@@ -261,6 +282,409 @@ static bool apple_aop_halt(struct apple_aop *aop) + APPLE_AOP_HALT_TIMEOUT_US); + } + ++static bool a9_aop_ramoops_ready(void) ++{ ++ struct console *con; ++ bool ready = false; ++ int cookie; ++ ++ cookie = console_srcu_read_lock(); ++ for_each_console_srcu(con) { ++ if (!strcmp(con->name, "ramoops") && ++ (console_srcu_read_flags(con) & (CON_ENABLED | CON_SUSPENDED)) == ++ CON_ENABLED) { ++ ready = true; ++ break; ++ } ++ } ++ console_srcu_read_unlock(cookie); ++ return ready; ++} ++ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++/* J71t warm reload integration. SPI0 machinery is the release6 wake16 code. */ ++#define AOP_DOMAINS 5 ++#define AOP_CPU_CONTROL 0x44 ++#define AOP_CPU_START BIT(4) ++#define AOP_OUTBOX_CONTROL 0x4020 ++#define AOP_INBOX_CONTROL 0x4008 ++#define AOP_OUTBOX_ENABLE BIT(0) ++#define AOP_MBOX_FAULTS GENMASK(19, 18) ++#define cold_defer 1 ++#define spi0_untested false ++ ++static u32 aop_rd(struct apple_aop *aop, u32 offset) ++{ ++ return readl(aop->warm_wrapper + offset); ++} ++ ++static bool a9_aop_force_stopped(struct apple_aop *aop) ++{ ++ return !(aop_rd(aop, AOP_CPU_CONTROL) & AOP_CPU_START) && ++ !readl(aop->control) && ++ !(aop_rd(aop, AOP_OUTBOX_CONTROL) & (AOP_OUTBOX_ENABLE | AOP_MBOX_FAULTS)) && ++ !(aop_rd(aop, AOP_INBOX_CONTROL) & AOP_MBOX_FAULTS); ++} ++ ++struct aop_warm_write { void __iomem *target; u32 value; }; ++static void aop_warm_store(void *data) ++{ ++ struct aop_warm_write *w = data; ++ ++ writel(w->value, w->target); ++ readl(w->target); ++} ++ ++static int aop_wr(struct apple_aop *aop, u32 offset, u32 value) ++{ ++ struct apple_pmgr_observation states[AOP_DOMAINS]; ++ struct aop_warm_write w = { aop->warm_wrapper + offset, value }; ++ ++ if ((aop_rd(aop, AOP_INBOX_CONTROL) | aop_rd(aop, AOP_OUTBOX_CONTROL)) & ++ AOP_MBOX_FAULTS) ++ return -EIO; ++ dev_info(aop->dev, "COLD-W step=%s bank=wrapper offset=%04x before=%08x value=%08x\n", ++ aop->experiment_step ?: "warm-stop", offset, readl(w.target), value); ++ { ++ int ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, aop_warm_store, &w); ++ ++ dev_info(aop->dev, "COLD-R offset=%04x after=%08x\n", offset, readl(w.target)); ++ return ret; ++ } ++} ++ ++static int a9_aop_cold_checkpoint_noflush(struct apple_aop *aop, const char *step) ++{ ++ if (!a9_aop_ramoops_ready()) ++ return -ENODEV; ++ dev_info(aop->dev, "COLD checkpoint=%s flush=skipped\n", step); ++ return 0; ++} ++ ++static int a9_aop_cold_checkpoint(struct apple_aop *aop, const char *step) ++{ ++ int ret = a9_aop_cold_checkpoint_noflush(aop, step); ++ ++ if (ret) ++ return ret; ++ if (!pr_flush(1000, false)) ++ return -ETIMEDOUT; ++ mb(); ++ return 0; ++} ++ ++#include "aop-warm-spi0.c.h" ++static int apple_aop_send(struct apple_aop_ep *ep, u8 opcode, u64 payload); ++ ++/* No cmdline override for untested states: default is the release6 policy. */ ++static atomic_t aop_warm_attempted = ATOMIC_INIT(0); ++ ++static int apple_aop_warm_stop(struct apple_aop *aop) ++{ ++ unsigned int fifo, i; ++ u32 offset, val; ++ int ret; ++ ++ ret = aop_wr(aop, 0x4020, aop_rd(aop, 0x4020) & ~BIT(0)); ++ if (!ret) ++ ret = aop_wr(aop, 0x4000, 0x1111); ++ if (!ret) ++ ret = aop_wr(aop, 0xc00, 0x1111); ++ if (!ret) ++ ret = aop_wr(aop, 0x44, aop_rd(aop, 0x44) & ~BIT(4)); ++ if (ret) ++ return ret; ++ writel(0, aop->control); ++ msleep(10); ++ if (!a9_aop_force_stopped(aop)) ++ return -EIO; ++ aop->force_halted = true; ++ for (fifo = 0; fifo < 2; fifo++) { ++ offset = fifo ? 0x4020 : 0x4008; ++ for (i = 0; i < 64; i++) { ++ val = aop_rd(aop, offset); ++ if (!a9_aop_force_stopped(aop) || (val & AOP_MBOX_FAULTS)) ++ return -EIO; ++ if (val & BIT(17)) ++ break; ++ readq(aop->warm_wrapper + (fifo ? 0x4038 : 0x4018)); ++ } ++ if (!(aop_rd(aop, offset) & BIT(17))) ++ return -ETIMEDOUT; ++ } ++ aop->force_drained = true; ++ return 0; ++} ++ ++static void apple_aop_warm_put_node(void *node) ++{ ++ of_node_put(node); ++} ++ ++static void apple_aop_warm_unmap(void *p) ++{ ++ memunmap(p); ++} ++ ++static int apple_aop_warm_prepare(struct apple_aop *aop) ++{ ++ struct device *dev = aop->dev; ++ struct device_node *np = dev->of_node, *mem; ++ struct apple_pmgr_observation states[AOP_DOMAINS]; ++ struct a9_sl_header header; ++ struct resource res; ++ char *text; ++ u32 auto_boot, outbox; ++ unsigned int i; ++ int ret; ++ ++ if (!of_property_read_bool(aop->dev->of_node, "apple,certified-reload") || !aop->hw->sram_window) ++ return 0; ++ /* Missing the new DT is not permission to run a retained instance. */ ++ mem = of_parse_phandle(np, "memory-region", 0); ++ if (!mem) ++ return dev_err_probe(dev, -EINVAL, "warm reload requires the matching J71t DT\n"); ++ ret = of_address_to_resource(mem, 0, &res); ++ of_node_put(mem); ++ if (ret || resource_size(&res) != A9_HO_PAGE || res.start != 0x8583fc000ULL || ++ aop->akf_base != A9_SPI0_WRAPPER_PA) ++ return -EINVAL; ++ aop->shutdown_log = memremap(res.start, A9_HO_PAGE, MEMREMAP_WB); ++ if (!aop->shutdown_log) ++ return -ENOMEM; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_unmap, aop->shutdown_log); ++ if (ret) ++ return ret; ++ /* Keep release6's transcript location and validation ABI. */ ++ aop->shutdown_log = (void *)aop->shutdown_log + A9_HO_LOG_OFFSET; ++ aop->shutdown_text = (char *)(aop->shutdown_log + 1); ++ memcpy(&header, aop->shutdown_log, sizeof(header)); ++ text = devm_kmemdup(dev, aop->shutdown_text, A9_SL_CAPACITY, GFP_KERNEL); ++ if (!text) ++ return -ENOMEM; ++ if (header.magic == A9_SL_MAGIC) { ++ aop->spi0.prev.state = A9_SPI0_PREV_INVALID; ++ if (a9_sl_valid(&header, text)) { ++ aop->spi0.prev.state = A9_SPI0_PREV_VALID; ++ a9_spi0_prev_parse(&aop->spi0.prev, text, header.length); ++ for (i = 0; i < header.length;) { ++ char *next = memchr(text + i, '\n', header.length - i); ++ unsigned int n = next ? next - text - i : header.length - i; ++ ++ dev_info(dev, "AOP-PREV %.*s\n", n, text + i); ++ i += n + 1; ++ } ++ } ++ } ++ aop->warm_wrapper = devm_ioremap(dev, aop->akf_base, 0x1c000); ++ if (!aop->warm_wrapper) ++ return -ENOMEM; ++ for (i = 0; i < AOP_DOMAINS; i++) { ++ aop->domains[i] = of_parse_phandle(np, "apple,observe-domains", i); ++ if (!aop->domains[i]) ++ return -EINVAL; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_put_node, aop->domains[i]); ++ if (ret) ++ return ret; ++ } ++ aop->spi0_pmgr = of_parse_phandle(np, "apple,spi0-domain", 0); ++ if (!aop->spi0_pmgr) ++ return -EINVAL; ++ ret = devm_add_action_or_reset(dev, apple_aop_warm_put_node, aop->spi0_pmgr); ++ if (ret) ++ return ret; ++ /* Check all domains before reading the wrapper; no power transitions. */ ++ ret = apple_pmgr_observe(aop->domains, AOP_DOMAINS, states, a9_aop_spi0_pmgr_on, &(bool){false}); ++ if (ret) ++ return dev_err_probe(dev, ret, "AOP domain guard refused\n"); ++ auto_boot = readl(aop->control); ++ outbox = aop_rd(aop, AOP_OUTBOX_CONTROL); ++ ret = a9_aop_spi0_policy(aop, auto_boot, outbox); ++ if (ret) ++ return ret; /* Before first halt, mask, SRAM or SPI0 write. */ ++ /* Orchard's old halt leaves +0x4020 enabled. On a known kexec, ++ * that alone cannot certify the cold wrapper baseline. Release6's ++ * wrapper discriminator was tied to its own outgoing stop routine. ++ */ ++ if (of_property_read_bool(of_chosen, "linux,booted-from-kexec") && ++ a9_spi0_decide(auto_boot, outbox & BIT(0), &aop->spi0.prev) != A9_SPI0_PREV_HALTED) { ++ dev_info(dev, "SPI0 policy decision=refuse reason=uncertified-kexec auto_boot=%u out4020=%08x: block not touched\n", ++ auto_boot, outbox); ++ return -EPERM; ++ } ++ aop->warm = true; ++ aop->spi0.reason = a9_spi0_decide(auto_boot, outbox & BIT(0), &aop->spi0.prev); ++ if (!a9_spi0_reason_runs(aop->spi0.reason)) { ++ /* Original cold path: no halt, drain, reset, or SPI0 access. */ ++ dev_info(dev, "SPI0 reset decision=skip reason=%s: block not touched\n", ++ a9_spi0_reason_name(aop->spi0.reason)); ++ return 0; ++ } ++ if (atomic_xchg(&aop_warm_attempted, 1)) ++ return dev_err_probe(dev, -EPERM, "refused-second-experiment-attempt\n"); ++ if (!a9_aop_ramoops_ready()) ++ return dev_err_probe(dev, -ENODEV, "warm reload requires ramoops console\n"); ++ aop->spi0.base = devm_ioremap(dev, A9_SPI0_PA, A9_SPI0_SIZE); ++ if (!aop->spi0.base) ++ return -ENOMEM; ++ /* Consume certification before the first control write. Reloading the ++ * module after a failure must not reuse it as a second attempt. ++ */ ++ aop->shutdown_log->magic = 0; ++ wmb(); ++ ret = apple_aop_warm_stop(aop); ++ if (!ret) ++ ret = a9_aop_spi0_prereset(aop, auto_boot, outbox); ++ if (ret) ++ dev_err(dev, "warm reload refused errno=%d; no copy/RUN, recover with DFU\n", ret); ++ return ret; ++} ++ ++static int apple_aop_warm_release(struct apple_aop *aop) ++{ ++ int ret; ++ ++ ret = a9_aop_spi0_recheck(aop); ++ if (!ret) ++ ret = a9_aop_spi0_trace(aop, "after-window"); ++ if (!ret) ++ ret = a9_aop_intc_ack(aop); ++ if (!ret) ++ ret = a9_aop_intc_reset(aop); ++ if (!ret) ++ ret = a9_aop_cold_checkpoint(aop, "before-run"); ++ if (!ret) ++ ret = a9_aop_spi0_gate(aop, "pre-run-final", "FINAL", true); ++ return ret; ++} ++ ++static void apple_aop_warm_hash(struct apple_aop *aop, u8 *digest) ++{ ++ struct sha256_ctx ctx; ++ unsigned int i; ++ ++ sha256_init(&ctx); ++ dma_rmb(); ++ if (aop->ext) ++ sha256_update(&ctx, aop->ext, aop->ext_size); ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) ++ if (aop->eps[i].buf) ++ sha256_update(&ctx, aop->eps[i].buf, aop->eps[i].buf_size); ++ sha256_final(&ctx, digest); ++} ++ ++static __printf(2, 3) void apple_aop_warm_log(struct apple_aop *aop, const char *fmt, ...) ++{ ++ struct a9_sl_header *h = aop->shutdown_log; ++ va_list args; ++ char line[512]; ++ size_t n; ++ ++ va_start(args, fmt); ++ n = vscnprintf(line, sizeof(line), fmt, args); ++ va_end(args); ++ dev_info(aop->dev, "%s", line); ++ if (n > h->capacity - h->length) { ++ h->truncated++; ++ return; ++ } ++ memcpy(aop->shutdown_text + h->length, line, n); ++ wmb(); ++ h->checksum = a9_sl_hash(h->checksum, line, n); ++ h->length += n; ++ h->lines++; ++ wmb(); ++} ++ ++static void apple_aop_warm_shutdown(struct apple_aop *aop) ++{ ++ struct a9_sl_header *h = aop->shutdown_log; ++ u8 before[SHA256_DIGEST_SIZE], after[SHA256_DIGEST_SIZE]; ++ unsigned long deadline = jiffies + msecs_to_jiffies(10000), start; ++ unsigned int i, acked = 0; ++ int ret = -EIO; ++ ++ memset(h, 0, sizeof(*h)); ++ h->version = A9_SL_VERSION; ++ h->capacity = A9_SL_CAPACITY; ++ h->checksum = A9_SL_HASH_SEED; ++ h->mode = 1; ++ h->begin_ns = ktime_get_boottime_ns(); ++ wmb(); ++ h->magic = A9_SL_MAGIC; ++ apple_aop_warm_log(aop, "SHUTDOWN begin mode=quiesce hello=%d phase=stopping iop_crashed=%d\n", ++ aop->hello, aop->iop_crashed); ++ if (!aop->hello || aop->iop_crashed || aop->shutdown_error) ++ goto done; ++ /* Rates-zero has stopped reports. Stop one AFK at a time, max 2s ++ * per endpoint and 10s in total, including send time. Never force halt ++ * after a failed ACK. Keep every grant allocated for the entire hook. ++ */ ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) { ++ struct apple_aop_ep *ep = &aop->eps[i]; ++ unsigned long limit; ++ ++ if (!ep->initialized) ++ continue; ++ if (time_after_eq(jiffies, deadline)) { ++ ret = -ETIMEDOUT; ++ goto done; ++ } ++ reinit_completion(&ep->stopped); ++ start = jiffies; ++ ret = apple_aop_send(ep, AFK_OPC_SHUTDOWN, 0); ++ if (ret) ++ goto done; ++ limit = time_before(jiffies, deadline) ? min_t(unsigned long, ++ deadline - jiffies, msecs_to_jiffies(2000)) : 0; ++ if (!limit || !wait_for_completion_timeout(&ep->stopped, limit)) { ++ ret = -ETIMEDOUT; ++ goto done; ++ } ++ acked++; ++ apple_aop_warm_log(aop, "QUIESCE step=afk-ack ep=0x%x state=stopped ms=%u\n", ++ ep->index, jiffies_to_msecs(jiffies - start)); ++ } ++ /* release6: complete every AFK stop before disabling endpoint routing. */ ++ if (acked != 17) { ++ ret = -ENOTCONN; ++ goto done; ++ } ++ for (i = 0; i < AFK_ENDPOINT_COUNT; i++) { ++ struct apple_aop_ep *ep = &aop->eps[i]; ++ ++ if (!ep->initialized) ++ continue; ++ /* MGMT STARTEP bit 0 disables routing, no separate stop ACK. */ ++ ret = apple_rtkit_send_message(aop->rtk, 0, ++ (5ULL << 52) | ((u64)ep->index << 32) | 1, NULL, false); ++ if (ret) ++ goto done; ++ } ++ if (acked != 17) { ++ ret = -ENOTCONN; ++ goto done; /* Never certify an incompletely initialized instance. */ ++ } ++ ret = apple_rtkit_shutdown_preserve(aop->rtk); ++ if (ret) ++ goto done; ++ msleep(20); ++ apple_aop_warm_hash(aop, before); ++ msleep(20); ++ apple_aop_warm_hash(aop, after); ++ ret = memcmp(before, after, sizeof(before)) ? -EBUSY : 0; ++ apple_aop_warm_log(aop, "QUIESCE step=ram-stable window_ms=20 status=%d\n", ret); ++ if (ret || aop->iop_crashed) ++ goto done; ++ ret = apple_aop_warm_stop(aop); ++done: ++ apple_aop_warm_log(aop, "QUIESCE step=complete status=%d quiesced=%d halted=%d drained=%d steps=0x%x afk_acks=%u iop_crashed=%d\n", ++ ret, !ret, aop->force_halted, aop->force_drained, ++ !ret ? 0x1bff : 0, acked, aop->iop_crashed); ++ if (ret) ++ dev_err(aop->dev, "quiesce failed; next AOP must refuse, recover with DFU\n"); ++} ++ + /* Firmware loading */ + + struct apple_aop_bootarg { +@@ -356,6 +780,7 @@ static int apple_aop_load(struct apple_aop *aop, u64 ecid) + __le64 ecid_le = cpu_to_le64(ecid), ext_le, wrapper_le; + __le32 target_le, chip_le, revision_le; + unsigned int count = 0; ++ size_t offset; + u8 *image; + int ret; + +@@ -424,10 +849,21 @@ static int apple_aop_load(struct apple_aop *aop, u64 ecid) + } + + ret = apple_aop_patch_bootargs(aop, image, main_fw->size, args, count); +- if (ret) ++ if (ret) { + dev_err(aop->dev, "unexpected boot arguments in %s: %d\n", main_name, ret); +- else ++ } else if (aop->spi0.ran) { ++ /* Warm reload: copy and verify the image with SPI0 gated around it */ ++ ret = a9_aop_spi0_gate(aop, "pre-copy-final", "PRECOPY", false); ++ if (!ret) ++ ret = a9_aop_copy_verify_wake10(aop, image, main_fw->size); ++ if (!ret) ++ ret = a9_aop_spi0_gate(aop, "after-copy", "AFTERCOPY", true); ++ if (!ret) ++ for (offset = main_fw->size; offset + 4 <= aop->sram_size; offset += 4) ++ writel_relaxed(0, aop->sram + offset); ++ } else { + apple_aop_write_sram(aop, image, main_fw->size); ++ } + + kfree_sensitive(image); + memzero_explicit(prng, sizeof(prng)); +@@ -571,6 +1007,8 @@ static void apple_aop_report(struct apple_aop_ep *ep, u32 channel, u16 subtype, + struct apple_aop_listener *listener; + bool handled = false; + ++ if (ep->index == 0x21 && subtype == 0xc2) ++ aop->accel_reports++; + mutex_lock(&aop->listeners_lock); + list_for_each_entry(listener, &aop->listeners, list) { + if (listener->endpoint == ep->index && listener->channel == channel) { +@@ -727,6 +1165,7 @@ static void apple_aop_recv_message(void *cookie, u8 endpoint, u64 msg) + + switch (FIELD_GET(AFK_MSG_OPCODE, msg)) { + case AFK_OPC_INIT: ++ ep->initialized = true; + apple_aop_send(ep, AFK_OPC_INIT_ACK, 0); + break; + case AFK_OPC_GET_BUF: +@@ -747,7 +1186,10 @@ static void apple_aop_recv_message(void *cookie, u8 endpoint, u64 msg) + break; + case AFK_OPC_INIT_UNK: + case AFK_OPC_START_ACK: ++ break; + case AFK_OPC_SHUTDOWN_ACK: ++ case AFK_OPC_HIBERNATE_ACK: ++ complete(&ep->stopped); + break; + default: + dev_dbg(aop->dev, "endpoint 0x%02x: message 0x%llx\n", endpoint, msg); +@@ -758,7 +1200,8 @@ static void apple_aop_crashed(void *cookie, const void *crashlog, size_t crashlo + { + struct apple_aop *aop = cookie; + +- dev_err(aop->dev, "firmware crashed\n"); ++ aop->iop_crashed = true; ++ dev_err(aop->dev, "firmware crashed iop_crashed=1\n"); + } + + static const struct apple_rtkit_ops apple_aop_rtkit_ops = { +@@ -863,6 +1306,8 @@ int apple_aop_call(const struct apple_aop_service *service, u16 subtype, const v + + if (!ep) + return -EINVAL; ++ if (service->aop->warm && READ_ONCE(service->aop->iop_crashed)) ++ return -EIO; + init_completion(&call.done); + + mutex_lock(&ep->lock); +@@ -997,7 +1442,11 @@ static int apple_aop_probe(struct platform_device *pdev) + if (ret) + return ret; + +- if (apple_aop_is_started(aop)) ++ ret = apple_aop_warm_prepare(aop); ++ if (ret) ++ return ret; ++ ++ if (!aop->spi0.ran && apple_aop_is_started(aop)) + return dev_err_probe(dev, -EBUSY, + "already started; the AOP starts once per boot\n"); + +@@ -1023,6 +1472,7 @@ static int apple_aop_probe(struct platform_device *pdev) + aop->eps[i].aop = aop; + aop->eps[i].index = AFK_ENDPOINT_START + i; + mutex_init(&aop->eps[i].lock); ++ init_completion(&aop->eps[i].stopped); + } + + ret = apple_aop_load(aop, ecid); +@@ -1050,10 +1500,24 @@ static int apple_aop_probe(struct platform_device *pdev) + prop = of_find_property(np, "apple,ecid", NULL); + if (prop) + of_remove_property(np, prop); +- writel(readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN, +- aop->akf + APPLE_AOP_CPU_CONTROL); ++ if (aop->spi0.ran) { ++ ret = apple_aop_warm_release(aop); ++ if (ret) ++ goto err_halt; ++ } ++ if (aop->spi0.ran) { ++ aop->experiment_step = "warm-run"; ++ ret = aop_wr(aop, APPLE_AOP_CPU_CONTROL, ++ readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN); ++ if (ret) ++ goto err_halt; ++ } else { ++ writel(readl(aop->akf + APPLE_AOP_CPU_CONTROL) | APPLE_AOP_CPU_CONTROL_RUN, ++ aop->akf + APPLE_AOP_CPU_CONTROL); ++ } + writel(1, aop->control + APPLE_AOP_CONTROL_AUTO_BOOT); + ++ dev_info(dev, "AOP released warm=%d\n", aop->spi0.ran); + /* + * The firmware was just loaded and starts from scratch: wait for its + * hello and its own power state report, as for any freshly started +@@ -1073,6 +1537,8 @@ static int apple_aop_probe(struct platform_device *pdev) + dev_warn(dev, "failed to start endpoint 0x%02x: %d\n", i, ret); + } + ++ aop->hello = true; ++ dev_info(dev, "AOP start complete warm=%d iop_crashed=0\n", aop->spi0.ran); + platform_set_drvdata(pdev, aop); + return 0; + +@@ -1102,12 +1568,41 @@ static void apple_aop_shutdown(struct platform_device *pdev) + mutex_lock(&aop->children_lock); + aop->removing = true; + list_for_each_entry_safe(child, tmp, &aop->children, list) { ++ ++ if (aop->warm) { ++ const struct apple_aop_service *service = dev_get_platdata(&child->pdev->dev); ++ static const u32 keys[] = { 0x16, 0x5c, 0 }; ++ ++ /* release6 sets the three accelerometer rates only. */ ++ if (strcmp(service->name, "accel")) ++ continue; ++ __le32 zero = 0; ++ unsigned int i; ++ int ret; ++ ++ for (i = 0; i < ARRAY_SIZE(keys); i++) { ++ ret = apple_aop_set_property(service, keys[i], &zero, sizeof(zero)); ++ if (ret) { ++ aop->shutdown_error = ret; ++ break; ++ } ++ } ++ } ++ /* Shutdown retains listeners, children and DMA grants, as release6 does. ++ * Unregister is remove-time work and can wait in IIO/driver core. ++ */ ++ if (aop->warm) ++ continue; + platform_device_unregister(child->pdev); + list_del(&child->list); + kfree(child); + } + mutex_unlock(&aop->children_lock); + ++ if (aop->warm) { ++ apple_aop_warm_shutdown(aop); ++ return; ++ } + if (!apple_aop_halt(aop)) + dev_err(aop->dev, "failed to halt\n"); + } +@@ -1129,11 +1624,26 @@ static const struct of_device_id apple_aop_of_match[] = { + }; + MODULE_DEVICE_TABLE(of, apple_aop_of_match); + ++static ssize_t warm_state_show(struct device *dev, struct device_attribute *attr, char *buf) ++{ ++ struct apple_aop *aop = dev_get_drvdata(dev); ++ ++ if (!aop) ++ return sysfs_emit(buf, "owned=0\n"); ++ return sysfs_emit(buf, "owned=%u reload=%u iop_crashed=%u accel_reports=%llu\n", ++ aop->hello, aop->spi0.ran, READ_ONCE(aop->iop_crashed), ++ READ_ONCE(aop->accel_reports)); ++} ++static DEVICE_ATTR_RO(warm_state); ++static struct attribute *apple_aop_attrs[] = { &dev_attr_warm_state.attr, NULL }; ++ATTRIBUTE_GROUPS(apple_aop); ++ + static struct platform_driver apple_aop_driver = { + .driver = { + .name = "apple-aop", + .of_match_table = apple_aop_of_match, + .suppress_bind_attrs = true, ++ .dev_groups = apple_aop_groups, + }, + .probe = apple_aop_probe, + .shutdown = apple_aop_shutdown, +diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c +index 725ab3fa7..d7928621a 100644 +--- a/drivers/soc/apple/rtkit.c ++++ b/drivers/soc/apple/rtkit.c +@@ -4,6 +4,8 @@ + * Copyright (C) The Asahi Linux Contributors + */ + ++#include ++#include + #include "rtkit-internal.h" + + enum { +@@ -867,6 +869,54 @@ int apple_rtkit_boot(struct apple_rtkit *rtk) + } + EXPORT_SYMBOL_GPL(apple_rtkit_boot); + ++/* Unlike shutdown(), retain all grants until the caller halts the CPU. ++ * Reinit frees DMA buffers and is unsafe before that point on S8000. ++ */ ++static void apple_rtkit_grants_hash(struct apple_rtkit *rtk, u8 *digest) ++{ ++ struct apple_rtkit_shmem *buffers[] = { ++ &rtk->ioreport_buffer, &rtk->crashlog_buffer, ++ &rtk->oslog_buffer, &rtk->syslog_buffer, ++ }; ++ struct sha256_ctx ctx; ++ unsigned int i; ++ ++ sha256_init(&ctx); ++ dma_rmb(); ++ for (i = 0; i < ARRAY_SIZE(buffers); i++) ++ if (buffers[i]->buffer) ++ sha256_update(&ctx, buffers[i]->buffer, buffers[i]->size); ++ sha256_final(&ctx, digest); ++} ++ ++int apple_rtkit_shutdown_preserve(struct apple_rtkit *rtk) ++{ ++ u8 before[SHA256_DIGEST_SIZE], after[SHA256_DIGEST_SIZE]; ++ int ret; ++ ++ if (rtk->crashed) ++ return -EIO; ++ ret = apple_rtkit_set_ap_power_state(rtk, APPLE_RTKIT_PWR_STATE_QUIESCED); ++ if (ret) ++ return ret; ++ dev_info(rtk->dev, "QUIESCE step=ap-ack state=0x10\n"); ++ ret = apple_rtkit_set_iop_power_state(rtk, APPLE_RTKIT_PWR_STATE_IDLE); ++ if (ret) ++ return ret; ++ dev_info(rtk->dev, "QUIESCE step=iop-ack state=0x201\n"); ++ msleep(20); ++ apple_rtkit_grants_hash(rtk, before); ++ msleep(20); ++ apple_rtkit_grants_hash(rtk, after); ++ if (rtk->crashed || memcmp(before, after, sizeof(before))) ++ return -EBUSY; ++ /* No more callback can race the caller's halt/drain. Do not reinit. */ ++ apple_mbox_stop(rtk->mbox); ++ flush_workqueue(rtk->wq); ++ return 0; ++} ++EXPORT_SYMBOL_GPL(apple_rtkit_shutdown_preserve); ++ + int apple_rtkit_shutdown(struct apple_rtkit *rtk) + { + int ret; +diff --git a/include/linux/soc/apple/pmgr-observe.h b/include/linux/soc/apple/pmgr-observe.h +new file mode 100644 +index 000000000..477b7fa22 +--- /dev/null ++++ b/include/linux/soc/apple/pmgr-observe.h +@@ -0,0 +1,27 @@ ++/* SPDX-License-Identifier: GPL-2.0-only */ ++#ifndef _LINUX_SOC_APPLE_PMGR_OBSERVE_H ++#define _LINUX_SOC_APPLE_PMGR_OBSERVE_H ++ ++#include ++ ++struct device_node; ++ ++struct apple_pmgr_observation { ++ u32 raw; ++ int genpd_status; ++}; ++ ++/* ++ * Diagnostic only. Nodes must be in child-before-parent lock order, with no ++ * duplicates (maximum five). Takes no power/runtime-PM reference. read() must ++ * not sleep or acquire genpd locks; it runs with IRQs disabled and all domain ++ * locks held, only if every genpd and hardware actual/target state is ON and ++ * reset/disable/parent-off are clear. Hardware auto-PM remains untouched and is ++ * recorded in raw; locks exclude software transitions, not autonomous gating. ++ * -EHOSTDOWN means read() was skipped. ++ */ ++int apple_pmgr_observe(struct device_node **nodes, unsigned int count, ++ struct apple_pmgr_observation *states, ++ void (*read)(void *), void *data); ++ ++#endif +diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h +index 1f9ebb618..c8e8df255 100644 +--- a/include/linux/soc/apple/rtkit.h ++++ b/include/linux/soc/apple/rtkit.h +@@ -124,6 +124,8 @@ int apple_rtkit_wake(struct apple_rtkit *rtk); + * Shutdown the co-processor + */ + int apple_rtkit_shutdown(struct apple_rtkit *rtk); ++/* AP 0x10 / IOP 0x201, stable system RAM, stop RX; retain every grant. */ ++int apple_rtkit_shutdown_preserve(struct apple_rtkit *rtk); + + /* + * Stop the mailbox of a shut-down co-processor, so that its power domain can diff --git a/kernel/patches/drivers/spi/0009-spi-apple-drain-and-idle-the-S8000-PIO-controller-on.patch b/kernel/patches/drivers/spi/0009-spi-apple-drain-and-idle-the-S8000-PIO-controller-on.patch new file mode 100644 index 00000000..daa284cf --- /dev/null +++ b/kernel/patches/drivers/spi/0009-spi-apple-drain-and-idle-the-S8000-PIO-controller-on.patch @@ -0,0 +1,122 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Sun, 8 Nov 2026 16:00:00 +0100 +Subject: [PATCH] spi: apple: drain and idle the S8000 PIO controller on + shutdown + +On S8000 the SPI controller runs in PIO mode. A kexec left it with +queued transfers, counters and an asserted chip select, and the next +kernel probed it in that state. + +On shutdown, suspend the SPI core queue, deassert the GPIO chip selects +and write the PIO registers back to idle while the controller is +powered. Do the same at probe, so that a controller inherited in any +state starts idle. Other SoCs use different DMA and interrupt registers +and keep their current behaviour. + +Assisted-by: LLM +--- + drivers/spi/spi-apple.c | 48 +++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 48 insertions(+) + +diff --git a/drivers/spi/spi-apple.c b/drivers/spi/spi-apple.c +index dc5154e1b..ed9f67a59 100644 +--- a/drivers/spi/spi-apple.c ++++ b/drivers/spi/spi-apple.c +@@ -15,6 +15,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -141,6 +142,7 @@ struct apple_spi { + struct dmaengine_result dma_result; + const struct apple_spi_hw *hw; + u32 word_delay; ++ struct spi_controller *ctlr; + }; + + static inline void reg_write(struct apple_spi *spi, int offset, u32 value) +@@ -231,6 +233,19 @@ static void apple_spi_init(struct apple_spi *spi) + reg_write(spi, APPLE_SPI_DELAY_POST, 0); + } + ++/* Lab 0015: normalize only the S8000 PIO register set, while powered. */ ++static int apple_spi_s8000_idle(struct apple_spi *spi) ++{ ++ reg_write(spi, APPLE_SPI_CTRL, 0); ++ reg_write(spi, APPLE_SPI_CFG, spi->hw->cfg_base); ++ reg_write(spi, APPLE_SPI_RXCNT, 0); ++ reg_write(spi, APPLE_SPI_TXCNT, 0); ++ reg_write(spi, APPLE_SPI_PIN, APPLE_SPI_PIN_CS); ++ apple_spi_t8010_clear_status(spi); ++ /* Complete posted writes before publishing children or leaving Linux. */ ++ return readl(spi->regs + APPLE_SPI_CTRL) ? -EIO : 0; ++} ++ + static int apple_spi_prepare_message(struct spi_controller *ctlr, + struct spi_message *msg) + { +@@ -1120,6 +1135,7 @@ static int apple_spi_probe(struct platform_device *pdev) + + spi = spi_controller_get_devdata(ctlr); + platform_set_drvdata(pdev, spi); ++ spi->ctlr = ctlr; + init_completion(&spi->done); + init_completion(&spi->dma_done); + spi->hw = of_device_get_match_data(&pdev->dev); +@@ -1214,6 +1230,11 @@ static int apple_spi_probe(struct platform_device *pdev) + } + + apple_spi_init(spi); ++ if (spi->hw->pio_only) { ++ ret = apple_spi_s8000_idle(spi); ++ if (ret) ++ return ret; ++ } + + ret = devm_spi_register_controller(&pdev->dev, ctlr); + if (ret < 0) +@@ -1223,6 +1244,32 @@ static int apple_spi_probe(struct platform_device *pdev) + return 0; + } + ++static void apple_spi_shutdown(struct platform_device *pdev) ++{ ++ struct apple_spi *spi = platform_get_drvdata(pdev); ++ struct spi_controller *ctlr = spi->ctlr; ++ unsigned int cs; ++ int ret; ++ ++ /* Other SoCs use different DMA/IRQ registers and keep their policy. */ ++ if (!spi->hw->pio_only) ++ return; ++ /* Children have shut down; drain the core queue before MMIO. */ ++ ret = spi_controller_suspend(ctlr); ++ if (!ret) ++ ret = pm_runtime_resume_and_get(&pdev->dev); ++ if (ret >= 0) { ++ if (ctlr->cs_gpiods) ++ for (cs = 0; cs < ctlr->num_chipselect; cs++) ++ gpiod_set_value_cansleep(ctlr->cs_gpiods[cs], 0); ++ ret = apple_spi_s8000_idle(spi); ++ } ++ if (ret < 0 && kexec_in_progress) ++ panic("S8000 SPI shutdown failed before kexec: %d", ret); ++ /* Keep runtime power pinned until the jump. */ ++ dev_info(&pdev->dev, "S8000 SPI shutdown result %d\n", ret); ++} ++ + static const struct apple_spi_hw apple_spi_hw_s8000 = { + .legacy_status = true, + .pio_only = true, +@@ -1260,6 +1307,7 @@ MODULE_DEVICE_TABLE(of, apple_spi_of_match); + + static struct platform_driver apple_spi_driver = { + .probe = apple_spi_probe, ++ .shutdown = apple_spi_shutdown, + .driver = { + .name = "apple-spi", + .of_match_table = apple_spi_of_match, 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/kernel/patches/drivers/watchdog/0001-dt-bindings-watchdog-retain-the-tethered-warm-boot-s.patch b/kernel/patches/drivers/watchdog/0001-dt-bindings-watchdog-retain-the-tethered-warm-boot-s.patch new file mode 100644 index 00000000..e166e49d --- /dev/null +++ b/kernel/patches/drivers/watchdog/0001-dt-bindings-watchdog-retain-the-tethered-warm-boot-s.patch @@ -0,0 +1,31 @@ +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: watchdog: retain the tethered warm boot state + +A tethered kernel inherits initialized hardware rather than power-on +state. Retain the board-selected warm handoff state so probe and +shutdown do not discard resources needed by the incoming kernel. + +Assisted-by: LLM +--- + Documentation/devicetree/bindings/watchdog/apple,wdt.yaml | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml b/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml +index ca4cf01db..a8179c208 100644 +--- a/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml ++++ b/Documentation/devicetree/bindings/watchdog/apple,wdt.yaml +@@ -13,6 +13,12 @@ allOf: + - $ref: watchdog.yaml# + + properties: ++ apple,tethered-boot: ++ type: boolean ++ description: | ++ A SoC reset leaves the tethered OS. The restart handler refuses warm ++ and soft reboot modes; cold reboot retains the normal SoC reset. ++ + compatible: + oneOf: + - items: diff --git a/kernel/patches/drivers/watchdog/0002-watchdog-apple-retain-the-tethered-warm-boot-state.patch b/kernel/patches/drivers/watchdog/0002-watchdog-apple-retain-the-tethered-warm-boot-state.patch new file mode 100644 index 00000000..a235291f --- /dev/null +++ b/kernel/patches/drivers/watchdog/0002-watchdog-apple-retain-the-tethered-warm-boot-state.patch @@ -0,0 +1,54 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Wed, 11 Nov 2026 23:30:21 +0100 +Subject: [PATCH] watchdog: apple: prioritize cold reset and delegate guarded + warm reset + +Tethered cold reset must beat PSCI priority 129: register at priority +255. On warm/soft modes do not program WD1, but return success so +watchdog_restart_notifier returns NOTIFY_DONE and PSCI can run; an error +would stop the chain. Other devices retain priority 128. + +Assisted-by: LLM +--- + drivers/watchdog/apple_wdt.c | 13 ++++++++++++- + 1 file changed, 12 insertions(+), 1 deletion(-) + +diff --git a/drivers/watchdog/apple_wdt.c b/drivers/watchdog/apple_wdt.c +index f53395329..04e287fe8 100644 +--- a/drivers/watchdog/apple_wdt.c ++++ b/drivers/watchdog/apple_wdt.c +@@ -14,6 +14,7 @@ + #include + #include + #include ++#include + #include + + /* +@@ -123,6 +124,12 @@ static int apple_wdt_restart(struct watchdog_device *wdd, unsigned long mode, + { + struct apple_wdt *wdt = to_apple_wdt(wdd); + ++ if (of_property_read_bool(wdd->parent->of_node, "apple,tethered-boot") && ++ (mode == REBOOT_WARM || mode == REBOOT_SOFT)) { ++ pr_emerg("apple-wdt: warm/soft reset delegated to PSCI on tethered boot\n"); ++ return 0; /* Continue the restart chain; NOTIFY_BAD would stop PSCI. */ ++ } ++ + writel_relaxed(APPLE_WDT_CTRL_RESET_EN, wdt->regs + APPLE_WDT_WD1_CTRL); + writel_relaxed(0, wdt->regs + APPLE_WDT_WD1_BITE_TIME); + writel_relaxed(0, wdt->regs + APPLE_WDT_WD1_CUR_TIME); +@@ -190,7 +197,11 @@ static int apple_wdt_probe(struct platform_device *pdev) + watchdog_init_timeout(&wdt->wdd, 0, dev); + apple_wdt_set_timeout(&wdt->wdd, wdt->wdd.timeout); + watchdog_stop_on_unregister(&wdt->wdd); +- watchdog_set_restart_priority(&wdt->wdd, 128); ++ /* The tethered cold escape must beat PSCI (priority 129). Warm/soft ++ * still refuse above, allowing the PSCI handler to run next. ++ */ ++ watchdog_set_restart_priority(&wdt->wdd, ++ of_property_read_bool(dev->of_node, "apple,tethered-boot") ? 255 : 128); + + return devm_watchdog_register_device(dev, &wdt->wdd); + } diff --git a/kernel/patches/drivers/watchdog/0003-watchdog-apple-initialize-the-parent-before-register.patch b/kernel/patches/drivers/watchdog/0003-watchdog-apple-initialize-the-parent-before-register.patch new file mode 100644 index 00000000..67e6cb21 --- /dev/null +++ b/kernel/patches/drivers/watchdog/0003-watchdog-apple-initialize-the-parent-before-register.patch @@ -0,0 +1,37 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Berkay=20K=C3=B6sebay?= +Date: Thu, 12 Nov 2026 00:43:06 +0100 +Subject: [PATCH] watchdog: apple: initialize the parent before registering + restart + +The tethered restart guard dereferences the parent, but probe never set +it. Initialize it before registration and tolerate a missing parent in +the restart callback so the cold watchdog writes remain reachable. + +Assisted-by: LLM +--- + drivers/watchdog/apple_wdt.c | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/drivers/watchdog/apple_wdt.c b/drivers/watchdog/apple_wdt.c +index 04e287fe8..6de9f863c 100644 +--- a/drivers/watchdog/apple_wdt.c ++++ b/drivers/watchdog/apple_wdt.c +@@ -124,7 +124,8 @@ static int apple_wdt_restart(struct watchdog_device *wdd, unsigned long mode, + { + struct apple_wdt *wdt = to_apple_wdt(wdd); + +- if (of_property_read_bool(wdd->parent->of_node, "apple,tethered-boot") && ++ if (wdd->parent && ++ of_property_read_bool(wdd->parent->of_node, "apple,tethered-boot") && + (mode == REBOOT_WARM || mode == REBOOT_SOFT)) { + pr_emerg("apple-wdt: warm/soft reset delegated to PSCI on tethered boot\n"); + return 0; /* Continue the restart chain; NOTIFY_BAD would stop PSCI. */ +@@ -185,6 +186,7 @@ static int apple_wdt_probe(struct platform_device *pdev) + + platform_set_drvdata(pdev, wdt); + ++ wdt->wdd.parent = dev; + wdt->wdd.ops = &apple_wdt_ops; + wdt->wdd.info = &apple_wdt_info; + wdt->wdd.max_hw_heartbeat_ms = U32_MAX / wdt->clk_rate * 1000; diff --git a/src/orchard/image/platform.py b/src/orchard/image/platform.py index ee55ae55..997d8180 100644 --- a/src/orchard/image/platform.py +++ b/src/orchard/image/platform.py @@ -85,7 +85,7 @@ def tools(hardware=None, cache=None, platform='apple-16k'): return prepare_tools(hardware, cache, device=sorted(boards(platform, hardware))[0]) / 'busybox' -def initrd(modules, release, busybox, *, hardware=None, storage_modules=STORAGE_MODULES): +def initrd(modules, release, busybox, *, hardware=None, storage_modules=STORAGE_MODULES, pristine_dtbs=None): """orchard's initrd for installed systems: BusyBox, the storage modules and their dependencies. MODULES is {path: (data, mode)} as module_files gives it. @@ -116,6 +116,15 @@ def initrd(modules, release, busybox, *, hardware=None, storage_modules=STORAGE_ entries['etc/orchard/modules'] = (stat.S_IFREG | 0o644, ('\n'.join(loaded) + '\n').encode()) if usb: entries['etc/orchard/usb-modules'] = (stat.S_IFREG | 0o644, ('\n'.join(usb) + '\n').encode()) + if pristine_dtbs: + entries['etc/orchard/dtbs'] = (directory, b'') + mapping = [] + for board, data in sorted(pristine_dtbs.items()): + if not board.replace('-', '').isalnum(): + raise ValueError('Invalid pristine DT board') + entries[f'etc/orchard/dtbs/{board}.dtb'] = (stat.S_IFREG | 0o644, data) + mapping.append(f'apple,{board} {board}.dtb\n') + entries['etc/orchard/pristine-map'] = (stat.S_IFREG | 0o644, ''.join(mapping).encode()) dependency_lines = [] for name in sorted(wanted): target = f'lib/modules/{release}/{name}' @@ -154,13 +163,13 @@ def files(bundle, platform, board_dtbs, *, busybox, hardware=None): base = f'usr/lib/modules/{release}/' result[base + 'vmlinuz'] = ((bundle / described['image']).read_bytes(), 0o644) modules_to_load = storage_modules(hardware, platform) - result[base + 'initrd'] = (initrd(result, release, busybox, hardware=hardware, - storage_modules=modules_to_load), 0o644) for dtb in sorted(set(board_dtbs.values())): path = bundle / 'dtbs' / (dtb + '.dtb') if path.is_symlink() or not path.is_file(): raise ValueError(f'Kernel bundle lacks {path.name}') result[base + 'dtb/' + path.name] = (path.read_bytes(), 0o644) + pristine = {board: result[base + 'dtb/' + dtb + '.dtb'][0] for board, dtb in board_dtbs.items()} + result[base + 'initrd'] = (initrd(result, release, busybox, hardware=hardware, pristine_dtbs=pristine, storage_modules=modules_to_load), 0o644) result.update(regulatory_files()) defaults = hardware / 'system/defaults' for path in sorted(defaults.rglob('*')): diff --git a/src/orchard/service.py b/src/orchard/service.py index 0d5d76c7..0340f16c 100644 --- a/src/orchard/service.py +++ b/src/orchard/service.py @@ -150,7 +150,8 @@ def entry_for(record, devicetree): name = record.get('name', 'Linux') return {'id': re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-') or 'linux', 'title': name, # Quiet as the recovery: no console text over the splash; the system maps its console. - 'options': record.get('cmdline', 'rootwait') + ' fbcon=map:9 vt.global_cursor_default=0', + 'options': record.get('cmdline', 'rootwait') + ' fbcon=map:9 vt.global_cursor_default=0' + ( + ' panic=0' if devicetree == 's8003-j71t.dtb' else ''), 'devicetree': devicetree} 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() diff --git a/tests/test_platform_layer.py b/tests/test_platform_layer.py index 013e405a..b7029826 100644 --- a/tests/test_platform_layer.py +++ b/tests/test_platform_layer.py @@ -84,6 +84,8 @@ def test_the_initrd_carries_busybox_and_the_storage_modules_only(self): self.assertEqual(names['init'], (ROOT / 'boot/initramfs/root/init').read_bytes()) self.assertEqual(names['etc/orchard/usb-identity'], (ROOT / 'src/orchard/usb-identity').read_bytes()) self.assertEqual(names['etc/orchard/modules'], b'nvme\n') + self.assertEqual(names['etc/orchard/dtbs/j71t.dtb'], b's8003-j71t') + self.assertIn(b'apple,j71t j71t.dtb\n', names['etc/orchard/pristine-map']) self.assertEqual(sorted(name for name in names if name.endswith('.ko')), [base + 'kernel/drivers/nvme/host/nvme-core.ko', base + 'kernel/drivers/nvme/host/nvme.ko']) self.assertEqual(names[base + 'modules.dep'],