Skip to content

fix(scsi): create target 0 at SBP-2 login, not at boot — HBA-side fix for mrmidi#54#1

Closed
mhellevang wants to merge 20 commits into
fix/gate-scsi-entitlementfrom
scsi/presence-login-target
Closed

fix(scsi): create target 0 at SBP-2 login, not at boot — HBA-side fix for mrmidi#54#1
mhellevang wants to merge 20 commits into
fix/gate-scsi-entitlementfrom
scsi/presence-login-target

Conversation

@mhellevang

Copy link
Copy Markdown
Owner

What

The HBA-side fix for mrmidi#54: SCSI target 0 is now created at SBP-2 login and destroyed at terminal logout, instead of being auto-created by the boot-time presence scan. This makes --scsi builds cold-boot-safe: a machine booting with no SBP-2 device on the bus has no target whose probe could strand the registry into watchdogd's 60 s busy-timeout panic (IOService.cpp:5986).

Stacked on fix/gate-scsi-entitlement (mrmidi#55). Draft until the hardware validation checklist below is complete; will be re-targeted upstream once mrmidi#55 merges.

Model (pure framework hotplug)

  • UserTargetPresentForID answers false unconditionally — the bring-up scan never creates a target, and (because the scan's timing is unordered against login edges) a login-driven create can never be duplicated by the scan (the HW-observed v49 duplicate-target wedge).
  • UserCreateTargetForID(0) on the login-up edge, UserDestroyTargetForID(0) on terminal logout/login-failure — delivered via the existing SBP2BridgeHub observer; the registry emits terminal edges only, so a transient bus-reset suspension leaves the target alone (reconnect re-asserts login).
  • Lifecycle calls run on a dedicated serial queue: not AuxiliaryQueue (the create call is routed through it — calling from it never dispatches) and not the Default queue (it services the framework's target-init upcalls).
  • The deferred-INQUIRY hold machinery is removed: the SAM registration probe now runs right after the login-edge create, while the session is up, so it returns the device's real identity. In the suspended window, INQUIRY answers BUSY like everything else — nothing is ever held without a deadline.

Adversarial review

Three confirmed blockers in the first draft, fixed in the second commit:

  1. Stop deadlock — Stop DispatchSync'd onto the lifecycle queue from the Default queue while an in-flight create needed the Default queue for its init upcall (three-way cycle → the same 60 s panic class). Stop no longer waits; a stopping flag gates queued blocks, and queues are released in free().
  2. Scan/create duplicate — presence reflecting attached-state could still be read true by a late bring-up scan. Presence is now constant false.
  3. Suspended-window strand — an indefinitely held INQUIRY was the SCSI related kernel panic mrmidi/ASFireWire#54 mechanism relocated (a device that vanishes while suspended emits no terminal edge). The hold is deleted; BUSY gives bounded retries.

Build with --scsi verified (personality present in the artifact); host suite 1243 green.

Hardware validation checklist (blocking — needs FW adapter + scanner)

  • 1. Cold boot, adapter attached, scanner OFF → clean boot (no panic at ~60–80 s), no SCSI target in ioreg, log shows no target until SBP-2 login
  • 2. Power scanner on → login → log shows target 0 created, scanner visible to VueScan, scan completes
  • 3. Scanner off / unplug → logout → log shows target 0 destroyed, no panic
  • 4. Restart with adapter attached + scanner off → clean boot (the original SCSI related kernel panic mrmidi/ASFireWire#54 scenario)
  • 5. Unplug adapter while idle, re-plug → no panic
  • 6. Cold boot with scanner ON → login → create → scan (original happy path)
  • 7. Bus reset mid-scan (hot-plug a second FireWire device) → scan resumes or fails cleanly, no wedge, no panic
  • Watch for: login edge stale (session not ready) on a normal scanner connect — if it appears, the observer edge fires before the registry state is queryable and the ordering needs adjustment
  • Watch for: UserCreateTargetForID(0) failed / UserDestroyTargetForID(0) failed — the create/destroy-vs-termination race is accepted-and-logged; confirm it stays harmless on the unplug paths

Log capture:

log show --last 10m --predicate 'eventMessage CONTAINS "[SCSIHBA]"' --info --debug

For boot scenarios use --boot -1 after the reboot instead of --last 10m.

Known accepted gaps (documented in code)

  • A lifecycle block that passed the stopping check can still be inside a create/destroy kernel call when the framework begins terminating the controller — the framework must tolerate hotplug calls racing termination (call fails, is logged).
  • A device that vanishes while suspended (never reconnects, never reaches a terminal state) leaves an attached target answering BUSY until teardown — cosmetic; a suspend timeout is future work.

Follow-ups (not this PR)

alicankaralar and others added 20 commits July 10, 2026 16:22
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 <noreply@anthropic.com>
… off

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 <noreply@anthropic.com>
…ld-driven power-down

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 <noreply@anthropic.com>
…oin happens after Start returns

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 <noreply@anthropic.com>
…retries

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 <noreply@anthropic.com>
…no queue-blocking sleep

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 <noreply@anthropic.com>
…ement

Fix: Reinitialize Device after Sleep / Subsequent mounts
fix(build): gate SCSI HBA behind opt-in build flag — default build can boot-panic at cold boot
…oker

FW-67: audio: extract clock request broker
…ofile

FW-74: audio: extract duplex stream profile
…nsaction

FW-70: Extract the duplex start transaction runner
… HBA fix)

The HBA answered UserTargetPresentForID(0) = true unconditionally, so
the kernel shim's bring-up presence scan auto-created target 0 on every
boot — device present or not. With no SBP-2 device on the bus, the
probe INQUIRY was then held with no deadline waiting for a login that
never arrives, target-0 registration never completed, and watchdogd
panicked the boot at 60 s (registry busy-timeout, IOService.cpp:5986).

Move the target lifecycle onto the SBP-2 session (framework hotplug
model):

- UserTargetPresentForID answers true only while targetAttached — the
  bring-up scan creates nothing, so a device-less boot has no target
  whose probe could strand the registry.
- The login observer (already delivered via SBP2BridgeHub; the registry
  emits terminal edges only — transient bus-reset suspension emits
  nothing, reconnect re-asserts login) now drives the lifecycle:
  UserCreateTargetForID(0) on login-up, held-INQUIRY flush +
  UserDestroyTargetForID(0) on terminal logout/login-failure.
- Lifecycle calls run on a dedicated serial queue: NOT auxQueue (the
  create call is routed through AuxiliaryQueue by the framework — a
  call from that queue never dispatches; the v49 wedge) and NOT the
  Default queue (it services the framework's target-init upcalls).
- Start catch-up: if the FireWire side is already logged in when the
  HBA starts (service restart), synthesize the up-edge.
- Stop: a stopping flag gates queued lifecycle blocks off
  create/destroy, then a sync barrier drains the queue before the aux
  flush. Explicit destroy in Stop remains forbidden (re-enters the
  framework's own child termination — the documented teardown panic).
- The deferred-INQUIRY machinery is kept for the one window where it
  still applies: a probe against an existing target while the session
  is suspended after a bus reset.

Known limitation: a device that vanishes while suspended (never
reconnects, never reaches a terminal state) leaves a zombie target that
answers BUSY; harmless, cleaned up at teardown. A suspend timeout is
future work.

Builds with --scsi (personality verified in the artifact); host suite
1243 green. HW validation pending — test plan:

1. Cold boot, adapter attached, scanner OFF → expect clean boot, no
   target in ioreg, log shows "no target until SBP-2 login".
2. Power scanner on → login → "target 0 created" in log, scanner
   visible to VueScan, scan works.
3. Scanner off/unplug → logout → "target 0 destroyed", no panic.
4. Full scan, then restart with adapter in + scanner off → clean boot.
5. Unplug adapter mid-idle and re-plug → no panic.
6. Boot with scanner ON → login → create → scan (original happy path).

Log filter:
  log show --last 10m --predicate 'eventMessage CONTAINS "[SCSIHBA]"' --info --debug
Fixes three confirmed findings against the previous commit:

1. Stop deadlock (blocker): Stop ran on the Default queue and
   DispatchSync'd onto lifecycleQueue while an in-flight
   UserCreateTargetForID cannot return until its target-init upcall is
   serviced on that same Default queue — a three-way cycle (Stop →
   lifecycleQueue → kernel create → Default) that wedges termination
   into the same 60 s registry busy-timeout panic this branch fixes.
   Stop no longer waits: queued lifecycle blocks are gated by the
   stopping flag, and queue objects are released in free(), which
   cannot run until every block (each holds a controller retain) has
   finished. This also fixes the failed-Start queue leak (free() now
   releases both queues; Stop is never called after a failed Start).

2. Scan/create duplicate (blocker): the bring-up presence scan is
   queued on the Default queue with no ordering against login edges on
   lifecycleQueue — a login-driven create landing before the scan
   would be duplicated by it (presence read true), the HW-observed v49
   duplicate-target wedge. UserTargetPresentForID now answers false
   UNCONDITIONALLY; all creation is explicit on the login edge.

3. Suspended-window strand (blocker): the deferred-INQUIRY hold had no
   deadline, and a device that vanishes while Suspended emits no
   terminal edge (RefreshTargets skips sessions whose unit no longer
   resolves) — a held completion would never fire, pinning the
   registry: the mrmidi#54 mechanism relocated, not removed. The hold
   machinery is deleted. The SAM registration probe now runs right
   after the login-edge create, while the session is up, so it is
   forwarded to the device and returns its real identity; in the
   suspended window INQUIRY answers BUSY like everything else, so
   bounded initiator retries land after the reconnect or fail cleanly.
   TUR/REQUEST SENSE still answer GOOD.

Also from the review: the up-edge re-checks IsReady() at execution
time (a stale Start catch-up edge can no longer create a target for a
session that has since logged out); the logout path logs destroy
failures honestly; SBP2BridgeHub.hpp's observer comment now states the
under-lock invariant Stop is load-bearing on.

Known accepted race (documented in HandleLoginEdge): a lifecycle block
that passed the stopping check can still be inside a create/destroy
kernel call when the framework begins terminating the controller; the
framework must tolerate hotplug calls racing termination.

Builds with --scsi; host suite 1243 green. HW test plan unchanged
(see previous commit), plus: bus reset mid-scan (plug second FW
device) → scan resumes or fails cleanly, no wedge.
@mhellevang

Copy link
Copy Markdown
Owner Author

Superseded by mrmidi#61

@mhellevang mhellevang closed this Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants