Fix suspend/sleep on T2 MacBooks#5129
Conversation
T2 MacBooks crash during suspend because the hardware doesn't support S3 (deep) sleep, the brcmfmac WiFi driver fails to enter D3 power state, and NVMe/T2 bridge devices crash when entering d3cold during longer sleep periods. This adds three fixes to the T2 Mac installer and a migration for existing installations: - Force s2idle (freeze) as the suspend mode via sleep.conf and kernel cmdline parameter mem_sleep_default=s2idle - Create a systemd service that dynamically disables d3cold on Apple NVMe and T2 Bridge PCI devices before suspend - Unload brcmfmac and apple-bce modules before suspend and reload them on wake to prevent driver crashes The d3cold fix uses dynamic PCI device discovery (vendor 106b, devices 2005/1801) rather than hardcoded bus addresses so it works across all T2 Mac models. Fixes basecamp#1840 Validated on MacBookAir9,1 with linux-t2 6.17.7. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses suspend/resume instability on Apple T2 MacBooks by forcing s2idle-style sleep behavior and adding a systemd hook to adjust PCI power states and reload problematic kernel modules around sleep, plus a migration to apply the fix to existing installs.
Changes:
- Add a migration to apply T2 sleep/suspend fixes on existing systems and remove legacy
suspend-t2.service. - Update the T2 hardware installer script to force
mem_sleep_default=s2idle, create asleep.conf.ddrop-in, and install/enable a newomarchy-suspend-t2.service. - Introduce a suspend hook service that disables d3cold for specific Apple PCI devices and unloads/reloads
brcmfmacandapple-bcearound sleep.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| migrations/1774617744.sh | Adds a T2-only migration to force freeze/s2idle behavior, update Limine cmdline, install/enable the new suspend hook, and remove legacy service. |
| install/config/hardware/fix-apple-t2.sh | Extends the T2 installer setup with s2idle kernel param, sleep.conf drop-in, and installs/enables the new suspend hook service. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| User=root | ||
| Type=oneshot | ||
| RemainAfterExit=yes | ||
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' |
There was a problem hiding this comment.
The unit unloads apple-bce with rmmod -f. Forced module removal can be unsafe and may leave the system without keyboard/trackpad if the module is still in use; it also makes failures harder to diagnose because errors are suppressed. Prefer unloading cleanly (e.g., stopping any dependent services first and using modprobe -r/rmmod without -f), and only proceed if removal succeeds.
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' | |
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; if ! modprobe -r apple-bce 2>/dev/null; then echo "omarchy-suspend-t2: failed to unload apple-bce safely; aborting suspend helper." >&2; exit 1; fi' |
| User=root | ||
| Type=oneshot | ||
| RemainAfterExit=yes | ||
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' |
There was a problem hiding this comment.
T2 detection elsewhere in this script matches 106b:1801 and 106b:1802, but the d3cold-disable logic only targets device 0x1801. If some T2 models expose the bridge device as 0x1802, this service won’t apply the d3cold workaround on those machines. Consider including 0x1802 (or otherwise aligning the device-ID list with the detection logic).
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' | |
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" || "$device" == "0x1802" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' |
| T2_CONF="/etc/limine-entry-tool.d/t2-mac.conf" | ||
| if [[ -f "$T2_CONF" ]] && ! grep -q "mem_sleep_default=s2idle" "$T2_CONF"; then | ||
| sudo sed -i 's/pcie_ports=compat"/pcie_ports=compat mem_sleep_default=s2idle"/' "$T2_CONF" | ||
| fi | ||
|
|
||
| # Create and enable suspend service if not already present | ||
| if [[ ! -f /etc/systemd/system/omarchy-suspend-t2.service ]]; then |
There was a problem hiding this comment.
This only edits /etc/limine-entry-tool.d/t2-mac.conf and only via a fragile string replacement of pcie_ports=compat". On many installs the effective kernel cmdline comes from /etc/default/limine (which can override drop-ins), so this migration may not actually apply mem_sleep_default=s2idle. Update the effective Limine config (e.g., /etc/default/limine when present) and run limine-update (as other migrations do) so the new cmdline takes effect, and append the param more robustly if the expected substring isn’t present.
| T2_CONF="/etc/limine-entry-tool.d/t2-mac.conf" | |
| if [[ -f "$T2_CONF" ]] && ! grep -q "mem_sleep_default=s2idle" "$T2_CONF"; then | |
| sudo sed -i 's/pcie_ports=compat"/pcie_ports=compat mem_sleep_default=s2idle"/' "$T2_CONF" | |
| fi | |
| # Create and enable suspend service if not already present | |
| if [[ ! -f /etc/systemd/system/omarchy-suspend-t2.service ]]; then | |
| add_t2_kernel_param() { | |
| local file="$1" | |
| local param="$2" | |
| [[ -f "$file" ]] || return 0 | |
| if grep -q "$param" "$file"; then | |
| return 0 | |
| fi | |
| # Prefer to add after pcie_ports=compat if present | |
| if grep -q "pcie_ports=compat" "$file"; then | |
| sudo sed -i 's/pcie_ports=compat/& '"$param"'/g' "$file" | |
| else | |
| # Fall back to appending inside common cmdline variables if present | |
| sudo sed -i 's/^\(LINUX_CMDLINE="[^"]*\)"/\1 '"$param"'"/' "$file" 2>/dev/null || true | |
| sudo sed -i 's/^\(CMDLINE="[^"]*\)"/\1 '"$param"'"/' "$file" 2>/dev/null || true | |
| fi | |
| } | |
| T2_CONF="/etc/limine-entry-tool.d/t2-mac.conf" | |
| DEFAULT_LIMINE="/etc/default/limine" | |
| add_t2_kernel_param "$T2_CONF" "mem_sleep_default=s2idle" | |
| add_t2_kernel_param "$DEFAULT_LIMINE" "mem_sleep_default=s2idle" | |
| # Apply Limine configuration if possible | |
| if command -v limine-update >/dev/null 2>&1; then | |
| sudo limine-update | |
| fi | |
| # Create and enable suspend service if not already present | |
| if [[ ! -f /etc/systemd/system/omarchy-suspend-t2.service ]]; then | |
| if [[ ! -f /etc/systemd/system/omarchy-suspend-t2.service ]]; then |
| User=root | ||
| Type=oneshot | ||
| RemainAfterExit=yes | ||
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' |
There was a problem hiding this comment.
As in the installer, the d3cold-disable loop targets only PCI device 0x1801 even though T2 detection for this migration allows 106b:1802. If the bridge device ID is 0x1802 on some machines, the workaround won’t be applied. Align the device list here with the detection logic (or document why 0x1802 should be excluded).
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' | |
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" || "$device" == "0x1802" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' |
| WantedBy=sleep.target | ||
| EOF | ||
|
|
||
| chrootable_systemctl_enable omarchy-suspend-t2.service |
There was a problem hiding this comment.
chrootable_systemctl_enable will run systemctl enable --now on non-chroot installs, which immediately starts omarchy-suspend-t2.service and executes ExecStart (unloading brcmfmac/apple-bce and toggling d3cold) during install. This service should be enabled without starting it immediately (or the helper should be avoided/extended here) so modules aren’t removed right away.
| chrootable_systemctl_enable omarchy-suspend-t2.service | |
| sudo systemctl enable omarchy-suspend-t2.service |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5d933479b4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| WantedBy=sleep.target | ||
| EOF | ||
|
|
||
| chrootable_systemctl_enable omarchy-suspend-t2.service |
There was a problem hiding this comment.
Enable suspend hook without starting it immediately
This call goes through chrootable_systemctl_enable, which runs systemctl enable --now when OMARCHY_CHROOT_INSTALL is not set (install/helpers/chroot.sh:3-7). Starting omarchy-suspend-t2.service immediately executes ExecStart and unloads brcmfmac/apple-bce outside any suspend flow, which can drop Wi‑Fi and internal keyboard/trackpad during install or first boot on T2 systems. This unit should be enabled only and allowed to start via sleep.target.
Useful? React with 👍 / 👎.
| User=root | ||
| Type=oneshot | ||
| RemainAfterExit=yes | ||
| ExecStart=/bin/bash -c 'for dev in /sys/bus/pci/devices/*/; do vendor=$(cat "$dev/vendor" 2>/dev/null); device=$(cat "$dev/device" 2>/dev/null); if [[ "$vendor" == "0x106b" ]] && [[ "$device" == "0x2005" || "$device" == "0x1801" ]]; then echo 0 > "$dev/d3cold_allowed" 2>/dev/null; fi; done; rmmod brcmfmac_wcc 2>/dev/null; rmmod brcmfmac 2>/dev/null; rmmod -f apple-bce 2>/dev/null' |
There was a problem hiding this comment.
Include PCI ID 0x1802 in the d3cold disable filter
The T2 detection logic in this script targets both 106b:1801 and 106b:1802, but the new suspend hook only disables d3cold_allowed for 0x1801 (plus NVMe 0x2005). On machines where the T2 bridge enumerates as 0x1802, the d3cold workaround is skipped, so the suspend/resume crash path this patch is addressing can still reproduce. Please include 0x1802 in this match (and in the duplicated migration unit text).
Useful? React with 👍 / 👎.
- Add PCI device ID 0x1802 to d3cold filter to match all T2 bridge variants (aligns with the T2 detection logic using 106b:180[12]) - Use plain systemctl enable instead of chrootable_systemctl_enable to avoid starting the service during install (which would unload WiFi and keyboard modules mid-install) - Run limine-mkinitcpio in migration so kernel cmdline change takes effect without waiting for a kernel update Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@fedesapuppo could you have a look at this one too? I don't have a T2 Mac to test. |
|
Hey!! Tested on my MacBookAir8,1 with linux-t2 6.18.13 (Jason validated on MacBookAir9,1 / 6.17.7, so different generation as well as kernel). 5 suspend cycles with the PR as-is: 1 success, 4 hard reboots. 🫣 Two failure modes:
RIP: 0010:iowrite32+0xd/0x60
Tried a local variant keeping d3cold + brcmfmac unload/reload and dropping apple-bce handling. Failure #1 went away, but #2 still reproduced, so the s2idle path itself is unreliable here. I couldn't find a reliable solution for this yet. Full logs attached! 📎 omarchy-pr-5129-logs.txt |
Summary
Fixes #1840
T2 MacBooks crash during suspend/resume due to three issues:
brcmfmacWiFi driver fails to enter D3 power state, causing an I/O error loop that retries every ~26 seconds until the system crashesThis PR adds to the T2 Mac installer (
fix-apple-t2.sh) and includes a migration for existing installations:sleep.conf.ddrop-in andmem_sleep_default=s2idlekernel parameteromarchy-suspend-t2.servicethat runsBefore=sleep.targetto:106b:2005) and T2 Bridge (106b:1801) PCI devicesbrcmfmacandapple-bcemodules before suspendExecStop)suspend-t2.serviceThe d3cold fix uses dynamic PCI device discovery rather than hardcoded bus addresses, so it works across all T2 Mac models.
Validated on MacBookAir9,1 with linux-t2 6.17.7 — tested short (2 min) and long (82 min) suspend/resume cycles successfully.
Test plan
🤖 Generated with Claude Code