# DF-0732 — TOCTOU in `wlan_acl` `acl_getioctl` MACCMD_LIST

## Verdict

**REPRODUCED** — heap OOB write (grow race) **and** uninitialized-heap info leak
(shrink race), confirmed at **two** levels:

1. **Deterministic userspace harness** (`harness.c`) — transcribes
   `acl_getioctl` MACCMD_LIST verbatim with a controlled interleaving point
   between the unlocked `as_nacls` read and `ACL_LOCK`. Prints
   `GROW RACE OOB WRITE CONFIRMED` and `SHRINK RACE UNINIT LEAK CONFIRMED`.
2. **Real-kernel object-level harness** (`harness_mod.ko` + `trigger.c` /
   `leakcheck.c`) — exercises the **actual** `acl_getioctl` via a fake vap +
   `/dev/df0732`, racing `iac_add`/`iac_flush` kthreads against the LIST ioctl.
   Produces a **real kernel panic** `acl_getioctl+0xd6` (grow-race OOB write
   faulting the page) and live shrink-race heap-residue leaks.

**Fix VALIDATED** (Phase 8): the `fix.diff` (lock-before-read + `M_ZERO` +
bounded foreach) rebuilds `wlan_acl.ko` clean (`-Werror`) and **eliminates both
manifestations** in the real kernel — no panic, zero `WEIRD_ADDR` residue.

**Runtime reachability note (the realistic-threat caveat):** the live 802.11
ACL ioctl path requires a `wlan(4)` vap on a parent wifi radio. This KVM audit
guest has **no wifi radio** (`ifconfig -l` = `vtnet0 lo0`; no `ath`/`iwm`/`iwn`;
`wlan_acl` is a loadable KLD, not in `X86_64_GENERIC`). So an unprivileged
runtime trigger is impossible **on this guest**; the bug is proven at the
object/harness level (real `acl_getioctl` panic + leak) and via deterministic
transcription. On a **wifi-equipped host** with a vap, a local user could race
`SIOCS80211 ADDMAC` vs `SIOCG80211 MACCMD_LIST` for OOB write / leak. This is
the same harness-precedent class as DF-0393/0594/0616/0753/0754.

## The bug — line-by-line (`sys/netproto/802_11/wlan_acl/ieee80211_acl.c`)

`acl_getioctl` handles `IEEE80211_MACCMD_LIST` (`:312-340`):

| Line | Code | Problem |
|------|------|---------|
| 313  | `space = as->as_nacls * IEEE80211_ADDR_LEN;` | **UNLOCKED read** of `as_nacls`. `ACL_LOCK` is NOT held here. |
| 314-317 | `if (ireq->i_len == 0) { i_len = space; return 0; }` | size-probe path (also uses the unlocked `space`). |
| 319-320 | `ap = kmalloc(space, M_TEMP, M_INTWAIT);` | **NO `M_ZERO`** — buffer is uninitialized heap. |
| 327  | `i = 0;` | |
| 328  | `ACL_LOCK(as);` | **Lock taken only NOW** — after the size read AND after kmalloc. |
| 329-332 | `TAILQ_FOREACH(acl, &as->as_list, acl_list) { ADDR_COPY(ap[i++].ml_macaddr, acl->acl_macaddr); }` | writes **every** current list entry; **not bounded** by `space/ADDR_LEN`. |
| 333  | `ACL_UNLOCK(as);` | |
| 334-338 | `copyout(ap, ireq->i_data, ...)` | ships `ap` to userland. |

`ACL_LOCK`/`ACL_UNLOCK` are real `lockmgr(&as->as_lock, LK_EXCLUSIVE)` locks
(`ieee80211_dragonfly.h:606/619`). `acl_add` (`:199-215`) and `_acl_free`
(`:150-159`) both mutate `as_nacls`/`as_list` **under** `ACL_LOCK`. So between
the unlocked read at `:313` and the lock at `:328`, a concurrent ADDMAC/DELMAC
can change the list out from under the lister.

### Grow race → heap OOB write (CWE-787)
Thread A (lister) reads `as_nacls = N` at `:313`, allocates `N*6` bytes at
`:319`. Thread B (adder) inserts K MACs under the lock. Thread A takes the lock
at `:328` and `TAILQ_FOREACH` writes `N+K` entries into the `N*6`-byte buffer:
`ap[N..N+K-1]` are written `K*6` bytes past the end. The MAC bytes are
attacker-controlled (`SIOCS80211 ADDMAC` takes the MAC from userland). Real-kernel
proof: `Fatal trap 12 ... supervisor write data, page not present ... Stopped at
acl_getioctl+0xd6: movw %si,0x4(%rcx)` — the OOB write faulted into an unmapped
page.

### Shrink race → uninitialized-heap info leak (CWE-908)
Thread A reads `as_nacls = N`, allocates `N*6` bytes (no `M_ZERO`). Thread B
(flusher) clears the list to `N-K`. Thread A locks, `TAILQ_FOREACH` writes only
`N-K` entries; the tail `K` slots `ap[N-K..N-1]` are **never written** and hold
uninitialized heap residue. `copyout` ships all `N*6` bytes to userland.
Real-kernel proof: `[shrink-leak] ret_len=49152 entries=8192 ... bytes: 00 00 43
6f 70 79` ("..Copy" — kernel string residue), varying run-to-run.

## Harness methodology

### `harness.c` — deterministic userspace transcription (PRIMARY proof)
Faithfully transcribes `acl_getioctl` MACCMD_LIST with the data structures
(`struct aclstate`, `struct ieee80211req_maclist` = 6 bytes `__packed`,
`ACL_LOCK` = pthread mutex). A racer thread mutates the list at a controlled
interleaving point between the unlocked `as_nacls` read and the lock. A poisoned
allocator fills the slab + red-zone with `0xAA` canary so OOB writes and uninit
tails are observable. Builds in two modes:
- `cc -O2 -pthread -o harness harness.c` — BUGGY transcription.
- `cc -O2 -pthread -DFIXED -o harness_fixed harness.c` — FIXED transcription
  (lock-before-read + `M_ZERO` + bounded foreach).

Results (`run.log`): BUGGY → `GROW RACE OOB WRITE CONFIRMED` (24 OOB bytes for
N=8,K=4) + `SHRINK RACE UNINIT LEAK CONFIRMED` (4 uninit tail slots). FIXED →
both `NOT TRIGGERED`.

### `harness_mod.c` + `trigger.c` / `leakcheck.c` — real-kernel object-level harness
`harness_mod.ko` (built against the running kernel) allocates a minimal fake
`ieee80211vap`, attaches the **real** "mac" aclator (`wlan_acl.ko`), exposes
`/dev/df0732` (0666) whose ioctl calls `acl->iac_getioctl(fake_vap, &ireq)`, and
runs `adder` + `flusher` kthreads that race the LIST path. The unprivileged
`trigger`/`leakcheck` drive it. This exercises the **actual** vulnerable kernel
function. `kldload` here loads the **test harness**, not an exploit — it is
primitive characterization of an otherwise-runtime-unreachable path (the
DF-0594/0616 object-harness precedent), **not** an escalation chain.

Results (`panic.txt`, `leak_sample.txt`, `trigger_run.log`): grow race →
`acl_getioctl+0xd6` panic (OOB write); shrink race → live heap-residue leak
(kernel string "..Copy", varying bytes).

## Exploit chain / impact ceiling

**Primitive:** attacker-controlled 6-byte heap writes into the slab object(s)
adjacent to the `ap` buffer (grow race); 6-byte uninit heap reads per shrunk
entry (shrink race). The `ap` buffer is `kmalloc(N*6, M_TEMP)`; for typical N
this lands in `kmalloc-128/256/512`. Attacker controls N (list size) and the MAC
bytes written. On a noinv kernel this is a classic slab-groom → corrupt-adjacent
→ escalate candidate (forge `ucred`/ops vector with no SMAP/SMEP/KASLR on this
guest). On default GENERIC, INVARIANTS slab checks (`WEIRD_ADDR` poisoning,
`chunk_mark_allocated`) catch the cross-slab corruption and panic — confirmed
by the real-kernel `acl_getioctl+0xd6` trap.

**Valid hard blocker (why no `uid=0` here):** the vulnerable runtime path
(`SIOCG80211 MACCMD_LIST` on a wlan vap) requires a wifi radio driver, which is
**absent on this KVM guest**. The primitive is therefore characterized at the
harness/object level (real `acl_getioctl` panic + leak, deterministic
transcription), not driven to a live unprivileged `uid=0`. On a wifi-equipped
host with a vap, this is a local-root candidate via slab grooming of the OOB
write; on this guest it is a confirmed DoS-panic + info leak.

## Fix (`fix.diff`)

Three changes to `acl_getioctl` MACCMD_LIST, all targeting the root cause:

1. **Move `ACL_LOCK(as)` BEFORE the `as_nacls` read (`:313`)** so the size used
   for kmalloc and the list iterated by `TAILQ_FOREACH` cannot diverge. Add
   `ACL_UNLOCK` on the early-return and ENOMEM paths. `ACL_LOCK` is a sleepable
   lockmgr lock, so holding it across `kmalloc(M_INTWAIT)` is safe.
2. **Add `M_ZERO` to the `kmalloc` (`:319`)** (`M_INTWAIT | M_ZERO`) — kills the
   uninit-leak even if a future divergence reappears.
3. **Bound the `TAILQ_FOREACH` write loop** to `space/IEEE80211_ADDR_LEN` entries
   (`if (i >= bound) break;`) — defense-in-depth so the loop can never write past
   the buffer.

`git apply --check` passes. The FreeBSD branch (`#else`) is given
`IEEE80211_M_ZERO` too for consistency.

## Phase 8 — fix validation

**Before (unpatched `#0` baseline, prebuilt `/boot/kernel/wlan_acl.ko`):**
- grow race → `Fatal trap 12 ... acl_getioctl+0xd6: movw %si,0x4(%rcx)` (panic,
  guest down). (`panic.txt`)
- shrink race → `[shrink-leak] ret_len=49152 ... bytes: 00 00 43 6f 70 79`
  (kernel string residue leaked). (`leak_sample.txt`, `trigger_run.log`)

**After (rebuilt `wlan_acl.ko` from patched source, sha256 `c23e324c...`,
`-Werror` clean):**
- grow race → **no panic**; 241489 LIST ioctls completed, guest stayed UP.
  (`fix_trigger_run.log`)
- shrink race → `leakcheck`: **WEIRD_ADDR residue = 0**, OTHER = 0
  (no heap residue leaked). (`fix_run.log`)
- Deterministic harness FIXED build → both races `NOT TRIGGERED`. (`run.log`,
  `fix_run.log`)

`fix_status: fixed`. The fix closes both the OOB write and the info leak in the
real kernel and in the deterministic transcription.

## PoC changes from the seeded version

The folder arrived with a prior runner's `harness_mod.c` / `trigger.c` / `Makefile`
(object-level harness) and their captured `panic.txt` / `leak_sample.txt`. This run:
- **Re-verified** the real-kernel panic (`acl_getioctl+0xd6`) and shrink-race
  leak against the unpatched prebuilt module (refreshed `panic.txt`,
  `leak_sample.txt`, new `trigger_run.log`).
- **Added** `harness.c` — a clean deterministic userspace transcription with a
  poisoned allocator and grow/shrink modes (the PRIMARY proof per the task
  framing).
- **Added** `leakcheck.c` — a definitive residue classifier that distinguishes
  real `WEIRD_ADDR` (`0xdeadc0de`) heap residue from valid adder MACs, removing
  the false positives in the original `trigger.c`'s `is_known_mac()` (which
  assumed adder byte3 always == `0xef`).
- **Authored** `fix.diff` (lock-before-read + `M_ZERO` + bounded foreach) and
  **validated** it end-to-end: `git apply --check`, rebuild `wlan_acl.ko`
  (`-Werror`), reload, re-run trigger/leakcheck → no panic, zero residue.
- Added `build.sh` / `run.sh` repro scripts, refreshed `build.log` / `run.log`,
  `fix_build.log` / `fix_run.log` / `fix_trigger_run.log`, `env.txt`.

## Reproduce

```
# userspace deterministic harness (PRIMARY proof; no root, no wifi needed)
cd findings/poc/DF-0732 && sh build.sh && sh run.sh
# BUGGY: GROW RACE OOB WRITE CONFIRMED + SHRINK RACE UNINIT LEAK CONFIRMED
# FIXED: both NOT TRIGGERED

# real-kernel object-level harness (needs root to kldload the harness module)
# kldload wlan; kldload wlan_acl; kldload ./harness_mod.ko
# sysctl debug.use_malloc_pattern=1
# ./trigger 200000 4   # unpatched wlan_acl => panic (acl_getioctl+0xd6)
# ./leakcheck 100000 4 # fixed wlan_acl    => WEIRD_ADDR residue = 0
```
