Recover Wi-Fi after resume on Apple Silicon Macs - #255
Conversation
The Broadcom firmware on BCM4378/BCM4387 wedges across s2idle: scans fail with -52 and every association is rejected with status_code=16, which NetworkManager surfaces as a wrong password. Toggling the radio does not reset the chip firmware - only a driver reload clears it. Upstream: AsahiLinux/linux#439 Reload brcmfmac from a service ordered after suspend.target, so it runs on resume without delaying it, and only when wifi fails to come back on its own: the repeating status_code=16 rejection never appears on a healthy association, so it serves as the wedge signature, with a 12s timer as backstop. That keeps the service a no-op on machines and kernels where the firmware bug does not bite. Fixes omacom#197 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FFzQTgH8HymEPL6heXWXv5
scottjones
left a comment
There was a problem hiding this comment.
The design here is careful and I want it in: reloading only when the wedge signature actually appears, respecting a deliberately disabled radio, falling back to a trailing log window when the clock steps backwards across resume, and ordering after suspend.target so it never delays a resume. It also stops acting by itself if the firmware bug is ever fixed, which is the right instinct for a workaround.
I verified your PCI gate on hardware you probably do not have. This machine is an M2 Max with BCM4388 (14e4:4434), driving the same brcmfmac_wcc/brcmfmac stack, so it sits just outside 14e4:(4425|4433). I suspended it to check whether that exclusion is right:
Aug 29 13:51:08 PM: suspend entry (s2idle)
Aug 29 13:56:56 PM: suspend exit
Six minutes of s2idle, and afterwards: wlan0 reconnected on its own, zero CTRL-EVENT-ASSOC-REJECT status_code=16, zero brcmfmac scan -52 errors. So BCM4388 does not wedge, and excluding it is correct rather than an oversight. Worth a line in the leaf saying so — this fork already shipped one brcmfmac ID list that omitted BCM4388 by accident, so the next reader will wonder whether this one did too.
(One concern I had and withdrew: grep -c exits 1 with no matches, but the script sets no -e, so wedged() returns false correctly.)
What I would like before merging: tests. A new bin/ command, an install leaf, an all.sh entry and a migration, with no coverage — while every sibling in install/hardware/apple/ has one (asahi-audio-install-test.sh, brcmfmac-supplicant-test.sh). It matters more than usual here because neither the maintainers nor I have hardware that reproduces the bug, so stubs are the only way anyone can check the logic holds.
Two things worth pinning, both stub-testable the way the sibling tests already do it:
- The gate: stub
lspci, assert the service is installed for14e4:4425and14e4:4433and not for14e4:4434, and not on x86 with the same IDs (your comment notes T2 Macs carry them). - The wedge detection: stub
journalctl, assertwedged()is true at twostatus_code=16events and false at one, and that a disabled radio exits without touching the driver.
Three bugs in this repo this week were invisible precisely because nothing exercised the path — a predicate that could never fire, a redirect that silently emptied a variable, a fixture asserting the wrong filesystem layout. This is the same shape of code: a gate and a signature match, both easy to get subtly wrong and impossible to notice without hardware.
Everything else looks right to me, and I will merge as soon as there is coverage.
Migrations run under pipefail, where grep -q exiting at the match kills lspci with SIGPIPE and the failed pipeline reads as "no such hardware". Read all of the output instead, and note that BCM4388 is excluded on purpose: an M2 Max rode out a six-minute s2idle with no rejects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019sJiLMSd7SL39X1ir2SP63
journalctl prints "-- No entries --" to stdout on an empty window, so the empty-output check never fired and the trailing-log fallback was dead code - and reviving it would count stale rejects from the last genuine wedge. A cursor captured at start is immune to the clock stepping backwards across resume and to pre-suspend history alike; a quiet --since window remains as the degraded path when cursor capture fails. Also anchor the status code so 160-169 do not match, and use omarchy-cmd-missing for the dependency guard per repo style. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019sJiLMSd7SL39X1ir2SP63
Stub lspci, uname, systemctl, nmcli, journalctl and modprobe to pin the PCI/architecture gate (BCM4378 and BCM4387 on aarch64 only - BCM4388 and T2 Intel Macs excluded), migration wiring and idempotence, and the recovery command's decisions: signature-confirmed reload, backstop, clock-step immunity, stale-history rejection, and loud failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019sJiLMSd7SL39X1ir2SP63
|
Thanks for the thorough review — and especially for suspending your own M2 Max to verify the BCM4388 exclusion. That's now recorded in the leaf: a comment on the gate says the omission is deliberate and cites your six-minute s2idle with zero rejects, so the next reader won't wonder. Coverage is in (
Writing the stubs flushed out two real bugs, which rather proves your point about this shape of code:
Also anchored the signature so One thing I left as-is, flagged for a deliberate decision rather than sneaking a change in: the backstop still reloads on any resume where Wi-Fi isn't connected within 12s, so a machine with the radio on but deliberately unassociated (out of range, unmanaged) gets a reload plus a failed unit each resume. If you'd rather gate the backstop on some post-resume wpa_supplicant activity, happy to add that — the test scaffolding makes it a small change now. |
scottjones
left a comment
There was a problem hiding this comment.
Tested this on an M2 Max (apple,j414c, t6021) running Asahi. The rework addresses what I raised last time — the journal cursor is a real fix for the clock-stepping problem, not a workaround, and the guards around a disabled radio and missing tools are right.
The test suite is the strongest in this batch. I mutation-tested it — reverting a production behavior and checking the suite goes red:
REJECTS 2 -> 1 caught
remove cursor, always use --since caught
remove the radio-disabled guard caught
skip the reload entirely caught
WAIT_BEFORE 12 -> 0 caught
drop \b from the reject signature NOT caught
Five of six. That is unusual — most suites I run this against catch one or two.
What I validated on hardware
My concern going in was that there is no chip gate: the install leaf puts the unit on every Apple Silicon Mac, and this machine has a BCM4388, which isn't in the BCM4378/BCM4387 set the header names. If M2 resume were slower than the 12s backstop, this would reload the driver after every resume on hardware that never had the bug.
It doesn't. Real suspend, 215 seconds, sampling nmcli every 500ms across the cycle:
resume +0.0s unavailable
+0.5s disconnected
+4.6s connecting
+5.7s connected
Wi-Fi recovered on its own at 5.7s, comfortably inside the 12s backstop, so the script exits "no reload needed" at ~6s. I confirmed that path directly — run in the connected state it returns wifi back after 0s on wlan0 - no reload needed, exit 0.
So the missing chip gate turns out not to matter, and I'd argue it shouldn't be added: gating on observed behavior rather than on a chip allowlist is the better design, and it means the fix reaches BCM4388 machines too if they ever wedge.
Two small things
- The
\binREJECT_SIGNATUREis the one documented behavior with no test — the comment says it keepsstatus_code=160-169from matching, and that mutation was the only one to survive. Cheap to pin. - The 6.3s margin came from one sample on a strong-signal PSK network. A weak-signal or enterprise-auth reconnect could exceed 12s. If Wi-Fi genuinely isn't up at 12s a reload isn't unreasonable, so I don't think this needs changing — worth being aware of.
One thing I could not test
I can't reproduce the wedge on a BCM4388, so what I validated is the absence of a regression on unaffected hardware — not the recovery itself. Nothing on this PR records that the reload actually recovers a wedged BCM4378/4387. If you've confirmed that on your own machine, could you say so on the PR? That's the claim the change rests on, and it would be good to have it written down.
|
Thanks for running the mutation pass and for the M2 Max resume timings — that 5.7s sample is a useful number to have on record next to the 12s backstop. The So that makes six of six. I suspect the mutation run predates 26ef217, which is the commit that added the suite. On the chip gate: the leaf and the migration both still gate on Confirmation that the reload recovers a wedged BCM4387. My machine is a 14-inch MacBook Pro M1 (2021) with a BCM4387 ( Across every boot the unit has logged on this machine:
Every reconnect landed 5–6s after the reload. The one caveat: the copy running on my machine is the pre-cursor revision of the script (it still uses Happy to add a line to the PR description recording the above if you would like it somewhere more durable than a comment. |
scottjones
left a comment
There was a problem hiding this comment.
Approving.
First, a correction: my mutation result was wrong. I reported the \b in REJECT_SIGNATURE as the one surviving mutation, five of six. You are right that it is caught, and I re-ran it on the current head to be sure:
$ sed -i 's/status_code=16\\b/status_code=16/' bin/omarchy-wifi-resume-fix
$ bash test/shell.d/wifi-resume-fix-test.sh
not ok - status_code=160 does not count as status_code=16
Six of six. Your explanation is almost certainly right — I ran that pass against a head that predated the commit adding the suite. That is my error, not a gap in your tests, and it is worth saying plainly because "the one documented behaviour with no test" was the only concrete thing left in my last review.
Thank you for the BCM4387 log. That was the claim the whole change rests on and it had not been written down anywhere:
| Outcome | Count |
|---|---|
| wedge confirmed by signature, reloaded | 8 |
| backstop expired, reloaded | 5 |
| reconnected after reload | 12 |
| Wi-Fi came back on its own, no reload | 4 |
Twelve reconnects, all landing 5-6s after the reload. And flagging yourself that your running copy predates the cursor change, so what hardware validates is the signature match, the reload and the reconnect, while the cursor path is stub-tested only — that is exactly the right way to report it. Please do add it to the PR description; a comment is easy to lose and this is the evidence the change stands on.
Re-verified on the M2 Max. The gate correctly excludes this machine:
01:00.0 Broadcom BCM4388 802.11ax [14e4:4434] <- not in 14e4:(4425|4433)
So the migration exits before touching anything here, which matches what I saw in the first round. 21 assertions pass, omarchy commands --check clean at 457, CI green, no unresolved threads, merges into quattro without conflict.
The migration's guards read well too — the lspci | grep without -q with its comment about SIGPIPE under pipefail (omacom#6608), and the is-enabled check for a machine another user already repaired.
One note for after this lands, not a blocker and not specific to this PR. ExecStart=/usr/bin/omarchy-wifi-resume-fix resolves to the omarchy package, which will not carry the new command until it is rebuilt:
/usr/bin/omarchy-wifi-resume-fix : MISSING
On affected BCM4378/4387 hardware the unit gets written and enabled, and until the package ships the command the service will fail on resume rather than silently skipping — no regression, since those machines were already wedging, but a failed unit is more visible than nothing. #298 and #303 are both in the same position right now, so it is worth someone tracking the package rebuild rather than each PR carrying the concern separately.
Good work on this one, and sorry for the bad mutation report.
Fixes #197.
The Broadcom firmware on Apple Silicon Macs (BCM4378/BCM4387) wedges across s2idle: scans fail with
-52and every association is rejected withstatus_code=16, which NetworkManager surfaces as a wrong password. Toggling the radio doesn't reset the chip firmware — only a driver reload clears it. Upstream: AsahiLinux/linux#439.What this does
bin/omarchy-wifi-resume-fix— waits after resume for Wi-Fi to come back on its own, and reloadsbrcmfmac_wcc/brcmfmaconly when it doesn't. The repeatingASSOC-REJECT status_code=16never appears on a healthy association, so two of them confirm the wedge (~7s after resume, instead of waiting out the timer); a 12s backstop covers anything else. It respects a deliberately disabled radio.install/hardware/apple/fix-wifi-resume.sh— installs and enablesomarchy-wifi-resume-fix.service, gated onaarch64plus PCI ID14e4:4425|4433(the same IDs also appear in T2 Intel Macs, where suspend takes a different path — hence the arch gate). Wired intoinstall/hardware/all.sh.migrations/1787753224.sh— runs the same leaf for existing installs.Design notes
suspend.target, not asystem-sleephook. Sleep hooks run synchronously, so anything that waits in one delays resume for every user.suspend.targetis only reached aftersystemd-suspend.servicereturns, so the service runs on resume without blocking it.Testing
Running on the machine from #197 (MacBook Pro 14" M1 Pro, BCM4387,
linux-asahi 7.1.6.asahi1-1) since 2026-08-23 — 10 resumes over 3 days, every code path exercised, zero manual interventions:status_code=16signature → reloaded ~4–7s after resume, reconnected ~5s laterTypical recovery, unattended:
Also verified:
test/clipasses with the new command's metadata, and the migration no-ops on a machine where the service is already enabled.🤖 Generated with Claude Code
https://claude.ai/code/session_01FFzQTgH8HymEPL6heXWXv5