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

Undersized kmalloc in ieee80211_alloc_countryie: 3-byte heap overflow when channel list has >=84 distinct power runs

Summary

ieee80211_alloc_countryie :248 kmalloc(IEEE80211_COUNTRY_MAX_SIZE=257) but struct ieee80211_appie has 2-byte ie_len header leaving ie_data only 255 bytes (alloc offsets 2-256). Country IE header ie/cc uses ie_data[0-4] frm starts ie_data[5]. With 84 runs (nruns cap=IEEE80211_COUNTRY_MAX_BANDS=84 checked :300) last run writes ie_data[254-256]=alloc[256-258]: alloc[257]=nchan(1) OOB+1 alloc[258]=ic_maxregpower attacker-controlled OOB+2. ie->len=255 odd triggers pad :319-321 *frm++=0 at ie_data[257]=alloc[259] OOB+3. Total 3-byte heap overflow into adjacent M_80211_NODE_IE slab object with one attacker-controlled byte. Trigger: SIOCS80211 IEEE80211_IOC_REGDOMAIN ioctl with ~90 channels non-consecutive ic_ieee distinct non-zero ic_maxregpower to force 84 new runs then bring up hostap vap to construct beacon. Gated by caps_priv_check_self(SYSCAP_NONET_WIFI) ieee80211_ioctl.c:3472. Fix: kmalloc(sizeof(struct ieee80211_appie)+IEEE80211_COUNTRY_MAX_SIZE+1) = 260 bytes.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0712 Β· 14 files
FileTypeDescriptionSize
harness.c trigger-source deterministic canary-guarded replication of ieee80211_alloc_countryie allocation+assembly; proves 3-byte OOB write at 257-byte alloc 7.4 KB view raw
harness_fixed.c exploit-chain same harness with alloc=260 (the fix); proves 0 OOB bytes (before/after code-level validation) 7.5 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 187 B view raw
run.sh run-script ./harness 135 B view raw
build.log build-log harness build output 65 B view raw
run.log run-log decisive harness run: 3-byte OOB write detected 914 B view raw
fix_harness_run.log run-log fixed-variant harness run: no overflow 462 B view raw
fix.diff suggested-fix kmalloc(sizeof(appie)+MAX_SIZE+1) at regdomain.c:248 & :251 628 B view raw
fix_build.log build-log single-fix kernel build (make -j6 nativekernel), rc=0 5.6 MB ↓ download
env.txt environment uname, cc version, no wifi modules, ifconfig 310 B view raw
VERDICT.md verdict full narrative: mechanism, impact, fix validation 9.2 KB ↓ raw
README.md readme build/run/expected + why a harness 1.9 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 build/run/expected + why a harness
↓ download raw

DF-0712 β€” reproduce

Build

./build.sh        # cc -O2 -Wall -o harness harness.c

Run

./run.sh          # ./harness

Expected (bug present β€” unpatched #0 kernel's logic)

The harness replicates ieee80211_alloc_countryie (sys/netproto/802_11/wlan/ieee80211_regdomain.c:248-326) with a canary-guarded allocation. With 84 channel power-runs (the IEEE80211_COUNTRY_MAX_BANDS cap), it reports a 3-byte heap OOB write past the 257-byte allocation:

  OOB+1: alloc[257] = 0x01   # #channels, constant
  OOB+2: alloc[258] = <ic_maxregpower>   # ATTACKER-CONTROLLED byte
  OOB+3: alloc[259] = 0x00   # odd-length IE pad byte, constant
RESULT: 3-byte heap OOB write detected past the 257-byte allocation.

Why a harness (not a live in-kernel PoC)

The live ieee80211_alloc_countryie path requires a WiFi radio (it is called while assembling a hostap beacon). This audit guest has no WiFi hardware (ifconfig -l β‡’ vtnet0 lo0; no wlan/80211 modules), so the path is unreachable at runtime. The harness replicates the exact allocation + IE-assembly logic byte-for-byte and proves the off-by-3 deterministically.

After the fix (harness_fixed.c, alloc = 260 bytes)

RESULT: no overflow detected (bug not present).

Notes

  • On the default GENERIC slab allocator, kmalloc(257) rounds up to a 288-byte bucket (31 bytes of intra-object tail padding), so the 3 overflow bytes land in padding β€” no adjacent object is corrupted and there is no panic. The bug is a real correctness/defense-in-depth defect; it would be a true cross-object corruption on a tighter allocator. See VERDICT.md for the full impact analysis.
  • SIOCS80211 (the ioctl to set the channel/regdomain trigger) is root-gated (caps_priv_check_self(SYSCAP_NONET_WIFI), ieee80211_ioctl.c:3472), so there is no unprivileged path to the sink even with WiFi hardware.
VERDICT.md verdict full narrative: mechanism, impact, fix validation
↓ download raw

DF-0712 β€” Undersized kmalloc in ieee80211_alloc_countryie: 3-byte heap overflow

Verdict

REPRODUCED (code-level). The off-by-3 heap overflow is real and mathematically certain; it is confirmed by a deterministic canary-guarded harness that replicates the exact allocation + IE-assembly logic of ieee80211_alloc_countryie (sys/netproto/802_11/wlan/ieee80211_regdomain.c:248-326). The live in-kernel path is unreachable on this guest (no WiFi radio: only vtnet0/lo0, no wlan/80211 modules), so live panic/corruption cannot be observed; the bug is proven at the code/harness level. The fix is validated: a single-fix kernel (#1, the +3 byte allocation) compiles cleanly, boots, and the before/after harness shows the overflow is present at the original 257-byte alloc and gone at the fixed 260-byte alloc.

Mechanism (trigger β†’ primitive β†’ effect), every hop cited

The function ieee80211_alloc_countryie allocates a buffer sized only for the country-IE content and then writes the IE into ie_data[] plus the struct ieee80211_appie 2-byte header, overflowing the tail by exactly 3 bytes.

  1. Allocation β€” regdomain.c:248: aie = kmalloc(IEEE80211_COUNTRY_MAX_SIZE, M_80211_NODE_IE, M_INTWAIT|M_ZERO) - IEEE80211_COUNTRY_MAX_SIZE (ieee80211.h:973) = sizeof(struct ieee80211_country_ie) + 3*(IEEE80211_COUNTRY_MAX_BANDS-1) = 8 + 3*83 = 257 bytes. - So kmalloc gets 257 bytes.

  2. ie_data[] lives inside that allocation, after the 2-byte header β€” regdomain.c:260: ie = (struct ieee80211_country_ie *) aie->ie_data. - struct ieee80211_appie (ieee80211_var.h:109) = { uint16_t ie_len; uint8_t ie_data[]; } β‡’ the 2-byte ie_len header consumes offsets 0-1, leaving ie_data only 255 bytes (valid indices ie_data[0..254], i.e. allocation offsets 2..256).

  3. Country-IE layout inside ie_data (ieee80211.h:961): ie@ie_data[0], len@ie_data[1], cc[3]@ie_data[2..4], band[0]@ie_data[5..7]. frm = &ie->band[0] = ie_data[5] (regdomain.c:281).

  4. Each "run" writes 3 bytes and advances frm += 3 (regdomain.c:307-310). nruns is capped at IEEE80211_COUNTRY_MAX_BANDS = 84 (regdomain.c:300). - run k (0-based) writes ie_data[5+3k], ie_data[6+3k], ie_data[7+3k]. - run 83 (the 84th) writes ie_data[254], ie_data[255], ie_data[256].

    • ie_data[254] = alloc offset 256 = last valid byte (c->ic_ieee).
    • ie_data[255] = alloc offset 257 = OOB+1 (constant 1, #channels).
    • ie_data[256] = alloc offset 258 = OOB+2 (c->ic_maxregpower, attacker-controlled via SIOCS80211 ioctl channel setup).
    • frm is now ie_data[257].
  5. Odd-length pad writes OOB+3 (regdomain.c:318-321): ie->len = frm - ie->cc = 255 (odd) β‡’ *frm++ = 0 writes ie_data[257] = alloc offset 259 = OOB+3 (constant 0).

Net: 3-byte heap write past the requested 257-byte allocation. The trigger condition (84 distinct channel power-runs) is exactly reachable at the IEEE80211_COUNTRY_MAX_BANDS cap β€” feed 84 channels with non-consecutive ic_ieee and distinct ic_maxregpower and the loop produces 84 runs. Harness output (run.log):

OOB+1: alloc[257] = 0x01 (was 0xcd)   # #channels, constant
OOB+2: alloc[258] = 0xf3 (was 0xcd)   # ic_maxregpower of run 83 = 0xA0+83, ATTACKER-CONTROLLED
OOB+3: alloc[259] = 0x00 (was 0xcd)   # odd-IE pad byte, constant

Realistic impact (honest ceiling)

Two independent factors keep this bug from being exploitable on the default GENERIC kernel, both verified:

  1. Slab-bucket rounding absorbs the overflow into intra-object padding. kmalloc(257) rounds up via zoneindex() (kern_slaballoc.c:638-656): n>=256 && n<512 β‡’ (n+31)&~31 = (257+31)&~31 = 288-byte slab object (31 bytes of tail padding). The 3 overflow bytes land at allocation offsets 257/258/259 β€” all inside the 288-byte object's unused tail. No adjacent slab object is touched, so there is no cross-object corruption, no function-pointer/ucred overwrite target reachable. With INVARIANTS ON (default GENERIC), kern_slaballoc.c only poisons/checked free chunks (WEIRD_ADDR / chunk_mark_free); the tail of an allocated-but-oversized object is neither poisoned nor checked, so the overflow is silent (no KASSERT, no panic).

  2. The live path is unreachable on this guest and root-gated even with WiFi. - This guest has no WiFi radio (ifconfig -l β‡’ vtnet0 lo0; no wlan/80211 modules loaded), so no ieee80211com exists and ieee80211_alloc_countryie (called from ieee80211_add_countryie while building a hostap beacon, ieee80211_output.c:2153) can never run. - Even with WiFi hardware, setting up the channel list / regdomain that produces 84 runs goes through SIOCS80211, gated by caps_priv_check_self(SYSCAP_NONET_WIFI) (ieee80211_ioctl.c:3472) β€” a root capability. An unprivileged user cannot craft the trigger.

Bottom line: a confirmed off-by-3 heap write (real correctness/defense-in-depth bug), but on the current allocator + this guest there is no demonstrated runtime corruption, no panic, no info leak, no privilege boundary crossed. Realistic impact ceiling: latent correctness bug that would become a real cross-object corruption on a tighter allocator or if M_80211_NODE_IE ever gained a custom zone. Severity Medium is defensible for defense-in-depth but the practical exploitability on default GENERIC is nil.

Exploit chain

Not pursued beyond primitive characterization — and validly so, because this is a genuine hard-blocker case (see Phase 6 valid blockers): - The write is reachable only from a root-gated ioctl (SYSCAP_NONET_WIFI, ieee80211_ioctl.c:3472) on hardware this guest lacks — there is no unprivileged path to the sink, so there is no privilege boundary to cross (root→kernel is game-over by definition). - Independently, on the current slab allocator the 3 bytes land in intra-object padding (kmalloc(257)→288-byte bucket), so even if reached they cannot corrupt an adjacent victim object (function pointer / ucred / refcount) — the corruption target required for any escalation does not exist here. There is therefore no grooming/victim/forge chain to develop; the primitive is confirmed but inert on this configuration. (No exploit.c written — a chain would have no victim object to corrupt and no unprivileged trigger.)

PoC changes

  • Authored harness.c β€” a deterministic, canary-guarded userspace replication of the exact ieee80211_alloc_countryie allocation + IE-assembly loop (struct layouts copied verbatim from ieee80211.h:961 / ieee80211_var.h:109). Feeds 84 channels forcing 84 runs and reports exactly which OOB bytes (1..3) are clobbered and with what values. Used because the live WiFi path is unreachable on this guest.
  • Authored harness_fixed.c β€” identical harness with the allocation changed to the fixed size (sizeof(appie)+MAX_SIZE+1 = 260). Proves 0 OOB bytes after the fix (the before/after code-level validation, since live functional testing is impossible).
  • Authored fix.diff β€” minimal one-logical-change fix (see below).
  • Wrote build.sh / run.sh repro wrappers.

sys/netproto/802_11/wlan/ieee80211_regdomain.c:248 (and the #else-branch twin at :251): allocate room for the struct ieee80211_appie 2-byte header and the worst-case odd-IE pad byte, matching how ieee80211_ioctl.c:2296 sizes appie allocations (sizeof(struct ieee80211_appie) + <datasize>):

-   aie = kmalloc(IEEE80211_COUNTRY_MAX_SIZE, M_80211_NODE_IE,
+   aie = kmalloc(sizeof(struct ieee80211_appie) + IEEE80211_COUNTRY_MAX_SIZE + 1, M_80211_NODE_IE,
        M_INTWAIT | M_ZERO);

2 + 257 + 1 = 260 bytes β‡’ ie_data gets 258 bytes (indices 0..257); the last write (the pad byte at ie_data[257]) is now in-bounds. The +1 covers the odd-length pad written at regdomain.c:321 in the worst case (84 runs β‡’ ie->len odd β‡’ pad).

Fix validation (Phase 8)

kernel harness alloc result
before #0 unpatched baseline (6.5-DEVELOPMENT #0, Jul 2) 257 3-byte OOB write (OOB+1=0x01, OOB+2=ic_maxregpower attacker-controlled, OOB+3=0x00)
after #1 single-fix kernel (6.5-DEVELOPMENT #1, Jul 8 22:03) 260 0 OOB bytes
  • fix.diff applied to in-guest /usr/src (both #if/#else branches).
  • make -j6 nativekernel KERNCONF=X86_64_GENERIC β‡’ rc=0, clean build (fix_build.log).
  • kernel.stripped installed to /boot/kernel/kernel (sha256 29ad6efc…, valid ELF, fresh BuildID ad9e59b1 vs baseline b18d2eb8).
  • Booted to 6.5-DEVELOPMENT #1 Wed Jul 8 22:03:09 UTC 2026, guest healthy.
  • Functional after-test via harness (live path unreachable: no WiFi) β‡’ fixed-variant reports no overflow.

fix_status: fixed β€” bad behavior (3-byte OOB) present before, absent after; single-fix kernel compiles and boots. (Live in-kernel functional test impossible on this guest β€” no WiFi radio β€” but the deterministic harness provides a clean before/after and the patched kernel source reads the corrected allocation.)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix. fix.diff applied to in-guest /usr/src (both #if/#else branches); make -j6 nativekernel KERNCONF=X86_64_GENERIC => rc=0 clean (fix_build.log); kernel.stripped installed to /boot/kernel/kernel; booted to 6.5-DEVELOPMENT #1 (Jul 8 22:03, sha256 29ad6efc, BuildID ad9e59b1 vs baseline b18d2eb8), guest healthy. Because the live ieee80211_alloc_countryie path is unreachable on this guest (no WiFi radio), functional before/after is demonstrated by the deterministic harness: baseline harness (alloc=257) reports '3-byte heap OOB write detected' (OOB+1=0x01, OOB+2=0xf3 attacker-controlled, OOB+3=0x00); fixed harness (alloc=260) reports 'no overflow detected (bug not present)'. The 3-byte OOB is present before the fix and absent after => fix closes the bug. fix_patched_reproduced=false reflects that the live in-kernel path cannot be exercised (no WiFi), not that the fix failed.

BEFORE (unpatched #0 kernel logic, harness alloc=257): OOB+1: alloc[257]=0x01, OOB+2: alloc[258]=0xf3 (ic_maxregpower, attacker-controlled), OOB+3: alloc[259]=0x00 => '3-byte heap OOB write detected past the 257-byte allocation'. AFTER (single-fix #1 kernel source, harness alloc=260): 'highest ie_data index written=257' (in-bounds for 260-byte alloc), 'RESULT: no overflow detected (bug not present)'. Single-fix kernel build rc=0; boots to #1 sha256 29ad6efc.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 22:03:09 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 /boot/kernel/kernel = 29ad6efc5400dda794a05dd23289750406695cfee868f4a28b2d96e9caf1e9a4)

Confirmed kernel references

Detail

Exploit chain

No escalation chain developed -- valid hard blocker (Phase 6). (1) The write is reachable only from a root-gated ioctl (SYSCAP_NONET_WIFI, ieee80211_ioctl.c:3472) on WiFi hardware this guest lacks -- there is NO unprivileged path to the sink, so no privilege boundary to cross (root->kernel is game-over by definition). (2) Independently, on the current slab allocator kmalloc(257)->288-byte bucket, so the 3 bytes land in intra-object padding (31 bytes of tail slack) and cannot corrupt an adjacent victim object (no function-pointer/ucred/refcount overwrite target exists) -- the corruption target required for any escalation is absent. The primitive is confirmed (3-byte write, OOB+2 attacker-controlled) but inert on default GENERIC. No exploit.c written: a chain would have no victim object to corrupt and no unprivileged trigger. This is the 'root-only reachability' + 'primitive cannot reach a victim object on this allocator' valid-blocker combination.

Evidence (decisive lines)

harness run.log (deterministic): alloc size=257, ie_data avail=255 (indices 0..254), nruns=84, highest ie_data index written=257, then OOB+1: alloc[257]=0x01, OOB+2: alloc[258]=0xf3 (=ic_maxregpower of run 83=0xA0+83, ATTACKER-CONTROLLED), OOB+3: alloc[259]=0x00 (pad) => '3-byte heap OOB write detected past the 257-byte allocation'. Fixed-variant harness (alloc=260): 'no overflow detected'. Single-fix kernel: make -j6 nativekernel rc=0; booted to 6.5-DEVELOPMENT #1 Wed Jul 8 22:03:09 UTC 2026 (sha256 29ad6efc..., BuildID ad9e59b1 vs baseline b18d2eb8). Live path unreachable: ifconfig -l => vtnet0 lo0, no wlan/80211 modules.

PoC changes

Authored findings/poc/DF-0712/harness.c (deterministic canary-guarded replication of ieee80211_alloc_countryie allocation+IE-assembly, struct layouts verbatim from ieee80211.h:961/ieee80211_var.h:109; feeds 84 channels to force 84 runs at the MAX_BANDS cap, reports OOB bytes 1-3 with values). Authored harness_fixed.c (identical, alloc=260, proves 0 OOB after the fix). Authored fix.diff (kmalloc(sizeof(appie)+MAX_SIZE+1)=260 at regdomain.c:248 & :251). Wrote build.sh/run.sh/VERDICT.md/README.md/manifest.json. No live in-kernel PoC possible (no WiFi hardware).

Verified recommended fix

At sys/netproto/802_11/wlan/ieee80211_regdomain.c:248 (DragonFly kmalloc branch) and :251 (FreeBSD #else twin), change the allocation from IEEE80211_COUNTRY_MAX_SIZE to sizeof(struct ieee80211_appie) + IEEE80211_COUNTRY_MAX_SIZE + 1 (=2+257+1=260), accounting for the 2-byte ie_len header and the worst-case odd-IE pad byte at :321 -- matching how ieee80211_ioctl.c:2296 sizes appie allocations. Full git-apply-able diff in findings/poc/DF-0712/fix.diff. Supersedes the finding proposal (same intent, adds the +1 pad byte the proposal also specified).

Verdict

REPRODUCED at the code level. The off-by-3 heap overflow in ieee80211_alloc_countryie is real and mathematically certain: kmalloc(IEEE80211_COUNTRY_MAX_SIZE=257) at regdomain.c:248 leaves ie_data[] only 255 bytes (the 2-byte ieee80211_appie.ie_len header consumes offsets 0-1), but with 84 channel power-runs (the IEEE80211_COUNTRY_MAX_BANDS cap, :300) the IE assembly writes up to ie_data[257]=alloc[259] -- a 3-byte OOB write (OOB+1=0x01 nchan constant, OOB+2=ic_maxregpower ATTACKER-CONTROLLED via SIOCS80211, OOB+3=0x00 odd-IE pad at :321). A canary-guarded harness replicating the exact allocation+assembly loop proves it deterministically (run.log). The live in-kernel path is UNREACHABLE on this guest (no WiFi radio: ifconfig -l => vtnet0 lo0, no wlan/80211 modules), so no live panic/corruption can be observed. Honest impact ceiling: on the default GENERIC slab allocator kmalloc(257) rounds up via zoneindex() (kern_slaballoc.c:652) to a 288-byte bucket with 31 bytes of intra-object tail padding, so the 3 overflow bytes land INSIDE the same object's unused tail -- no adjacent slab object is corrupted, INVARIANTS does not check intra-object padding (no KASSERT/panic), and SIOCS80211 is root-gated (SYSCAP_NONET_WIFI, ieee80211_ioctl.c:3472). Net: a confirmed correctness/defense-in-depth heap OOB write that is inert on this configuration (would be a true cross-object corruption on a tighter allocator or a custom M_80211_NODE_IE zone).