From d4fca256fca3bbfdf5b535e6da62fb27848f342f Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:56:55 -0500 Subject: [PATCH 1/4] rtc: apple-pmu: be the J700's rtc0, set its clock and allow long alarms On the J700 rtc0 is the SMC RTC, which has no alarm, and the PMU RTC that wakes the machine is rtc1. rtcwake, and anything else that arms /dev/rtc0 or rtc0's wakealarm by default, can't arm a wake, so a sleep it starts never ends by itself. Alarm-clock timers, such as systemd's WakeSystem=, reach rtc1 already, but the PMU RTC limits alarms to an hour, so the alarm timer cuts a longer one short and the machine wakes early. Make the PMU RTC rtc0 through the aliases. The first RTC is also the one the kernel sets the system time from, and the one hwclock and timedated write, so: - set Baku's whole-second offset, the cell the SMC RTC writes, the same way: from the counter rounded to the nearest second; - build the driver in, as the SMC RTC is. Built as a module, rtc0 only appears once the module loads, and until then the time isn't set from any RTC; - let Baku alarms reach as far as the comparator. The hour is a limit for suspend-to-idle, where nothing else is known to wake the system; on the J700 this alarm is the wake source of macOS's own deep sleeps. The other PMUs keep the hour, and their clock is still set through the SMC RTC. The wake test scripts find the PMU RTC by name instead of assuming rtc1. Signed-off-by: Diljit Singh --- arch/arm64/boot/dts/apple/t8140-j700.dts | 5 +- arch/arm64/configs/asahi.config | 2 +- drivers/rtc/Kconfig | 12 +++-- drivers/rtc/rtc-apple-pmu.c | 49 ++++++++++++++++-- tools/asahi/pmu-rtc-tests.py | 60 ++++++++++++++++++++-- tools/asahi/run-host-tests.py | 3 +- tools/aurora-wake/bt-wake-cycle.sh | 14 ++++- tools/aurora-wake/bt-wake-power.sh | 14 ++++- tools/aurora-wake/s2idle-rtc-regression.sh | 16 +++++- 9 files changed, 156 insertions(+), 19 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8140-j700.dts b/arch/arm64/boot/dts/apple/t8140-j700.dts index 26d21bcbb1a92..8b38a4313b7b9 100644 --- a/arch/arm64/boot/dts/apple/t8140-j700.dts +++ b/arch/arm64/boot/dts/apple/t8140-j700.dts @@ -27,8 +27,9 @@ sep = &sep; serial0 = &serial0; pmp = &pmp; - rtc0 = &smc_rtc; - rtc1 = &pmu_rtc; + /* First the RTC that can wake the system */ + rtc0 = &pmu_rtc; + rtc1 = &smc_rtc; wifi0 = &wifi0; /* used by the loader to hand over the display */ dcp = &dcp; diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index f97bdc3225d59..fa753abbe7e61 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -96,7 +96,7 @@ CONFIG_USB4_APPLE_SOC=m CONFIG_USB_XHCI_PCI_ASMEDIA=y CONFIG_TYPEC_SN201202X=m CONFIG_RTC_DRV_MACSMC=y -CONFIG_RTC_DRV_APPLE_PMU=m +CONFIG_RTC_DRV_APPLE_PMU=y CONFIG_APPLE_ADMAC=m CONFIG_APPLE_SIO=m CONFIG_MFD_MACSMC=y diff --git a/drivers/rtc/Kconfig b/drivers/rtc/Kconfig index bae3a9f856c20..a7e6eba6e7910 100644 --- a/drivers/rtc/Kconfig +++ b/drivers/rtc/Kconfig @@ -2099,10 +2099,14 @@ config RTC_DRV_APPLE_PMU depends on ARCH_APPLE && NVMEM_APPLE_SPMI && PM_SLEEP help Read the PMU clock on M1 Pro/Max/Ultra, M2, M4 Pro and A18 Pro - systems and use its alarm to wake the system - from suspend-to-idle. Alarms are limited to one hour; deeper - suspend states are not supported. Continue setting the shared - clock through the SMC RTC. + systems and use its alarm to wake the system from suspend-to-idle, + and on A18 Pro from a firmware-driven suspend too. A18 Pro alarms + reach as far as the PMU's comparator, and the clock can be set here; + elsewhere alarms are limited to one hour and the shared clock is set + through the SMC RTC. + + Say Y where this is the system's first RTC (rtc0), so that it sets + the system time at boot. This driver can be built as the module rtc-apple-pmu. diff --git a/drivers/rtc/rtc-apple-pmu.c b/drivers/rtc/rtc-apple-pmu.c index 5a5bc4e576722..33accb6dea010 100644 --- a/drivers/rtc/rtc-apple-pmu.c +++ b/drivers/rtc/rtc-apple-pmu.c @@ -17,6 +17,7 @@ #define APPLE_PMU_RTC_ARM BIT(6) #define APPLE_PMU_RTC_ALARM BIT(0) #define APPLE_PMU_RTC_HZ_SHIFT 16 +/* The suspend-to-idle limit, where the alarm is all that wakes the system. */ #define APPLE_PMU_RTC_MAX_DELAY 3600 /* @@ -32,6 +33,8 @@ struct apple_pmu_rtc_hw { u8 event_bit; u8 monitor_bit; bool offset_in_seconds; + /* How far ahead an alarm may be, in seconds */ + u32 max_delay; }; static const struct apple_pmu_rtc_hw apple_pmu_rtc_baku = { @@ -42,6 +45,8 @@ static const struct apple_pmu_rtc_hw apple_pmu_rtc_baku = { .event = 0xf80c, .event_bit = BIT(0), .offset_in_seconds = true, + /* As far as the comparator reaches: macOS's own deep-sleep wakes use it. */ + .max_delay = U32_MAX, }; static const struct apple_pmu_rtc_hw apple_pmu_rtc_abbey = { @@ -52,6 +57,7 @@ static const struct apple_pmu_rtc_hw apple_pmu_rtc_abbey = { .event = 0x2000, .event_bit = BIT(1), .monitor_bit = BIT(0), + .max_delay = APPLE_PMU_RTC_MAX_DELAY, }; /* M2 (t8112): Baku's register block with the offset in 1/32768 s. */ @@ -63,6 +69,7 @@ static const struct apple_pmu_rtc_hw apple_pmu_rtc_stowe = { .event = 0xf80c, .event_bit = BIT(0), .monitor_bit = BIT(0), + .max_delay = APPLE_PMU_RTC_MAX_DELAY, }; /* M1 Pro, Max and Ultra (t600x), offset in 1/32768 s */ @@ -74,6 +81,7 @@ static const struct apple_pmu_rtc_hw apple_pmu_rtc_maverick = { .event = 0x14c0, .event_bit = BIT(0), .monitor_bit = BIT(0), + .max_delay = APPLE_PMU_RTC_MAX_DELAY, }; struct apple_pmu_rtc { @@ -147,6 +155,31 @@ static int apple_pmu_rtc_read_time(struct device *dev, struct rtc_time *time) return 0; } +/* + * Baku's offset is in whole seconds, and this sets it as the SMC RTC does: the + * counter rounded to the nearest second, so the clock is at most half a second off. + */ +static int apple_pmu_rtc_set_time(struct device *dev, struct rtc_time *time) +{ + struct apple_pmu_rtc *rtc = dev_get_drvdata(dev); + __le64 value; + u64 counter; + s64 offset; + int error; + + guard(mutex)(&rtc->transaction); + error = apple_pmu_rtc_counter(rtc, &counter); + if (error) + return error; + offset = rtc_tm_to_time64(time) - + (s64)((counter + BIT(APPLE_PMU_RTC_HZ_SHIFT - 1)) >> APPLE_PMU_RTC_HZ_SHIFT); + if (offset < 0 || offset > RTC_TIMESTAMP_END_9999 - U32_MAX) + return -ERANGE; + value = cpu_to_le64(offset); + error = nvmem_cell_write(rtc->offset, &value, sizeof(value)); + return error < 0 ? error : 0; +} + /* All comparator/control accesses are serialized with the IRQ thread. */ static int apple_pmu_rtc_ack_alarm(struct apple_pmu_rtc *rtc) { @@ -224,7 +257,7 @@ static int apple_pmu_rtc_validate_alarm(struct apple_pmu_rtc *rtc, u64 comparato counter >>= APPLE_PMU_RTC_HZ_SHIFT; if (comparator <= counter) return -ETIME; - if (comparator - counter > APPLE_PMU_RTC_MAX_DELAY) + if (comparator - counter > rtc->hw->max_delay) return -ERANGE; return 0; } @@ -371,6 +404,7 @@ static int apple_pmu_rtc_init_alarm(struct apple_pmu_rtc *rtc) return apple_pmu_rtc_stop_alarm(rtc); } +/* The others' offsets are set through the SMC RTC. */ static const struct rtc_class_ops apple_pmu_rtc_ops = { .read_time = apple_pmu_rtc_read_time, .read_alarm = apple_pmu_rtc_read_alarm, @@ -378,6 +412,14 @@ static const struct rtc_class_ops apple_pmu_rtc_ops = { .alarm_irq_enable = apple_pmu_rtc_alarm_enable, }; +static const struct rtc_class_ops apple_pmu_rtc_seconds_ops = { + .read_time = apple_pmu_rtc_read_time, + .set_time = apple_pmu_rtc_set_time, + .read_alarm = apple_pmu_rtc_read_alarm, + .set_alarm = apple_pmu_rtc_set_alarm, + .alarm_irq_enable = apple_pmu_rtc_alarm_enable, +}; + static int apple_pmu_rtc_prepare(struct device *dev) { struct apple_pmu_rtc *rtc = dev_get_drvdata(dev); @@ -437,10 +479,11 @@ static int apple_pmu_rtc_probe(struct platform_device *pdev) rtc->clock = devm_rtc_allocate_device(dev); if (IS_ERR(rtc->clock)) return PTR_ERR(rtc->clock); - rtc->clock->ops = &apple_pmu_rtc_ops; + rtc->clock->ops = rtc->hw->offset_in_seconds ? &apple_pmu_rtc_seconds_ops : + &apple_pmu_rtc_ops; rtc->clock->range_min = 0; rtc->clock->range_max = RTC_TIMESTAMP_END_9999; - rtc->clock->alarm_offset_max = APPLE_PMU_RTC_MAX_DELAY; + rtc->clock->alarm_offset_max = rtc->hw->max_delay; clear_bit(RTC_FEATURE_UPDATE_INTERRUPT, rtc->clock->features); error = device_init_wakeup(dev, true); if (error) diff --git a/tools/asahi/pmu-rtc-tests.py b/tools/asahi/pmu-rtc-tests.py index 6632caeb25fb3..a018e2f9040d9 100644 --- a/tools/asahi/pmu-rtc-tests.py +++ b/tools/asahi/pmu-rtc-tests.py @@ -23,6 +23,9 @@ 'maverick-event': ('.event = 0x14c0', '.event = 0x140c'), 'stowe-offset': ('static const struct apple_pmu_rtc_hw apple_pmu_rtc_stowe = {', 'static const struct apple_pmu_rtc_hw apple_pmu_rtc_stowe = {\n\t.offset_in_seconds = true,'), + 'baku-limit': ('.max_delay = U32_MAX,', '.max_delay = APPLE_PMU_RTC_MAX_DELAY,'), + 'set-no-round': ('(counter + BIT(APPLE_PMU_RTC_HZ_SHIFT - 1))', '(counter)'), + 'set-negative': ('if (offset < 0 || offset', 'if (offset'), } PRELUDE = r''' @@ -39,12 +42,14 @@ typedef uint64_t u64; typedef int64_t s64; typedef uint32_t __le32; +typedef uint64_t __le64; #define BIT(n) (1U << (n)) #define U32_MAX UINT32_MAX #define RTC_TIMESTAMP_END_9999 253402300799LL #define cpu_to_le32(x) (x) #define le32_to_cpu(x) (x) #define le64_to_cpu(x) (x) +#define cpu_to_le64(x) (x) #define IS_ERR(x) ((intptr_t)(x) < 0) #define PTR_ERR(x) ((int)(intptr_t)(x)) #define kfree free @@ -61,7 +66,7 @@ struct rtc_wkalrm { struct rtc_time time; bool enabled, pending; }; struct rtc_device { int events; }; struct device { void *data; bool wake; int wake_events; }; -struct nvmem_cell { u8 data[8]; size_t bytes; int error; }; +struct nvmem_cell { u8 data[8]; size_t bytes; int error, write_error; }; struct regmap { u8 bytes[65536]; unsigned ctrl, counter, compare, mask, event, bit; int ops, fail; }; static int disabled, pm_suspend_target_state = PM_SUSPEND_TO_IDLE; static bool firmware_suspend; @@ -86,6 +91,10 @@ void *p = malloc(c->bytes); assert(p); memcpy(p, c->data, c->bytes); *bytes = c->bytes; return p; } +static int nvmem_cell_write(struct nvmem_cell *c, void *buf, size_t bytes) { + if (c->write_error) return c->write_error; + assert(bytes == c->bytes); memcpy(c->data, buf, bytes); return bytes; +} static bool io_error(struct regmap *m) { return ++m->ops == m->fail; } static int regmap_read(struct regmap *m, unsigned reg, unsigned *v) { if (io_error(m)) return -EIO; @@ -197,16 +206,59 @@ assert(!apple_pmu_rtc_validate_alarm(&rtc, 101)); assert(apple_pmu_rtc_validate_alarm(&rtc, 100) == -ETIME); assert(!apple_pmu_rtc_validate_alarm(&rtc, 3700)); - assert(apple_pmu_rtc_validate_alarm(&rtc, 3701) == -ERANGE); alarm.time.seconds = 1100; assert(apple_pmu_rtc_set_alarm(&dev, &alarm) == -ETIME); alarm.time.seconds = 4701; - assert(apple_pmu_rtc_set_alarm(&dev, &alarm) == -ERANGE); + if (a) { + /* One hour, where the alarm only ends a suspend-to-idle. */ + assert(apple_pmu_rtc_validate_alarm(&rtc, 3701) == -ERANGE); + assert(apple_pmu_rtc_set_alarm(&dev, &alarm) == -ERANGE); + } else { + /* Baku: as far as the 32-bit comparator reaches. */ + assert(!apple_pmu_rtc_validate_alarm(&rtc, 3701)); + assert(!apple_pmu_rtc_validate_alarm(&rtc, U32_MAX)); + assert(!apple_pmu_rtc_set_alarm(&dev, &alarm) && comparator() == 3701); + alarm.time.seconds = 86400 * 7 + 1000; + assert(!apple_pmu_rtc_set_alarm(&dev, &alarm) && comparator() == 86400 * 7); + alarm.time.seconds = 1000 + (s64)U32_MAX + 1; + assert(apple_pmu_rtc_set_alarm(&dev, &alarm) == -ERANGE); + assert(!apple_pmu_rtc_alarm_enable(&dev, 0)); + } /* Reject malformed offsets and propagate NVMEM errors. */ offset.bytes = a ? 8 : 6; assert(apple_pmu_rtc_offset(&rtc, &ticks) == -EINVAL); offset.error = -EIO; assert(apple_pmu_rtc_read_time(&dev, &time) == -EIO); + if (!a) { + /* Baku sets its whole-second offset, rounding the counter to the second. */ + reset(v); + time.seconds = 5000; + set_counter((100ULL << 16) + 32767); + assert(!apple_pmu_rtc_set_time(&dev, &time)); + assert(get_unaligned_le64(offset.data) == 4900); + assert(!apple_pmu_rtc_read_time(&dev, &time) && time.seconds == 5000); + time.seconds = 5000; + set_counter((100ULL << 16) + 32768); + assert(!apple_pmu_rtc_set_time(&dev, &time)); + assert(get_unaligned_le64(offset.data) == 4899); + /* Alarms follow the new offset. */ + alarm.time.seconds = 5030; + assert(!apple_pmu_rtc_set_alarm(&dev, &alarm) && comparator() == 131); + assert(!apple_pmu_rtc_alarm_enable(&dev, 0)); + /* Before the counter's own time, and past what the offset may hold. */ + time.seconds = 99; + assert(apple_pmu_rtc_set_time(&dev, &time) == -ERANGE); + time.seconds = RTC_TIMESTAMP_END_9999; + assert(apple_pmu_rtc_set_time(&dev, &time) == -ERANGE); + assert(get_unaligned_le64(offset.data) == 4899); + time.seconds = 5000; + offset.write_error = -EIO; + assert(apple_pmu_rtc_set_time(&dev, &time) == -EIO); + offset.write_error = 0; + map.ops = 0; map.fail = 1; + assert(apple_pmu_rtc_set_time(&dev, &time) == -EIO); + assert(get_unaligned_le64(offset.data) == 4899); + } reset(v); set_offset(-32768); if (a) { @@ -254,7 +306,7 @@ } int main(void) { run_variant(BAKU); run_variant(ABBEY); run_variant(STOWE); run_variant(MAVERICK); - puts("PMU RTC: Baku, Abbey, Stowe and Maverick arithmetic, W1C transactions and fault injection passed"); + puts("PMU RTC: Baku, Abbey, Stowe and Maverick arithmetic, W1C transactions, Baku clock setting and fault injection passed"); return 0; } ''' diff --git a/tools/asahi/run-host-tests.py b/tools/asahi/run-host-tests.py index b878404b2d9fa..f7188ed814308 100755 --- a/tools/asahi/run-host-tests.py +++ b/tools/asahi/run-host-tests.py @@ -153,7 +153,8 @@ def mutations(flag, names, base, expect=KILL): 'pmu-rtc-tests.py': [(out(), PASS)] + mutations('--mutation', ('ack-clobber', 'arm-ack', 'offset-seconds', 'offset-sign', 'event-bit', 'wrong-control', - 'maverick-event', 'stowe-offset'), out()), + 'maverick-event', 'stowe-offset', 'baku-limit', + 'set-no-round', 'set-negative'), out()), 'qspi-nvram-tests.py': [(out('--output'), PASS)], 'rtkit-oslog-tests.py': [(out(), PASS)] + mutations('--mutate', ('admission', 'channel', 'counter', 'reset', 'buffer'), out()), diff --git a/tools/aurora-wake/bt-wake-cycle.sh b/tools/aurora-wake/bt-wake-cycle.sh index 3c10162256e97..2eb419b345823 100755 --- a/tools/aurora-wake/bt-wake-cycle.sh +++ b/tools/aurora-wake/bt-wake-cycle.sh @@ -17,9 +17,21 @@ set -u +# The PMU RTC, which can wake the system: rtc0 on J700, rtc1 on others. +pmu_rtc() { + local name + for name in /sys/class/rtc/rtc*/name; do + [[ $(cat "$name" 2> /dev/null) == apple-pmu-rtc* ]] || continue + name=${name%/name} + echo "${name##*/}" + return + done + echo rtc0 +} + mode= secs=120 -rtc=rtc1 +rtc=$(pmu_rtc) addr= out=/var/tmp/bt-wake/cycle-$(date +%Y%m%d-%H%M%S) bdf=0000:01:00.1 diff --git a/tools/aurora-wake/bt-wake-power.sh b/tools/aurora-wake/bt-wake-power.sh index af5d49dadb216..be6d1eb06c8f6 100755 --- a/tools/aurora-wake/bt-wake-power.sh +++ b/tools/aurora-wake/bt-wake-power.sh @@ -9,6 +9,17 @@ # - the battery energy used, when running on battery (energy_now). # Usage: bt-wake-power.sh [-s secs] [-n pairs] [-o outdir] set -u +# The PMU RTC, which can wake the system: rtc0 on J700, rtc1 on others. +pmu_rtc() { + local name + for name in /sys/class/rtc/rtc*/name; do + [[ $(cat "$name" 2> /dev/null) == apple-pmu-rtc* ]] || continue + name=${name%/name} + echo "${name##*/}" + return + done + echo rtc0 +} secs=300 pairs=2 out=/var/tmp/bt-wake/power-$(date +%Y%m%d-%H%M%S) @@ -19,6 +30,7 @@ done dev=/sys/bus/pci/devices/0000:01:00.1 bat=/sys/class/power_supply/macsmc-battery irq=$(awk -F: '/mt7932-bt/ { gsub(/ /, "", $1); print $1; exit }' /proc/interrupts) +rtc=$(pmu_rtc) mkdir -p "$out" orig=$(cat "$dev/power/wakeup") trap 'echo "$orig" > "$dev/power/wakeup"' EXIT @@ -42,7 +54,7 @@ for p in $(seq 1 "$pairs"); do e0=$(cat "$bat/energy_now") st=$(cat "$bat/status") t0=$(date +%s.%N) - rtcwake -d rtc1 -m freeze -s "$secs" > /dev/null 2>&1 + rtcwake -d "$rtc" -m freeze -s "$secs" > /dev/null 2>&1 t1=$(date +%s.%N) b1=$(irq_sum "$irq"); a1=$(irq_sum all); e1=$(cat "$bat/energy_now") dmesg > "$out/dmesg-$p-$mode.txt" diff --git a/tools/aurora-wake/s2idle-rtc-regression.sh b/tools/aurora-wake/s2idle-rtc-regression.sh index 89a131aca0b59..74697560040db 100755 --- a/tools/aurora-wake/s2idle-rtc-regression.sh +++ b/tools/aurora-wake/s2idle-rtc-regression.sh @@ -3,7 +3,7 @@ # # Unattended suspend-to-idle regression test for Apple silicon (J700 and others). # -# Suspends with an RTC alarm (rtc1, apple-pmu-rtc, by default) several times and +# Suspends with an RTC alarm (apple-pmu-rtc by default) several times and # checks after every resume that: # - the time actually spent suspended (CLOCK_BOOTTIME minus CLOCK_MONOTONIC) # lasted until the alarm: no spurious wakeup, e.g. from a running xHC, @@ -24,9 +24,21 @@ set -u +# The PMU RTC, which can wake the system: rtc0 on J700, rtc1 on others. +pmu_rtc() { + local name + for name in /sys/class/rtc/rtc*/name; do + [[ $(cat "$name" 2> /dev/null) == apple-pmu-rtc* ]] || continue + name=${name%/name} + echo "${name##*/}" + return + done + echo rtc0 +} + cycles=3 secs=20 -rtc=rtc1 +rtc=$(pmu_rtc) out= ignore= dry_run=0 From 49992cd5a0183cbacc9ed0085ade76eef13e2f4e Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:56:56 -0500 Subject: [PATCH 2/4] soc: apple: sep: start the enclave again after a sleep refused past its request A sleep refused after the enclave took its request, for instance when the enclave gated only after the sleep's last step stopped waiting for it, left the enclave marked lost: Touch ID and SEP-backed keys failed until the next real sleep or a reboot. macOS has no such state. AppleSEPManager starts an enclave that answered its sleep request the same way whether the system slept or not: its power state change back from 0 runs the provider's startCPU and the wake, and an aborted sleep goes through it too. Do the same. Once the refused sleep's enclave reads gated and its domain off, start it as a real wake does: DEV_DISABLE cleared, DART restored, the doorbell, then both wake messages. It's marked lost only if that fails. The mailbox of an enclave whose domain is off is still never touched: the gate and the power state are read in the PMGR word. Also, as reviewed in sep-hardening: - a last-refusal registration that fails for any reason but an unsupported platform refuses the sleep while nothing was sent, instead of falling back to the early request; - a failed read of the domain word is an error, not an expired wait, and never leads to a mailbox probe. So an enclave node without power-domains (some older SoCs) stays lost after an aborted firmware suspend instead of being probed, as nothing says it's safe to. A test-only parameter, sleep_refuse_after_gate, refuses the next deep sleep after the enclave gated, so that the restart can be checked on hardware. Signed-off-by: Diljit Singh --- drivers/soc/apple/pm.rs | 37 ++- drivers/soc/apple/pm_shim.c | 63 +++- drivers/soc/apple/sep.rs | 73 +++-- drivers/soc/apple/shim.h | 1 + drivers/soc/apple/t8140-sleep.c | 2 +- include/linux/soc/apple/sleep.h | 2 +- tools/asahi/run-host-tests.py | 4 + tools/asahi/sep-suspend-errors-tests.py | 412 ++++++++++++++++++++++++ 8 files changed, 551 insertions(+), 43 deletions(-) create mode 100644 tools/asahi/sep-suspend-errors-tests.py diff --git a/drivers/soc/apple/pm.rs b/drivers/soc/apple/pm.rs index a65e5920102f4..843d2a638128b 100644 --- a/drivers/soc/apple/pm.rs +++ b/drivers/soc/apple/pm.rs @@ -37,6 +37,7 @@ extern "C" { fn sep_pm_unregister(); fn sep_pm_keep_domains(dev: *mut c_void) -> c_int; fn sep_pm_wait_gated(dev: *mut c_void, timeout_ms: c_uint) -> c_int; + fn sep_pm_wait_off(dev: *mut c_void, timeout_ms: c_uint) -> c_int; fn sep_pm_clear_dev_disable(dev: *mut c_void) -> c_int; fn sep_pm_dart_prepare(dev: *mut c_void) -> c_int; fn sep_pm_dart_restore(dev: *mut c_void); @@ -92,10 +93,26 @@ impl SleepGate { } } -/// Whether the enclave gated itself within the bound. -pub(crate) fn wait_gated(dev: &kernel::device::Device) -> bool { +/// Whether the enclave gated itself within the bound. Observation errors are preserved. +pub(crate) fn wait_gated(dev: &kernel::device::Device) -> Result { // SAFETY: `dev` is the bound SEP device. - unsafe { sep_pm_wait_gated(dev.as_raw().cast(), GATE_TIMEOUT_MS) == 0 } + let rc = unsafe { sep_pm_wait_gated(dev.as_raw().cast(), GATE_TIMEOUT_MS) }; + match rc { + 0 => Ok(true), + 1 => Ok(false), + _ => Err(Error::from_errno(rc)), + } +} + +/// Whether the gated enclave's domain also went off within the bound. +pub(crate) fn wait_off(dev: &kernel::device::Device) -> Result { + // SAFETY: `dev` is the bound SEP device. + let rc = unsafe { sep_pm_wait_off(dev.as_raw().cast(), GATE_TIMEOUT_MS) }; + match rc { + 0 => Ok(true), + 1 => Ok(false), + _ => Err(Error::from_errno(rc)), + } } /// Saves the enclave's DART while it can be read, before the enclave is told. @@ -110,14 +127,14 @@ pub(crate) fn dart_prepare(dev: &kernel::device::Device) -> Result<()> { /// Leaves `request` to the sleep driver's last refusal, which sends it with interrupts off /// and saves the DART just before. Its answer is the message whose `mask` bits read `reply`. -/// False where there's no such step, and the request goes now. +/// `Ok(false)` where there's no such step, and the request goes now. pub(crate) fn leave_for_commit( dev: &kernel::device::Device, mbox: *mut kernel::bindings::apple_mbox, request: kernel::soc::apple::mailbox::Message, reply: u64, mask: u64, -) -> bool { +) -> Result { // SAFETY: `dev` is the bound SEP device and `mbox` its mailbox, which outlives the sleep: // `commit_finish` takes the request back at the resume. let rc = unsafe { @@ -130,7 +147,15 @@ pub(crate) fn leave_for_commit( mask, ) }; - rc == 0 + if rc == 0 { + Ok(true) + } else { + let err = Error::from_errno(rc); + match err { + EOPNOTSUPP | ENOSYS => Ok(false), + _ => Err(err), + } + } } /// After a sleep whose request was left to its last refusal: whether it went out. `None` if diff --git a/drivers/soc/apple/pm_shim.c b/drivers/soc/apple/pm_shim.c index adfb9c90688a4..d5c31cf79ff79 100644 --- a/drivers/soc/apple/pm_shim.c +++ b/drivers/soc/apple/pm_shim.c @@ -27,10 +27,19 @@ #include #include +#include + #include "shim.h" /* The state word of a PMGR power domain, as pmgr-pwrstate has it. */ #define SEP_PS_DEV_DISABLE BIT(10) +#define SEP_PS_ACTUAL GENMASK(7, 4) + +/* Test only, one sleep: refuse it at its last step once the enclave has gated. */ +static bool sleep_refuse_after_gate; +module_param(sleep_refuse_after_gate, bool, 0644); +MODULE_PARM_DESC(sleep_refuse_after_gate, + "Test only: refuse the next deep sleep after the enclave gated, to show it comes back"); static void (*sep_pm_event)(bool entering); static int (*sep_pm_sleep)(void); @@ -87,23 +96,49 @@ static int sep_pm_domain(struct device *dev, struct regmap **regmap, u32 *offset } /* - * Returns 0 once the enclave has gated itself, which shows as DEV_DISABLE, and - * -ENOENT if there is no domain to read. The actual state is no guide: a running - * enclave idles at 0 with auto-enable on. + * Returns 0 once the enclave has gated itself, 1 if the deadline expired, or a + * negative observation error (including -ENOENT if there is no domain to read). + * Keep a read's -ETIMEDOUT distinct from the polling deadline. DEV_DISABLE is + * the evidence: a running enclave idles at actual state 0 with auto-enable on. */ int sep_pm_wait_gated(void *ptr, unsigned int timeout_ms) { struct regmap *regmap; u32 offset, val; - int ret; + int ret, poll_ret; + + ret = sep_pm_domain(ptr, ®map, &offset); + if (ret) + return ret; + + poll_ret = read_poll_timeout(regmap_read, ret, ret || (val & SEP_PS_DEV_DISABLE), + 1000, timeout_ms * USEC_PER_MSEC, false, + regmap, offset, &val); + if (ret) + return ret; + + return !!poll_ret; +} + +/* As sep_pm_wait_gated(), until the gated domain also reads off. */ +int sep_pm_wait_off(void *ptr, unsigned int timeout_ms) +{ + struct regmap *regmap; + u32 offset, val; + int ret, poll_ret; ret = sep_pm_domain(ptr, ®map, &offset); if (ret) return ret; - return regmap_read_poll_timeout(regmap, offset, val, - val & SEP_PS_DEV_DISABLE, - 1000, timeout_ms * USEC_PER_MSEC); + poll_ret = read_poll_timeout(regmap_read, ret, + ret || ((val & SEP_PS_DEV_DISABLE) && !(val & SEP_PS_ACTUAL)), + 1000, timeout_ms * USEC_PER_MSEC, false, + regmap, offset, &val); + if (ret) + return ret; + + return !!poll_ret; } int sep_pm_clear_dev_disable(void *ptr) @@ -166,8 +201,8 @@ void sep_pm_dart_restore(void *ptr) /* * The sleep request, sent by the sleep driver as its very last refusal, with interrupts off: - * an enclave that gated doesn't come back with its power kept, so a sleep refused anywhere - * before must find it never told. The driver hands over the request and its answer's match + * an enclave that gated has to be started again, so a sleep refused anywhere before should + * find it never told. The driver hands over the request and its answer's match * at .suspend, with its receive interrupt held. */ static struct sep_pm_commit { @@ -265,6 +300,10 @@ static int sep_pm_commit_tell(void *data) apple_mbox_set_recv_enabled(c->mbox, false); c->gate_ret = readl_poll_timeout_atomic(c->ps, c->state, c->state & SEP_PS_DEV_DISABLE, SEP_PM_POLL_US, SEP_PM_TIMEOUT_US); + if (!c->gate_ret && READ_ONCE(sleep_refuse_after_gate)) { + WRITE_ONCE(sleep_refuse_after_gate, false); + return -EAGAIN; + } return c->gate_ret; } @@ -283,7 +322,9 @@ int sep_pm_commit_prepare(void *ptr, void *mbox, u64 msg0, u32 msg1, u64 reply, u32 offset; int ret; - if (!READ_ONCE(sep_pm_suspending)) + /* Check capability before resources that legacy providers need not have. */ + if (!READ_ONCE(sep_pm_suspending) || !IS_ENABLED(CONFIG_APPLE_T8140_SLEEP) || + !is_kernel_in_hyp_mode()) return -EOPNOTSUPP; if (c->registered) return -EBUSY; @@ -385,6 +426,8 @@ static int __maybe_unused sep_pm_suspend_noirq(struct device *dev) return 0; ret = sep_pm_wait_gated(dev, 5000); + if (ret > 0) + ret = -ETIMEDOUT; if (ret == -ENOENT) return 0; if (ret) { diff --git a/drivers/soc/apple/sep.rs b/drivers/soc/apple/sep.rs index 04f2f7613be6c..623cdd559aefe 100644 --- a/drivers/soc/apple/sep.rs +++ b/drivers/soc/apple/sep.rs @@ -1408,8 +1408,18 @@ impl SepData { return Err(EBUSY); } - if self.sleep_leave_for_commit() { - return Ok(()); + match self.sleep_leave_for_commit() { + Ok(true) => return Ok(()), + Ok(false) => {} + Err(e) => { + dev_err!( + self.dev, + "sleep refused: its last refusal could not be armed ({:?})\n", + e + ); + self.set_sleep_gate(pm::SleepGate::Open); + return Err(e); + } } if let Err(e) = pm::dart_prepare(&self.dev) { @@ -1439,10 +1449,10 @@ impl SepData { } /// Where the sleep driver takes it, the request goes out as its very last refusal, with - /// interrupts off: a gated enclave comes back only through a real sleep, so any earlier - /// refusal has to find it never told. Its answer is polled there, so the receive + /// interrupts off: a gated enclave has to be started again, so any earlier refusal should + /// find it never told. Its answer is polled there, so the receive /// interrupt is held from now on. The gate stays at draining until the wake. - fn sleep_leave_for_commit(&self) -> bool { + fn sleep_leave_for_commit(&self) -> Result { let request = proto::encode_control(&proto::op_sleep(), proto::TAG_SLEEP); let left = match self.mbox.lock().as_ref() { Some(mbox) => pm::leave_for_commit( @@ -1452,13 +1462,13 @@ impl SepData { proto::SLEEP_REPLY, proto::SLEEP_REPLY_MASK, ), - None => false, - }; + None => Err(ENODEV), + }?; if left { self.sleep_hold_rx(); dev_dbg!(self.dev, "the sleep request is left for the sleep's last refusal\n"); } - left + Ok(left) } /// Holds off the receive interrupt, whose handler reads the mailbox. @@ -1481,12 +1491,12 @@ impl SepData { /// Back from a sleep, or a suspend that was aborted. The enclave has kept /// its state and takes no new endpoint setup. /// - /// Reading the mailbox of a gated enclave can stall the CPU for good, so only - /// the power cycle of a real sleep ends one. A sleep refused after the request, - /// which only the request itself can do where the sleep driver sends it, leaves - /// the enclave asleep, and requests fail until the next real sleep or a reboot. - /// If it never gated it was never stopped, and a doorbell would reach an - /// endpoint: ask it something instead. + /// Reading the mailbox of a gated enclave can stall the CPU for good, so it's + /// touched only once the enclave is started again. macOS starts an enclave that + /// took its sleep request the same way whether the system slept or not, so a + /// sleep refused after the enclave gated starts it as a real sleep does, once its + /// domain reads off. If it never gated it was never stopped, and a doorbell would + /// reach an endpoint: ask it something instead. pub(crate) fn sleep_exit(&self) { // Left to the sleep's last refusal: told only if it went out. match pm::commit_finish(&self.dev) { @@ -1509,17 +1519,16 @@ impl SepData { } let answered = if real { self.sleep_kick() - } else if pm::wait_gated(&self.dev) { - self.sleep_hold_rx(); - self.set_sleep_gate(pm::SleepGate::Lost); - dev_warn!( - self.dev, - "the enclave stays asleep until the next real sleep or a reboot\n" - ); - return; } else { - dev_warn!(self.dev, "the enclave never gated its power domain\n"); - self.sleep_probe() + match pm::wait_gated(&self.dev) { + Ok(true) => self.sleep_restart(), + Ok(false) => { + dev_warn!(self.dev, "the enclave did not gate within the timeout\n"); + self.sleep_probe() + } + // Without a domain observation, do not touch the mailbox. + Err(e) => Err(e), + } }; match answered { Ok(()) => { @@ -1528,13 +1537,27 @@ impl SepData { dev_info!(self.dev, "the enclave is back from its sleep request\n"); } Err(e) => { - dev_err!(self.dev, "the enclave did not come back ({:?}); it stays unused\n", e); + dev_err!( + self.dev, + "the enclave did not come back ({:?}); it stays unused until the next real sleep or a reboot\n", + e + ); self.sleep_hold_rx(); self.set_sleep_gate(pm::SleepGate::Lost); } } } + /// Starts an enclave that gated in a sleep that was refused, once its domain is off. + fn sleep_restart(&self) -> Result<()> { + if !pm::wait_off(&self.dev)? { + dev_warn!(self.dev, "the enclave gated, but its domain stayed on\n"); + return Err(ETIMEDOUT); + } + dev_info!(self.dev, "the sleep was refused after the enclave gated: starting it again\n"); + self.sleep_kick() + } + /// Starts a gated enclave and waits for the messages that say it's up. fn sleep_kick(&self) -> Result<()> { pm::clear_dev_disable(&self.dev)?; diff --git a/drivers/soc/apple/shim.h b/drivers/soc/apple/shim.h index e9188184095e9..7cd52a389db8f 100644 --- a/drivers/soc/apple/shim.h +++ b/drivers/soc/apple/shim.h @@ -31,6 +31,7 @@ int sep_pm_register(void *dev, void (*event)(bool entering), int (*sleep)(void), void sep_pm_unregister(void); int sep_pm_keep_domains(void *dev); int sep_pm_wait_gated(void *dev, unsigned int timeout_ms); +int sep_pm_wait_off(void *dev, unsigned int timeout_ms); int sep_pm_clear_dev_disable(void *dev); int sep_pm_dart_prepare(void *dev); void sep_pm_dart_restore(void *dev); diff --git a/drivers/soc/apple/t8140-sleep.c b/drivers/soc/apple/t8140-sleep.c index d885143cad2f4..08eb747718a89 100644 --- a/drivers/soc/apple/t8140-sleep.c +++ b/drivers/soc/apple/t8140-sleep.c @@ -869,7 +869,7 @@ static int t8140_sleep_cpus_hold(struct t8140_sleep *s) return ret; } -/* Last of all the refusals: an enclave that gated comes back only through a real sleep. */ +/* Last of all the refusals: an enclave that gated has to be started again. */ static int t8140_sleep_enclave_tell(struct t8140_sleep *s) { int ret; diff --git a/include/linux/soc/apple/sleep.h b/include/linux/soc/apple/sleep.h index 7f931af1a5e4c..33c6c8274fa31 100644 --- a/include/linux/soc/apple/sleep.h +++ b/include/linux/soc/apple/sleep.h @@ -43,7 +43,7 @@ void apple_sleep_coproc_unregister(struct device *dev); int apple_sleep_coproc_set_last_check(struct device *dev, int (*last_check)(void *data)); /* - * The enclave's sleep request, which it comes back from only through a real sleep: @tell + * The enclave's sleep request, after which it has to be started again: @tell * runs with interrupts off as the very last refusal, after every other one and before any * coprocessor stops. Nonzero refuses the sleep. */ diff --git a/tools/asahi/run-host-tests.py b/tools/asahi/run-host-tests.py index f7188ed814308..4652138620817 100755 --- a/tools/asahi/run-host-tests.py +++ b/tools/asahi/run-host-tests.py @@ -148,6 +148,10 @@ def mutations(flag, names, base, expect=KILL): + [(out('--output') + ['--old-usb'], KILL), (out('--output') + ['--old-regulatory'], KILL)], 'pciec-policy-tests.py': [(out(), PASS)], 'pmgr-bus-tests.py': [([], PASS)], + 'sep-suspend-errors-tests.py': [(out(), PASS)] + + mutations('--mutation', ('arm-fallback', 'read-as-deadline', + 'probe-on-error', 'no-capability-check', 'stay-lost', + 'start-while-on', 'off-ignores-actual'), out()), 'pmp-agx-vote-tests.py': [(out('--output'), PASS), (out('--output') + ['--mutations'], PASS)], 'pmp-j613-hwmon-tests.py': [([], PASS)], 'pmu-rtc-tests.py': [(out(), PASS)] diff --git a/tools/asahi/sep-suspend-errors-tests.py b/tools/asahi/sep-suspend-errors-tests.py new file mode 100644 index 0000000000000..3c6c2274b253e --- /dev/null +++ b/tools/asahi/sep-suspend-errors-tests.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Compile actual SEP suspend/error paths with host-only dependency stubs. + +Extract C prepare/wait/noirq and Rust wrappers/enter/leave/exit/restart verbatim. +Kernel poll macros are also extracted verbatim; virtual time replaces sleeping. +These controls test error classification and AP decisions, not firmware or MMIO +safety against autonomous late gating. No target access or kernel build occurs. +""" +import argparse +from pathlib import Path +import re +import resource +import subprocess + +ROOT = Path(__file__).resolve().parents[2] + + +def function(source, name): + match = re.search(r'^.*\b(?:fn|int(?: __maybe_unused)?) ' + name + r'\([^;]*?\)\s*(?:->[^\{]+)?\{', + source, re.M) + if not match: + raise AssertionError(name) + start = source.index('{', match.start()) + end, depth = start + 1, 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[match.start():end] + + +def macro(source, name): + return re.search(r'^#define ' + name + r'\([^\n]*\n(?:.*\\\n)*.*', + source, re.M).group() + + +C_PREFIX = r''' +#include +#include +#include +#include +#include +#include +typedef uint32_t u32; typedef uint64_t u64; typedef long long ktime_t; +#define BIT(n) (1u << (n)) +#define SEP_PS_DEV_DISABLE BIT(10) +#define GENMASK(h, l) (((~0u) << (l)) & (~0u >> (31 - (h)))) +#define SEP_PS_ACTUAL GENMASK(7, 4) +#define USEC_PER_MSEC 1000 +#define READ_ONCE(x) (x) +#define IS_ENABLED(x) (config_enabled) +#define CONFIG_APPLE_T8140_SLEEP 1 +#define __maybe_unused +#define dev_err(...) ((void)0) +#define dev_dbg(...) ((void)0) +struct device_node { struct device_node *parent; }; +struct device { struct device_node *of_node; }; +struct resource { unsigned long start; }; +struct regmap { int unused; }; +struct apple_mbox_msg { u64 msg0; u32 msg1; }; +struct apple_mbox {}; +static struct device_node parent, node = { &parent }; +static struct device device = { &node }, dart; +static struct regmap regmap; +static bool config_enabled, hyp, sep_pm_suspending, dart_present, map_ok; +static int domain_error, read_error, read_error_after, register_error, gate_after, reads; +static int actual_until; +static int puts, maps, unmaps, registrations, lookups; +static ktime_t now; +static bool (*sep_pm_told)(void); +static bool yes(void) { return true; } +static bool pm_suspend_via_firmware(void) { return true; } +static bool is_kernel_in_hyp_mode(void) { return hyp; } +static int sep_pm_domain(void *p, struct regmap **m, u32 *o) { + lookups++; *m = ®map; *o = 0; return domain_error; +} +static int regmap_read(struct regmap *m, u32 o, u32 *v) { + ++reads; if (read_error && reads >= read_error_after) return read_error; + *v = gate_after && reads >= gate_after ? SEP_PS_DEV_DISABLE : 0; + if (reads < actual_until) *v |= SEP_PS_ACTUAL; + return 0; +} +#define might_sleep_if(x) ((void)0) +#define barrier() ((void)0) +#define cpu_relax() ((void)0) +static ktime_t ktime_get(void) { return now; } +static ktime_t ktime_add_us(ktime_t t, u64 us) { return t + us; } +static int ktime_compare(ktime_t a, ktime_t b) { return (a > b) - (a < b); } +static void usleep_range(unsigned long a, unsigned long b) { now += b; } +static struct device_node *of_parse_phandle(struct device_node *n, const char *p, int i) { + lookups++; return domain_error ? NULL : &node; +} +static int of_property_read_u32(struct device_node *n, const char *p, u32 *v) { *v = 0; return 0; } +static int of_address_to_resource(struct device_node *n, int i, struct resource *r) { r->start = 0; return 0; } +static void of_node_put(struct device_node *n) {} +static unsigned resource_size(struct resource *r) { return 4; } +static struct device *sep_pm_dart(struct device *d) { return dart_present ? &dart : NULL; } +static void *dev_get_drvdata(struct device *d) { return d; } +static void *ioremap(unsigned long start, unsigned size) { ++maps; return map_ok ? ®map : NULL; } +static void iounmap(void *p) { ++unmaps; } +static void put_device(struct device *d) { ++puts; } +static int sep_pm_commit_tell(void *data) { assert(0 && "registration must not send"); return 0; } +static int apple_sleep_enclave_register(struct device *d, int (*tell)(void *), void *data) { + ++registrations; return register_error; +} +static struct sep_pm_commit { + struct device *dev, *dart; struct apple_mbox *mbox; void *ps; + struct apple_mbox_msg request; u64 reply, reply_mask; + bool registered, sent; +} sep_pm_commit; +void fixture(void) { + memset(&sep_pm_commit, 0, sizeof(sep_pm_commit)); + config_enabled = hyp = sep_pm_suspending = dart_present = map_ok = true; + domain_error = read_error = register_error = gate_after = reads = 0; + puts = maps = unmaps = registrations = lookups = 0; now = 0; read_error_after = 1; + actual_until = 0; + sep_pm_told = yes; +} +void parameter(int which, int value) { + switch (which) { + case 0: config_enabled = value; break; case 1: hyp = value; break; + case 2: sep_pm_suspending = value; break; case 3: dart_present = value; break; + case 4: map_ok = value; break; case 5: domain_error = value; break; + case 6: read_error = value; break; case 7: register_error = value; break; + case 8: gate_after = value; break; case 9: sep_pm_commit.registered = value; break; + case 10: read_error_after = value; break; case 11: actual_until = value; break; + } +} +int counter(int which) { + switch (which) { case 0: return puts; case 1: return maps; + case 2: return unmaps; case 3: return registrations; case 4: return lookups; + default: return reads; } +} +void *device_pointer(void) { return &device; } +''' + +RUST_PREFIX = r''' +#![allow(dead_code, unused_imports, unused_variables)] +use std::cell::{Cell, RefCell, RefMut}; +use std::ffi::{c_int, c_void}; +use std::sync::atomic::{AtomicU32, Ordering::Relaxed}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Error(i32); +impl Error { fn from_errno(n: i32) -> Self { assert!(n < 0); Self(n) } } +type Result = std::result::Result; +const EBUSY: Error = Error(-16); const ENODEV: Error = Error(-19); +const ENOMEM: Error = Error(-12); const EIO: Error = Error(-5); +const ENOENT: Error = Error(-2); const ETIMEDOUT: Error = Error(-110); +const EOPNOTSUPP: Error = Error(-95); const ENOSYS: Error = Error(-38); +macro_rules! dev_err { ($($a:tt)*) => {{}}; } +macro_rules! dev_warn { ($($a:tt)*) => {{}}; } +macro_rules! dev_dbg { ($($a:tt)*) => {{}}; } +macro_rules! dev_info { ($($a:tt)*) => {{}}; } +extern "C" { + fn fixture(); fn parameter(which: i32, value: i32); fn counter(which: i32) -> i32; + fn device_pointer() -> *mut c_void; + fn noirq() -> i32; +} +thread_local! { static REAL: Cell = const { Cell::new(false) }; } +mod bindings { pub unsafe fn pm_resume_via_firmware() -> bool { super::REAL.with(|v| v.get()) } } +mod kernel { + pub mod device { + pub struct Device; + impl Device { pub fn as_raw(&self) -> *mut std::ffi::c_void { unsafe { crate::device_pointer() } } } + } + pub mod bindings { #[allow(non_camel_case_types)] pub enum apple_mbox {} } + pub mod soc { pub mod apple { pub mod mailbox { + pub struct Message { pub msg0: u64, pub msg1: u32 } + } } } + pub mod time { + pub struct Delta; impl Delta { pub fn from_millis(_: i64) -> Self { Self } } + pub mod delay { pub fn fsleep(_: super::Delta) {} } + } +} +mod proto { + pub const EP_CONTROL: u32 = 0; pub const CONTROL_TIMEOUT_MS: u32 = 5000; + pub const TAG_SLEEP: u32 = 0; pub const SLEEP_REPLY: u64 = 0; + pub const SLEEP_REPLY_MASK: u64 = 0; + pub fn op_sleep() {} + pub fn encode_control(_: &(), _: u32) -> crate::kernel::soc::apple::mailbox::Message { + crate::kernel::soc::apple::mailbox::Message { msg0: 0, msg1: 0 } + } +} +mod control { + pub fn wait_idle bool, D: FnMut(u32)>(_: u32, _: u32, mut f: F, _: D) -> bool { !f() } +} +struct Lock(RefCell); +impl Lock { fn lock(&self) -> RefMut<'_, T> { self.0.borrow_mut() } } +struct Control; impl Control { fn requests_in_flight(&self) -> bool { false } } +struct Mbox; +impl Mbox { + fn as_raw(&self) -> *mut kernel::bindings::apple_mbox { std::ptr::null_mut() } + fn set_recv_enabled(&self, _: bool) {} +} +const PHASE_READY: u32 = 1; +struct SepData { + dev: kernel::device::Device, phase: AtomicU32, control: Lock, mbox: Lock>, + gate: Cell, held: Cell, probes: Cell, requests: Cell, + kicks: Cell, releases: Cell, finish: Cell>, reply: Cell>, +} +impl SepData { + fn new() -> Self { + Self { dev: kernel::device::Device, phase: AtomicU32::new(PHASE_READY), + control: Lock(RefCell::new(Control)), mbox: Lock(RefCell::new(Some(Mbox))), + gate: Cell::new(pm::SleepGate::Open), held: Cell::new(false), + probes: Cell::new(0), requests: Cell::new(0), kicks: Cell::new(0), releases: Cell::new(0), + finish: Cell::new(None), reply: Cell::new(Ok(())) } + } + fn endpoint_present(&self, _: u32) -> bool { true } + fn sleep_gate(&self) -> pm::SleepGate { self.gate.get() } + fn set_sleep_gate(&self, g: pm::SleepGate) { self.gate.set(g); } + fn sleep_hold_rx(&self) { self.held.set(true); } + fn sleep_release_rx(&self) { self.held.set(false); self.releases.set(self.releases.get()+1); } + fn sleep_request(&self) -> Result<()> { self.requests.set(self.requests.get()+1); Ok(()) } + fn sleep_probe(&self) -> Result<()> { self.probes.set(self.probes.get()+1); self.sleep_release_rx(); self.reply.get() } + fn sleep_kick(&self) -> Result<()> { self.kicks.set(self.kicks.get()+1); self.sleep_release_rx(); self.reply.get() } +} +''' + +RUST_TESTS = r''' +fn reset() { unsafe { fixture() }; REAL.with(|v| v.set(false)); pm::FINISH.with(|v|v.set(None)); } +fn param(n: i32, v: i32) { unsafe { parameter(n, v) } } +fn count(n: i32) -> i32 { unsafe { counter(n) } } +#[test] +fn registration_failures_refuse_unsent() { + for (p,v,e) in [(7,-16,EBUSY),(4,0,ENOMEM),(3,0,ENODEV),(5,-2,ENOENT),(9,1,EBUSY),(7,-19,ENODEV)] { + reset(); param(p,v); let s=SepData::new(); + assert_eq!(s.sleep_enter(),Err(e)); + assert_eq!(s.gate.get(),pm::SleepGate::Open); + assert_eq!(s.requests.get(),0); assert!(!s.held.get()); + assert_eq!(count(0), if p==7 || p==4 {1} else {0}); + assert_eq!(count(2), if p==7 {1} else {0}); + } +} +#[test] +fn explicit_unsupported_keeps_legacy_path() { + for p in [0,1,2] { + reset(); param(p,0); param(5,-2); let s=SepData::new(); + assert_eq!(s.sleep_enter(),Ok(())); assert_eq!(s.requests.get(),1); + assert_eq!(s.gate.get(),pm::SleepGate::Told); assert!(s.held.get()); + assert_eq!(count(4),0); assert_eq!(count(1),0); + } + for e in [-95,-38] { + reset(); param(7,e); let s=SepData::new(); + assert_eq!(s.sleep_enter(),Ok(())); assert_eq!(s.requests.get(),1); + assert_eq!(count(0),1); assert_eq!(count(2),1); + } +} +#[test] +fn successful_registration_defers_and_unsent_refusal_reopens() { + reset(); let s=SepData::new(); assert_eq!(s.sleep_enter(),Ok(())); + assert_eq!(s.requests.get(),0); assert_eq!(s.gate.get(),pm::SleepGate::Draining); + assert!(s.held.get()); assert_eq!(count(3),1); + pm::FINISH.with(|v| v.set(Some(false))); s.sleep_exit(); + assert_eq!(s.gate.get(),pm::SleepGate::Open); assert!(!s.held.get()); + assert_eq!(s.probes.get()+s.kicks.get(),0); +} +#[test] +fn missing_mailbox_is_an_error() { + reset(); let s=SepData::new(); *s.mbox.lock()=None; + assert_eq!(s.sleep_enter(),Err(ENODEV)); assert_eq!(s.requests.get(),0); + assert_eq!(s.gate.get(),pm::SleepGate::Open); +} +#[test] +fn observation_errors_never_probe_or_release_rx() { + for (p,e,at) in [(5,-2,1),(5,-5,1),(6,-5,1),(6,-110,1),(6,-5,3),(6,-110,3)] { + reset(); param(p,e); param(10,at); let s=SepData::new(); + s.gate.set(pm::SleepGate::Told); s.held.set(true); pm::FINISH.with(|v| v.set(Some(true))); + assert_eq!(pm::wait_gated(&s.dev),Err(Error(e))); + s.sleep_exit(); assert_eq!(s.gate.get(),pm::SleepGate::Lost); + assert!(s.held.get()); assert_eq!(s.probes.get()+s.kicks.get()+s.releases.get(),0); + } +} +#[test] +fn deadline_expiry_preserves_existing_probe_outcomes() { + for reply in [Ok(()),Err(EIO)] { + reset(); let s=SepData::new(); s.gate.set(pm::SleepGate::Told); s.held.set(true); s.reply.set(reply); + assert_eq!(pm::wait_gated(&s.dev),Ok(false)); assert!(count(5)>1); + s.sleep_exit(); assert_eq!(s.probes.get(),1); assert_eq!(s.kicks.get(),0); + assert_eq!(s.gate.get(),if reply.is_ok() {pm::SleepGate::Open} else {pm::SleepGate::Lost}); + } +} +#[test] +fn observed_gate_starts_the_enclave_as_a_real_wake_does() { + for reply in [Ok(()),Err(ETIMEDOUT)] { + reset(); param(8,3); let s=SepData::new(); s.gate.set(pm::SleepGate::Told); s.held.set(true); + s.reply.set(reply); pm::FINISH.with(|v| v.set(Some(true))); + s.sleep_exit(); + assert_eq!(s.kicks.get(),1); assert_eq!(s.probes.get(),0); + if reply.is_ok() { + assert_eq!(s.gate.get(),pm::SleepGate::Open); assert!(!s.held.get()); + } else { + assert_eq!(s.gate.get(),pm::SleepGate::Lost); assert!(s.held.get()); + } + } +} +#[test] +fn a_gated_domain_still_on_is_never_started() { + reset(); param(8,3); param(11,i32::MAX); let s=SepData::new(); + s.gate.set(pm::SleepGate::Told); s.held.set(true); + assert_eq!(pm::wait_gated(&s.dev),Ok(true)); + let before=count(5); assert_eq!(pm::wait_off(&s.dev),Ok(false)); assert!(count(5)-before>1); + s.sleep_exit(); assert_eq!(s.gate.get(),pm::SleepGate::Lost); assert!(s.held.get()); + assert_eq!(s.probes.get()+s.kicks.get()+s.releases.get(),0); +} +#[test] +fn the_domain_going_off_late_still_starts_it() { + reset(); param(8,3); param(11,40); let s=SepData::new(); + s.gate.set(pm::SleepGate::Told); s.held.set(true); + s.sleep_exit(); assert_eq!(s.kicks.get(),1); assert_eq!(s.gate.get(),pm::SleepGate::Open); +} +#[test] +fn off_wait_errors_never_start_it() { + for (e,at) in [(-5,4),(-110,4)] { + reset(); param(8,3); param(6,e); param(10,at); let s=SepData::new(); + s.gate.set(pm::SleepGate::Told); s.held.set(true); + s.sleep_exit(); assert_eq!(s.gate.get(),pm::SleepGate::Lost); assert!(s.held.get()); + assert_eq!(s.probes.get()+s.kicks.get()+s.releases.get(),0); + } +} +#[test] +fn the_final_read_after_the_deadline_decides() { + reset(); let s=SepData::new(); assert_eq!(pm::wait_gated(&s.dev),Ok(false)); let n=count(5); + // A gate first seen at the read after the deadline counts. + reset(); param(8,n); assert_eq!(pm::wait_gated(&s.dev),Ok(true)); assert_eq!(count(5),n); + // One read later it was never seen. + reset(); param(8,n+1); assert_eq!(pm::wait_gated(&s.dev),Ok(false)); assert_eq!(count(5),n); + // That last read failing is an error, not a deadline. + for e in [-5,-110] { + reset(); param(6,e); param(10,n); assert_eq!(pm::wait_gated(&s.dev),Err(Error(e))); + } + // The same for the domain going off. + reset(); param(8,1); param(11,n); assert_eq!(pm::wait_off(&s.dev),Ok(true)); assert_eq!(count(5),n); + reset(); param(8,1); param(11,n+1); assert_eq!(pm::wait_off(&s.dev),Ok(false)); + reset(); param(8,1); param(11,i32::MAX); param(6,-5); param(10,n); + assert_eq!(pm::wait_off(&s.dev),Err(EIO)); +} +#[test] +fn real_resume_and_lost_refusal_keep_existing_branches() { + reset(); let s=SepData::new(); s.gate.set(pm::SleepGate::Lost); s.held.set(true); + assert_eq!(s.sleep_enter(),Ok(())); s.sleep_exit(); assert_eq!(s.requests.get()+s.probes.get()+s.kicks.get(),0); + REAL.with(|v| v.set(true)); s.sleep_exit(); assert_eq!(s.kicks.get(),1); + assert_eq!(s.gate.get(),pm::SleepGate::Open); +} +#[test] +fn noirq_preserves_errno_contract_and_legacy_missing_domain() { + for (p,v,expected) in [(8,1,0),(8,0,-110),(5,-2,0),(6,-5,-5),(6,-110,-110)] { + reset(); param(p,v); assert_eq!(unsafe { noirq() },expected); + } +} +''' + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--out', type=Path, required=True) + parser.add_argument('--source', type=Path, default=ROOT) + parser.add_argument('--contracts', type=Path) + parser.add_argument('--mutation', choices=['arm-fallback', 'read-as-deadline', 'probe-on-error', + 'no-capability-check', 'stay-lost', 'start-while-on', + 'off-ignores-actual']) + args = parser.parse_args() + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + args.out.mkdir(parents=True, exist_ok=True) + contracts = args.contracts or args.source + pm = (args.source / 'drivers/soc/apple/pm.rs').read_text() + sep = (args.source / 'drivers/soc/apple/sep.rs').read_text() + shim = (args.source / 'drivers/soc/apple/pm_shim.c').read_text() + if args.mutation == 'arm-fallback': + pm = pm.replace('_ => Err(err),', '_ => Ok(false),') + if args.mutation == 'read-as-deadline': + shim = shim.replace('if (ret)\n\t\treturn ret;\n\n\treturn !!poll_ret;', 'if (ret)\n\t\treturn 1;\n\n\treturn !!poll_ret;') + if args.mutation == 'probe-on-error': + sep = sep.replace('Err(e) => Err(e),', 'Err(_e) => self.sleep_probe(),') + if args.mutation == 'stay-lost': + sep = sep.replace('Ok(true) => self.sleep_restart(),', 'Ok(true) => Err(ETIMEDOUT),') + if args.mutation == 'start-while-on': + sep = sep.replace('if !pm::wait_off(&self.dev)? {', 'if false {') + if args.mutation == 'off-ignores-actual': + shim = shim.replace('((val & SEP_PS_DEV_DISABLE) && !(val & SEP_PS_ACTUAL))', '(val & SEP_PS_DEV_DISABLE)') + if args.mutation == 'no-capability-check': + shim = shim.replace(' || !IS_ENABLED(CONFIG_APPLE_T8140_SLEEP) ||\n\t !is_kernel_in_hyp_mode()', '') + iopoll = (contracts / 'include/linux/iopoll.h').read_text() + regmap = (contracts / 'include/linux/regmap.h').read_text() + c = C_PREFIX + '\n' + '\n'.join([macro(iopoll, 'poll_timeout_us'), macro(iopoll, 'read_poll_timeout'), macro(regmap, 'regmap_read_poll_timeout')]) + c += '\n' + '\n'.join(function(shim, n) for n in ['sep_pm_wait_gated', 'sep_pm_wait_off', 'sep_pm_commit_prepare', 'sep_pm_suspend_noirq']) + c += '\nint noirq(void) { return sep_pm_suspend_noirq(&device); }\n' + cpath, obj = args.out / 'control.c', args.out / 'control.o' + cpath.write_text(c) + subprocess.run(['cc', '-std=gnu11', '-Wall', '-Wextra', '-Wno-unused-parameter', '-c', str(cpath), '-o', str(obj)], check=True) + rust = RUST_PREFIX + '\nmod pm {\nuse crate::*;\nconst GATE_TIMEOUT_MS:u32=5000;\n' + rust += '''extern "C" { + fn sep_pm_wait_gated(dev:*mut c_void, ms:u32)->c_int; + fn sep_pm_wait_off(dev:*mut c_void, ms:u32)->c_int; + fn sep_pm_commit_prepare(dev:*mut c_void, mbox:*mut c_void, a:u64, b:u32, c:u64, d:u64)->c_int; + } + pub fn dart_prepare(_: &kernel::device::Device)->Result<()> { Ok(()) } + pub fn commit_finish(_: &kernel::device::Device)->Option { FINISH.with(|v|v.get()) } + thread_local! { pub static FINISH:Cell>=const { Cell::new(None) }; } +''' + rust += pm[pm.index('#[derive(Clone, Copy, Debug, PartialEq, Eq)]'):pm.index('/// Whether the enclave gated')] + rust += '\n'.join(function(pm, n) for n in ['wait_gated', 'wait_off', 'leave_for_commit']) + '\n}\nimpl SepData {\n' + rust += '\n'.join(function(sep, n) for n in ['sleep_enter', 'sleep_leave_for_commit', 'sleep_exit', 'sleep_restart']) + rust += '\n}\n' + RUST_TESTS + rpath, binary = args.out / 'control.rs', args.out / 'control' + rpath.write_text(rust) + subprocess.run(['rustc', '--edition=2021', '--test', str(rpath), '-C', f'link-arg={obj.resolve()}', '-o', str(binary)], check=True) + result = subprocess.run([str(binary), '--test-threads=1'], check=False) + raise SystemExit(result.returncode) + + +if __name__ == '__main__': + main() From 755cf6e35ff30a949205abef3cf841e6306fca3a Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:56:56 -0500 Subject: [PATCH 3/4] soc: apple: t8140-sleep: refuse a sleep whose commit can't stop the GPU or PMP On T8140 the GPU's two firmware instances and the PMP stop only at the sleep's commit, after the last refusal, and only a real sleep restarts them. Nothing from there on can fail or turn back: their last stops record a failure and the sleep goes on, and the rest of the way to the WFI returns nothing. But where their stop couldn't be left to the commit, both drivers fell back to stopping the firmware at .suspend or .suspend_late. Any refusal after that left GFX and GFX1, or the PMP, stopped until the next real sleep. Refuse the sleep instead, before anything stops, when the sleep's commit stops the registered coprocessors. Elsewhere the early stop is unchanged. Signed-off-by: Diljit Singh --- drivers/gpu/drm/asahi/g17/sleep.c | 6 ++++++ drivers/gpu/drm/asahi/g17/sleep.h | 1 + drivers/gpu/drm/asahi/g17/sleep.rs | 22 +++++++++++++++++----- drivers/soc/apple/pmp-export.c | 5 +++++ drivers/soc/apple/t8140-sleep.c | 6 ++++++ include/linux/soc/apple/sleep.h | 11 +++++++++++ tools/asahi/display26-tests.py | 3 ++- 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/asahi/g17/sleep.c b/drivers/gpu/drm/asahi/g17/sleep.c index 57af9cc3dfef0..6207c99613172 100644 --- a/drivers/gpu/drm/asahi/g17/sleep.c +++ b/drivers/gpu/drm/asahi/g17/sleep.c @@ -325,6 +325,12 @@ struct asahi_g17_commit *asahi_g17_commit_prepare(struct device *dev, return ERR_PTR(ret); } +/* Whether only the sleep's commit may stop the GPU: then nothing else may. */ +bool asahi_g17_commit_required(void) +{ + return apple_sleep_commit_stops_coprocs(); +} + /* * After the sleep, refused or not: whether the commit stopped the GPU, and in @saved the * domain's word from before, if it did. Frees @c. diff --git a/drivers/gpu/drm/asahi/g17/sleep.h b/drivers/gpu/drm/asahi/g17/sleep.h index 21b91dfca0a2e..e4473c73e19d3 100644 --- a/drivers/gpu/drm/asahi/g17/sleep.h +++ b/drivers/gpu/drm/asahi/g17/sleep.h @@ -15,6 +15,7 @@ struct asahi_g17_commit *asahi_g17_commit_prepare(struct device *dev, struct apple_rtkit *secondary_rtk, struct apple_mbox *secondary_mbox, bool bridge); bool asahi_g17_commit_finish(struct asahi_g17_commit *c, u32 *saved); +bool asahi_g17_commit_required(void); int asahi_g17_power_auto_off(struct device *dev, u32 *saved); int asahi_g17_power_lower(struct device *dev); int asahi_g17_power_raise(struct device *dev, u32 saved); diff --git a/drivers/gpu/drm/asahi/g17/sleep.rs b/drivers/gpu/drm/asahi/g17/sleep.rs index 0db5f0a9a3d23..fe5779ee5d5e1 100644 --- a/drivers/gpu/drm/asahi/g17/sleep.rs +++ b/drivers/gpu/drm/asahi/g17/sleep.rs @@ -47,6 +47,7 @@ unsafe extern "C" { bridge: bool, ) -> *mut core::ffi::c_void; fn asahi_g17_commit_finish(commit: *mut core::ffi::c_void, saved: *mut u32) -> bool; + fn asahi_g17_commit_required() -> bool; } /// What `.complete` does after a sleep whose stop was left to the commit. @@ -427,13 +428,24 @@ impl Shared { true } - /// Leaves the stop to the commit. False, with the reason logged, if it can't be. - fn sleep_leave_for_commit(&self, firmware: &mut Firmware) -> bool { + /// Leaves the stop to the commit. False, with the reason logged, if it can't be. Where only + /// the commit may stop it, that refuses the sleep instead: a refusal after an earlier stop + /// would leave it stopped. + fn sleep_leave_for_commit(&self, firmware: &mut Firmware) -> Result { match firmware.leave_for_commit() { Ok(commit) => { self.sleep_commit.store(commit, Ordering::Release); dev_dbg!(self.dev.as_ref(), "GPU firmware left running for the sleep's commit\n"); - true + Ok(true) + } + // SAFETY: Reads the sleep driver's state. + Err(error) if unsafe { asahi_g17_commit_required() } => { + dev_err!( + self.dev.as_ref(), + "no GPU stop at the commit ({:?}): refusing the sleep\n", + error + ); + Err(EBUSY) } Err(error) => { dev_warn!( @@ -441,7 +453,7 @@ impl Shared { "no GPU stop at the commit ({:?}): stopping it now\n", error ); - false + Ok(false) } } } @@ -490,7 +502,7 @@ impl Shared { if !firmware.sleep_quiet(super::now_ns())? { return Err(EBUSY); } - if self.sleep_leave_for_commit(firmware) { + if self.sleep_leave_for_commit(firmware)? { return Ok(()); } down(firmware) diff --git a/drivers/soc/apple/pmp-export.c b/drivers/soc/apple/pmp-export.c index 93a8aa9123757..195452b8f0d02 100644 --- a/drivers/soc/apple/pmp-export.c +++ b/drivers/soc/apple/pmp-export.c @@ -209,6 +209,11 @@ static int apple_pmp_suspend_late(struct device *dev) if (pmp_device != dev || pmp_asleep) return 0; at_commit = apple_pmp_commit_available(dev); + /* Idled any earlier, a refusal after it would leave it stopped. */ + if (!at_commit && apple_sleep_commit_stops_coprocs()) { + dev_err(dev, "no stop at the sleep's commit: refusing the sleep\n"); + return -EBUSY; + } apple_pmp_report_sleep(dev, at_commit); ret = at_commit ? apple_pmp_commit_prepare(dev) : apple_pmp_sleep(pmp_data); if (ret) { diff --git a/drivers/soc/apple/t8140-sleep.c b/drivers/soc/apple/t8140-sleep.c index 08eb747718a89..f151407a3421f 100644 --- a/drivers/soc/apple/t8140-sleep.c +++ b/drivers/soc/apple/t8140-sleep.c @@ -802,6 +802,12 @@ bool apple_sleep_coproc_stopped(struct device *dev) } EXPORT_SYMBOL_GPL(apple_sleep_coproc_stopped); +bool apple_sleep_commit_stops_coprocs(void) +{ + return READ_ONCE(t8140_sleep) && pm_suspend_via_firmware(); +} +EXPORT_SYMBOL_GPL(apple_sleep_commit_stops_coprocs); + /* * The last thing that can fail on the way to the commit: no CPU but this one is up and * interrupts are off, so no lock can be held. A coprocessor stopped here comes back only diff --git a/include/linux/soc/apple/sleep.h b/include/linux/soc/apple/sleep.h index 33c6c8274fa31..2a22638377a11 100644 --- a/include/linux/soc/apple/sleep.h +++ b/include/linux/soc/apple/sleep.h @@ -56,6 +56,12 @@ void apple_sleep_coproc_set_ready(struct device *dev, bool ready); /* Whether the last step stopped it: after a refused sleep, false means it still runs. */ bool apple_sleep_coproc_stopped(struct device *dev); +/* + * Whether this sleep's commit stops the registered coprocessors. Then one that can't be + * registered refuses the sleep: stopped any earlier, a later refusal would strand it. + */ +bool apple_sleep_commit_stops_coprocs(void); + /* * The USB host sleeps in place for this sleep, as macOS's does: its always-on domain stays * up through the commit. Its PHY sets it once the controller is in its hibernation state, @@ -163,6 +169,11 @@ static inline bool apple_sleep_coproc_stopped(struct device *dev) return false; } +static inline bool apple_sleep_commit_stops_coprocs(void) +{ + return false; +} + static inline void apple_sleep_usb_set_asleep(bool asleep) { } diff --git a/tools/asahi/display26-tests.py b/tools/asahi/display26-tests.py index c43323f0d856f..f0c993fe03624 100644 --- a/tools/asahi/display26-tests.py +++ b/tools/asahi/display26-tests.py @@ -113,7 +113,8 @@ def main(): 'static inline int apple_sleep_coproc_register(struct device *d, void __iomem *a, bool p,\n' '\t\t\t\t\t int (*f)(void *), void *x) { assert(!"no system sleep here"); return -EIO; }\n' 'static inline void apple_sleep_coproc_set_ready(struct device *d, bool r) { assert(!"no system sleep here"); }\n' - 'static inline bool apple_sleep_coproc_stopped(struct device *d) { assert(!"no system sleep here"); return false; }\n') + 'static inline bool apple_sleep_coproc_stopped(struct device *d) { assert(!"no system sleep here"); return false; }\n' + 'static inline bool apple_sleep_commit_stops_coprocs(void) { assert(!"no system sleep here"); return false; }\n') write(tmp, 'linux/soc/apple/pmp-display.h', '#include \nbool apple_pmp_display_retained_board(void);\n') write(tmp, 'mock.h', '''#pragma once #include From a078d7772fbd8849e707759765fd82afa23e0dd4 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 08:56:57 -0500 Subject: [PATCH 4/4] phy: apple: atc: power-cycle a USB host that didn't come back, as macOS does A USB host whose core didn't come back from a sleep in place, or whose domains didn't come on again, stayed off until a reboot, and every later deep sleep was refused. Suspend-to-idle still went ahead, and its xHCI suspend reads PORTSC on a core that may be unpowered: an SError. macOS's raise from sleep only logs a missing D0_ON and carries on, and it takes a controller with a hardware error Off, then starts it cold. Linux can't carry on into a core that may be unpowered, so it does the Off and the cold start at once: the core clamped in reset, ATC0_USB, ATC0_COMMON and ATC0_USB_AON off, then on again, with up to three tries for each domain. The power controller comes back reset, as after a sleep that took its power, and the host starts over as it does then. If that fails at the wake, the controller tries once more when the system is back, and again before every later sleep. Only while the core may still be unpowered is a sleep refused, suspend-to-idle too, with a log line that says so. Signed-off-by: Diljit Singh --- drivers/phy/apple/atc.c | 87 +++++++++++++++++++++++++++++++++-- drivers/usb/dwc3/dwc3-apple.c | 51 ++++++++++++++++---- include/linux/phy/apple-atc.h | 11 +++++ 3 files changed, 134 insertions(+), 15 deletions(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 7a5d4f660f0c4..438d2a2d85051 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -913,7 +913,7 @@ struct apple_atcphy { bool sleep_dead; struct device *sleep_darts[ATCPHY_SLEEP_DARTS]; unsigned int sleep_ndarts; - struct device_node *pd_usb, *pd_common; + struct device_node *pd_usb, *pd_common, *pd_aon; u32 *bridge_gate; u32 *bridge_saved; unsigned int bridge_count; @@ -6360,6 +6360,10 @@ static int atcphy_probe_sleep_power(struct apple_atcphy *atcphy) return ret; if (!atcphy->pd_common) return 0; + atcphy->pd_aon = of_parse_phandle(atcphy->pd_common, "power-domains", 0); + ret = devm_add_action_or_reset(dev, atcphy_put_node, atcphy->pd_aon); + if (ret) + return ret; n = of_count_phandle_with_args(ctrl, "iommus", "#iommu-cells"); if (n <= 0 || n > ATCPHY_SLEEP_DARTS) @@ -6810,6 +6814,21 @@ static void atcphy_bridge_gate(struct apple_atcphy *atcphy, bool open) } } +#define ATCPHY_DOMAIN_TRIES 3 + +static int atcphy_sleep_domain_up(struct device_node *pd) +{ + int i, ret; + + for (i = 0; i < ATCPHY_DOMAIN_TRIES; i++) { + ret = apple_sleep_domain_set(pd, true); + if (!ret) + break; + } + + return ret; +} + /* * Back up in macOS's order: ATC0_COMMON, the bridge, ATC0_USB, then the DARTs. -ENODEV if * a domain didn't come on, when nothing in it may be touched. @@ -6820,13 +6839,13 @@ static int atcphy_sleep_power_up(struct apple_atcphy *atcphy) int ret; atcphy->sleep_powered_down = false; - ret = apple_sleep_domain_set(atcphy->pd_common, true); + ret = atcphy_sleep_domain_up(atcphy->pd_common); if (!ret) { atcphy_bridge_gate(atcphy, true); - ret = apple_sleep_domain_set(atcphy->pd_usb, true); + ret = atcphy_sleep_domain_up(atcphy->pd_usb); } if (ret) { - dev_err(atcphy->dev, "USB host's power domains not back on: %d, USB off until a reboot\n", + dev_err(atcphy->dev, "USB host's power domains not back on: %d, USB stays off\n", ret); WRITE_ONCE(atcphy->sleep_dead, true); atcphy->sleep_no_keep = true; @@ -6956,6 +6975,45 @@ static int atcphy_sleep_come_back(struct apple_atcphy *atcphy, bool common_lost, return d0 < 0 ? d0 : ret ?: d0; } +/* + * macOS takes a USB host that fails its raise Off, then starts it cold. The same here, with + * nothing touching the core: clamped in reset, its domains and the always-on one off, then + * on again. The block comes back reset, as after a sleep that took its power, and the host + * starts over. -ENODEV if the core still may be unpowered. + */ +static int atcphy_sleep_recover(struct apple_atcphy *atcphy) +{ + int ret; + + lockdep_assert_held(&atcphy->lock); + if (!atcphy->sleep_power || !atcphy->pd_aon) + return -ENODEV; + dev_warn(atcphy->dev, "USB controller didn't come back: power cycling it, as macOS's Off does\n"); + /* Domains that didn't come on get another try first. */ + if (READ_ONCE(atcphy->sleep_dead)) { + WRITE_ONCE(atcphy->sleep_dead, false); + atcphy->sleep_powered_down = true; + if (atcphy_sleep_power_up(atcphy)) + return -ENODEV; + } + _atcphy_dwc3_reset_assert(atcphy); + atcphy_usb2_sleep_enter(atcphy); + ret = atcphy_sleep_power_down(atcphy); + if (!ret) { + ret = apple_sleep_domain_set(atcphy->pd_aon, false); + ret = atcphy_sleep_domain_up(atcphy->pd_aon) ?: ret; + } + if (ret) + dev_warn(atcphy->dev, "USB power cycle incomplete: %d\n", ret); + ret = atcphy_sleep_come_back(atcphy, true, false); + if (ret < 0) + dev_err(atcphy->dev, "USB controller not back after the power cycle: %d\n", ret); + else + dev_info(atcphy->dev, "USB controller back after a power cycle: the host starts over\n"); + + return ret; +} + static int atcphy_suspend_noirq(struct device *dev) { struct apple_atcphy *atcphy = dev_get_drvdata(dev); @@ -6973,8 +7031,11 @@ static int atcphy_suspend_noirq(struct device *dev) return -EBUSY; } ret = atcphy_controller_hibernate(atcphy); - if (ret) + if (ret) { + if (atcphy->sleep_ret == -ENODEV) + atcphy->sleep_ret = atcphy_sleep_recover(atcphy); return ret; + } atcphy->hibernated = true; ret = atcphy_sleep_power_down(atcphy); if (ret) { @@ -6982,6 +7043,8 @@ static int atcphy_suspend_noirq(struct device *dev) atcphy->sleep_no_keep = true; atcphy->sleep_ret = atcphy_sleep_come_back(atcphy, atcphy->sleep_powered_down, true); + if (atcphy->sleep_ret == -ENODEV) + atcphy->sleep_ret = atcphy_sleep_recover(atcphy); atcphy->hibernated = false; return ret; } @@ -7018,6 +7081,8 @@ static int atcphy_resume_noirq(struct device *dev) apple_sleep_usb_set_asleep(false); /* The PHY took ATC0_COMMON down itself, so this goes for a refused sleep too. */ atcphy->sleep_ret = atcphy_sleep_come_back(atcphy, true, false); + if (atcphy->sleep_ret == -ENODEV) + atcphy->sleep_ret = atcphy_sleep_recover(atcphy); atcphy->hibernated = false; if (!atcphy->sleep_ret) dev_dbg(dev, "USB controller back in D0 after the sleep, always-on side kept, PHY in mode %d\n", @@ -7105,6 +7170,18 @@ int apple_atcphy_sleep_result(struct phy *usb2_phy) } EXPORT_SYMBOL_GPL(apple_atcphy_sleep_result); +int apple_atcphy_sleep_recover(struct phy *usb2_phy) +{ + struct apple_atcphy *atcphy = atcphy_from_usb2_phy(usb2_phy); + + if (!atcphy) + return -EOPNOTSUPP; + guard(mutex)(&atcphy->lock); + + return atcphy_sleep_recover(atcphy); +} +EXPORT_SYMBOL_GPL(apple_atcphy_sleep_recover); + /* * A controller that was restarted after the sleep is set up as at boot and may hibernate * again. Only the PHY's own failures stop it for the later sleeps; a sleep refused for a diff --git a/drivers/usb/dwc3/dwc3-apple.c b/drivers/usb/dwc3/dwc3-apple.c index 5afe5268cdf98..046ef6a2eff0a 100644 --- a/drivers/usb/dwc3/dwc3-apple.c +++ b/drivers/usb/dwc3/dwc3-apple.c @@ -185,7 +185,7 @@ struct dwc3_apple { bool sleep_reinit; /* Looks for the fixed hub once a host restarted after a sleep has had time to find it. */ struct delayed_work hub_check; - /* The core didn't come back from a sleep, and nothing may touch it until a reboot. */ + /* The core didn't come back from a sleep or a power cycle: nothing may touch it. */ bool sleep_dead; /* Stopped for a deep sleep: the state to come back to, and the wakeup setting. */ bool sleep_stopped; @@ -1163,13 +1163,43 @@ static int dwc3_apple_setup_role_switch(struct dwc3_apple *appledwc) return 0; } +/* + * A core that may be unpowered gets one more power cycle, as macOS's Off and cold start, and + * the host starts over once it's back. Until then nothing may touch it, and every sleep is + * refused, as xHCI's suspend would touch it. Returns whether it's back. + */ +static bool dwc3_apple_recover(struct dwc3_apple *appledwc) +{ + int ret; + + lockdep_assert_held(&appledwc->lock); + ret = apple_atcphy_sleep_recover(appledwc->usb2_phy); + if (ret < 0) { + if (!appledwc->sleep_dead) + dev_err(appledwc->dev, + "USB host unpowered after the sleep (%d): USB off, sleeps refused until it's back or a reboot\n", + ret); + appledwc->sleep_dead = true; + return false; + } + if (appledwc->sleep_dead) + dev_info(appledwc->dev, "USB host back after a power cycle: restarting it\n"); + appledwc->sleep_dead = false; + ret = dwc3_apple_exit(appledwc); + if (ret) + dev_err(appledwc->dev, "Failed to stop after the power cycle, err=%d\n", ret); + else + dwc3_apple_start_after_sleep(appledwc, DWC3_APPLE_HOST); + + return true; +} + /* * After a sleep in place: the PHY says whether the core came back to D0, and the GUID register * whether it kept its registers. A core set up again at the resume kept its host and devices, * which reset-resumed. Otherwise restart the host as after a stop, and the devices enumerate - * again. With a core that may be unpowered even that would touch it, so USB stays off, and - * later sleeps are refused, as xHCI's suspend would touch it too. Returns whether the host - * was kept. + * again. A core that may be unpowered gets another power cycle first. Returns whether the + * host was kept. */ static bool dwc3_apple_sleep_kept(struct dwc3_apple *appledwc) { @@ -1180,8 +1210,7 @@ static bool dwc3_apple_sleep_kept(struct dwc3_apple *appledwc) appledwc->sleep_in_place = false; ret = apple_atcphy_sleep_result(appledwc->usb2_phy); if (ret == -ENODEV) { - appledwc->sleep_dead = true; - dev_err(appledwc->dev, "the core didn't come back from the sleep: USB off until a reboot\n"); + dwc3_apple_recover(appledwc); return false; } if (appledwc->sleep_reinit) { @@ -1227,15 +1256,17 @@ static int dwc3_apple_pm_notify_locked(struct dwc3_apple *appledwc, unsigned lon switch (action) { case PM_SUSPEND_PREPARE: + /* Any sleep: xHCI's suspend would touch a core that may be unpowered. */ + if (appledwc->sleep_dead && !appledwc->shutting_down && + !dwc3_apple_recover(appledwc)) { + dev_err(dev, "refusing the sleep: the USB host is still unpowered\n"); + return notifier_from_errno(-EIO); + } if (pm_suspend_default_s2idle() || appledwc->shutting_down) { *finish = in_place; *kept = true; break; } - if (appledwc->sleep_dead) { - dev_err(dev, "refusing the sleep: the core is gone until a reboot\n"); - return notifier_from_errno(-EIO); - } /* xHCI suspends and resumes as usual, and the wakeup setting stays. */ if (in_place) { appledwc->sleep_guid = dwc3_readl(&appledwc->dwc, DWC3_GUID); diff --git a/include/linux/phy/apple-atc.h b/include/linux/phy/apple-atc.h index bc91b4206310f..fe4631bf22b39 100644 --- a/include/linux/phy/apple-atc.h +++ b/include/linux/phy/apple-atc.h @@ -31,6 +31,12 @@ int apple_atcphy_sleep_prepare(struct phy *usb2_phy); */ int apple_atcphy_sleep_result(struct phy *usb2_phy); +/* + * For a controller that may be unpowered after a sleep: power cycles it, as macOS's Off + * and cold start do. APPLE_ATCPHY_SLEEP_POWER_LOST once it's back, reset, or -ENODEV. + */ +int apple_atcphy_sleep_recover(struct phy *usb2_phy); + /* * Ends a sleep that prepare took on, with the SuperSpeed side back as the mux wants it. * Without @kept the host was restarted; only a failure of the PHY's own then makes the @@ -52,6 +58,11 @@ static inline int apple_atcphy_sleep_result(struct phy *usb2_phy) return -EOPNOTSUPP; } +static inline int apple_atcphy_sleep_recover(struct phy *usb2_phy) +{ + return -EOPNOTSUPP; +} + static inline void apple_atcphy_sleep_finish(struct phy *usb2_phy, bool kept) { }