ieee80211_parse_tdma() join path skips IE length and tdma_slot validation, enabling heap OOB write via setbit() from crafted beacon
| Field | Value |
|---|---|
| ID | DF-0612 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/netproto/802_11/wlan/ieee80211_tdma.c |
| Lines | 645-669 (primary); 530-559 (sibling with correct guards) |
| Area | netproto/802_11 (TDMA wireless protocol) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
ieee80211_parse_tdma() β the TDMA IE handler invoked on BSS join from
ieee80211_node.c:848 β casts the attacker-supplied vendor IE directly to
struct ieee80211_tdma_param * and immediately executes
setbit(ts->tdma_inuse, tdma->tdma_slot) with no validation of the IE
length and no range-check on tdma_slot. Its sibling function
tdma_process_params() (the beacon-during-RUN handler) performs both
checks (ieee80211_tdma.c:536 and :555), but ieee80211_parse_tdma
performs neither. A crafted beacon with an out-of-range tdma_slot
(8..255) drives setbit() β defined as (a)[(i)/NBBY] |= 1<<((i)%NBBY)
in sys/param.h:390 β up to 32 bytes past the 1-byte ts->tdma_inuse[]
array, corrupting adjacent heap fields in struct ieee80211_tdma_state.
Root cause
ieee80211_parse_tdma() at sys/netproto/802_11/wlan/ieee80211_tdma.c:644-669:
644: void
645: ieee80211_parse_tdma(struct ieee80211_node *ni, const uint8_t *ie)
646: {
647: struct ieee80211vap *vap = ni->ni_vap;
648:
649: if (vap->iv_caps & IEEE80211_C_TDMA) {
650: const struct ieee80211_tdma_param *tdma =
651: (const struct ieee80211_tdma_param *)ie;
652: struct ieee80211_tdma_state *ts = vap->iv_tdma;
...
657: setbit(ts->tdma_inuse, tdma->tdma_slot); /* <-- NO CHECK */
658: (void) tdma_update(vap, tdma, ni, 1);
Line 657 executes setbit() with tdma->tdma_slot taken verbatim from the
IE. tdma_slot is a u_int8_t (ieee80211_tdma.h:54), so it ranges
0..255. setbit (sys/param.h:390) computes
ts->tdma_inuse[tdma_slot/8]; ts->tdma_inuse is declared
uint8_t tdma_inuse[1] (ieee80211_tdma.h:73), so any tdma_slot >= 8
indexes past the array. With tdma_slot=255 the write lands at
ts->tdma_inuse[31], i.e. struct offset 8 + 31 = 39. The struct layout
(ieee80211_tdma.h:67-87) places tdma_active@9, tdma_count@12,
tdma_peer@16, tdma_lastprint@24-39 in that range β all corruptible via
a single OR-bit-write.
Contrast with tdma_process_params() which guards the same operation:
536: if (len < sizeof(*tdma) - 2) { /* IE length check */
...
555: if (tdma->tdma_slot >= TDMA_MAXSLOTS) { /* slot range check */
ieee80211_parse_tdma has neither guard.
Additionally, because the length is never checked, reading
tdma->tdma_slot (offset 8), tdma_slotcnt, tdma_slotlen,
tdma_bintval, tdma_inuse from a deliberately short IE (the
istdmaoui check at ieee80211_input.h:118-121 only requires ie[1] > 3)
is a heap OOB read within the ies->data blob if the TDMA IE is the last
element.
Threat model & preconditions
- Attacker position: an unauthenticated wireless peer within radio
range of a DragonFlyBSD host operating a TDMA-capable vap
(
IEEE80211_C_TDMA, typically anath/ath9klong-distance point-to-point link). Attack is unauthenticated because 802.11 beacon/probe-response frames are broadcast and never cryptographically protected before parsing. - Required config: the victim vap must be scanning/joining (the join
path is what reaches
ieee80211_parse_tdmaviaieee80211_node.c:832-848). - Trigger: the attacker spoofs the victim's target SSID in a beacon
carrying a TDMA vendor IE (OUI
00:03:7f, type01, subtype01, version2) withtdma_slotset to an out-of-range value. When the victim selects and joins the forged BSS,ieee80211_parse_tdmafires once, OR-writing a single chosen bit up to 32 bytes pastts->tdma_inuse. - Impact: deterministic corruption of TDMA runtime state β
tdma_active(slot-occupancy mask sent in subsequent beacons),tdma_count(active/inuse refresh countdown,:730/:744),tdma_peer(peer-node cookie,:469/:510), andtdma_lastprint(ppsratecheckstate). This disrupts slot tracking (DoS), causes the master to advertise wrong occupancy (protocol confusion), and desynchronizes the TDMA link. - Limits: the OOB write is bounded to the struct (a
uint8index cannot reach the function pointers at offset 48+), so this is state corruption / DoS rather than direct RIP control.
Proof of concept
PoC source: findings/poc/DF-0612/inject_tdma_beacon.py β a WiFi beacon
injector (no victim-side binary; the bug is in kernel frame parsing).
Requires an 802.11 monitor+inject-capable radio (e.g. Atheros ath9k)
within range of the victim's TDMA link.
Build & run
sudo python3 inject_tdma_beacon.py wlan0mon <target_ssid>
Expected output
When a vulnerable DragonFlyBSD host with a TDMA-mode vap (created e.g. by
ifconfig wlanX create wlandev ath0 wlanmode adhoc tdmaslot 1 then scanning
for SSID) selects and joins the forged BSS, ieee80211_parse_tdma()
executes setbit(ts->tdma_inuse, 64), OR-ing bit 0 into the tdma_peer
pointer. Observable effects:
- TDMA link desynchronization,
- the victim's beacons carry a corrupted tdma_inuse mask,
- the active-slot refresh cadence (tdma_count) is disrupted.
With SLOT values targeting tdma_count (32..63) the master's slot-mask
refresh stalls or fires every beacon.
Impact
- Blast radius: any DragonFlyBSD host with a TDMA-capable wireless vap that scans/joins in an environment where an attacker can inject beacons. TDMA mode is niche (long-distance point-to-point links) but the vap does not need to be fully associated β merely selecting the forged BSS as a join candidate is enough.
- Severity rationale: Medium. Unauthenticated adjacent-network
(
AV:A/AC:L/PR:N), deterministic OOB write once the forged beacon is selected, but bounded to TDMA state (no RIP control), and requires TDMA mode which is uncommon. - Reliability: 100% once the forged beacon is selected for join β no race within the function itself.
Recommended fix
Add the same IE-length and tdma_slot range checks that
tdma_process_params() already performs, before any field is touched.
--- a/sys/netproto/802_11/wlan/ieee80211_tdma.c
+++ b/sys/netproto/802_11/wlan/ieee80211_tdma.c
@@ -647,14 +647,29 @@ ieee80211_tdma_getslot(struct ieee80211vap *vap)
void
ieee80211_parse_tdma(struct ieee80211_node *ni, const uint8_t *ie)
{
struct ieee80211vap *vap = ni->ni_vap;
+ const struct ieee80211_tdma_param *tdma;
+ struct ieee80211_tdma_state *ts;
if (vap->iv_caps & IEEE80211_C_TDMA) {
- const struct ieee80211_tdma_param *tdma =
- (const struct ieee80211_tdma_param *)ie;
- struct ieee80211_tdma_state *ts = vap->iv_tdma;
+ tdma = (const struct ieee80211_tdma_param *)ie;
+ ts = vap->iv_tdma;
+ /*
+ * Validate the IE before use. Unlike the beacon receive
+ * path (tdma_process_params), this join path previously
+ * performed no length or slot validation, so a crafted
+ * beacon with an out-of-range tdma_slot drove setbit()
+ * past the 1-byte ts->tdma_inuse[] array and corrupted
+ * adjacent TDMA state on the heap.
+ */
+ if (ie[1] < sizeof(*tdma) - 2) {
+ IEEE80211_DISCARD_IE(vap,
+ IEEE80211_MSG_ELEMID | IEEE80211_MSG_TDMA,
+ NULL, "tdma", "too short, len %u", ie[1]);
+ return;
+ }
+ if (tdma->tdma_slot >= TDMA_MAXSLOTS)
+ return;
/*
* Adopt TDMA configuration when joining an
* existing network.
This mirrors tdma_process_params() at ieee80211_tdma.c:536 and :555.
The same length check also closes the secondary heap OOB read of
tdma_slot/slotcnt/slotlen/bintval/inuse from a deliberately short IE.
Defense-in-depth (out of scope for this file): tdma_isfull() in
ieee80211_scan_sta.c:960-969 β a separate consumer of the unvalidated
scan-cached IE β should get the same length guard.
References
sys/netproto/802_11/wlan/ieee80211_tdma.c:645-669βieee80211_parse_tdma, the vulnerable join path.sys/netproto/802_11/wlan/ieee80211_tdma.c:530-559βtdma_process_params, the sibling that has the correct guards.sys/netproto/802_11/ieee80211_tdma.h:73βuint8_t tdma_inuse[1], the 1-byte array that is overrun.sys/netproto/802_11/ieee80211_tdma.h:40βTDMA_MAXSLOTS 2.sys/sys/param.h:390β#define setbit(a,i) ((a)[(i)/NBBY] |= 1<<((i)%NBBY)).sys/netproto/802_11/wlan/ieee80211_node.c:832-848β join path that reachesieee80211_parse_tdma.
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-0612 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace replica of exact kernel structs + buggy setbit line; proves OOB write (slot OOB + short IE) and the FIXED-function rejection | 9.3 KB | view raw |
| inject_tdma_beacon.py | trigger-source | scapy beacon injector carrying forged TDMA vendor IE (needs real WiFi monitor+inject radio; not runnable on QEMU) | 2.2 KB | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff: add IE-length and tdma_slot>=TDMA_MAXSLOTS guards before setbit (mirrors sibling at :536/:555) | 1.2 KB | view raw |
| build.sh | build-script | copies verbatim kernel header + harness into guest, builds with cc -O2 -Wall | 584 B | view raw |
| run.sh | run-script | builds + runs the harness | 293 B | view raw |
| run.log | run-log | decisive harness run (full output): OOB confirmed on UNFIXED, blocked on FIXED | 2.6 KB | view raw |
| env.txt | environment | uname, cc version, ifconfig -l (vtnet0 lo0 only), wlan modules (none), ieee80211_parse_tdma symbol present | 730 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, harness proof, binary before/after, fix validation | 7.9 KB | β raw |
| fix_build.log | build-log | full 'make -j6 nativekernel' output of the single-fix kernel (NK_DONE rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | harness run on the booted #1 single-fix kernel: Case 5 FIX HOLDS | 2.6 KB | view raw |
| logs/baseline_harness.log | run-log | harness run on the #0 baseline kernel | 2.6 KB | view raw |
| logs/baseline_parse_tdma.disasm | disasm | ieee80211_parse_tdma in #0 baseline kernel: NO guards, OOB setbit at variable offset | 2.2 KB | β download |
| logs/patched_parse_tdma.disasm | disasm | ieee80211_parse_tdma in #1 single-fix kernel: cmp $0x15 (len) + cmp $0x1 (slot) guards, fixed-offset setbit | 2.4 KB | β download |
| README.md | readme | human reproduce doc | 4.6 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-0612 β PoC: ieee80211_parse_tdma() join-path heap OOB write via setbit()
Unauthenticated adjacent-network (WiFi) heap OOB write PoC.
Status β VERIFIED (code-level) + FIX VALIDATED
The bug is real and present in the default GENERIC kernel
(ieee80211_parse_tdma is compiled directly into /boot/kernel/kernel). The
live 802.11 receive/join path is unreachable on this QEMU guest (only
vtnet0/lo0, no WiFi radio, no wlan module loaded). Per the per-PoC
procedure, a deterministic code-level harness (harness.c) replicates the
exact kernel structs and the buggy setbit() line and proves the OOB; a
binary-level before/after on the actual shipped kernel proves the fix
(VERDICT.md, logs/*.disasm). The inject_tdma_beacon.py injector is kept
for real WiFi hardware (not runnable on QEMU).
The fix was validated end-to-end: fix.diff applied to in-guest /usr/src,
make -j6 nativekernel built a single-fix kernel (#1, sha 8048ccf4β¦),
installed + rebooted; ieee80211_parse_tdma in the booted kernel now contains
the length guard (cmp $0x15) and slot guard (cmp $0x1) and the harness's
OOB case is blocked. See VERDICT.md and fix_run.log.
Files
inject_tdma_beacon.pyβ scapy-based beacon injector carrying a forged TDMA vendor IE withtdma_slot=64(OOB). When a DragonFlyBSD TDMA-mode vap selects this forged BSS as a join candidate,ieee80211_parse_tdma()runssetbit(ts->tdma_inuse, 64)which writes atts->tdma_inuse[8]β 8 bytes past the 1-bytetdma_inuse[]array β corrupting thetdma_peerpointer instruct ieee80211_tdma_state.
Build & run
There are two artifacts:
harness.cβ a self-contained, deterministic userspace harness (runnable on this guest) that replicates the exact kernel struct layouts (verbatim header) and the buggysetbit()line. It proves the OOB write for several crafted IEs (slot=64 hitstdma_peer; slot=8 hitstdma_active; slot=255 hitstdma_lastprint) and the secondary short-IE OOB read, and shows the FIXED-function variant rejects them. This is what reproduces on QEMU.
Build (guest, as root β needs /usr/src for the header):
./build.sh # or: cc -O2 -Wall -o harness harness.c (with the header in -I)
./run.sh # or: ./harness
Expected: prints OOB WRITE CONFIRMED for the UNFIXED cases and
FIX HOLDS / OOB write confirmed on UNFIXED, blocked on FIXED.
inject_tdma_beacon.pyβ the live-frame injector for real WiFi hardware. Not reproducible in QEMU.
The victim host must have a TDMA-mode vap that is scanning for SSID:
ifconfig wlanX create wlandev ath0 wlanmode adhoc tdmaslot 1 ifconfig wlanX up scan # triggers ieee80211_parse_tdma on join
Expected outcome
- TDMA link desynchronization,
- the victim's beacons carry a corrupted
tdma_inusemask, - the active-slot refresh cadence (
tdma_count) is disrupted.
With SLOT values in 32..63 the master's slot-mask refresh stalls or
fires every beacon.
Slot-to-offset table
| tdma_slot | setbit offset (past tdma_inuse) | hits field |
|---|---|---|
| 0-7 | 0 | tdma_inuse[0] (in-bounds) |
| 8-15 | 1 | tdma_active byte 0 |
| 16-23 | 2 | tdma_active/tdma_count |
| 24-31 | 3 | tdma_count |
| 32-39 | 4 | tdma_count/tdma_peer |
| 40-47 | 5 | tdma_peer |
| 48-55 | 6 | tdma_peer/tdma_lastprint |
| 56-63 | 7 | tdma_lastprint |
| 64-71 | 8 | tdma_lastprint |
| ... | ... | ... |
| 248-255 | 31 | past struct |
(Struct layout per sys/netproto/802_11/ieee80211_tdma.h:67-87.)
Notes for the per-PoC verifier
- Cannot be reproduced in QEMU (no physical WiFi radio). The runner should
confirm the code path statically:
ieee80211_sta_join(ieee80211_node.c:798) ->ieee80211_ies_expand(:833) ->ieee80211_parse_tdma(:848) ->setbit(ieee80211_tdma.c:657). - If a net80211 loopback/inject test harness exists in-tree, feed the crafted IE directly to the join path.
- The fix adds the same IE-length and
tdma_slot >= TDMA_MAXSLOTSguards thattdma_process_params()(:536,:555) already has. Verify withgit apply findings/poc/DF-0612/fix.diff.
DF-0612 β VERDICT
Finding: ieee80211_parse_tdma() join path skips IE-length and tdma_slot
validation β heap OOB write via setbit() from a crafted TDMA vendor IE.
File: sys/netproto/802_11/wlan/ieee80211_tdma.c:644-669
Verdict: REPRODUCED (code-level) β fix VALIDATED on a single-fix kernel
The bug is real and present in the default GENERIC kernel (nm /boot/kernel/kernel
shows ieee80211_parse_tdma as a T symbol, compiled in directly). The
sibling handler tdma_process_params() guards the same setbit() with an
IE-length check (ieee80211_tdma.c:536) and a tdma_slot range check
(:555); ieee80211_parse_tdma() has neither.
The live 802.11 receive/join path is unreachable on this QEMU guest (only
vtnet0/lo0, no WiFi radio, no wlan module loaded), so the bug cannot be
triggered end-to-end here. Per the per-PoC procedure, a deterministic
code-level harness that replicates the exact kernel struct layouts and the
exact buggy line is used to prove the primitive, and a binary-level
before/after on the actual shipped kernel proves the fix.
Mechanism (trigger β primitive β effect), path:line
ieee80211_node.c:848β join path:if (ni->ni_ies.tdma_ie != NULL) ieee80211_parse_tdma(ni, ni->ni_ies.tdma_ie);(guarded byIEEE80211_SUPPORT_TDMA).ieee80211_tdma.c:650-652β casts attacker IE directly toconst struct ieee80211_tdma_param *, fetchests = vap->iv_tdma.ieee80211_tdma.c:657β the buggy line:setbit(ts->tdma_inuse, tdma->tdma_slot);with no prior validation.sys/param.h:390β#define setbit(a,i) ((a)[(i)/NBBY] |= 1<<((i)%NBBY)),NBBY=8.ieee80211_tdma.h:73βuint8_t tdma_inuse[1];β a 1-byte array.ieee80211_tdma.h:54βtdma_slotisu_int8_t(0..255).
setbit therefore writes at &ts->tdma_inuse[0] + tdma_slot/8, i.e. struct
offset 8 + slot/8. Verified struct offsets (__packed-false, x86_64):
tdma_inuse@8, tdma_active@9, tdma_count@12, tdma_peer@16,
tdma_lastprint@24, tdma_fails@40, function pointers (tdma_newstate
etc.)@48.
- slot 0..7 β offset 8 (in-bounds)
- slot 8..15 β offset 9 (
tdma_active) - slot 32..39 β offset 12 (
tdma_count) - slot 64..71 β offset 16 (
tdma_peerbyte 0) - slot 248..255 β offset 39 (last byte of
tdma_lastprint)
Max reach is offset 39 (slot is u8 β 255/8=31). The function pointers at
offset 48 are NOT reachable β this bounds the primitive to TDMA state
corruption / protocol-DoS, with no RIP control / no uid0 path.
Secondary bug: because the IE length is never checked, reading
tdma->tdma_slot (offset 8), tdma_slotcnt, tdma_slotlen, tdma_bintval,
tdma_inuse from a deliberately short IE (the istdmaoui accept test only
requires ie[1] > 3) is a heap OOB read within the ies->data blob.
Harness proof (deterministic)
harness.c includes the verbatim kernel header and reproduces the exact
setbit(ts->tdma_inuse, tdma->tdma_slot) line. Decisive runs:
[Case 1] slot=64 -> setbit writes tdma_peer byte 0 (offset 16)
[before] ... tdma_peer=0xdeadbeefcafebabe ...
[UNFIXED] ... tdma_peer=0xdeadbeefcafebabf ...
-> tdma_peer changed 0xdeadbeefcafebabe -> 0xdeadbeefcafebabf : OOB WRITE CONFIRMED
[Case 3] slot=8 -> tdma_active[0] = 0x01 : OOB WRITE CONFIRMED (1 past the array)
[Case 4] short IE (len=6) -> tdma_slot read as 0xab (offset 8) : OOB READ CONFIRMED
[Case 5] FIXED rejects slot=64; tdma_peer UNCHANGED (0xdeadbeefcafebabe): FIX HOLDS
[Case 6] FIXED accepts slot=1 in-bounds; legit path preserved
Binary-level before/after (the fix is in the shipped kernel)
objdump -d --disassemble=ieee80211_parse_tdma:
#0 baseline (Thu Jul 2 06:02:54, sha baseline):
movzbl 0x8(%rsi),%ecx ; read tdma_slot, NO length check shr $0x3,%al ; and $0x1f,%eax or %dl,0x8(%rdi,%rax,1) ; setbit at variable offset 8+(slot/8) -> OOB
No cmp against ie[1]; no cmp against TDMA_MAXSLOTS.
#1 single-fix (Wed Jul 8 20:37:20, sha 8048ccf4β¦, built from fix.diff):
movzbl 0x1(%rsi),%r8d ; cmp $0x15,%r8b ; jbe reject ; len guard: ie[1]<22 -> reject movzbl 0x8(%rsi),%ecx ; cmp $0x1,%cl ; jbe accept ; slot guard: slot>=2 -> reject ... accept path: or %al,0x8(%rdx) ; setbit at FIXED offset 8 (slot provably 0..1)
Both guards compiled in; setbit now writes only at fixed offset 8 (the
compiler proved slot/8 β‘ 0 for slot β {0,1}). The OOB is eliminated.
Exploit chain / escalation
Not applicable β two valid hard blockers:
1. Reachability blocker: the live path requires a WiFi radio + TDMA vap
scanning/joining. This QEMU guest has only vtnet0/lo0; no wlan module
is loaded; the path is unreachable here. (On a real DragonFlyBSD TDMA host
β e.g. an ath/ath9k long-distance link β it is reachable by an
unauthenticated adjacent-network attacker via a spoofed beacon carrying the
forged TDMA vendor IE.)
2. Primitive ceiling: even on a real host, the OOB write is bounded to
struct offsets 9..39 (slot is u_int8_t, max index 31). It cannot reach
the function pointers at offset 48+, so there is no RIP control and no
path to uid=0. The realistic impact is TDMA link desynchronization /
state corruption (DoS) of the affected TDMA vap, plus a secondary OOB read
from a short IE.
Severity Medium (matches the finding): unauthenticated adjacent-network OOB write, deterministic once a forged beacon is selected for join, but bounded to TDMA state and requiring the niche TDMA mode.
PoC changes
- Added
harness.cβ a faithful userspace replica of the exact kernelstruct ieee80211_tdma_state/ieee80211_tdma_paramlayouts (verbatim header) and the exact buggysetbit(ts->tdma_inuse, tdma->tdma_slot)line (ieee80211_tdma.c:657), plus a FIXED-function variant mirroring the proposed guards. Drives crafted IEs (slot=64/255/8 OOB; short IE OOB read) and demonstrates the corruption and that the fix blocks it. - The original
inject_tdma_beacon.pyis kept as the live-frame injector for real WiFi hardware (not runnable on QEMU). - Added
fix.diffβ standalonegit apply-able unified diff adding the length check (ie[1] < sizeof(*tdma)-2) and slot range check (tdma->tdma_slot >= TDMA_MAXSLOTS) before thesetbit, mirroring the sibling guards at:536/:555.git apply --checkpasses. - Added
build.sh/run.shrepro scripts.
Fix validation (Phase 8) β VALIDATED
- Baseline (#0): harness Case 1 corrupts
tdma_peer(...babeβ...babf);ieee80211_parse_tdmain/boot/kernel/kernel.debughas no guards. - Applied
fix.diffto in-guest/usr/src, builtmake -j6 nativekernel KERNCONF=X86_64_GENERIC(fix_build.log,NK_DONE rc=0),make installkernel, rebooted. - Patched (#1,
Wed Jul 8 20:37:20, sha8048ccf4β¦): harness Case 5 rejects slot=64,tdma_peerunchanged;ieee80211_parse_tdmain the booted/boot/kernel/kernel.debugnow containscmp $0x15(length guard) andcmp $0x1(slot guard) before a fixed-offsetor %al,0x8(%rdx).
fix_status: fixed β the OOB write present in the #0 kernel is gone in the
1 single-fix kernel; legit in-range slots (0..1) still work.
Recommended fix
The fix in fix.diff matches the finding markdown's proposal (same two
guards, same IEEE80211_DISCARD_IE style as the sibling). One minor
refinement over the finding's diff: the slot-range reject branch also emits an
IEEE80211_DISCARD_IE (matching tdma_process_params:556-558) rather than a
bare return, for observability.
Environment
- Guest: DragonFly 6.5-DEVELOPMENT x86_64, gcc 8.3 (DragonFly).
- Interfaces:
vtnet0 lo0only β no WiFi radio; wlan module not loaded. ieee80211_parse_tdmais compiled directly into/boot/kernel/kernel(defaultX86_64_GENERIC), so the vulnerable code ships by default; only the live trigger path is absent on this guest.
Fix verification
fixedVALIDATED. fix.diff applied to in-guest /usr/src, built make -j6 nativekernel KERNCONF=X86_64_GENERIC (NK_DONE rc=0), make installkernel + reboot into #1 single-fix kernel. Binary-level proof (objdump -d --disassemble=ieee80211_parse_tdma /boot/kernel/kernel.debug): #0 baseline ieee80211_parse_tdma has NO guards -- 'movzbl 0x8(%rsi),%ecx; or %dl,0x8(%rdi,%rax,1)' writes setbit at variable offset 8+(slot/8) (OOB). #1 single-fix kernel adds 'cmp $0x15,%r8b; jbe reject' (ie[1]<22 length guard) and 'cmp $0x1,%cl; jbe accept' (slot>=2 guard) before a fixed-offset 'or %al,0x8(%rdx)' setbit (slot provably 0..1, so slot/8==0). The OOB write present in the shipped #0 kernel is eliminated in the #1 single-fix kernel; legit in-range slots (0..1) still work. (Live end-to-end run impossible -- no WiFi hardware on QEMU -- so validation is harness + binary-level before/after on the actual booted kernel.)
BEFORE (#0 baseline kernel) harness Case 1: '[UNFIXED] ... tdma_peer=0xdeadbeefcafebabf' (was 0xdeadbeefcafebabe) -> OOB WRITE CONFIRMED; disasm: 'or %dl,0x8(%rdi,%rax,1)' variable offset, no guards. AFTER (#1 single-fix kernel) harness Case 5: '[FIXED] rejected: tdma_slot=64 >= TDMA_MAXSLOTS=2' -> tdma_peer UNCHANGED (0xdeadbeefcafebabe == 0xdeadbeefcafebabe): FIX HOLDS; disasm: 'cmp $0x15,%r8b; jbe reject' + 'cmp $0x1,%cl; jbe accept' + 'or %al,0x8(%rdx)' fixed offset.
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_tdma.c:657
- sys/netproto/802_11/wlan/ieee80211_tdma.c:644
- sys/netproto/802_11/wlan/ieee80211_tdma.c:536
- sys/netproto/802_11/wlan/ieee80211_tdma.c:555
- sys/netproto/802_11/ieee80211_tdma.h:73
- sys/netproto/802_11/ieee80211_tdma.h:54
- sys/netproto/802_11/ieee80211_tdma.h:40
- sys/sys/param.h:390
- sys/netproto/802_11/wlan/ieee80211_node.c:848
Detail
Exploit chain
Not applicable (two valid hard blockers). (1) Reachability blocker: the live path requires a WiFi radio + TDMA vap scanning/joining; this QEMU guest has only vtnet0/lo0 and no wlan module, so no unprivileged local user can drive a crafted frame into ieee80211_parse_tdma here. (2) Primitive ceiling: even on a real TDMA host, the OOB write is bounded to struct offsets 9..39 (slot is u_int8_t, max index 31) and CANNOT reach the function pointers at offset 48+ -> no RIP control, no ucred/proc pointer corruption, no path to uid=0. Realistic impact is TDMA link desynchronization / state corruption (DoS) of the affected TDMA vap plus a secondary OOB read from a short IE. No chain file written (no write-capable primitive reachable from userspace).
Evidence (decisive lines)
[Case 1] slot=64 -> setbit writes tdma_peer byte 0 (offset 16)
[before] ... tdma_peer=0xdeadbeefcafebabe ...
[UNFIXED] ... tdma_peer=0xdeadbeefcafebabf ...
-> tdma_peer changed 0xdeadbeefcafebabe -> 0xdeadbeefcafebabf : OOB WRITE CONFIRMED
[Case 3] slot=8 -> tdma_active[0] = 0x01 : OOB WRITE CONFIRMED
[Case 4] short IE (len=6) -> tdma_slot=0xab : OOB READ CONFIRMED
Binary #0 kernel ieee80211_parse_tdma: movzbl 0x8(%rsi),%ecx; or %dl,0x8(%rdi,%rax,1) -- NO guards.
Binary #1 single-fix: cmp $0x15,%r8b; jbe reject (len); cmp $0x1,%cl; jbe accept (slot); or %al,0x8(%rdx) -- FIXED offset.
PoC changes
Added harness.c (faithful userspace replica of exact kernel struct layouts via the verbatim ieee80211_tdma.h header + the exact buggy setbit line at ieee80211_tdma.c:657; drives crafted IEs slot=64/255/8 OOB + short-IE OOB read, plus a FIXED-function variant). Added fix.diff (git-apply-able; adds ie[1] < sizeof(*tdma)-2 length guard and tdma_slot >= TDMA_MAXSLOTS range guard before setbit, mirroring the sibling guards at :536/:555). Added build.sh/run.sh repro scripts. Kept the original inject_tdma_beacon.py as the live-frame injector (needs real WiFi hardware). git apply --check passes.
Verified recommended fix
In ieee80211_parse_tdma (sys/netproto/802_11/wlan/ieee80211_tdma.c:649-657), add before setbit: (1) if (ie[1] < sizeof(*tdma) - 2) { IEEE80211_DISCARD_IE(...); return; } and (2) if (tdma->tdma_slot >= TDMA_MAXSLOTS) { IEEE80211_DISCARD_IE(...); return; } -- mirroring the sibling tdma_process_params guards at :536 and :555. The length check also closes the secondary heap OOB read of tdma_slot/slotcnt/slotlen/bintval/inuse from a deliberately short IE. Matches finding proposal (finding's slot-reject was a bare return; this emits IEEE80211_DISCARD_IE for parity with the sibling). Full git-apply-able diff in findings/poc/DF-0612/fix.diff.
Verdict
REPRODUCED at code level. ieee80211_parse_tdma (sys/netproto/802_11/wlan/ieee80211_tdma.c:657) executes setbit(ts->tdma_inuse, tdma->tdma_slot) with NO IE-length check and NO tdma_slot range check, unlike its sibling tdma_process_params which guards the same op at :536 and :555. tdma_inuse is uint8_t[1] (ieee80211_tdma.h:73) and tdma_slot is u_int8_t, so setbit (sys/sys/param.h:390) writes at struct offset 8+slot/8 -- OOB for any slot>=8, up to offset 39 (slot=255). Verified struct offsets (x86_64): tdma_active@9, tdma_count@12, tdma_peer@16, tdma_lastprint@24-39; function pointers start @48 and are NOT reachable (u8 idx caps at 31) -> state-corruption/DoS, no RIP control. The buggy code ships in the default GENERIC kernel (nm /boot/kernel/kernel shows ieee80211_parse_tdma as a T symbol). The LIVE 802.11 receive/join path (ieee80211_node.c:848) is UNREACHABLE on this QEMU guest: only vtnet0/lo0, no WiFi radio, no wlan module loaded. Per the per-PoC procedure, a deterministic code-level harness (harness.c) replicating the exact kernel structs + the exact buggy line proves the OOB: slot=64 flips tdma_peer 0xdeadbeefcafebabe->0xdeadbeefcafebabf, slot=8 sets tdma_active[0]=0x01, and a short IE (len=6) reads tdma_slot=0xab from beyond the IE body (OOB read). Binary proof on the shipped #0 kernel: ieee80211_parse_tdma disassembles to 'movzbl 0x8(%rsi),%ecx; or %dl,0x8(%rdi,%rax,1)' with NO cmp against ie[1] and NO cmp against TDMA_MAXSLOTS.
No comments yet.