Patching the Broadcom wl driver for kernel 6.17


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.

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.

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.

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.

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
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.

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.

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>

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.

The kernel-side changes behind each breakage:

×
Page views: