ieee80211_fragment: fragment size can exceed MCLBYTES (FRAG_MAX=2346 default) -> m_align() computes negative offset -> packet-controlled heap underflow write before the mbuf cluster (release) / KASSERT panic (INVARIANTS)
Summary
ieee80211_encap() fragments with mtu=vap->iv_fragthreshold whose DEFAULT is IEEE80211_FRAG_MAX=2346 and whose ioctl range (256..2346) exceeds MCLBYTES=2048. For any frame >= ~4.1KB, fragsize saturates at mtu; m_align(m, fragsize-ciphdrsize) computes adjust = -290 (no clamp in uipc_mbuf.c) and m_data lands 288 bytes BEFORE the cluster; the following memcpy+m_copydata write ~2346 controlled bytes straddling it (~262-byte heap underflow + 2-byte overflow on release kernels). The only guard is a KASSERT compiled out of release kernels. VERIFIED panic on INVARIANTS guest: 'panic: fragment size 2346 too big!' from ieee80211_encap via the exported ieee80211_vap_pkt_send_dest with one 5000-byte frame on a C_TXFRAG vap; release-kernel underflow form established by line-level analysis. Currently privileged-only (wlan MTU clamped to 2290), but BPF writes and in-kernel callers of the exported pkt_send_dest bypass MTU accounting - one caller away from corruption. Drivers affected: ath, wpi, rt2560, rt2661. Fix validated (clamp mtu to MCLBYTES-1).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3283 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.1 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| verdict.json | β | 4.4 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw | |
| fw.c | β | 9.7 KB | view raw | |
| Makefile | β | 177 B | β download | |
| build.sh | β | 87 B | view raw | |
| run.sh | β | 111 B | view raw | |
| build.log | β | 5.6 KB | view raw | |
| run.log | β | 1.6 KB | view raw | |
| panic.txt | β | 2.2 KB | view raw | |
| env.txt | β | 256 B | view raw | |
| fix.diff | β | 781 B | view raw |
DF-3283 β ieee80211_fragment: fragment size can exceed MCLBYTES β m_align() negative offset β heap underflow write (release) / KASSERT panic (INVARIANTS)
File: sys/netproto/802_11/wlan/ieee80211_output.c (pass 2 audit)
What
ieee80211_fragment() (lines 1773-1877) computes per-fragment
fragsize = min(totalhdrsize + remainder, mtu) where mtu =
vap->iv_fragthreshold, user-configurable via
IEEE80211_IOC_FRAGTHRESHOLD to [IEEE80211_FRAG_MIN(256),
IEEE80211_FRAG_MAX(2346)] (ieee80211_ioctl.c:3081-3089) β and the
default is FRAG_MAX=2346 (ieee80211_var.h:85). Each fragment is
built in a single mbuf cluster:
/* XXX fragsize can be >2048! */ <- developer's own note
KASSERT(fragsize < MCLBYTES, ...);
if (fragsize > MHLEN)
m = m_getcl(...); /* 2048-byte cluster */
m_align(m, fragsize - ciphdrsize); /* 2338 > 2048 */
m_align() (sys/kern/uipc_mbuf.c:1935) computes
adjust = ext_size - len = 2048 - 2338 = -290, rounds to -288, and does
m_data += adjust β m_data lands 288 bytes before the cluster. The
following memcpy(whf, wh, hdrsize) + m_copydata(m0, off, payload, ...)
then write ~2346 bytes straddling the cluster: ~262 bytes of
attacker-supplied payload before the cluster and 2 bytes past its end.
On INVARIANTS kernels the KASSERT turns this into a panic instead.
Bug triggers whenever a frame of β₯ ~4.1 KB reaches ieee80211_encap()
on a TXFRAG-capable driver (fragthresholdΒ²-scale). Unprivileged reach is
currently blocked only because SIOCSIFMTU clamps wlan MTU to
IEEE80211_MTU_MAX=2290 (ieee80211_ioctl.c:3481-3487), keeping stack
frames < 2346+2022; privileged callers bypass that gate trivially
(BPF write to the wlan ifnet, or any in-kernel caller of the exported
ieee80211_vap_pkt_send_dest()). This is latent memory corruption whose
only guard today is an assert compiled out of release kernels.
Build / run (repo root)
sh findings/poc/DF-3283/build.sh # builds /root/fw/fw.ko in guest sh findings/poc/DF-3283/run.sh # baseline: panic
The harness fw.c advertises IEEE80211_C_TXFRAG (as ath/wpi/rt2560/
rt2661 do), brings up a hostap vap (beacon path proven in DF-3282), then
sysctl kern.fw_fire=1 pushes one 5000-byte 802.3 frame through the
real exported TX entry ieee80211_vap_pkt_send_dest().
Expected output
Baseline (stock INVARIANTS kernel):
fw.fire: vap wlan0, pushing 5000 byte frame (fragthreshold 2346) panic: fragment size 2346 too big! ieee80211_encap() at ieee80211_encap+0xb01 ieee80211_vap_pkt_send_dest() at ...
Patched (fix.diff clamps mtu to MCLBYTES): "fw.fire: returned (no panic)" β frame fragmented into β€2048-byte fragments, guest stays up.
Threat
Privileged local memory corruption / panic on TXFRAG drivers. The write is packet-data controlled (heap underflow before an mbuf cluster + overflow past its end), so on release kernels it is a classic slab-corruption primitive; the trigger length needs only a >4.1 KB mbuf chain, which ifnet-MTU accounting no longer enforces for BPF/in-kernel callers.
DF-3283 VERDICT β VERIFIED / reproduced / fixed (primitive characterized)
Bottom line
Reproduced: yes. Impact: panic on INVARIANTS kernels; on release (non-INVARIANTS) kernels this is a packet-data-controlled heap underflow write of ~260-300 bytes in front of an mbuf cluster. Fixed by clamping the fragment size to MCLBYTES-1.
Root cause (path:line)
sys/netproto/802_11/wlan/ieee80211_output.c:1727-1729βieee80211_encap()callsieee80211_fragment(vap, m, hdrsize, ciphdrsize, vap->iv_fragthreshold).sys/netproto/802_11/ieee80211_var.h:85+wlan/ieee80211_proto.c:339β defaultiv_fragthreshold = IEEE80211_FRAG_MAX = 2346.sys/netproto/802_11/ieee80211.h:1330β FRAG_MAX=2346 > MCLBYTES=2048.sys/netproto/802_11/wlan/ieee80211_output.c:1804-1817βfragsize = min(totalhdrsize + remainder, mtu); when a frame of β₯ ~4.1 KB (fragthreshold + 2048-hdr) reaches the fragmenter, fragsize saturates at mtu (up to 2346). The code itself notes/* XXX fragsize can be >2048! */(line 1807) and only guards withKASSERT(fragsize < MCLBYTES)β compiled out on release kernels.sys/kern/uipc_mbuf.c:1935-1946βm_align(m, fragsize - ciphdrsize)computesadjust = ext_size - len = 2048 - 2338 = -290(rounded to -288): m_data is moved 288 bytes before the cluster start; the followingmemcpy(whf, wh, hdrsize)(line 1827) andm_copydata(m0, off, payload, ...)(line 1846) then write ~2346 bytes straddling the cluster β ~262 bytes of controlled payload before the cluster (heap underflow) and ~2 bytes past its end.
Reproduction (stock INVARIANTS kernel #0 Jul 2 06:02:54)
Same fw.c harness as DF-3282's pack (software radio advertising
IEEE80211_C_TXFRAG, as ath/wpi/rt2560/rt2661 do), hostap vap up, then
sysctl kern.fw_fire=1 pushes one 5000-byte 802.3 frame through the real
exported TX entry ieee80211_vap_pkt_send_dest()
(sys/netproto/802_11/wlan/ieee80211_output.c:126) β the privilege
equivalent of a BPF write to the wlan interface:
fw.fire: vap wlan0, pushing 5000 byte frame (fragthreshold 2346)
panic: fragment size 2346 too big! <- KASSERT at output.c:1808
Trace: ieee80211_encap+0xb01
ieee80211_vap_pkt_send_dest+0xb1
fw_fire_sysctl
Full capture in panic.txt. The KASSERT proves the fragmenter computes a
2346-byte fragment for a 2048-byte cluster; with INVARIANTS compiled out,
the same code path performs the negative-offset m_align and the copy
smashes the heap (analysis above; not runnable on the INVARIANTS guest,
and rebuilding a no-INVARIANTS kernel was out of scope for this run).
Reachability honesty
A >4.1 KB single mbuf chain must reach ieee80211_encap(). The normal
stack cannot produce one today because SIOCSIFMTU clamps wlan MTU to
IEEE80211_MTU_MAX=2290 (ieee80211_ioctl.c:3481-3487), which keeps frames
below the ~4.1 KB trigger; DF-0275-family drivers with A-MSDU/FF excluded
(M_FF/M_AMPDU_MPDU check at output.c:1706). The gate that remains is
ifnet-MTU accounting, which privileged paths bypass trivially (BPF write,
in-kernel callers of the exported ieee80211_vap_pkt_send_dest). Hence
Low severity now β but it is real memory-unsafe code whose only guard is
an assert absent from release kernels.
Fix validation
fix.diff clamps mtu to MCLBYTES-1 at the top of
ieee80211_fragment().
- First validation attempt with a
MCLBYTESclamp still panicked (fragment size 2048 too big!) because the KASSERT is strictly<β corrected toMCLBYTES-1; recorded here for honesty (attempts=3). - Baseline (stock kernel): panic
fragment size 2346 too big!(panic.txt). - Patched (kernel #1 Sep 10 04:41:20, both fixes applied): fired the
5000-byte frame twice β
fw.fire: returned (no panic)β the frame was fragmented into β€2047-byte fragments, delivered to the transmit path, and the guest stayed up (vm.sh status=> up; seerun.log).
fix_status: fixed, fix_baseline_reproduced: 1, fix_patched_reproduced:
0.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedPatched kernel (make nativekernel + installkernel in-guest) with the MCLBYTES-1 clamp: the exact 5000-byte-frame trigger fired twice, 'fw.fire: returned (no panic)', fragments built and freed through the transmit path, guest stayed up. Baseline stock kernel panicked at ieee80211_encap+0xb01. (First fix attempt clamping to MCLBYTES was insufficient due to the strict KASSERT and was corrected.)
panic.txt (baseline), run.log (patched no-panic runs), serial console 'fw.fire: returned (no panic)' x2 with vm.sh status => up.
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_output.c:1703
- sys/netproto/802_11/wlan/ieee80211_output.c:1727
- sys/netproto/802_11/wlan/ieee80211_output.c:1804
- sys/netproto/802_11/wlan/ieee80211_output.c:1808
- sys/netproto/802_11/wlan/ieee80211_output.c:1817
- sys/kern/uipc_mbuf.c:1940
- sys/netproto/802_11/ieee80211.h:1330
- sys/netproto/802_11/ieee80211_var.h:85
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:3483
Detail
Exploit chain
privileged: BPF write / in-kernel mbuf >= ~4.1KB to ieee80211_vap_pkt_send_dest on a TXFRAG driver (default fragthreshold 2346) -> ieee80211_encap -> ieee80211_fragment -> m_align negative offset -> ~262 bytes of packet-controlled data written before the mbuf cluster + ~2 bytes past it (release kernels) or KASSERT panic (INVARIANTS). No unprivileged trigger found: ifnet MTU clamp 2290 keeps stack frames under the trigger length.
Evidence (decisive lines)
["panic.txt ('panic: fragment size 2346 too big!' + ieee80211_encap backtrace)", 'run.log (transcript incl. patched no-panic runs)', 'fw.c (C_TXFRAG software radio + kern.fw_fire sysctl pushing 5000B frame)', 'build.log']
PoC changes
No seed; harness authored from scratch (fw.c). Discovered during fix validation that clamping to exactly MCLBYTES still trips the strict '<' KASSERT (second panic, 'fragment size 2048 too big!') and corrected the fix to MCLBYTES-1.
Verified recommended fix
Clamp the fragment threshold to MCLBYTES-1 at the top of ieee80211_fragment() (or reject fragthreshold > MCLBYTES in the IEEE80211_IOC_FRAGTHRESHOLD ioctl) so m_align() can never compute a negative offset.
Verdict
ieee80211_fragment() computes fragsize = min(totalhdrsize+remainder, iv_fragthreshold) with the DEFAULT threshold IEEE80211_FRAG_MAX=2346 > MCLBYTES=2048; each fragment is built in a 2048-byte cluster and m_align(m, fragsize-ciphdrsize) computes a negative adjust, moving m_data ~288 bytes BEFORE the cluster before ~2346 controlled bytes are copied (heap underflow on release kernels). On the stock INVARIANTS guest the guard KASSERT fires: 'panic: fragment size 2346 too big!' from ieee80211_encap+0xb01, reached via the exported ieee80211_vap_pkt_send_dest() with a single 5000-byte frame on a C_TXFRAG-advertising vap. Unprivileged reach is currently blocked only by the SIOCSIFMTU clamp to IEEE80211_MTU_MAX=2290 (frames stay < ~4.1KB); privileged callers (BPF write, in-kernel callers) bypass it, hence Low severity despite the corruption-class primitive. fix.diff clamps mtu to MCLBYTES-1; validated by kernel rebuild: same trigger now returns cleanly ('fw.fire: returned (no panic)') twice, guest stays up.
No comments yet.