β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3283

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3283 Β· 13 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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() calls ieee80211_fragment(vap, m, hdrsize, ciphdrsize, vap->iv_fragthreshold).
  • sys/netproto/802_11/ieee80211_var.h:85 + wlan/ieee80211_proto.c:339 β€” default iv_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 with KASSERT(fragsize < MCLBYTES) β€” compiled out on release kernels.
  • sys/kern/uipc_mbuf.c:1935-1946 β€” m_align(m, fragsize - ciphdrsize) computes adjust = ext_size - len = 2048 - 2338 = -290 (rounded to -288): m_data is moved 288 bytes before the cluster start; the following memcpy(whf, wh, hdrsize) (line 1827) and m_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 MCLBYTES clamp still panicked (fragment size 2048 too big!) because the KASSERT is strictly < β€” corrected to MCLBYTES-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; see run.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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 10 04:41:20 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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.