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)
PoC verification
Evidence pack
findings/poc/DF-0712 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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. SeeVERDICT.mdfor 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.
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.
-
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 = 257bytes. - Sokmallocgets 257 bytes. -
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-byteie_lenheader consumes offsets 0-1, leavingie_dataonly 255 bytes (valid indicesie_data[0..254], i.e. allocation offsets2..256). -
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). -
Each "run" writes 3 bytes and advances
frm += 3(regdomain.c:307-310).nrunsis capped atIEEE80211_COUNTRY_MAX_BANDS = 84(regdomain.c:300). - runk(0-based) writesie_data[5+3k], ie_data[6+3k], ie_data[7+3k]. - run 83 (the 84th) writesie_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 (constant1,#channels).ie_data[256]= alloc offset 258 = OOB+2 (c->ic_maxregpower, attacker-controlled viaSIOCS80211ioctl channel setup).frmis nowie_data[257].
-
Odd-length pad writes OOB+3 (
regdomain.c:318-321):ie->len = frm - ie->cc = 255(odd) β*frm++ = 0writesie_data[257]= alloc offset 259 = OOB+3 (constant0).
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:
-
Slab-bucket rounding absorbs the overflow into intra-object padding.
kmalloc(257)rounds up viazoneindex()(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. WithINVARIANTSON (default GENERIC),kern_slaballoc.conly 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). -
The live path is unreachable on this guest and root-gated even with WiFi. - This guest has no WiFi radio (
ifconfig -lβvtnet0 lo0; nowlan/80211modules loaded), so noieee80211comexists andieee80211_alloc_countryie(called fromieee80211_add_countryiewhile 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 throughSIOCS80211, gated bycaps_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 exactieee80211_alloc_countryieallocation + IE-assembly loop (struct layouts copied verbatim fromieee80211.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.shrepro wrappers.
Recommended fix (fix.diff β supersedes finding proposal)
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.diffapplied to in-guest/usr/src(both#if/#elsebranches).make -j6 nativekernel KERNCONF=X86_64_GENERICβrc=0, clean build (fix_build.log).kernel.strippedinstalled to/boot/kernel/kernel(sha25629ad6efcβ¦, valid ELF, fresh BuildIDad9e59b1vs baselineb18d2eb8).- 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
fixedVALIDATED 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.
Confirmed kernel references
- sys/netproto/802_11/wlan/ieee80211_regdomain.c:248
- sys/netproto/802_11/wlan/ieee80211_regdomain.c:260
- sys/netproto/802_11/wlan/ieee80211_regdomain.c:281
- sys/netproto/802_11/wlan/ieee80211_regdomain.c:300
- sys/netproto/802_11/wlan/ieee80211_regdomain.c:307
- sys/netproto/802_11/wlan/ieee80211_regdomain.c:318
- sys/netproto/802_11/wlan/ieee80211_regdomain.c:321
- sys/netproto/802_11/ieee80211.h:961
- sys/netproto/802_11/ieee80211.h:973
- sys/netproto/802_11/ieee80211_var.h:109
- sys/netproto/802_11/wlan/ieee80211_ioctl.c:3472
- sys/kern/kern_slaballoc.c:652
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).
No comments yet.