The four fixes in this post are packaged as a patch and an install script at github.com/jamieede123/macbookair6-1-mint-tweaks . For working Wi-Fi without reading further, run
sudo ./broadcom/fix.sh; the rest of this post is why each change is needed.
The machine is an 11-inch MacBook Air from mid 2013 (MacBookAir6,1, a Haswell Core i5-4250U), running Linux Mint rather than macOS. It is a capable little laptop a decade on, with one catch: its wifi is the Broadcom card that Linux has always handled badly. A routine apt upgrade pulled in kernel 6.17, and the upgrade ended with a wall of errors. The kernel packages would not configure:
dpkg: error processing package linux-image-6.17.0-35-generic (--configure):
installed linux-image-6.17.0-35-generic package post-installation script
subprocess returned error exit status 11
Errors were encountered while processing:
linux-headers-6.17.0-35-generic
linux-headers-generic-hwe-24.04
linux-generic-hwe-24.04
linux-image-6.17.0-35-generic
The cause was one line further up:
dkms autoinstall on 6.17.0-35-generic/x86_64 failed for broadcom-sta(10)
That card is a Broadcom BCM4360 (14e4:43a0), the Apple-branded variant of the chip Apple fitted to this generation of Air. It needs the proprietary wl driver from broadcom-sta. The open-source brcmfmac does not drive this variant. The driver is built as a DKMS module, so it recompiles against each new kernel. It compiles cleanly on 6.14. It does not compile on 6.17, and broadcom-sta has not had a meaningful upstream release in years.
Why a driver build failure breaks dpkg ¶
DKMS hooks into the kernel package’s post-install script. When linux-image-6.17 is configured, the hook tries to build every registered DKMS module for the new kernel. The broadcom-sta build returns non-zero, the hook returns non-zero, and dpkg treats the whole package configuration as failed. The kernel image and headers are left half-configured, and the linux-*-hwe-24.04 metapackages that depend on them are left unconfigured behind them.
Nothing was broken yet. The system was still booted on 6.14, where wl was loaded and wifi worked. But the package state was dirty, and a boot into 6.17 would come up with no wireless. The fix is to make broadcom-sta build on 6.17.
There were four separate breakages. Each one is a kernel build-system or API change between 6.14 and 6.17, and each surfaced only after the previous one was cleared.
1. typedefs.h: No such file or directory ¶
The build died on the first source file:
src/shared/linux_osl.c:23:10: fatal error: typedefs.h: No such file or directory
23 | #include <typedefs.h>
typedefs.h is the driver’s own header, in src/include. The Makefile adds that directory with -I$(src)/src/include. On older kernels $(src) was an absolute path to the module source. Kbuild changed this: in a modern external-module build $(src) is relative to the kernel tree, not the module.
A debug line in the Makefile confirms it. During the real compile pass:
src=./. srctree=/usr/src/linux-headers-6.17.0-35-generic M=/var/lib/dkms/broadcom-sta/6.30.223.271/build
So $(src) is ./. and $(srctree) points at the kernel tree. The one variable that holds the absolute path to the module source is $(M). Anchoring the include paths to $(M) fixes it:
-EXTRA_CFLAGS += -I$(src)/src/include -I$(src)/src/common/include
-EXTRA_CFLAGS += -I$(src)/src/wl/sys -I$(src)/src/wl/phy -I$(src)/src/wl/ppr/include
-EXTRA_CFLAGS += -I$(src)/src/shared/bcmwifi/include
+ccflags-y += -I$(M)/src/include -I$(M)/src/common/include
+ccflags-y += -I$(M)/src/wl/sys -I$(M)/src/wl/phy -I$(M)/src/wl/ppr/include
+ccflags-y += -I$(M)/src/shared/bcmwifi/include
The variable rename in those lines is the second breakage.
2. The include flags were being ignored entirely ¶
After switching to $(M), the build failed with the same typedefs.h error. The path was now correct, so the flag was not reaching the compiler at all. A verbose build shows the actual cc invocation:
make -C /lib/modules/6.17.0-35-generic/build M="$PWD" src/shared/linux_osl.o V=1
The command line for linux_osl.o contained none of the driver’s -I paths, and no -DUSE_CFG80211 either. The entire EXTRA_CFLAGS was absent. EXTRA_CFLAGS and EXTRA_LDFLAGS have been deprecated kbuild spellings for a long time, and 6.17 no longer honours them for external modules. The supported names are ccflags-y and ldflags-y:
-EXTRA_CFLAGS :=
+ccflags-y :=
-EXTRA_LDFLAGS := $(src)/lib/$(SHIPPED)
+ldflags-y := $(M)/lib/$(SHIPPED)
With the includes anchored to $(M) and the flags under the names the kernel actually reads, the driver compiled past its headers and into the kernel API changes.
3. The timer API ¶
src/wl/sys/wl_linux.c:2373: error: implicit declaration of function 'from_timer'
src/wl/sys/wl_linux.c:2478: error: implicit declaration of function 'del_timer'
Two timer functions were removed in 6.16. from_timer() became timer_container_of(), and del_timer() became timer_delete(). The driver still has to build on 6.14, so the replacements go behind version guards rather than replacing the old calls outright:
wl_timer_t *t =
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 15, 0)
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 16, 0)
+ timer_container_of(t, tl, timer);
+#else
from_timer(t, tl, timer);
+#endif
#else
(wl_timer_t *)data;
#endif
t->set = FALSE;
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 16, 0)
+ if (!timer_delete(&t->timer)) {
+#else
if (!del_timer(&t->timer)) {
+#endif
4. The cfg80211 operations grew a parameter ¶
src/wl/sys/wl_cfg80211_hybrid.c:1827: error: initialization of
'int (*)(struct wiphy *, int, u32)' from incompatible pointer type
's32 (*)(struct wiphy *, u32)'
Three cfg80211_ops callbacks gained an int radio_idx parameter in 6.16, for multi-radio wiphy support: set_wiphy_params, set_tx_power, and get_tx_power. The compiler prints the exact expected signature in each error, so there is no guesswork. Each function needs a new version-guarded variant with the extra parameter. The declaration and definition of set_wiphy_params are the clearest example:
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 16, 0)
+static s32 wl_cfg80211_set_wiphy_params(struct wiphy *wiphy, int radio_idx, u32 changed)
+#else
static s32 wl_cfg80211_set_wiphy_params(struct wiphy *wiphy, u32 changed)
+#endif
{
set_tx_power takes the new int after wdev, and get_tx_power takes it before the link_id argument that an earlier kernel had already added. The bodies do not use radio_idx; the kernel build already passes -Wno-unused-parameter, so an unused parameter is not an error here.
Build, sign, install ¶
With all four fixed, DKMS builds and signs the module for 6.17:
$ sudo dkms build broadcom-sta/6.30.223.271 -k 6.17.0-35-generic
...
Signing module /var/lib/dkms/broadcom-sta/6.30.223.271/build/wl.ko
$ sudo dkms install broadcom-sta/6.30.223.271 -k 6.17.0-35-generic
dkms status now shows the module built for both kernels:
broadcom-sta/6.30.223.271, 6.14.0-37-generic, x86_64: installed
broadcom-sta/6.30.223.271, 6.17.0-35-generic, x86_64: installed
The module is MOK-signed, which matters with Secure Boot enabled. It carries the same key that already worked on 6.14, so it will load:
$ modinfo -k 6.17.0-35-generic wl | grep -E 'filename|signer|vermagic'
filename: /lib/modules/6.17.0-35-generic/updates/dkms/wl.ko.zst
vermagic: 6.17.0-35-generic SMP preempt mod_unload modversions
signer: localhost.localdomain Secure Boot Module Signature key
$ modprobe --dry-run --show-depends -S 6.17.0-35-generic wl
insmod /lib/modules/6.17.0-35-generic/kernel/net/wireless/cfg80211.ko.zst
insmod /lib/modules/6.17.0-35-generic/updates/dkms/wl.ko.zst
With a working module for 6.17, the half-configured packages can finish. dpkg --configure -a reruns the DKMS hook, which now succeeds, regenerates the initramfs, and updates grub:
$ sudo dpkg --configure -a
...
update-initramfs: Generating /boot/initrd.img-6.17.0-35-generic
Generating grub configuration file ...
done
$ sudo apt-get -f install
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
After reboot ¶
Booted into 6.17, the driver is loaded and the card is connected:
$ uname -r
6.17.0-35-generic
$ lsmod | grep -E '^wl|cfg80211'
wl 6492160 0
cfg80211 1462272 1 wl
$ ip -br link show wlp3s0
wlp3s0 UP b8:e8:56:0f:6b:ec <BROADCAST,MULTICAST,UP,LOWER_UP>
The caveat ¶
These edits live in the DKMS source tree under /usr/src/broadcom-sta-6.30.223.271. They survive kernel upgrades, because DKMS rebuilds from that source for each new kernel. They do not survive a package update: if broadcom-sta-dkms is ever reinstalled or upgraded, it overwrites these files and the next kernel build fails the same way. Keep a copy of the patched files, or the .orig backups taken before editing, so re-applying is quick. The repo
keeps the patch for exactly this reason: after such an upgrade, re-run sudo ./broadcom/fix.sh to re-apply and rebuild.
This is a stopgap for an abandoned driver. The right long-term fix is hardware that the in-tree drivers support. Until then, the pattern holds for the next kernel too: build, read the first error, fix it behind a version guard, build again. The compiler names every changed signature, so the work is mechanical rather than guesswork.
References ¶
The kernel-side changes behind each breakage:
- The working directory and relative-path change for
M=external module builds: kbuild: change working directory to external module directory with M= (Masahiro Yamada), and the Building External Modules documentation. - The supported compiler and linker flag variables:
ccflags-yandldflags-yin the kbuild Makefile documentation . - The timer API rename: timers: Simple cleanup of the timer API namespace
(Ingo Molnar), which includes
from_timer()totimer_container_of(). - The per-radio
cfg80211operations: wifi: cfg80211/mac80211: Set/get wiphy parameters on per-radio basis , which adds the radio index argument.