Prevent macOS kernel panics when your Mac goes to sleep while a Thunderbolt dock with built-in mass storage is attached.
This is a small, event-driven macOS LaunchDaemon. When you connect a compatible
Thunderbolt dock it switches the AC power profile to sleep 0 / standby 0;
when you disconnect the dock it restores the values you had right before
attaching, unless you changed them yourself while the dock was connected (each
setting is restored independently — your edits are never overwritten).
Detection is event-driven via IOKit notifications: no polling.
On macOS 26A428 (and adjacent builds), this MacBook Pro 14-inch M1 Max panic-crashed on system sleep while an Orico Thunderbolt 3 dock was attached:
IOSCSITargetDevice::setPowerState(1 -> 0) timed out after ~101s @IOServicePM.cpp:5678
The root cause chain: the dock exposes a built-in USB mass storage device through an external (Intel) USB controller. On sleep, macOS's IOKit power cascade times out while putting that device's SCSI target to sleep, and the kernel panics.
pmset disksleep 0 does not help: disksleep only governs disk spin-down while
the system is awake; system sleep performs a separate IOKit power cascade. The
direct workaround is to prevent system sleep while the dock is attached:
pmset -c sleep 0 (and the related standby 0). This daemon does that
automatically, event-driven, and restores your previous settings afterwards.
- Registers
IOServiceAddMatchingNotification(IOServiceFirstMatch) forIOUSBMassStorageInterfaceNub— exactly the node that producesIOSCSITargetDeviceand triggers the panic. - A dock counts as attached when at least one USB Mass Storage service has an
ancestor whose class or registry name contains any pattern from
/usr/local/etc/dock-panic-guard.conf(default:pci8086,15f0— the dock's Intel USB controller — andIOPCI2PCIBridge— the PCI bridge). - Matching is case-insensitive and walks the whole ancestor chain (not just the nearest parent), which tolerates different controller/bridge variants.
- Built-in MacBook USB ports (Apple
AppleT6000USBXHCI) never produce that chain, so a flash drive plugged directly into the Mac is not treated as a dock. - On match, it also registers an interest notification (
kIOGeneralInterest) on the concrete mass storage service. - Disconnection is delivered as
kIOMessageServiceIsTerminatedon the interest notification; the daemon restores the savedpmsetvalues. - Note:
IOServiceTerminatedviaIOServiceAddMatchingNotificationis unsupported on macOS 27 (kIOReturnUnsupported), so detach is handled through the interest notification.
sleepandstandbyof the AC profile are read and written to the state file at dock-attach time, not at daemon start. If you changed settings before attaching, those are the values restored on detach.- Only
sleepandstandbyare set (to0).disksleepand every other AC parameter are left untouched. - On detach, each parameter is restored independently: it is restored to the
saved value only if it is still
0(i.e. the user did not touch it while the dock was attached). If you changed, say,sleepyourself, it stays as you set it; the untouchedstandbyis still restored. - A full AC profile snapshot is written to
/usr/local/var/dock-power-state.acfor diagnostics only (never read by the daemon; safe to delete — it is recreated on the next attach). - State file:
/usr/local/var/dock-power-state.state(dock,saved_sleep,saved_standby).
- Dock not attached → nothing to do; the daemon just waits for events.
- Dock attached:
- if current
sleep/standbyare already0and saved values exist → "continuation" (the Mac shut down/rebooted with the dock attached) → nothing is saved or changed; - otherwise → "fresh" start: save current
sleep/standby, then apply0/0.
- if current
This avoids relying on whether the daemon wrote state before shutdown (crash/power loss): the decision is based on "values already 0/0 + saved values present".
| File | Purpose |
|---|---|
dock-panic-guard.c |
Daemon source (C, IOKit + CoreFoundation) |
dock-panic-guard.conf |
Pattern config template (installed to /usr/local/etc/, only if absent) |
com.local.dock-panic-guard.plist |
Root LaunchDaemon: RunAtLoad + KeepAlive |
install.sh |
Install (sudo): build, snapshot AC values, install binary + service |
uninstall.sh |
Rollback (sudo): stop service, remove files, restore pmset from snapshot |
README.md |
This document |
LICENSE |
MIT |
The binary is not committed; install.sh builds it locally.
git clone <your-repo-url> dock-panic-guard
cd dock-panic-guard
sudo bash install.shService label: system/com.local.dock-panic-guard
File: /usr/local/etc/dock-panic-guard.conf.
One line — one case-insensitive substring, matched in the class or registry
name of any node of the Mass Storage ancestor chain. Blank lines and lines
starting with # are ignored.
# Example: Orico TB3 Dock
pci8086,15f0
IOPCI2PCIBridge
If the file is missing or empty, built-in values are used:
pci8086,15f0
IOPCI2PCIBridge
install.sh installs the default config only if it does not exist — your
edits are never overwritten.
- Connect the dock.
- Inspect the Mass Storage chain:
ioreg -r -c IOUSBMassStorageInterfaceNub -lor (with ancestors):
ioreg -r -l | grep -B2 -A20 'IOUSBMassStorageInterfaceNub'- Look for a node that is characteristic of the dock: USB controller or PCI
bridge names, a vendor/device ID such as
pci8086,15f0, the dock chip model. - Add one substring per line. Multiple patterns are allowed — any match triggers.
Matching is done against any ancestor, so generic strings (Intel, USB,
Hub) cause false positives on ordinary USB devices and enable the protection
without a real dock. Use specific identifiers.
sudo launchctl kickstart -k system/com.local.dock-panic-guard
log show --predicate 'eventMessage CONTAINS "dock-panic-guard"' --last 5mThe log shows how many patterns were loaded and attach/detach events.
# Service status
sudo launchctl print system/com.local.dock-panic-guard | head
# While the dock is attached, AC must show sleep 0 / standby 0
pmset -g custom
# Logs
log show --predicate 'eventMessage CONTAINS "dock-panic-guard"' --last 1hObserved log from a real attach/detach cycle:
dock attached: saved sleep=1 standby=1
pmset -c sleep 0 -> rc=0
pmset -c standby 0 -> rc=0
...
dock detached: saved sleep=1 standby=1, current sleep=0 standby=0, restoreSleep=1 restoreStandby=1
pmset -c sleep 1 -> rc=0
pmset -c standby 1 -> rc=0
Per-parameter case (user edited sleep to 5 while the dock was attached):
dock detached: saved sleep=1 standby=1, current sleep=5 standby=0, restoreSleep=0 restoreStandby=1
sleep: left as-is (user changed or no saved value)
pmset -c standby 1 -> rc=0
sudo bash uninstall.sh- Stop and unload the service.
- If the daemon had applied AC values (dock was attached), restore
sleepandstandbyfrom/usr/local/var/dock-power-state.before(disksleepis untouched). - Remove the plist, binary, log, state file and config.
- The
dock-power-state.beforesnapshot is kept — delete it manually if you want.
- AC profile only (
-c); the battery profile is never touched. - Running on battery with the dock attached: the panic may still occur on battery sleep, since the daemon does not modify the battery profile (out of scope by design).
- Closing the lid (clamshell) initiates sleep even with
sleep 0; if the panic reproduces there, it requires a different approach. - This is a workaround, not a kernel fix — please also file a report with Apple (Feedback Assistant).
- Machine: MacBook Pro (14-inch, 2021), Apple M1 Max, 64 GB —
MacBookPro18,4 - OS: macOS 27.0 (build 26A428)
- Dock: Orico Thunderbolt 3 dock (Intel JHL8440, "Tamales Module 2"),
exposing:
- an external Thunderbolt switch (
IOThunderboltSwitchType3), - an internal Intel USB controller (PCI ID
pci8086,15f0), - a built-in USB mass storage device.
- an external Thunderbolt switch (
- Verified behavior:
- panic reproduced on system sleep with the dock attached;
- attach →
sleep 0/standby 0; detach → values restored; - reboot with the dock already attached → daemon starts automatically, sees the already-protected values and does not re-apply;
- user edited
sleepwhile attached → the edit is preserved, the untouchedstandbyis still restored; - a Thunderbolt Ethernet dock without built-in storage did not trigger the panic and is correctly ignored by the daemon.
MIT — see LICENSE.