Skip to content

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

Draft
mhellevang wants to merge 8 commits into
mrmidi:mainfrom
mhellevang:scsi/presence-login-target
Draft

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

Conversation

@mhellevang

@mhellevang mhellevang commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What

The HBA-side fix for #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).

Builds on the opt-in gate from #55 (merged). Draft until the hardware validation checklist below is complete — validation hardware is unavailable for a couple of weeks.

Model (pure framework hotplug)

  • UserTargetPresentForID answers false unconditionally — the bring-up scan never creates a target, so a login-driven create can never be duplicated by the scan (the HW-observed v49 duplicate-target wedge is dead by construction; anything short of constant false can still be read true by a late scan).
  • 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). Stop does not wait on this queue — an in-flight create needs the Default queue for its init upcall, so a synchronous Stop would deadlock into the same 60 s panic class; instead a stopping flag gates queued blocks and the queues are released in free().
  • The deferred-INQUIRY hold machinery is removed: the SAM registration probe 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 (an indefinite hold is the SCSI related kernel panic #54 mechanism relocated: a device that vanishes while suspended emits no terminal edge).

Build with --scsi verified (personality present in the artifact); host suite 1254 green (re-run after rebasing onto post-#55 main).

Known accepted gaps

  • 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. Follow-up (not this PR): a suspend timeout (Suspended → Failed after reconnect-hold expiry) so vanished devices reach a terminal edge.

Rebase note — supersedes main's competing #54 fix

Rebased onto main @ a437b36. In the interim main landed its own #54 fix
(c276546 "Stability improvements" + 2a618db "improve driver lifecycle
diagnostics"): a held-INQUIRY approach — target 0 stays present from boot
(UserTargetPresentForID returns true unconditionally), the pre-login probe
INQUIRY is held, and a new 20 s holdTimer + loginWindowExpired
backpressure bounds the hold so it can't cross the 60 s registry busy-timeout.

This PR takes the opposite approach — no target until SBP-2 login
(UserTargetPresentForID returns false pre-login), so the premature probe
never happens. The two designs are mutually exclusive: they set the same
presence bit to opposite values. With presence false, main's held-INQUIRY
machinery (holdTimer, loginWindowExpired, HeldInquiry) is unreachable dead
code, so the rebase drops it. Rationale — the login-edge model:

  • removes the cause (present-but-not-ready target) rather than bounding the
    symptom's duration;
  • kills the v49 duplicate-target teardown wedge by construction (constant
    false can't race the bring-up scan into a duplicate create);
  • handles "login never completes" with no target to strand at all.

Main's 20 s timer was a genuine fallback for the post-create suspended window;
this PR handles that window explicitly and the target only exists while a login
is up, shrinking it. Salvaged from main's commits: the %{public}s logging
hygiene in SBP2TargetBridge. Flagging so the discard of c276546/2a618db's
SCSI work is a visible, reviewable decision — not a silent merge artifact.

Replaces draft mhellevang#1 (was stacked on the #55 branch in the fork; a PR can't be re-targeted across repos).

Update 2026-07-30 — split per review feedback

  • The ROMScanner stale-bus crash (found during HW validation, pre-existing on main) is fixed in its own PR: fix(lifecycle): recreate the ROM scanner across runtime rebuilds instead of reusing it against a freed bus #90. The fix commit is reverted on this branch — HBA and Config ROM work kept separate. Net content = the three login-target commits.
  • Also reverted refactor(scsi): remove unused ASFWSBP2Nub placeholder: on dev the nub is no longer unused (SBP2NubPublisher creates one per discovered SBP-2 unit), so deleting it here would only manufacture an integration conflict.
  • Reverts instead of a history rewrite to keep the branch fast-forward-only; happy to squash on merge.

Working notes — HW validation (author's tracking, not review material)

Everything below is my running validation log so the work can be picked up from any machine. Reviewers can skip it; the review-relevant summary is the sections above.

Validation checklist · 2026-07-29 session findings · remaining work · CI housekeeping

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, no panic — ⚠️ but the destroy leg was never actually exercised: targetAttached stayed false after the failed create (finding 1 below), so the destroy was silently skipped. Re-run after the create fix.
  • 4. Restart with adapter attached + scanner off → clean boot (the original SCSI related kernel panic #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 — OBSERVED on every login edge, but as a systematic collision with the family's own target-0 node, not the termination race (finding 1 below)
  • 8. (optional experiment, non-blocking) Flip UserDoesHBAPerformDeviceManagement to true and re-run scenarios 1, 4, and 6. If the bring-up scan disappears from the log (no UserTargetPresentForID upcalls), true kills the SCSI related kernel panic #54 scan at the root — keep the presence guard regardless as defense in depth. If anything regresses, revert to false: the current value is the HW-validated configuration.

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.

HW validation findings — 2026-07-29 session (FW adapter + Nikon CoolScan 9000, Tahoe)

Tests 1, 2, 3, 5, 6 ran on real hardware; scanning works end-to-end. Three findings:

  1. Create collision (this PR — fix pending). UserCreateTargetForID(0) failed 0xe00002bc on every login edge. With UserDoesHBAPerformDeviceManagement=false the family runs its own bring-up scan and creates its own target-0 node (UserInitializeTargetForID 0 upcall at HBA start), so the login-edge create collides with it. Scanning still works because the family's INQUIRY retry registers that node after login — but the retry window closes ~1 s after boot, so the primary SCSI related kernel panic #54 path (boot without device, power on much later) currently works by timing luck, not by design. Side effect: targetAttached stays false, so our destroy is skipped (hence the test-3 caveat above). Fix direction: treat "already exists" as success, or skip the create when the family node is present — needs an IOSCSIParallelFamily (apple-oss) semantics check of CreateTargetForID on an existing ID first.
  2. Data-out broken after device power-cycle re-login (fix pending). After the scanner is power-cycled and re-logs-in within the same driver lifetime, every data-out command (SET WINDOW 0x24, SEND 0x2a, vendor 0xE0) fails with sense 5/26/00 while data-in (INQUIRY, sense, autofocus) is fine. The log shows only "login up" with no new "session created": the SBP-2 session object is reused against the rebooted device — suspected stale ORB/fetch-agent/page-table state in the session layer. Counter-proof both ways: cold boot with scanner on (fresh session) scans fine, and a bus-reset re-login within the same device power-on also kept data-out working.
  3. Dext SIGSEGV on scanner power-on — pre-existing main bug (stale ROMScanner reused over a freed bus after a runtime rebuild), split out and fixed in fix(lifecycle): recreate the ROM scanner across runtime rebuilds instead of reusing it against a freed bus #90.

Remaining before undraft: fix findings 1 and 2, then tests 4 and 7, plus a re-run of test 3's destroy leg (blocked by finding 1).

CI housekeeping: Homebrew's xcodegen is now 2.46.0 and emits the pbxproj group order differently; this branch and #90 carry a one-line regeneration commit. main needs the same regeneration on its next change or its CI gate will fail there too.

… 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.
…ment

The old comment said "let the SAM manage device objects", but the SAM does
not manage them — the driver does (login-driven create/destroy). The value
only gates family-initiated scanning, never the driver's own
UserCreateTargetForID/UserDestroyTargetForID calls; the HW-validated
lifecycle ran entirely with false. Flipping to true might suppress the
bring-up presence scan at the root (legacy kernel-family semantics), but
the DriverKit shim source is unpublished, so that stays an HW experiment.
@mhellevang
mhellevang force-pushed the scsi/presence-login-target branch from 970c1e7 to a156372 Compare July 21, 2026 08:01
The nub was created unconditionally in StartRuntime() in every build
(SCSI on or off, device present or not), but nothing has ever matched
on it — the HBA co-matches the PCI device directly. Pure registry and
resource waste. Reintroduce with the per-unit personality work if that
lands.
@mhellevang
mhellevang force-pushed the scsi/presence-login-target branch from 9ac6236 to 8dd9480 Compare July 21, 2026 08:18
…st the new bus

ROMScanner, ROMReader and ROMScanSession hold a bare IFireWireBus& fixed at
construction, pointing into the controller-owned FireWireBusImpl.
ServiceContext::Reset destroyed the controller (and with it the bus) but never
dropped deps.romScanner, so EnsureRomScanner reused the stale scanner after
every internal runtime rebuild ("Reusing existing ROMScanner instance") and the
next scan dereferenced freed memory in ROMReader's ReadQuad path (fault offset
0x10 = FireWireBusImpl::async_). Observed as a dext SIGSEGV on real hardware
when scanner power-on triggered a bus reset during a runtime rebuild
(2026-07-29); deterministic on every scan after the first rebuild.

Detach the controller's shared_ptr copy first so ~ROMScanner runs while the bus
is still alive. The destructor only marks the session aborted and queues
weak_ptr cleanup — no bus IO — and every queued FSM entry point checks
aborted_ before touching the bus. EnsureRomScanner then takes its existing
rebuild branch, binding the fresh scanner to the new controller's Bus() and the
new scheduler queue (which was equally stale on reuse).

Pre-existing main bug, surfaced during HW validation of the login-target work;
same class as the IRMClient teardown fix, but the scanner was never rebuilt at
all.
…ds against the new bus"

Moved to its own PR (mrmidi#90) — HBA and Config ROM work kept separate per review.
Keep the nub: on dev it is no longer unused (SBP2NubPublisher creates one per
discovered SBP-2 unit), so deleting it here would only manufacture an
integration conflict.
Homebrew's xcodegen moved to 2.46.0, which emits the ASFW/ASFWDriver group
order differently than the committed project. CI regenerates and diffs the
pbxproj, so every branch fails until the committed file matches the new
generator output. main needs the same regeneration on its next change.
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.

1 participant