From ea310f0fa449cf7ad656affac6abd88a9297fe4d Mon Sep 17 00:00:00 2001 From: alicankaralar Date: Sun, 5 Jul 2026 12:26:54 +0200 Subject: [PATCH 1/6] feat(power): rebuild the runtime across system sleep/wake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OHCI controller loses its programmed state when the system sleeps (IntMask, LinkControl, PHY link). The dext never handled power events, so after wake it stayed alive but deaf: no bus-reset interrupts, so a device plugged after any sleep never mounted until the adapter was re-plugged or the machine rebooted (field failure 2026-07-05: Venice invisible after an overnight idle; full recovery only via TB re-enumeration). Override IOService::SetPowerState (matched services are always in the PM tree). Sleep quiesces and resets the runtime while the controller still answers MMIO; wake rebuilds it via the same bring-up as Start — full OHCI re-init ending in a forced bus reset, after which normal discovery re-publishes devices. This mirrors Linux firewire-ohci (pci_suspend = software_reset; pci_resume = the same ohci_enable as cold probe, ohci.c:3762) and Apple IOFireWireController::setPowerState (quiesce + gate on sleep, re-init + UpdateROM + resetBus on wake). Start/Stop bodies are factored into StartRuntime/QuiesceRuntime and shared with the power path; RegisterService() stays once-per-instance. Co-Authored-By: Claude Fable 5 --- ASFWDriver/ASFWDriver.cpp | 72 ++++++++++++++++++++++++++++++++++----- ASFWDriver/ASFWDriver.iig | 16 +++++++++ 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index 59cbfb73..bb0cc8ba 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -211,6 +211,14 @@ kern_return_t IMPL(ASFWDriver, Start) { return kr; if (!ivars || !ivars->context) return kIOReturnNoMemory; + ivars->powerProvider = provider; + return StartRuntime(provider); +} + +kern_return_t ASFWDriver::StartRuntime(IOService* provider) { + if (!ivars || !ivars->context) + return kIOReturnNoMemory; + kern_return_t kr = kIOReturnSuccess; auto& ctx = *ivars->context; ctx.stopping.store(false, std::memory_order_release); DriverWiring::EnsureDeps(this, ctx); @@ -347,11 +355,13 @@ kern_return_t IMPL(ASFWDriver, Start) { const uint32_t initialMask = IntMaskBits::kMasterIntEnable | kBaseIntMask; ctx.deps.hardware->IntMaskSet(initialMask); - // Publish the SBP-2 nub. The SCSI HBA currently co-matches the PCI device - // directly (see Info.plist ASFWSCSIControllerService), so nothing matches on - // this nub yet — it is staged for a future per-unit personality carrying - // login/unit identity, and kept published now to reserve the discovery seam. - { + // Publish once per instance: StartRuntime() is re-entered on wake, and the + // SBP-2 nub and RegisterService() must not repeat across sleep/wake cycles. + if (!ivars->serviceRegistered) { + // Publish the SBP-2 nub. The SCSI HBA currently co-matches the PCI device + // directly (see Info.plist ASFWSCSIControllerService), so nothing matches on + // this nub yet — it is staged for a future per-unit personality carrying + // login/unit identity, and kept published now to reserve the discovery seam. IOService* sbp2NubService = nullptr; kern_return_t nubKr = Create(this, "ASFWSBP2NubProperties", &sbp2NubService); if (nubKr != kIOReturnSuccess || sbp2NubService == nullptr) { @@ -360,15 +370,24 @@ kern_return_t IMPL(ASFWDriver, Start) { // IOKit retains the nub as our child; the nub's Start() calls RegisterService(). sbp2NubService->release(); } - } - RegisterService(); + RegisterService(); + ivars->serviceRegistered = true; + } ASFW_LOG(Controller, "ASFWDriver::Start() complete"); return kIOReturnSuccess; } kern_return_t IMPL(ASFWDriver, Stop) { + QuiesceRuntime(); + if (ivars) { + ivars->powerProvider = nullptr; + } + return Stop(provider, SUPERDISPATCH); +} + +void ASFWDriver::QuiesceRuntime() { if (ivars && ivars->context) { auto& ctx = *ivars->context; ctx.stopping.store(true, std::memory_order_release); @@ -411,7 +430,44 @@ kern_return_t IMPL(ASFWDriver, Stop) { if (ctx.deps.configRomStager && ctx.deps.hardware) ctx.deps.configRomStager->Teardown(*ctx.deps.hardware); } - return Stop(provider, SUPERDISPATCH); +} + +kern_return_t IMPL(ASFWDriver, SetPowerState) { + const bool poweredOn = (powerFlags & kIOServicePowerCapabilityOn) != 0; + ASFW_LOG(Controller, "SetPowerState: powerFlags=0x%08x (%{public}s)", powerFlags, + poweredOn ? "on" : "sleep/low"); + + if (ivars) { + if (!poweredOn) { + // Sleep: quiesce everything and reset the runtime while the + // controller still answers MMIO. The silicon loses its programmed + // state in low power (Linux ohci.c pci_suspend does software_reset; + // Apple gates all hardware access while asleep). + if (!ivars->runtimeSuspended && ivars->context) { + ASFW_LOG(Controller, "SetPowerState: quiescing runtime for sleep"); + QuiesceRuntime(); + ivars->context->Reset(); + ivars->runtimeSuspended = true; + } + } else if (ivars->runtimeSuspended) { + // Wake: rebuild the runtime from scratch — full OHCI re-init ending + // in a forced bus reset, after which normal discovery re-publishes + // devices (Linux pci_resume runs the same ohci_enable as cold probe). + ivars->runtimeSuspended = false; + if (ivars->powerProvider) { + ASFW_LOG(Controller, "SetPowerState: wake - rebuilding runtime"); + const kern_return_t kr = StartRuntime(ivars->powerProvider); + if (kr != kIOReturnSuccess) { + ASFW_LOG(Controller, + "SetPowerState: ❌ wake runtime rebuild failed: 0x%08x", kr); + } + } else { + ASFW_LOG(Controller, "SetPowerState: wake with no provider; skipping rebuild"); + } + } + } + + return SetPowerState(powerFlags, SUPERDISPATCH); } kern_return_t ASFWDriver::CopyControllerStatus(OSDictionary** status) { diff --git a/ASFWDriver/ASFWDriver.iig b/ASFWDriver/ASFWDriver.iig index e0cd9bd4..4427dcdd 100644 --- a/ASFWDriver/ASFWDriver.iig +++ b/ASFWDriver/ASFWDriver.iig @@ -26,6 +26,19 @@ public: kern_return_t Start(IOService* provider) override; kern_return_t Stop(IOService* provider) override; + // System sleep/wake. The OHCI controller loses its programmed state across + // a power transition; without this the driver survives sleep but the + // silicon wakes unprogrammed and deaf (no bus-reset interrupts, so a device + // plugged after wake never mounts). Sleep quiesces and resets the runtime; + // wake rebuilds it (full re-init + forced bus reset), mirroring Linux + // firewire-ohci pci_suspend/pci_resume and Apple IOFireWireController + // setPowerState. + virtual kern_return_t SetPowerState(uint32_t powerFlags) override; + + // Runtime bring-up/quiesce, shared by Start/Stop and SetPowerState. + kern_return_t StartRuntime(IOService* provider) LOCALONLY; + void QuiesceRuntime() LOCALONLY; + kern_return_t CopyControllerStatus(OSDictionary** status) LOCALONLY; kern_return_t CopyControllerSnapshot(OSDictionary** status, uint64_t* sequence, @@ -93,4 +106,7 @@ public: struct ASFWDriver_IVars { ServiceContext* context; + IOService* powerProvider; // borrowed from Start; cleared in Stop + bool runtimeSuspended; // true between sleep quiesce and wake rebuild + bool serviceRegistered; // RegisterService() must run once per instance }; From 276588e3f51a6797bba0b4f5dec2fd3ba9e800b6 Mon Sep 17 00:00:00 2001 From: alicankaralar Date: Sun, 5 Jul 2026 13:52:49 +0200 Subject: [PATCH 2/6] =?UTF-8?q?fix(power):=20pin=20controller=20power=20de?= =?UTF-8?q?sire=20to=20On=20=E2=80=94=20nub=20teardown=20idled=20us=20off?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SetPowerState(0) fired 2ms after the last audio nub terminated (device switched off): with no child expressing a power demand, idle power management powered the controller domain down, the sleep path tore down the runtime, and no SetPowerState(On) ever followed — nothing demanded power again, so the driver was dead until reboot (field trace 2026-07-05 13:41). A bus controller must stay fully powered with no devices attached: plug detection requires a programmed, interrupting controller. Declare the desire with ChangePowerState(kIOServicePowerCapabilityOn) at StartRuntime; capability 0 then only arrives for real system sleep, and wake restores the declared desire via SetPowerState(On). Co-Authored-By: Claude Fable 5 --- ASFWDriver/ASFWDriver.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index bb0cc8ba..aa97f547 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -374,6 +374,17 @@ kern_return_t ASFWDriver::StartRuntime(IOService* provider) { RegisterService(); ivars->serviceRegistered = true; } + + // Pin our power desire to full-on. A bus controller must stay powered even + // with no devices attached (plug detection needs a programmed, interrupting + // controller). Without this, terminating the last audio nub drops the PM + // subtree's demand and the system sends SetPowerState(0) — which tore down + // the runtime and nothing ever demanded power again. With the desire pinned, + // capability 0 arrives only for real system sleep, and wake restores our + // declared desire via SetPowerState(On). + const kern_return_t pmKr = ChangePowerState(kIOServicePowerCapabilityOn); + ASFW_LOG(Controller, "ASFWDriver: ChangePowerState(On) -> 0x%08x", pmKr); + ASFW_LOG(Controller, "ASFWDriver::Start() complete"); return kIOReturnSuccess; From a5a79081da34d9f8bbb1b8747d61f95563177632 Mon Sep 17 00:00:00 2001 From: alicankaralar Date: Sun, 5 Jul 2026 18:32:53 +0200 Subject: [PATCH 3/6] =?UTF-8?q?fix(power):=20SetPowerOverride(true)=20?= =?UTF-8?q?=E2=80=94=20desire=20pin=20alone=20didn't=20stop=20child-driven?= =?UTF-8?q?=20power-down?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HW test showed SetPowerState(0) still arrives 1ms after the last audio nub terminates, even with ChangePowerState(On) pinned: the audio driver matched on our nub is a PM-tree child, and our power state was still governed by its demand. Per IOService.iig, SetPowerOverride makes the state governed solely by our own desire — children can come and go, capability 0 then means real system sleep only. Co-Authored-By: Claude Fable 5 --- ASFWDriver/ASFWDriver.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index aa97f547..9a1ee937 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -377,13 +377,18 @@ kern_return_t ASFWDriver::StartRuntime(IOService* provider) { // Pin our power desire to full-on. A bus controller must stay powered even // with no devices attached (plug detection needs a programmed, interrupting - // controller). Without this, terminating the last audio nub drops the PM - // subtree's demand and the system sends SetPowerState(0) — which tore down - // the runtime and nothing ever demanded power again. With the desire pinned, - // capability 0 arrives only for real system sleep, and wake restores our - // declared desire via SetPowerState(On). + // controller). The audio driver matched on our nub sits in the PM tree as + // our child; when the last nub terminates, the child's power demand vanishes + // and the system sends SetPowerState(0) ~1ms later — which tore down the + // runtime and nothing ever demanded power again. ChangePowerState(On) alone + // did not prevent that (HW-verified 2026-07-05): our state was still governed + // by the children. SetPowerOverride makes it governed solely by our own + // desire, so capability 0 arrives only for real system sleep, and wake + // restores our declared desire via SetPowerState(On). const kern_return_t pmKr = ChangePowerState(kIOServicePowerCapabilityOn); - ASFW_LOG(Controller, "ASFWDriver: ChangePowerState(On) -> 0x%08x", pmKr); + const kern_return_t ovKr = SetPowerOverride(true); + ASFW_LOG(Controller, "ASFWDriver: ChangePowerState(On) -> 0x%08x, SetPowerOverride(true) -> 0x%08x", + pmKr, ovKr); ASFW_LOG(Controller, "ASFWDriver::Start() complete"); From 36cdf7c60cc0d54d922653d36acf20167fb44771 Mon Sep 17 00:00:00 2001 From: alicankaralar Date: Sun, 5 Jul 2026 19:00:28 +0200 Subject: [PATCH 4/6] =?UTF-8?q?fix(power):=20pin=20power=20desire=20from?= =?UTF-8?q?=20SetPowerState(On),=20not=20Start=20=E2=80=94=20PM=20join=20h?= =?UTF-8?q?appens=20after=20Start=20returns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HW test on b10cc02 showed SetPowerOverride(true) returning kIOReturnError (0xe00002bc) and the child-driven power-down still firing. Root cause from xnu IOUserServer.cpp: serviceStarted -> serviceJoinPMTree runs only AFTER the dext's Start() returns, so PM calls made during Start() hit an uninitialized PM object — powerOverrideOnPriv returns IOPMNotYetInitialized, and ChangePowerState_Impl silently discards the same failure (returns success). Move ChangePowerState(On) + SetPowerOverride(true) into the SetPowerState(On) callback, which the kernel delivers right after the PM join. Same log also confirmed the wake-rebuild path works end to end: the domain repowered at 18:54:59, rebuild forced a bus reset, and the device mounted unattended. Co-Authored-By: Claude Fable 5 --- ASFWDriver/ASFWDriver.cpp | 43 +++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index 9a1ee937..c87a8fc3 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -375,20 +375,14 @@ kern_return_t ASFWDriver::StartRuntime(IOService* provider) { ivars->serviceRegistered = true; } - // Pin our power desire to full-on. A bus controller must stay powered even - // with no devices attached (plug detection needs a programmed, interrupting - // controller). The audio driver matched on our nub sits in the PM tree as - // our child; when the last nub terminates, the child's power demand vanishes - // and the system sends SetPowerState(0) ~1ms later — which tore down the - // runtime and nothing ever demanded power again. ChangePowerState(On) alone - // did not prevent that (HW-verified 2026-07-05): our state was still governed - // by the children. SetPowerOverride makes it governed solely by our own - // desire, so capability 0 arrives only for real system sleep, and wake - // restores our declared desire via SetPowerState(On). - const kern_return_t pmKr = ChangePowerState(kIOServicePowerCapabilityOn); - const kern_return_t ovKr = SetPowerOverride(true); - ASFW_LOG(Controller, "ASFWDriver: ChangePowerState(On) -> 0x%08x, SetPowerOverride(true) -> 0x%08x", - pmKr, ovKr); + // NOTE: do NOT call ChangePowerState/SetPowerOverride here. The kernel + // joins a dext into the PM tree only after Start() returns + // (xnu IOUserServer.cpp serviceStarted -> serviceJoinPMTree), so PM calls + // made during Start() are dropped: powerOverrideOnPriv returns + // IOPMNotYetInitialized (surfaced as kIOReturnError) and + // ChangePowerState_Impl silently discards the same failure. The power + // desire is pinned in SetPowerState() on the first On callback instead, + // which the kernel delivers right after the PM join. ASFW_LOG(Controller, "ASFWDriver::Start() complete"); @@ -465,7 +459,26 @@ kern_return_t IMPL(ASFWDriver, SetPowerState) { ivars->context->Reset(); ivars->runtimeSuspended = true; } - } else if (ivars->runtimeSuspended) { + } else { + // Pin our power desire to full-on. A bus controller must stay + // powered even with no devices attached (plug detection needs a + // programmed, interrupting controller). The audio driver matched on + // our nub is a PM-tree child; when the last nub terminates, the + // child's demand vanishes and the system sends SetPowerState(0) + // ~1ms later — which tore down the runtime, leaving the controller + // dead until the PM domain happened to repower minutes later. + // SetPowerOverride makes our power state governed solely by our own + // desire (children ignored), so capability 0 then means real system + // sleep only. These calls only work once the PM join has happened + // (after Start() returns) — this callback is the earliest reliable + // point. Idempotent, so unconditional on every On is fine. + const kern_return_t pmKr = ChangePowerState(kIOServicePowerCapabilityOn); + const kern_return_t ovKr = SetPowerOverride(true); + ASFW_LOG(Controller, + "SetPowerState: pin desire On -> 0x%08x, override -> 0x%08x", + pmKr, ovKr); + } + if (poweredOn && ivars->runtimeSuspended) { // Wake: rebuild the runtime from scratch — full OHCI re-init ending // in a forced bus reset, after which normal discovery re-publishes // devices (Linux pci_resume runs the same ohci_enable as cold probe). From 5d46c8638faac0030cb1200c14bf8e583f984c49 Mon Sep 17 00:00:00 2001 From: alicankaralar Date: Sun, 5 Jul 2026 19:17:03 +0200 Subject: [PATCH 5/6] feat(power): verify wake rebuild took effect; self-heal with bounded retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third HW test: device off/on now survives (SetPowerOverride holds — no power-down on nub termination), but after real system sleep the wake rebuild ran during dark wake (SetPowerState(On) at T+4s, full wake at T+7s) and completed cleanly over MMIO — PHY reads, version register, IBR write all fine — yet the forced bus reset never delivered an interrupt: upstream DMA/MSI state was evidently lost between dark wake and full wake, leaving a deaf controller. Every wake rebuild ends in a forced bus reset, and BusResetCoordinator's resetCount only advances through the full interrupt path, so a completed reset observed shortly after the rebuild proves the controller is alive end to end. VerifyWakeRuntime checks that 3s after wake (plus linkEnable/MMIO readback to classify the failure mode) and rebuilds the runtime again if the check fails, up to 5 attempts. Co-Authored-By: Claude Fable 5 --- ASFWDriver/ASFWDriver.cpp | 68 +++++++++++++++++++++++++++++++++++++++ ASFWDriver/ASFWDriver.iig | 9 ++++++ 2 files changed, 77 insertions(+) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index c87a8fc3..88bc0d20 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -41,6 +41,7 @@ #include "Audio/Core/AudioCoordinator.hpp" #include "Audio/Core/AudioEndpointRuntime.hpp" #include "Audio/Core/AudioRuntimeRegistry.hpp" +#include "Bus/BusResetCoordinator.hpp" #include "Bus/SelfIDCapture.hpp" #include "Common/DriverKitOwnership.hpp" #include "ConfigROM/ConfigROMStager.hpp" @@ -442,6 +443,12 @@ void ASFWDriver::QuiesceRuntime() { } } +// Wake verification cadence. 3s puts the first check well past the dark-wake → +// full-wake transition (~2s observed) while staying invisible to the user; 5 +// attempts bound the self-heal at ~15s. +static constexpr uint64_t kWakeVerifyDelayNs = 3'000'000'000ull; +static constexpr uint64_t kWakeVerifyMaxAttempts = 5; + kern_return_t IMPL(ASFWDriver, SetPowerState) { const bool poweredOn = (powerFlags & kIOServicePowerCapabilityOn) != 0; ASFW_LOG(Controller, "SetPowerState: powerFlags=0x%08x (%{public}s)", powerFlags, @@ -489,6 +496,11 @@ kern_return_t IMPL(ASFWDriver, SetPowerState) { if (kr != kIOReturnSuccess) { ASFW_LOG(Controller, "SetPowerState: ❌ wake runtime rebuild failed: 0x%08x", kr); + } else if (ivars->context && ivars->context->deps.scheduler) { + // The On callback can arrive during dark wake; verify the + // rebuild actually took once the platform has settled. + ivars->context->deps.scheduler->DispatchAsyncAfter( + kWakeVerifyDelayNs, [this] { VerifyWakeRuntime(1); }); } } else { ASFW_LOG(Controller, "SetPowerState: wake with no provider; skipping rebuild"); @@ -499,6 +511,62 @@ kern_return_t IMPL(ASFWDriver, SetPowerState) { return SetPowerState(powerFlags, SUPERDISPATCH); } +void ASFWDriver::VerifyWakeRuntime(uint64_t attempt) { + if (!ivars || !ivars->context || ivars->runtimeSuspended) { + return; // slept again (or tearing down) before the check fired + } + auto& ctx = *ivars->context; + if (ctx.stopping.load(std::memory_order_acquire) || !ctx.deps.hardware || + !ctx.deps.busReset) { + return; + } + + // The wake rebuild always ends in a forced bus reset, and resetCount only + // advances via the full interrupt path (IRQ → Self-ID → coordinator). A + // completed reset therefore proves interrupt delivery end to end. + const uint32_t resets = ctx.deps.busReset->Metrics().resetCount; + const uint32_t hcControl = ctx.deps.hardware->Read(Register32::kHCControl); + const bool mmioAlive = (hcControl != 0xFFFFFFFFu); + const bool linkEnabled = mmioAlive && (hcControl & HCControlBits::kLinkEnable); + + if (resets > 0 && linkEnabled) { + ASFW_LOG(Controller, "Wake verify: ✅ alive (resets=%u HCControl=0x%08x attempt=%llu)", + resets, hcControl, attempt); + return; + } + + // Distinguish the failure mode for the log: busReset pending in IntEvent + // with resetCount==0 means the reset happened but the IRQ never arrived + // (interrupt path dead); linkEnable clear means the controller was reset + // under us after the rebuild; 0xFFFFFFFF means MMIO itself is gone. + const uint32_t intEvent = mmioAlive ? ctx.deps.hardware->Read(Register32::kIntEvent) : 0; + ASFW_LOG(Controller, + "Wake verify: ❌ dead controller (resets=%u HCControl=0x%08x IntEvent=0x%08x " + "attempt=%llu/%llu) - rebuilding", + resets, hcControl, intEvent, attempt, kWakeVerifyMaxAttempts); + + if (attempt >= kWakeVerifyMaxAttempts) { + ASFW_LOG(Controller, "Wake verify: ❌ giving up after %llu attempts", attempt); + return; + } + if (!ivars->powerProvider) { + ASFW_LOG(Controller, "Wake verify: no provider; cannot rebuild"); + return; + } + + QuiesceRuntime(); + ctx.Reset(); + const kern_return_t kr = StartRuntime(ivars->powerProvider); + if (kr != kIOReturnSuccess) { + ASFW_LOG(Controller, "Wake verify: ❌ rebuild failed: 0x%08x", kr); + } + if (ivars->context && ivars->context->deps.scheduler) { + const uint64_t next = attempt + 1; + ivars->context->deps.scheduler->DispatchAsyncAfter( + kWakeVerifyDelayNs, [this, next] { VerifyWakeRuntime(next); }); + } +} + kern_return_t ASFWDriver::CopyControllerStatus(OSDictionary** status) { if (!status) return kIOReturnBadArgument; diff --git a/ASFWDriver/ASFWDriver.iig b/ASFWDriver/ASFWDriver.iig index 4427dcdd..37498268 100644 --- a/ASFWDriver/ASFWDriver.iig +++ b/ASFWDriver/ASFWDriver.iig @@ -39,6 +39,15 @@ public: kern_return_t StartRuntime(IOService* provider) LOCALONLY; void QuiesceRuntime() LOCALONLY; + // Post-wake health check. The SetPowerState(On) callback can arrive during + // dark wake, before the PCIe/Thunderbolt path is fully restored: the rebuild + // then completes cleanly over MMIO but the forced bus reset never delivers + // an interrupt (observed on HW 2026-07-05 — interrupt/bus-mastering state + // lost between dark wake and full wake). Every wake rebuild ends in a forced + // bus reset, so a completed reset must be observed shortly after; if not, + // the runtime is rebuilt again (bounded retries). + void VerifyWakeRuntime(uint64_t attempt) LOCALONLY; + kern_return_t CopyControllerStatus(OSDictionary** status) LOCALONLY; kern_return_t CopyControllerSnapshot(OSDictionary** status, uint64_t* sequence, From 8812ae4f46734025037ad8b55af0de61b4e61f74 Mon Sep 17 00:00:00 2001 From: alicankaralar Date: Fri, 10 Jul 2026 16:41:52 +0200 Subject: [PATCH 6/6] =?UTF-8?q?fix(power):=20wake=20verify=20via=20IOTimer?= =?UTF-8?q?DispatchSource=20=E2=80=94=20no=20this-capture,=20no=20queue-bl?= =?UTF-8?q?ocking=20sleep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on the wake self-heal path: - Both delayed VerifyWakeRuntime dispatches captured `this` in a lambda; a Stop/free before the callback ran would dereference a freed service. - Scheduler::DispatchAsyncAfter implements delays as IOSleep *on* the bound queue, so each 3 s verify attempt froze the driver work queue (up to 15 s over 5 retries), starving the very interrupt path the check verifies. Replace both call sites with ScheduleWakeVerify(attempt): an IOTimerDispatchSource + OSAction targeting WakeVerifyTimerFired, the same pattern as the async watchdog and SBP-2 session timers. The OSAction retains the service, so a pending callback cannot outlive the driver; Stop disables and releases the timer (disable-then-release, no Cancel — see WatchdogCoordinator::Stop) which also breaks the action→service retain cycle. The timer binds to ctx.workQueue, which is the service default queue, so it stays valid across the sleep/wake runtime rebuilds it supervises and serializes with Start/Stop/ SetPowerState. Also document the blocking behavior on Scheduler::DispatchAsyncAfter for the remaining (short-delay) callers. Co-Authored-By: Claude Fable 5 --- ASFWDriver/ASFWDriver.cpp | 72 ++++++++++++++++++++++++++--- ASFWDriver/ASFWDriver.iig | 18 ++++++++ ASFWDriver/Scheduling/Scheduler.hpp | 3 ++ 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/ASFWDriver/ASFWDriver.cpp b/ASFWDriver/ASFWDriver.cpp index 88bc0d20..02293a8f 100644 --- a/ASFWDriver/ASFWDriver.cpp +++ b/ASFWDriver/ASFWDriver.cpp @@ -393,6 +393,18 @@ kern_return_t ASFWDriver::StartRuntime(IOService* provider) { kern_return_t IMPL(ASFWDriver, Stop) { QuiesceRuntime(); if (ivars) { + if (ivars->wakeVerifyTimer) { + // Disable only, then release — Cancel() dispatches async and can + // run after the source is freed (see WatchdogCoordinator::Stop). + // Releasing the action breaks the OSAction→service retain cycle. + ivars->wakeVerifyTimer->SetEnableWithCompletion(false, nullptr); + ivars->wakeVerifyTimer->release(); + ivars->wakeVerifyTimer = nullptr; + } + if (ivars->wakeVerifyAction) { + ivars->wakeVerifyAction->release(); + ivars->wakeVerifyAction = nullptr; + } ivars->powerProvider = nullptr; } return Stop(provider, SUPERDISPATCH); @@ -496,11 +508,10 @@ kern_return_t IMPL(ASFWDriver, SetPowerState) { if (kr != kIOReturnSuccess) { ASFW_LOG(Controller, "SetPowerState: ❌ wake runtime rebuild failed: 0x%08x", kr); - } else if (ivars->context && ivars->context->deps.scheduler) { + } else { // The On callback can arrive during dark wake; verify the // rebuild actually took once the platform has settled. - ivars->context->deps.scheduler->DispatchAsyncAfter( - kWakeVerifyDelayNs, [this] { VerifyWakeRuntime(1); }); + ScheduleWakeVerify(1); } } else { ASFW_LOG(Controller, "SetPowerState: wake with no provider; skipping rebuild"); @@ -560,11 +571,58 @@ void ASFWDriver::VerifyWakeRuntime(uint64_t attempt) { if (kr != kIOReturnSuccess) { ASFW_LOG(Controller, "Wake verify: ❌ rebuild failed: 0x%08x", kr); } - if (ivars->context && ivars->context->deps.scheduler) { - const uint64_t next = attempt + 1; - ivars->context->deps.scheduler->DispatchAsyncAfter( - kWakeVerifyDelayNs, [this, next] { VerifyWakeRuntime(next); }); + ScheduleWakeVerify(attempt + 1); +} + +void ASFWDriver::ScheduleWakeVerify(uint64_t attempt) { + if (!ivars || !ivars->context) { + return; + } + if (!ivars->wakeVerifyTimer) { + // ctx.workQueue is the service's default queue (DriverWiring:: + // PrepareQueue), so the timer stays valid across runtime rebuilds and + // the verify serializes with Start/Stop/SetPowerState. + auto& queue = ivars->context->workQueue; + if (!queue) { + return; + } + IOTimerDispatchSource* timer = nullptr; + kern_return_t kr = IOTimerDispatchSource::Create(queue.get(), &timer); + if (kr != kIOReturnSuccess || !timer) { + ASFW_LOG(Controller, "Wake verify: ❌ timer create failed: 0x%08x", kr); + return; + } + OSAction* action = nullptr; + kr = CreateActionWakeVerifyTimerFired(0, &action); + if (kr != kIOReturnSuccess || !action) { + ASFW_LOG(Controller, "Wake verify: ❌ timer action create failed: 0x%08x", kr); + timer->release(); + return; + } + kr = timer->SetHandler(action); + if (kr != kIOReturnSuccess) { + ASFW_LOG(Controller, "Wake verify: ❌ timer SetHandler failed: 0x%08x", kr); + action->release(); + timer->release(); + return; + } + (void)timer->SetEnableWithCompletion(true, nullptr); + ivars->wakeVerifyTimer = timer; + ivars->wakeVerifyAction = action; + } + + ivars->wakeVerifyAttempt = attempt; + (void)ASFW::Timing::initializeHostTimebase(); + const uint64_t deadline = + mach_absolute_time() + ASFW::Timing::nanosToHostTicks(kWakeVerifyDelayNs); + (void)ivars->wakeVerifyTimer->WakeAtTime(kIOTimerClockMachAbsoluteTime, deadline, 0); +} + +void ASFWDriver::WakeVerifyTimerFired_Impl(ASFWDriver_WakeVerifyTimerFired_Args) { + if (!ivars) { + return; } + VerifyWakeRuntime(ivars->wakeVerifyAttempt); } kern_return_t ASFWDriver::CopyControllerStatus(OSDictionary** status) { diff --git a/ASFWDriver/ASFWDriver.iig b/ASFWDriver/ASFWDriver.iig index 37498268..d08d20cd 100644 --- a/ASFWDriver/ASFWDriver.iig +++ b/ASFWDriver/ASFWDriver.iig @@ -48,6 +48,13 @@ public: // the runtime is rebuilt again (bounded retries). void VerifyWakeRuntime(uint64_t attempt) LOCALONLY; + // Arms the wake-verify timer for `attempt`. A hardware timer keeps the + // settle delay off the dispatch queue — Scheduler::DispatchAsyncAfter + // sleeps *on* the bound queue, which would stall interrupt and discovery + // work for the whole verification window. The timer's OSAction retains + // this service, so a pending callback cannot outlive the driver. + void ScheduleWakeVerify(uint64_t attempt) LOCALONLY; + kern_return_t CopyControllerStatus(OSDictionary** status) LOCALONLY; kern_return_t CopyControllerSnapshot(OSDictionary** status, uint64_t* sequence, @@ -77,6 +84,10 @@ public: uint64_t time) TYPE(IOTimerDispatchSource::TimerOccurred); + virtual void WakeVerifyTimerFired(OSAction* action, + uint64_t time) + TYPE(IOTimerDispatchSource::TimerOccurred); + virtual void ProviderNotificationReady(OSAction* action) TYPE(IOServiceNotificationDispatchSource::ServiceNotificationReady); @@ -118,4 +129,11 @@ struct ASFWDriver_IVars { IOService* powerProvider; // borrowed from Start; cleared in Stop bool runtimeSuspended; // true between sleep quiesce and wake rebuild bool serviceRegistered; // RegisterService() must run once per instance + + // Wake-verify timer. Lives on the default queue (stable for the service + // lifetime, unlike per-rebuild runtime state); released in Stop, which also + // breaks the OSAction→service retain cycle. + IOTimerDispatchSource* wakeVerifyTimer; // retained + OSAction* wakeVerifyAction; // retained + uint64_t wakeVerifyAttempt; // attempt carried to the armed timer }; diff --git a/ASFWDriver/Scheduling/Scheduler.hpp b/ASFWDriver/Scheduling/Scheduler.hpp index aa14ed52..31706659 100644 --- a/ASFWDriver/Scheduling/Scheduler.hpp +++ b/ASFWDriver/Scheduling/Scheduler.hpp @@ -19,6 +19,9 @@ class Scheduler { void Bind(OSSharedPtr queue); void DispatchAsync(const std::function& work); + // WARNING: implemented as IOSleep *on the bound queue* (IODispatchQueue has + // no native delayed dispatch) — the queue is blocked for the whole delay. + // Only use for short delays; long waits need an IOTimerDispatchSource. void DispatchAsyncAfter(uint64_t delayNs, const std::function& work); void DispatchSync(const std::function& work);