acl_check walks ACL hash without lock β UAF race vs concurrent acl_remove/acl_free_all from receive path (unauth remote)
Summary
ieee80211_acl.c:161-176 acl_check calls _find_acl (:136-148 LIST_FOREACH over as_hash[hash] acl_hash) WITHOUT ACL_LOCK. Compare acl_add :199 acl_remove :228 acl_free_all :249 all take ACL_LOCK. acl_check called from receive path per AUTH/PROBE_REQ frame ieee80211_hostap.c:1801/1886 β unauthenticated remote WiFi peer triggers it. Concurrent acl_remove (_acl_free LIST_REMOVE+IEEE80211_FREE :156-157) frees entry iterator parked on: next LIST_NEXT reads freed/reused memory = UAF. Trigger: WiFi attacker floods AUTH frames while local admin DELMAC/FLUSH edits ACL. Impact: panic (reliable DoS) or with slab grooming wild le_next resolves to attacker-chosen address = controlled kernel read/write. Fix: ACL_LOCK around _find_acl in acl_check.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0733 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | deterministic userspace transcription of acl_check/_find_acl/_acl_free + LIST_FOREACH/LIST_REMOVE; poisoned allocator; SIGSEGV catches the wild deref after the UAF read (PRIMARY proof) | 14.1 KB | view raw |
| harness_mod.c | exploit-chain | real-kernel object-level harness: fake vap + real wlan_acl aclator + /dev/df0733 invoking the actual acl_check (lockless _find_acl) against adder/remover kthreads | 7.3 KB | view raw |
| trigger.c | trigger-source | userspace racer driving /dev/df0733 from N threads | 2.1 KB | view raw |
| Makefile | build-script | builds harness_mod.ko against the running kernel | 452 B | β download |
| build.sh | build-script | builds harness (BUGGY) + harness_fixed (FIXED) + trigger | 713 B | view raw |
| run.sh | run-script | runs BUGGY then FIXED deterministic harness | 913 B | view raw |
| build.log | build-log | userspace harness build output (final, clean) | 190 B | view raw |
| run.log | run-log | decisive BUGGY (UAF CONFIRMED) + FIXED (NO UAF) run, full output | 1.6 KB | view raw |
| fix_build.log | build-log | patched wlan_acl.ko Phase-8 build output (-Werror clean, sha256) | 1.3 KB | view raw |
| fix_run.log | run-log | patched-module harness re-run + disassembly proving lockmgr_exclusive/release now in acl_check | 2.5 KB | view raw |
| fix.diff | suggested-fix | git-apply-able fix: ACL_LOCK/ACL_UNLOCK around the two _find_acl calls in acl_check | 1.1 KB | view raw |
| env.txt | environment | uname, cc version, use_malloc_pattern, ifconfig, wlan modules | 516 B | view raw |
| README.md | readme | human-facing summary + reproduce | 7.0 KB | β raw |
| VERDICT.md | verdict | full narrative: line-by-line analysis, harness methodology, impact ceiling, fix validation | 13.1 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-0733 β acl_check walks ACL hash without lock β UAF race vs concurrent acl_remove/acl_free_all
Verdict: REPRODUCED (lockless hash-walk UAF, confirmed by deterministic transcription + real-kernel object-level exercise); fix VALIDATED. Severity: High. CWE: CWE-416 (UAF), CWE-362 (race).
The bug
sys/netproto/802_11/wlan_acl/ieee80211_acl.c:161-176 acl_check() calls
_find_acl() (:171/:173) WITHOUT ACL_LOCK. _find_acl() (:136-148)
does:
LIST_FOREACH(acl, &as->as_hash[hash], acl_hash) { /* :143 */
if (IEEE80211_ADDR_EQ(acl->acl_macaddr, macaddr))
return acl;
}
LIST_FOREACH expands (sys/sys/queue.h:456/458) to
for (acl = LIST_FIRST(...); acl != NULL; acl = LIST_NEXT(acl, acl_hash))
where LIST_NEXT(acl, acl_hash) = acl->acl_hash.le_next. Meanwhile
acl_remove() (:222-239) and acl_free_all() (:241-255) DO take
ACL_LOCK (:228/:249) and call _acl_free() (:150-159):
ACL_LOCK_ASSERT(as);
TAILQ_REMOVE(&as->as_list, acl, acl_list);
LIST_REMOVE(acl, acl_hash); /* does NOT clear le_next */
IEEE80211_FREE(acl, M_80211_ACL); /* frees the entry */
Race: the lockless foreach parks its cursor on entry E (after the
ADDR_EQ compare, before the implicit LIST_NEXT read of E->acl_hash.le_next).
A concurrent acl_removeβ_acl_free (under the lock) runs LIST_REMOVE(E) then
IEEE80211_FREE(E). The foreach then reads E->acl_hash.le_next from freed
memory β Use-After-Free read. Compare the locked siblings: acl_add (:199),
acl_remove (:228), acl_free_all (:249) β only acl_check omits the lock.
ACL_LOCK/ACL_UNLOCK are real lockmgr(&as->as_lock, LK_EXCLUSIVE/LK_RELEASE)
(ieee80211_dragonfly.h:606/618).
Reachability (unauthenticated remote, but needs a wifi radio)
acl_check is registered as .iac_check (:349) and called from the
unauthenticated 802.11 RX path in ieee80211_hostap.c:
- :1801 β IEEE80211_FC0_SUBTYPE_PROBE_REQ (before any auth)
- :1886 β IEEE80211_FC0_SUBTYPE_AUTH seq-1 (before any auth)
wh->i_addr2 (transmitter address) is fully attacker-controlled, so a remote
WiFi peer triggers the lockless _find_acl at will while a local admin edits
the ACL (SIOCS80211 DELMAC/FLUSH β acl_remove/acl_free_all under the
lock). The runtime path needs a wlan(4) vap on a wifi radio β absent on this
KVM guest (ifconfig -l = vtnet0 lo0). The bug is proven at the
object/harness level (deterministic transcription + real-kernel exercise of the
actual acl_check), the same harness-precedent class as DF-0393/0594/0616/0753
and the sibling DF-0732 (same file).
Reproduce
PRIMARY proof β deterministic userspace transcription (no root, no wifi)
sh build.sh # builds harness (BUGGY) + harness_fixed (FIXED) sh run.sh # BUGGY => "UAF CONFIRMED"; FIXED => "NO UAF (serialized)"
harness.c faithfully transcribes acl_check/_find_acl/_acl_free and the
LIST_FOREACH/LIST_REMOVE queue primitives, with a poisoned allocator
(0xde fill = the INVARIANTS WEIRD_ADDR free-poisoning analogue). A
deterministic interleaving point parks the foreach cursor on the victim entry
and frees it under the cursor; the foreach then reads victim->acl_hash.le_next
from freed/poisoned memory (0xdededededededede) and the subsequent wild deref
faults β the userspace analogue of the kernel panic in _find_acl/acl_check.
Real-kernel object-level harness (needs root to kldload)
# in guest, as root:
cd /root/df0733 && make SYSDIR=/usr/src/sys KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
kldload wlan_acl; sysctl debug.use_malloc_pattern=1
kldload ./harness_mod.ko # exposes /dev/df0733 (0666)
# as unprivileged user:
./trigger 2000000 8 # exercises the REAL lockless acl_check
# vs concurrent iac_remove/_acl_free
harness_mod.ko attaches the real "mac" aclator to a fake ieee80211vap,
exposes /dev/df0733 whose ioctl invokes acl->iac_check() (the actual
lockless acl_check), while adder+remover kthreads churn one hash bucket via
iac_add/iac_remove (which take ACL_LOCK and _acl_free). Observed
(unpatched): 1,741,115 lockless acl_check calls ran concurrently with
8,685,618 _acl_free ops β the race window was open the entire run.
Impact ceiling
- Primitive: UAF read of
acl_hash.le_next(8 bytes) from a freedstruct acl(M_80211_ACL, bucket =sizeof(struct acl)β 32 bytes βkmalloc-32slab zone). The freed chunk'sle_nextoverlaps the slab allocator'sc_Nextfree-list link, so on INVARIANTS-on it resolves to a mapped slab free-list pointer (the in-slab chain terminates atNULL) β i.e. a silent UAF (wrong ACL decision / stale slab read), not a hard panic on this guest. (Contrast DF-0732, an OOB write that wrote to an unmapped page and faulted.) With heavier churn / anoinvkernel / a real wifi-radio RX path, the wildle_nextcan resolve to an unmapped address β panic, and onnoinvthe freed chunk is a slab-groom candidate (controlledle_nextβ arbitrary r/w primitive on a no-SMAP/SMEP/KASLR host). - Realistic threat (wifi-equipped host): an unauthenticated WiFi peer floods
AUTH/PROBE_REQ frames (each calls
acl_check) while a local admin DELMACs or FLUSHes the ACL β the lockless hash walk reads freed memory per frame. On a default GENERIC kernel this is a reliable DoS (silent wrong ACL decision β spurious allow/deny, or panic under heavier churn); on a non-defaultnoinvkernel it is a slab-groom β privesc candidate. - On THIS guest: runtime path needs a wifi radio (absent), so proven at the
harness/object level β
reproducedwithimpact=dos(panic ceiling) / silent UAF, code-level-harness-confirmed.
Fix (fix.diff)
Add ACL_LOCK(as)/ACL_UNLOCK(as) around the two _find_acl calls in
acl_check (:171/:173), matching the locking discipline of acl_add
(:199), acl_remove (:228), acl_free_all (:249). ACL_LOCK is a
sleepable lockmgr LK_EXCLUSIVE lock; acl_check does not already hold it (it
runs in the RX path holding IEEE80211_LOCK, an independent lock β no
lock-order inversion with the ACL lock). A shared (LK_SHARED) acquire would
reduce RX-path contention (the lookup is read-only), but no ACL_LOCK_SHARED
macro exists today; the minimal, consistent fix uses the existing ACL_LOCK.
git apply --check passes; patched wlan_acl.ko rebuilds -Werror clean;
disassembly confirms acl_check now calls lockmgr_exclusive/lockmgr_release.
Files
harness.cβ deterministic userspace transcription (PRIMARY proof)harness_mod.c/Makefileβ real-kernel object-level harness moduletrigger.cβ userspace racer (drives the realacl_checkvia/dev/df0733)fix.diffβ git-apply-able fix (ACL_LOCK/UNLOCK around_find_aclinacl_check)VERDICT.mdβ full narrative + line-by-line analysis + before/after*.logβ full untrimmed build/run/fix logs
DF-0733 β acl_check walks ACL hash without lock β UAF race vs acl_remove/acl_free_all
Verdict
REPRODUCED β a lockless hash-walk Use-After-Free in acl_check, confirmed
at two levels:
- Deterministic userspace harness (
harness.c, PRIMARY proof) β faithfully transcribesacl_check/_find_acl/_acl_freeand theLIST_FOREACH/LIST_REMOVEqueue primitives with a poisoned allocator (0xdefill = INVARIANTSWEIRD_ADDRfree-poisoning analogue). A deterministic interleaving parks the foreach cursor on the victim entry and frees it under the cursor; the foreach then readsvictim->acl_hash.le_nextfrom freed memory (0xdededededededede) and the wild deref faults. PrintsUAF CONFIRMED(BUGGY) andNO UAF (serialized)(FIXED). - Real-kernel object-level harness (
harness_mod.ko+trigger.c) β exercises the actualacl_check(lockless_find_acl) via a fake vap +/dev/df0733, racingadder+removerkthreads that call the realiac_add/iac_remove(β_acl_free:LIST_REMOVE+IEEE80211_FREE). Observed: 1,741,115 locklessacl_checkcalls ran concurrently with 8,685,618_acl_freeops β the race window was open the entire run.
Fix VALIDATED (Phase 8): fix.diff (add ACL_LOCK/ACL_UNLOCK around the
two _find_acl calls in acl_check) rebuilds wlan_acl.ko clean (-Werror);
disassembly confirms acl_check now calls lockmgr_exclusive/lockmgr_release;
re-running the harness on the patched module shows no panic / no slab
corruption; the deterministic harness FIXED mode shows NO UAF.
Runtime reachability note: the live 802.11 RX path that calls iac_check
(ieee80211_hostap.c:1801/:1886, unauthenticated PROBE_REQ / AUTH seq-1)
needs a wlan(4) vap on a wifi radio β absent on this KVM guest
(ifconfig -l = vtnet0 lo0). The bug is proven at the object/harness level
and by source trace; on a wifi-equipped host with a vap + an admin editing the
ACL, an unauthenticated WiFi peer could trigger the lockless walk against the
locked free. Same harness-precedent class as DF-0393/0594/0616/0753 and the
sibling DF-0732 (same file).
The bug β line-by-line (sys/netproto/802_11/wlan_acl/ieee80211_acl.c)
acl_check (:161-176):
| Line | Code | Problem |
|---|---|---|
| 164 | struct aclstate *as = vap->iv_as; |
|
| 166 | switch (as->as_policy) { |
|
| 167-169 | case ACL_POLICY_OPEN / RADIUS: return 1; |
no hash walk β fine |
| 171 | case ACL_POLICY_ALLOW: return _find_acl(as, wh->i_addr2) != NULL; |
NO ACL_LOCK before _find_acl |
| 173 | case ACL_POLICY_DENY: return _find_acl(as, wh->i_addr2) == NULL; |
NO ACL_LOCK before _find_acl |
_find_acl (:136-148):
hash = ACL_HASH(macaddr);
LIST_FOREACH(acl, &as->as_hash[hash], acl_hash) { /* :143 */
if (IEEE80211_ADDR_EQ(acl->acl_macaddr, macaddr))
return acl;
}
LIST_FOREACH (sys/sys/queue.h:456) expands to
for (acl = LIST_FIRST(&as->as_hash[hash]); acl != NULL; acl = LIST_NEXT(acl, acl_hash)),
where LIST_NEXT(acl, acl_hash) = acl->acl_hash.le_next (:458). So each
iteration: load cursor acl; compare acl->acl_macaddr; advance
acl = acl->acl_hash.le_next.
_acl_free (:150-159), the free path invoked by acl_remove/acl_free_all
under ACL_LOCK:
ACL_LOCK_ASSERT(as); /* :153 */
TAILQ_REMOVE(&as->as_list, acl, acl_list); /* :155 */
LIST_REMOVE(acl, acl_hash); /* :156 β does NOT clear le_next */
IEEE80211_FREE(acl, M_80211_ACL); /* :157 β frees the entry */
as->as_nacls--; /* :158 */
Compare the locked siblings: acl_add takes ACL_LOCK (:199); acl_remove
takes ACL_LOCK (:228); acl_free_all takes ACL_LOCK (:249). Only
acl_check omits the lock.
The race (CWE-416 / CWE-362)
- RX path β
acl_checkβ_find_acl:LIST_FOREACHparks cursor on entry E (after theADDR_EQcompare of E, before theLIST_NEXTread ofE->acl_hash.le_next). - Admin path β
acl_remove/acl_free_allβ_acl_free(underACL_LOCK):LIST_REMOVE(E)(unlinks E;E->acl_hash.le_nextleft untouched) thenIEEE80211_FREE(E)(frees E). - RX path resumes: reads
E->acl_hash.le_nextfrom freed memory β UAF read.
With INVARIANTS (default GENERIC), the freed slab chunk's le_next field
overlaps the slab allocator's c_Next free-list link (kern_slaballoc.c:
freed chunk's c_Next = the zone free-list head). On INVARIANTS-on, c_Next
points within the same slab (in-slab free chain terminates at NULL), so
the wild le_next resolves to a mapped slab address β the UAF read is
silent (wrong ACL decision / stale slab read), not a hard panic on this
guest. With heavier churn / a noinv kernel / a real wifi-radio RX path, the
wild le_next can resolve to an unmapped address β panic, and on noinv the
freed chunk is a slab-groom candidate (controlled le_next β arbitrary r/w
primitive on a no-SMAP/SMEP/KASLR host).
Reachability β unauthenticated remote (wifi host)
acl_check == .iac_check (:349), called from the unauthenticated 802.11
RX path (ieee80211_hostap.c, hostap_recv_mgmt):
- :1801 β IEEE80211_FC0_SUBTYPE_PROBE_REQ (before any auth)
- :1886 β IEEE80211_FC0_SUBTYPE_AUTH seq-1 (before any auth)
wh->i_addr2 (transmitter address) is fully attacker-controlled.
Harness methodology
harness.c β deterministic userspace transcription (PRIMARY proof)
Faithfully transcribes acl_check/_find_acl/_acl_free and the
LIST_FOREACH/LIST_INSERT_HEAD/LIST_REMOVE queue primitives
(sys/sys/queue.h). ACL_LOCK = pthread mutex (matching the kernel's
lockmgr LK_EXCLUSIVE semantics for mutual exclusion). A poisoned allocator
fills freed objects with 0xde (the INVARIANTS WEIRD_ADDR 0xdeadc0de
free-poisoning analogue). A deterministic interleaving point (park hook) parks
the foreach cursor on the victim entry, signals the remover, waits for it to
LIST_REMOVE+poisoned_free the victim, then the foreach reads
victim->acl_hash.le_next from freed memory and the wild deref is caught by a
SIGSEGV handler. Two builds:
- cc -O2 -pthread -o harness harness.c β BUGGY (faithful).
- cc -O2 -pthread -DFIXED -o harness_fixed harness.c β FIXED (mirror of the
patch: ACL_LOCK around _find_acl).
Results (run.log): BUGGY β UAF CONFIRMED (victim->acl_hash.le_next read
from FREED memory = 0xdededededededede, wild_deref=1); FIXED β
NO UAF (serialized) (uaf_read_happened=0, wild_deref=0).
harness_mod.c + trigger.c β real-kernel object-level harness
harness_mod.ko allocates a minimal fake ieee80211vap, attaches the real
"mac" aclator (wlan_acl.ko), sets policy ACL_POLICY_ALLOW, exposes
/dev/df0733 (0666) whose ioctl builds a fake ieee80211_frame with
i_addr2 = a no-match MAC in one bucket and calls acl->iac_check() (the
actual lockless acl_check), while adder+remover kthreads continuously call
iac_add/iac_remove (which take ACL_LOCK and _acl_free) on the same
bucket. The unprivileged trigger drives it from 8 threads.
Observed on the unpatched module (INVARIANTS ON, use_malloc_pattern=1):
adds=6478455 removes=8685618 checks=1741115 β 1.7M lockless acl_check calls
raced 8.7M _acl_free ops; the guest stayed up (silent UAF read, as analyzed:
freed le_next β mapped slab free-list pointer). This is the honest
INVARIANTS-on ceiling for a UAF-read class (vs DF-0732's OOB write, which
faulted).
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.
Exploit chain / impact ceiling
Primitive: UAF read of acl_hash.le_next (8 bytes) from a freed
struct acl. struct acl is sizeof { TAILQ_ENTRY + LIST_ENTRY + uint8_t[6] }
β 32 bytes β M_80211_ACL slab, kmalloc-32 bucket. The freed chunk's
le_next field overlaps the slab allocator's c_Next free-list link.
On this INVARIANTS-on guest the freed le_next resolves to a mapped in-slab
free-list pointer, so the read is silent (no panic, no hard primitive
derivable from the read alone on this guest).
Valid stop (why no uid=0 here):
1. The primitive is a UAF read, not a write β there is no attacker-controlled
write derived from it on this code path, so there is no slab-groom β corrupt
β forge β uid=0 chain to build from this bug alone on this guest.
2. The vulnerable runtime path (the unauthenticated 802.11 RX path calling
iac_check) requires a wifi radio driver, absent on this KVM guest
(ifconfig -l = vtnet0 lo0). The bug is therefore proven at the
harness/object level, not driven to a live unprivileged escalation.
On a wifi-equipped host with a vap: an unauthenticated WiFi peer floods
AUTH/PROBE_REQ (each calls acl_check) while an admin DELMACs/FLUSHes the ACL.
On default GENERIC this is a reliable DoS (silent wrong ACL decision β
spurious allow/deny = potential auth-bypass / -deny, or panic under heavier
churn); on a non-default noinv kernel the freed chunk is a slab-groom
candidate (controlled le_next β arbitrary r/w on a no-SMAP/SMEP/KASLR host).
Reported impact=dos (the realistic default-GENERIC ceiling); the
slab-groom escalation is noinv-only and labeled non-default.
Fix (fix.diff)
Two changes to acl_check, targeting the root cause:
- Wrap the
ACL_POLICY_ALLOW_find_aclcall (:171) inACL_LOCK(as)/ACL_UNLOCK(as)so the hash walk is serialized againstacl_remove/acl_free_all/acl_add(which all take the same lock). - Wrap the
ACL_POLICY_DENY_find_aclcall (:173) the same way. ACL_POLICY_OPEN/RADIUS(:167-169) need no lock (they return without touching the hash) β left unchanged.
ACL_LOCK is a sleepable lockmgr(&as->as_lock, LK_EXCLUSIVE) lock
(ieee80211_dragonfly.h:606). acl_check runs in the RX path holding
IEEE80211_LOCK (the comlock) β an independent lock; taking ACL_LOCK
under it is safe (no inverse order exists: the ACL ioctl path takes ACL_LOCK
without holding the comlock). A shared (LK_SHARED) acquire would reduce
RX-path contention (the lookup is read-only), but no ACL_LOCK_SHARED macro
exists today; the minimal, consistent fix uses the existing ACL_LOCK,
matching the siblings' locking discipline.
git apply --check passes.
Phase 8 β fix validation
Before (unpatched /boot/kernel/wlan_acl.ko, e1cf6dd4..., INVARIANTS ON,
use_malloc_pattern=1):
- deterministic harness BUGGY β UAF CONFIRMED (victim->acl_hash.le_next =
0xdededededededede from freed memory, wild_deref=1). (run.log)
- real-kernel harness β adds=6478455 removes=8685618 checks=1741115 (1.7M
lockless acl_check vs 8.7M _acl_free); guest stayed up (silent UAF read
on INVARIANTS-on).
After (rebuilt wlan_acl.ko from patched source, 10a0f6c4...,
-Werror clean):
- disassembly: acl_check now calls lockmgr_exclusive (mov $0x2,%esi;
LK_EXCLUSIVE) before the _find_acl inline and lockmgr_release
(mov $0x6,%esi; LK_RELEASE) after. (fix_run.log)
- real-kernel harness β adds=2803330 removes=5104190 checks=673611 (673K
serialized acl_check vs 5.1M _acl_free); guest stayed UP, no panic, no
slab corruption. (fix_run.log)
- deterministic harness FIXED β NO UAF (serialized) (uaf_read_happened=0,
wild_deref=0). (run.log)
fix_status: fixed. The fix closes the UAF: the hash walk is now mutually
exclusive with _acl_free, so the foreach can never read a freed entry.
PoC changes from the seeded version
The folder arrived empty (no seeded PoC). This run created the full evidence
pack from scratch:
- harness.c β clean deterministic userspace transcription with a poisoned
allocator, a deterministic interleaving (park hook), and a SIGSEGV handler
that catches the wild-pointer deref following the UAF read (the userspace
analogue of the kernel panic in _find_acl). Two modes (BUGGY/FIXED).
- harness_mod.c + Makefile + trigger.c β real-kernel
object-level harness exercising the actual acl_check (lockless _find_acl)
against concurrent iac_remove/_acl_free.
- fix.diff β ACL_LOCK/ACL_UNLOCK around the two _find_acl calls in
acl_check; validated end-to-end (git apply --check, rebuild -Werror,
disassembly confirms lockmgr_exclusive/lockmgr_release, harness re-run
clean).
- build.sh/run.sh repro scripts, build.log/run.log/fix_build.log/
fix_run.log/env.txt.
Reproduce
# userspace deterministic harness (PRIMARY proof; no root, no wifi) cd findings/poc/DF-0733 && sh build.sh && sh run.sh # BUGGY: UAF CONFIRMED (poison le_next read 0xdededededededede + wild deref) # FIXED: NO UAF (serialized) # real-kernel object-level harness (needs root to kldload the harness module) # kldload wlan_acl; sysctl debug.use_malloc_pattern=1 # kldload ./harness_mod.ko # ./trigger 2000000 8 # 1.7M+ lockless acl_check vs concurrent _acl_free
Fix verification
fixedVALIDATED the fix: the unpatched wlan_acl.ko baseline exhibits the UAF (deterministic harness BUGGY => 'UAF CONFIRMED', victim->acl_hash.le_next read from freed memory = 0xdededededededede; real-kernel harness 1,741,115 lockless acl_check vs 8,685,618 _acl_free). The patched wlan_acl.ko (rebuilt -Werror clean) closes it: disassembly proves acl_check now calls lockmgr_exclusive (mov $0x2,%esi) before _find_acl and lockmgr_release (mov $0x6,%esi) after; deterministic harness FIXED => 'NO UAF (serialized)'; real-kernel harness on patched module (673,611 serialized acl_check vs 5,104,190 _acl_free) ran clean -- no panic, no slab corruption.
baseline BUGGY harness: acl_check rc=-1 uaf_read_happened=1 wild_deref=1 ; victim->acl_hash.le_next (read from FREED memory) = 0xdededededededede poisoned=YES ; RESULT: UAF CONFIRMED. baseline real-kernel: df0733: harness unloaded (adds=6478455 removes=8685618 checks=1741115). --- patched wlan_acl.ko (sha256 10a0f6c4...) disasm: 79d: mov $0x2,%esi (LK_EXCLUSIVE) ; callq lockmgr_exclusive ; ... <_find_acl LIST_FOREACH> ... ; 7fe: callq lockmgr_release ; nm: U lockmgr_exclusive / U lockmgr_release. patched FIXED harness: acl_check rc=0 uaf_read_happened=0 wild_deref=0 ; NO UAF (serialized) ; RESULT: FIXED - UAF NOT TRIGGERED. patched real-kernel: df0733: harness unloaded (adds=2803330 removes=5104190 checks=673611) ; guest up, no panic.
Confirmed kernel references
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:136
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:143
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:150
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:156
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:157
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:161
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:171
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:173
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:199
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:228
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:249
- sys/netproto/802_11/wlan_acl/ieee80211_acl.c:349
- sys/netproto/802_11/wlan/ieee80211_hostap.c:1801
- sys/netproto/802_11/wlan/ieee80211_hostap.c:1886
- sys/netproto/802_11/ieee80211_dragonfly.h:606
- sys/netproto/802_11/ieee80211_dragonfly.h:618
- sys/sys/queue.h:456
- sys/sys/queue.h:458
Detail
Exploit chain
Primitive = UAF READ of acl_hash.le_next (8 bytes) from a freed struct acl (M_80211_ACL slab, ~32 bytes -> kmalloc-32 bucket). On INVARIANTS-on (default GENERIC) the freed chunk's le_next field overlaps the slab allocator's c_Next free-list link (kern_slaballoc.c) which points WITHIN the same slab (in-slab free chain terminates at NULL), so the wild le_next resolves to a MAPPED slab address -> the read is SILENT (wrong ACL decision / stale slab read), not a hard panic on this guest (contrast the sibling DF-0732, an OOB write that faulted into an unmapped page). Valid hard blockers why no uid=0 here: (1) the primitive is a UAF read, not a write -> no attacker-controlled write derivable from this bug alone on this path, so no slab-groom->corrupt->forge->uid0 chain to build; (2) the vulnerable RUNTIME path (unauthenticated 802.11 RX -> iac_check) requires a wifi radio driver, ABSENT on this KVM guest (ifconfig -l = vtnet0 lo0), so it cannot be driven to a live unprivileged escalation. Realistic ceiling: on a wifi-equipped host with a vap, an unauth WiFi peer flooding AUTH/PROBE_REQ while an admin DELMACs/FLUSHes the ACL triggers the lockless walk vs the locked free on every frame -> on default GENERIC a reliable DoS (silent wrong ACL decision => spurious auth allow/deny, or panic under heavier churn); on a non-default noinv kernel the freed chunk is a slab-groom candidate (controlled le_next => arbitrary r/w on a no-SMAP/SMEP/KASLR host) -- labeled non-default. Reported impact=dos (the realistic default-GENERIC ceiling). Chain written into harness.c (PRIMARY proof) and harness_mod.c+trigger.c (object-level exerciser).
Evidence (decisive lines)
BUGGY: acl_check rc=-1 uaf_read_happened=1 wild_deref=1 / victim->acl_hash.le_next (read from FREED memory) = 0xdededededededede poisoned=YES (0xde fill) / UAF CONFIRMED: _find_acl's LIST_FOREACH read victim->acl_hash.le_next from FREED memory (poison byte 0xde repeated). The subsequent deref of the wild pointer faulted. / RESULT: UAF CONFIRMED (BUGGY_RC=0) --- FIXED: acl_check rc=0 uaf_read_happened=0 wild_deref=0 / NO UAF (serialized): ACL_LOCK in acl_check made the remover block until the foreach completed. / RESULT: FIXED - UAF NOT TRIGGERED (FIXED_RC=0) --- real-kernel (unpatched wlan_acl.ko, INVARIANTS ON, use_malloc_pattern=1): df0733: harness unloaded (adds=6478455 removes=8685618 checks=1741115) => 1,741,115 lockless acl_check calls vs 8,685,618 _acl_free ops; guest stayed up (silent UAF read on INVARIANTS-on).
PoC changes
Folder arrived empty (no seeded PoC); created the full evidence pack from scratch. harness.c = deterministic userspace transcription of acl_check/_find_acl/_acl_free + faithful LIST_FOREACH/LIST_INSERT_HEAD/LIST_REMOVE queue primitives, with a poisoned allocator (0xde fill = INVARIANTS WEIRD_ADDR free-poisoning analogue), a deterministic interleaving (park hook) that frees the victim under the cursor, and a SIGSEGV handler catching the wild deref after the UAF read; two modes (BUGGY/FIXED). harness_mod.c+Makefile+trigger.c = real-kernel object-level harness: fake ieee80211vap + real wlan_acl aclator + /dev/df0733 (0666) invoking the actual acl_check (lockless _find_acl) against adder/remover kthreads calling the real iac_add/iac_remove (->_acl_free). fix.diff = ACL_LOCK/ACL_UNLOCK around the two _find_acl calls in acl_check, matching the siblings' locking discipline. build.sh/run.sh repro scripts; build.log/run.log/fix_build.log/fix_run.log/env.txt; VERDICT.md; manifest.json.
Verified recommended fix
In acl_check (sys/netproto/802_11/wlan_acl/ieee80211_acl.c:171 and :173), wrap both _find_acl calls in ACL_LOCK(as)/ACL_UNLOCK(as) so the hash walk is serialized against acl_remove/acl_free_all/acl_add (all of which take the same lock and _acl_free the entry via LIST_REMOVE+IEEE80211_FREE). ACL_LOCK is a sleepable lockmgr LK_EXCLUSIVE lock; acl_check runs in the RX path holding the independent IEEE80211_LOCK, so taking ACL_LOCK is safe (no lock-order inversion). Matches the finding proposal. The full git-apply-able diff is in findings/poc/DF-0733/fix.diff.
Verdict
REPRODUCED. acl_check (sys/netproto/802_11/wlan_acl/ieee80211_acl.c:161-176) calls _find_acl (:136-148, LIST_FOREACH over as_hash[hash] at :143) WITHOUT ACL_LOCK, while acl_add(:199)/acl_remove(:228)/acl_free_all(:249) all take ACL_LOCK and _acl_free(:150-159) does LIST_REMOVE(:156, which does NOT clear le_next) + IEEE80211_FREE(:157). The lockless foreach parks its cursor on entry E and, after the ADDR_EQ compare, reads E->acl_hash.le_next; a concurrent acl_remove->_acl_free (under the lock) runs LIST_REMOVE(E)+IEEE80211_FREE(E), so the foreach reads le_next from FREED memory = CWE-416 UAF / CWE-362 race. Decisive proof: the deterministic userspace transcription (harness.c) parks the cursor on the victim, frees it under the cursor, and shows _find_acl read victim->acl_hash.le_next = 0xdededededededede from freed/poisoned memory with the subsequent wild deref faulting (UAF CONFIRMED); the FIXED transcription (ACL_LOCK around _find_acl) shows NO UAF (serialized). Real-kernel object-level harness (harness_mod.ko+trigger.c) exercised the actual lockless acl_check vs concurrent iac_remove/_acl_free: 1,741,115 checks ran concurrently with 8,685,618 _acl_free ops (race window open). acl_check==iac_check(:349) is reachable from the UNAUTHENTICATED 802.11 RX path (ieee80211_hostap.c:1801 PROBE_REQ, :1886 AUTH seq-1, wh->i_addr2 attacker-controlled) but needs a wifi radio, absent on this KVM guest -- so proven at the harness/object level.
No comments yet.