diff --git a/install/hardware/all.sh b/install/hardware/all.sh index 9a9f1098..6adcff9c 100644 --- a/install/hardware/all.sh +++ b/install/hardware/all.sh @@ -35,6 +35,7 @@ run_logged "$OMARCHY_INSTALL/hardware/framework/qmk-hid.sh" run_logged "$OMARCHY_INSTALL/hardware/apple/fix-spi-keyboard.sh" run_logged "$OMARCHY_INSTALL/hardware/apple/fix-suspend-nvme.sh" run_logged "$OMARCHY_INSTALL/hardware/apple/fix-t2.sh" +run_logged "$OMARCHY_INSTALL/hardware/apple/fix-brcmfmac-supplicant.sh" run_logged "$OMARCHY_INSTALL/hardware/lenovo/fix-yoga-pro7-bass-speakers.sh" diff --git a/install/hardware/apple/fix-brcmfmac-supplicant.sh b/install/hardware/apple/fix-brcmfmac-supplicant.sh new file mode 100644 index 00000000..33238c9c --- /dev/null +++ b/install/hardware/apple/fix-brcmfmac-supplicant.sh @@ -0,0 +1,35 @@ +# Apple Macs ship Broadcom Wi-Fi driven by brcmfmac, whose firmware runs the WPA +# handshake itself. On these parts that offload fails against an access point in +# WPA2/WPA3 transition mode: the client associates, the four-way handshake never +# completes, and NetworkManager reports the password as wrong. +# +# feature_disable turns off the firmware supplicant (FWSUP, 0x2000) and firmware +# authenticator (FWAUTH, 0x80000), handing the handshake back to wpa_supplicant +# in software. +# +# This quirk already shipped for T2 Macs, detected by the T2 PCI ID that every +# one of them carries. The bug is in the Broadcom firmware, not in the T2 bridge, +# so every Mac whose Wi-Fi brcmfmac drives needs it: a MacBookPro11,4 with +# BCM43602 and 2015 firmware fails exactly this way, and connects once the +# offload is disabled. +# +# The IDs are brcmfmac's own, from brcm_hw_ids.h: BCM43602 and its single-band +# variants in 2015-2017 Macs, BCM4350, BCM4355 and BCM4364 in the 2018-2019 +# machines including the T2-less iMac19,1 and iMac19,2, and BCM4377/4378/4387 +# from the T2 era on. The BCM4360 in 2013-2015 Macs is deliberately absent: it +# runs the out-of-tree wl driver, which never reads a brcmfmac option. +sys_vendor="$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || true)" + +if lspci -nn | grep "106b:180[12]" >/dev/null || + { [[ $sys_vendor == Apple* ]] && + lspci -nn | grep -E "14e4:(43ba|43bb|43bc|43a3|43dc|4464|4488|4425|4433)" >/dev/null; }; then + echo "Detected a Mac with Broadcom Wi-Fi; running the WPA handshake in software" + + mkdir -p /etc/modprobe.d + cat > /etc/modprobe.d/brcmfmac.conf <<'EOF' +# Broadcom's firmware supplicant and authenticator fail the WPA four-way +# handshake on Apple hardware, which surfaces as a rejected password. Disable +# both so wpa_supplicant performs the handshake instead. +options brcmfmac feature_disable=0x82000 +EOF +fi diff --git a/install/hardware/apple/fix-t2.sh b/install/hardware/apple/fix-t2.sh index 7936a794..948828d4 100644 --- a/install/hardware/apple/fix-t2.sh +++ b/install/hardware/apple/fix-t2.sh @@ -26,12 +26,6 @@ if lspci -nn | grep "106b:180[12]" >/dev/null; then echo "MODULES+=(t2bce_vhci usbhid hid_apple hid_generic xhci_pci xhci_hcd)" > \ /etc/mkinitcpio.conf.d/apple-t2.conf - mkdir -p /etc/modprobe.d - cat > /etc/modprobe.d/brcmfmac.conf <<'EOF' -# Fix for T2 MacBook WiFi connectivity issues -options brcmfmac feature_disable=0x82000 -EOF - mkdir -p /etc/limine-entry-tool.d cat > /etc/limine-entry-tool.d/t2-mac.conf <<'EOF' # Generated by Omarchy installer for T2 Mac support diff --git a/migrations/1786391100.sh b/migrations/1786391100.sh new file mode 100644 index 00000000..b67ecf82 --- /dev/null +++ b/migrations/1786391100.sh @@ -0,0 +1,43 @@ +echo "Run the WPA handshake in software on Macs with Broadcom Wi-Fi" + +# The install-time quirk only reaches machines set up after it shipped, and it +# never covered Macs without a T2 at all, so an existing install on one still +# cannot join a WPA2/WPA3 transition-mode network. See +# install/hardware/apple/fix-brcmfmac-supplicant.sh for the failure it fixes and +# for where this list of brcmfmac PCI IDs comes from. +dmi_vendor="${OMARCHY_BRCMFMAC_DMI_VENDOR:-/sys/class/dmi/id/sys_vendor}" +conf="${OMARCHY_BRCMFMAC_CONF:-/etc/modprobe.d/brcmfmac.conf}" + +sys_vendor="$(cat "$dmi_vendor" 2>/dev/null || true)" + +if ! lspci -nn | grep "106b:180[12]" >/dev/null && + ! { [[ $sys_vendor == Apple* ]] && + lspci -nn | grep -E "14e4:(43ba|43bb|43bc|43a3|43dc|4464|4488|4425|4433)" >/dev/null; }; then + exit 0 +fi + +# T2 installs already carry this from the installer, so the common case is a +# no-op for the first user and every user after them. Only an active options +# line counts: someone who commented theirs out still needs this. +if [[ -f $conf ]] && + grep -Eq '^[[:space:]]*options[[:space:]]+brcmfmac[[:space:]].*feature_disable=0x82000' "$conf"; then + exit 0 +fi + +sudo mkdir -p "$(dirname "$conf")" + +# Append rather than overwrite, so anything else a user keeps here survives: +# modprobe reads every options line for a module, and nothing else sets +# feature_disable. The leading newline also covers a file that ends without one. +sudo tee -a "$conf" >/dev/null <<'EOF' + +# Broadcom's firmware supplicant and authenticator fail the WPA four-way +# handshake on Apple hardware, which surfaces as a rejected password. Disable +# both so wpa_supplicant performs the handshake instead. +options brcmfmac feature_disable=0x82000 +EOF + +# modprobe only reads this when the module loads. Reloading brcmfmac here would +# drop a Wi-Fi connection that works on the network the user is on right now, +# including the one carrying this update. +omarchy-state set reboot-required diff --git a/test/shell.d/brcmfmac-supplicant-test.sh b/test/shell.d/brcmfmac-supplicant-test.sh new file mode 100755 index 00000000..7e476d73 --- /dev/null +++ b/test/shell.d/brcmfmac-supplicant-test.sh @@ -0,0 +1,178 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +leaf="$ROOT/install/hardware/apple/fix-brcmfmac-supplicant.sh" +fix_t2="$ROOT/install/hardware/apple/fix-t2.sh" +all="$ROOT/install/hardware/all.sh" +migration="$ROOT/migrations/1786391100.sh" + +grep -q 'apple/fix-brcmfmac-supplicant.sh' "$all" || + fail "the brcmfmac quirk runs during hardware setup" + +# Two leaves writing one config means the later one silently wins, and which is +# later is a detail of all.sh nobody would think to check. +! grep -q 'brcmfmac' "$fix_t2" || + fail "only one leaf owns /etc/modprobe.d/brcmfmac.conf" +pass "the brcmfmac quirk has a single owner and runs during setup" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +stub_bin="$test_tmp/bin" +calls="$test_tmp/calls.log" +conf="$test_tmp/etc/modprobe.d/brcmfmac.conf" +mkdir -p "$stub_bin" "$test_tmp/dmi" + +cat >"$stub_bin/lspci" <<'SH' +#!/bin/bash + +# Chatty like real lspci: keep writing well past the pipe buffer after the +# match, so a grep -q consumer would kill this stub with SIGPIPE and pipefail +# would read that as "no such hardware" (#6608). +if (( ${T2_HARDWARE:-0} == 1 )); then + echo '01:00.0 Bridge [0680]: Apple Inc. T2 Security Chip [106b:1801]' +fi +if [[ -n ${WIFI_ID:-} ]]; then + echo "03:00.0 Network controller [0280]: Broadcom Inc. Wireless [14e4:$WIFI_ID]" +fi +for _ in {1..4096}; do + echo '02:00.0 Host bridge [0600]: Filler Device [ffff:0000]' +done +SH + +cat >"$stub_bin/sudo" <<'SH' +#!/bin/bash + +printf 'sudo' >>"$TEST_LOG" +printf '\t%s' "$@" >>"$TEST_LOG" +printf '\n' >>"$TEST_LOG" +"$@" +SH + +# Stubbed rather than run: the real one would write the running user's state. +cat >"$stub_bin/omarchy-state" <<'SH' +#!/bin/bash + +printf 'omarchy-state' >>"$TEST_LOG" +printf '\t%s' "$@" >>"$TEST_LOG" +printf '\n' >>"$TEST_LOG" +SH + +chmod +x "$stub_bin"/* + +# The leaf reads the vendor from an absolute path, so point it at a fixture by +# running with a fake root on PATH-independent state. pipefail is on, so a +# grep -q gate would go silent here the way #6608 did. +run_leaf() { + local vendor="$1" wifi_id="${2:-}" t2="${3:-0}" + rm -rf "$test_tmp/etc" + mkdir -p "$test_tmp/etc" + printf '%s' "$vendor" >"$test_tmp/dmi/sys_vendor" + + # Redirect both absolute paths the leaf touches into the sandbox. + local script="$test_tmp/leaf.sh" + sed -e "s|/sys/class/dmi/id/sys_vendor|$test_tmp/dmi/sys_vendor|g" \ + -e "s|/etc/modprobe.d|$test_tmp/etc/modprobe.d|g" \ + "$leaf" >"$script" + + WIFI_ID="$wifi_id" T2_HARDWARE="$t2" PATH="$stub_bin:$PATH" \ + bash -eE -o pipefail -c 'source "$1"' bash "$script" /dev/null +grep -q 'feature_disable=0x82000' "$conf" 2>/dev/null || + fail "a T2 Mac still gets the quirk" "$(ls -R "$test_tmp/etc" 2>&1)" +pass "a T2 Mac still gets the quirk" + +# Every Broadcom part brcmfmac drives, on a Mac with no T2 to detect: BCM43602 +# and its single-band variants, BCM4350, BCM4355, BCM4364, BCM4378, BCM4387. +for wifi_id in 43ba 43bb 43bc 43a3 43dc 4464 4425 4433; do + run_leaf "Apple Inc." "$wifi_id" 0 >/dev/null + [[ -f $conf ]] || fail "a Mac without a T2 gets the quirk" "14e4:$wifi_id" +done +pass "every brcmfmac part on a Mac without a T2 gets the quirk" + +# Older Macs report the vendor differently. +run_leaf "Apple Computer, Inc." 43ba 0 >/dev/null +[[ -f $conf ]] || fail "the older Apple vendor string is recognized" +pass "the older Apple vendor string is recognized" + +# BCM4360 Macs run the out-of-tree wl driver, which never reads this option, so +# writing it would only look like the machine had been dealt with. +run_leaf "Apple Inc." 43a0 0 >/dev/null +[[ ! -f $conf ]] || fail "a Mac whose Wi-Fi brcmfmac does not drive is left alone" +pass "a Mac whose Wi-Fi brcmfmac does not drive is left alone" + +# Plenty of non-Apple hardware uses brcmfmac and does not share this bug. +run_leaf "LENOVO" 43ba 0 >/dev/null +[[ ! -f $conf ]] || fail "non-Apple hardware is left alone" +pass "non-Apple hardware is left alone" + +run_leaf "Apple Inc." "" 0 >/dev/null +[[ ! -f $conf ]] || fail "a Mac with no wireless device is left alone" +pass "a Mac with no wireless device is left alone" + +# Installs that predate the quirk never ran the leaf, so the migration has to +# reach them. It runs as the user under pipefail, the context #6608 was about. +run_migration() { + local vendor="$1" wifi_id="${2:-}" t2="${3:-0}" + printf '%s' "$vendor" >"$test_tmp/dmi/sys_vendor" + : >"$calls" + + WIFI_ID="$wifi_id" T2_HARDWARE="$t2" PATH="$stub_bin:$PATH" TEST_LOG="$calls" \ + OMARCHY_BRCMFMAC_DMI_VENDOR="$test_tmp/dmi/sys_vendor" \ + OMARCHY_BRCMFMAC_CONF="$conf" \ + bash -euo pipefail "$migration" >/dev/null +} + +# A T2 install from before the quirk shipped has no config at all, so this is +# the case that proves the T2 gate itself still fires -- and it is the piped +# grep, run under pipefail, that #6608 was about. +rm -rf "$test_tmp/etc" +run_migration "Apple Inc." 4488 1 +grep -q '^options brcmfmac feature_disable=0x82000$' "$conf" 2>/dev/null || + fail "the migration fixes a T2 install that never got the quirk" "$(ls -R "$test_tmp/etc" 2>&1)" +# The option only reaches the driver when brcmfmac next loads. +grep -Fq $'omarchy-state\tset\treboot-required' "$calls" || + fail "the migration asks for the reboot that applies it" "$(cat "$calls")" +pass "the migration fixes a T2 install that never got the quirk" + +run_migration "Apple Inc." 4488 1 +(( $(grep -c '^options brcmfmac feature_disable=0x82000$' "$conf") == 1 )) || + fail "the migration is idempotent" "$(cat "$conf")" +[[ ! -s $calls ]] || fail "a repaired install is left untouched" "$(cat "$calls")" +pass "the migration is idempotent" + +# The machine this was written for, with no T2 to fall back on. +rm -rf "$test_tmp/etc" +run_migration "Apple Inc." 43ba 0 +grep -q '^options brcmfmac feature_disable=0x82000$' "$conf" 2>/dev/null || + fail "the migration fixes an install on a Mac without a T2" "$(ls -R "$test_tmp/etc" 2>&1)" +pass "the migration fixes an install on a Mac without a T2" + +# Written without a trailing newline, the way a hand-edited config often is. +printf '%s' 'options brcmfmac roamoff=1' >"$conf" +run_migration "Apple Inc." 43ba 0 +grep -qx 'options brcmfmac roamoff=1' "$conf" || + fail "the migration keeps other options in the config" "$(cat "$conf")" +grep -qx 'options brcmfmac feature_disable=0x82000' "$conf" || + fail "the migration appends the quirk to an existing config" "$(cat "$conf")" +pass "the migration appends to a config that holds other options" + +# Someone who commented their line out is still on the broken default. +printf '#options brcmfmac feature_disable=0x82000\n' >"$conf" +run_migration "Apple Inc." 43ba 0 +grep -qx 'options brcmfmac feature_disable=0x82000' "$conf" || + fail "a commented-out option does not count as applied" "$(cat "$conf")" +pass "a commented-out option does not count as applied" + +rm -rf "$test_tmp/etc" +run_migration "Apple Inc." 43a0 0 +[[ ! -e $conf ]] || fail "the migration skips a Mac brcmfmac does not drive" "$(cat "$conf")" +[[ ! -s $calls ]] || fail "the migration escalates nothing on unaffected Macs" "$(cat "$calls")" +pass "the migration skips hardware brcmfmac does not drive"