β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0612

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 an ath/ath9k long-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_tdma via ieee80211_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, type 01, subtype 01, version 2) with tdma_slot set to an out-of-range value. When the victim selects and joins the forged BSS, ieee80211_parse_tdma fires once, OR-writing a single chosen bit up to 32 bytes past ts->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), and tdma_lastprint (ppsratecheck state). 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 uint8 index 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.

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

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-0612 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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 with tdma_slot=64 (OOB). When a DragonFlyBSD TDMA-mode vap selects this forged BSS as a join candidate, ieee80211_parse_tdma() runs setbit(ts->tdma_inuse, 64) which writes at ts->tdma_inuse[8] β€” 8 bytes past the 1-byte tdma_inuse[] array β€” corrupting the tdma_peer pointer in struct ieee80211_tdma_state.

Build & run

There are two artifacts:

  1. harness.c β€” a self-contained, deterministic userspace harness (runnable on this guest) that replicates the exact kernel struct layouts (verbatim header) and the buggy setbit() line. It proves the OOB write for several crafted IEs (slot=64 hits tdma_peer; slot=8 hits tdma_active; slot=255 hits tdma_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.

  1. 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_inuse mask,
  • 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_MAXSLOTS guards that tdma_process_params() (:536,:555) already has. Verify with git apply findings/poc/DF-0612/fix.diff.
VERDICT.md verdict full narrative: mechanism, harness proof, binary before/after, fix validation
↓ download raw

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 by IEEE80211_SUPPORT_TDMA).
  • ieee80211_tdma.c:650-652 β€” casts attacker IE directly to const struct ieee80211_tdma_param *, fetches ts = 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_slot is u_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_peer byte 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 kernel struct ieee80211_tdma_state/ieee80211_tdma_param layouts (verbatim header) and the exact buggy setbit(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.py is kept as the live-frame injector for real WiFi hardware (not runnable on QEMU).
  • Added fix.diff β€” standalone git apply-able unified diff adding the length check (ie[1] < sizeof(*tdma)-2) and slot range check (tdma->tdma_slot >= TDMA_MAXSLOTS) before the setbit, mirroring the sibling guards at :536/:555. git apply --check passes.
  • Added build.sh/run.sh repro scripts.

Fix validation (Phase 8) β€” VALIDATED

  • Baseline (#0): harness Case 1 corrupts tdma_peer (...babeβ†’...babf); ieee80211_parse_tdma in /boot/kernel/kernel.debug has no guards.
  • Applied fix.diff to in-guest /usr/src, built make -j6 nativekernel KERNCONF=X86_64_GENERIC (fix_build.log, NK_DONE rc=0), make installkernel, rebooted.
  • Patched (#1, Wed Jul 8 20:37:20, sha 8048ccf4…): harness Case 5 rejects slot=64, tdma_peer unchanged; ieee80211_parse_tdma in the booted /boot/kernel/kernel.debug now contains cmp $0x15 (length guard) and cmp $0x1 (slot guard) before a fixed-offset or %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.

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 lo0 only β€” no WiFi radio; wlan module not loaded.
  • ieee80211_parse_tdma is compiled directly into /boot/kernel/kernel (default X86_64_GENERIC), so the vulnerable code ships by default; only the live trigger path is absent on this guest.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 20:37:20 UTC 2026 (sha256 /boot/kernel/kernel = 8048ccf4e062f5947daa029f7f44d6f7c9df0d5d4cc30ce6a5c56f00bb1df936)

Confirmed kernel references

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.