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

tdma_isfull()/match_bss() heap out-of-bounds read via unvalidated tdma_ie slot count (up to ~30 bytes past the blob allocation)

Summary

ies_expand exposes se_ies.tdma_ie for any TDMA-OUI vendor IE with ie[1] >= 4 only. match_bss()'s AHDEMO+TDMA branch reads tdma_version (offset 7), tdma_slot (8), tdma_slotcnt (9) unvalidated, and tdma_isfull() scans isclr(tdma->tdma_inuse, slot) for slot down from the attacker-controlled slotcnt-1 although tdma_inuse[] is 1 byte - slotcnt=255 reads tdma_inuse[31] = IE byte 44, up to 30 bytes past the blob allocation when the IE is last. match_bss also dereferences vap->iv_tdma with no NULL check. Config-dependent reachability (AHDEMO+TDMA vap, root-created; TDMA compiled into X86_64_GENERIC). PROVEN with kernel-internal probes on the real code: identical beacons, only the heap bytes past the allocation differ (0x00 vs 0xff) -> candidate ACCEPTED (clear at slot 254 read from byte +44) vs REJECTED (FULL); GCC -Warray-bounds independently rejects tdma_inuse[31] as above the declared array bounds. No uid0 route (read-only oracle). Fix: require a complete parameters IE + clamp the scan to 8*nitems(tdma_inuse) + NULL-check iv_tdma.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3364 Β· 9 files
FileTypeDescriptionSize
fwscan.c β€” 29.5 KB view raw
Makefile β€” 263 B ↓ download
build.sh β€” 87 B view raw
build.log β€” 5.6 KB view raw
run.log β€” 2.5 KB view raw
README.md β€” 2.4 KB ↓ raw
fix.diff β€” 2.0 KB view raw
verdict.json β€” 3.8 KB view raw
manifest.json β€” 684 B view raw

DF-3364 β€” tdma_isfull()/match_bss() heap OOB read via unvalidated tdma_ie slot count

File: sys/netproto/802_11/wlan/ieee80211_scan_sta.c (pass 2)

What

ieee80211_ies_expand() exposes se_ies.tdma_ie for any vendor IE carrying the TDMA OUI β€” istdmaoui() only requires ie[1] > 3 (ieee80211_input.h:118-121). match_bss()'s AHDEMO+TDMA branch (ieee80211_scan_sta.c:1016-1028) then reads tdma_version (IE offset 7), tdma_slot (8), tdma_slotcnt (9) and calls tdma_isfull() (:959-969), which scans isclr(tdma->tdma_inuse, slot) for slot down from the attacker-controlled slotcnt-1 although tdma_inuse[] is 1 byte (8 slots) and the whole IE may be as short as 6 bytes.

With slotcnt = 255 the reads reach tdma_inuse[31] = IE byte 44 = up to 30 bytes past the end of the exactly-sized blob allocation when the TDMA IE is the blob's last IE. tdma->tdma_version (offset 7) is already past a minimal OUI-only IE.

Reproduced (instrumented kernel #1 β€” stock logic + kprintf probes only)

AHDEMO+TDMA vap (ifconfig wlan create wlandev fws0 wlanmode tdma up; TDMA + MESH are compiled into X86_64_GENERIC, sys/config/X86_64_GENERIC:257). Beacon C: TDMA master (slot 0, version 2 == vap's), inuse[0]=0xff (all 8 legal slots busy), slotcnt=255, TDMA IE last in a 64-byte blob:

  • groom 0x00 past the allocation: kernel tdma_isfull printed inuse[31]=00 β†’ clear at slot 254 (byte +44) β†’ fail=0x0 β†’ C ACCEPTED β€” the acceptance came from an out-of-bounds byte.
  • groom 0xff: inuse[31]=ff β†’ FULL β†’ fail=0x1000 (MATCH_TDMA_NOSLOT) β†’ C REJECTED.
  • control slotcnt=8: decision from in-bounds bytes only.

GCC itself corroborates the class: instrumenting the read as tdma_inuse[31] trips -Werror=array-bounds ("array subscript 31 is above array bounds of const u_int8_t[1]").

Threat

Requires an AHDEMO+TDMA vap (root-created, TDMA-capable driver) β€” config-dependent reachability, but unauthenticated beacon input. Impact: heap OOB read ≀ ~30 bytes whose value flips candidate acceptance (same oracle grade as DF-3363).

Build / run

Same harness as DF-3363 (fwscan.ko): sysctl kern.fwscan_test=11|12|14|17 with the tdma vap. Fix validated together with DF-3363 (see ../DF-3363/fix_validation.log): post-fix all short TDMA IEs are rejected (NOIE), a full-length valid TDMA IE (test 17) is accepted.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

with fix.diff, the 12-byte TDMA IE variants (11/12/14) are all rejected (MATCH_TDMA_NOIE) regardless of heap groom; the full-length valid IE (17) is accepted. No OOB-dependent behavior remains.

findings/poc/DF-3363/fix_validation.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2 Fri Sep 11 12:44:59 UTC 2026

Confirmed kernel references

Detail

Exploit chain

unauthenticated beacon to an AHDEMO+TDMA vap (root-created, driver with C_TDMA) -> short TDMA-OUI vendor IE saved verbatim in the blob -> ies_expand exposes tdma_ie (len>=4 only) -> match_bss reads tdma_version/slot past minimal IEs -> tdma_isfull scans 248 attacker-directed bitmap bytes beyond the allocation -> OOB heap bytes decide candidate acceptance. Oracle-grade; no write primitive.

Evidence (decisive lines)

["run.log: 'DBG isfull: ... slotcnt=255 inuse[0]=ff inuse[31]=00 / clear at slot 254 (byte +44)' -> ACCEPTED vs 'inuse[31]=ff / FULL / fail=0x1000' -> REJECTED", '../DF-3363/fix_validation.log: post-fix TDMA matrix (17 accepted, 11/12/14 rejected as malformed)', 'build.log excerpt in VERDICT: gcc -Werror=array-bounds rejects tdma_inuse[31] as above array bounds']

PoC changes

same harness as DF-3363 plus: TDMA vap must be created via 'wlanmode tdma' (ifconfig clone flag IEEE80211_CLONE_TDMA); C's rates must include 11 Mbps (the TDMA vap pins ucastrate=2*11 via TDMA_TXRATE_11B_DEFAULT); IEEE80211_SCAN_NOJOIN used so the selection verdict is observable without driving the TDMA join state machine (which hangs on the fake radio); the flaky early runs were background-scan se_notseen bumps, fixed by re-injecting C right before scan_end.

Verified recommended fix

require tdma_len >= sizeof(struct ieee80211_tdma_param)-2 before reading fields, clamp slotcnt to 8*nitems(tdma_inuse); see fix.diff.

Verdict

tdma_isfull() (ieee80211_scan_sta.c:965) indexes tdma_inuse[slot>>3] for slot up to the attacker-controlled slotcnt-1 (255) although the array is one byte inside an IE that can be as short as 6 bytes; with the TDMA IE last in a 64-byte blob the kernel read tdma_inuse[31] = IE byte 44 = 30 bytes past the heap allocation, and that byte's content flipped the candidate decision: 0x00 groom -> 'clear at slot 254' -> fail=0x0 -> C ACCEPTED; 0xff groom -> FULL -> fail=0x1000 -> C REJECTED. Proven with kprintf instrumentation inside the real tdma_isfull/match_bss on an otherwise-stock kernel (probe-only patch); gcc -Warray-bounds independently flags the same index as out-of-bounds. Post-fix (fix.diff kernel #2) all short TDMA IEs are rejected as malformed and the full-length valid IE (test 17) is accepted.