if_cloners list and if_cloners_count accessed without synchronization β unprivileged SIOCIFGCLONERS races module load/unload UAF
Summary
if_cloners/if_cloners_count globals :43-44 no dedicated lock. Readers: if_clone_lookup :300 LIST_FOREACH no lock called from if_clone_create :67 before ifnet_lock :74 and if_clone_destroy :116 after ifnet_unlock :112. if_clone_list :207,216,219 reads count+LIST_FOREACH no lock reachable from UNPRIVILEGED SIOCIFGCLONERS (if.c:2017-2018 no caps_priv_check). Writers: if_clone_attach :147,166-167 LIST_INSERT_HEAD count++ before ifnet_lock. if_clone_detach :192-194 LIST_REMOVE kfree(ifc_units) count-- no lock at all. Race: unprivileged SIOCIFGCLONERS reader traverses list while root kldunload detaches entry: LIST_NEXT follows freed link ifc_name reads freed memory UAF. Requires concurrent module activity by root so not independently unprivileged triggerable. Fix: ifnet_lock for all reads+writes of if_cloners/if_cloners_count.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0726 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| reader.c | trigger-source | single-threaded SIOCIFGCLONERS hammer (unprivileged reader) | 2.0 KB | view raw |
| reader2.c | trigger-source | multi-threaded reader with madvise(MADV_DONTNEED) copyout-window widener | 2.2 KB | view raw |
| build.sh | build-script | cc -O2 -pthread -o reader2 reader2.c | 146 B | view raw |
| run.sh | run-script | starts reader as maxx + kldload/kldunload loop as root | 2.1 KB | view raw |
| fix.diff | suggested-fix | ifnet_lock wrapping all list/count accesses in if_clone_attach/detach/list/lookup | 3.1 KB | view raw |
| VERDICT.md | verdict | full analysis: mechanism trace, 3 race attempts, fix validation | 9.4 KB | β raw |
| README.md | readme | summary + reproduce instructions | 1.9 KB | β raw |
| run.log | run-log | unpatched-kernel race results: 3 attempts, 265M iters, 50k+ writer cycles, 0 panics | 3.3 KB | view raw |
| fix_build.log | build-log | full kernel build output (rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | patched-kernel functional test: cloner list correct, modules load/unload OK, no panic | 1.7 KB | view raw |
| env.txt | environment | uname, kern.version, cc, kernel sha256 | 802 B | view 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-0726 β if_cloners list / if_cloners_count unlocked race
Summary
The if_cloners list (sys/net/if_clone.c:43) and if_cloners_count
(:44) are accessed without holding ifnet_lock in four functions:
if_clone_attach (:147,:166-167), if_clone_detach (:192-194),
if_clone_list (:207,:216,:219-221), and if_clone_lookup (:300).
The reader (if_clone_list) is reachable from the unprivileged
SIOCIFGCLONERS ioctl (sys/net/if.c:2017-2018 β no caps_priv_check).
The writers (if_clone_attach/if_clone_detach) are called from module
load/unload (root-only kldload/kldunload).
A concurrent reader-vs-writer race can cause the reader to dereference a
freed/unmapped ifc pointer (UAF read) after module unload calls
vm_map_remove (sys/kern/link_elf_obj.c:904), panicking the kernel.
How to reproduce
./build.sh # cc -O2 -pthread -o reader2 reader2.c (and reader)
./run.sh # starts reader as maxx + kldload/kldunload loop as root
Expected (bug present): kernel panic (fatal trap in if_clone_list or
copyout during list traversal, reading unmapped module memory).
Expected (fixed): no panic; reader returns correct cloner list; modules
load/unload cleanly.
Files
reader.cβ single-threaded SIOCIFGCLONERS hammer (unprivileged reader).reader2.cβ multi-threaded +madvise(MADV_DONTNEED)copyout-window widener.build.sh/run.shβ exact build/run commands.VERDICT.mdβ full analysis, mechanism trace, reproduction attempts, fix validation.fix.diffβ verified fix (ifnet_lock wrapping all list accesses).fix_build.logβ full kernel build output (rc=0).fix_run.logβ patched-kernel functional test results.run.logβ unpatched-kernel race attempt results (3 attempts, 265M iters, no panic).env.txtβ guest environment (uname, kern.version, cc, kernel sha256).manifest.jsonβ artifact catalog.
DF-0726 β if_cloners list / if_cloners_count unlocked race
Verdict
NOT REPRODUCED (code-confirmed race, too narrow to trigger in practice on
this guest). The race condition is real at the source level β all four
accessor functions (if_clone_attach, if_clone_detach, if_clone_list,
if_clone_lookup) read/mutate the global if_cloners list and
if_cloners_count without holding ifnet_lock β but three aggressive race
attempts totaling ~265M unprivileged reader iterations and ~50k root
kldload/kldunload cycles produced zero panics. The writer side requires
root (kldload/kldunload), so the bug is not independently unprivileged
triggerable; it is a cooperative race (unprivileged reader vs. root module
activity). Impact ceiling if hit: kernel panic (DoS) via UAF read of unmapped
module memory. No write primitive, no escalation path.
Mechanism (code-level trace, confirmed)
The shared state (no dedicated lock)
sys/net/if_clone.c:43βstatic LIST_HEAD(, if_clone) if_cloners(global linked list of registered interface cloners)sys/net/if_clone.c:44βstatic int if_cloners_count(count of entries)
These globals have no dedicated lock. ifnet_lock() (a sleep mutex,
ifnet_mtx at sys/net/if.c:195) exists and is used by some paths but is
not held around the list/count accesses in the four functions below.
Reader (unprivileged)
sys/net/if.c:2017-2018βSIOCIFGCLONERScase inifioctl(): callsif_clone_list()directly with nocaps_priv_check(contrastSIOCIFCREATEat :2007 andSIOCIFDESTROYat :2013, which both requireSYSCAP_RESTRICTEDROOT). Confirmed unprivileged β maxx (uid 1001) successfully invokes SIOCIFGCLONERS and gets the cloner list.sys/net/if_clone.c:201-230βif_clone_list():- :207 reads
if_cloners_count(no lock) - :216 reads
if_cloners_countagain (no lock, TOCTOU vs :207) - :219-221
LIST_FIRST+LIST_NEXTtraversal (no lock) - :223 reads
ifc->ifc_name(no lock β dereferences the list node) sys/net/if_clone.c:296-306βif_clone_lookup():- :300
LIST_FOREACHtraversal (no lock) - Called from
if_clone_create()at :67 (beforeifnet_lock()at :74) andif_clone_destroy()at :116 (afterifnet_unlock()at :112).
Writer (root-only, via kldload/kldunload)
sys/net/if_clone.c:141-183βif_clone_attach()(called from module MOD_LOAD):- :147
LIST_FOREACHduplicate-name check (no lock) - :166
LIST_INSERT_HEAD(&if_cloners, ifc, ifc_list)(no lock β beforeifnet_lock()at :169) - :167
if_cloners_count++(no lock) - The
ifnet_lock()at :169 only covers theminifsallocation loop, not the list mutation. sys/net/if_clone.c:189-195βif_clone_detach()(called from module MOD_UNLOAD):- :192
LIST_REMOVE(ifc, ifc_list)(no lock) - :193
kfree(ifc->ifc_units, M_CLONE)(no lock β frees the unit bitmap) - :194
if_cloners_count--(no lock) - No
ifnet_lock()at all in this function.
The UAF sink (page unmap)
sys/kern/kern_linker.c:477-573βlinker_file_unload():- :514
module_unload(mod)β calls the module's MOD_UNLOAD handler (e.g.tapmodeventatsys/net/tap/if_tap.c:190-201), which callsif_clone_detach(&tap_cloner)at if_tap.c:194. - :556
file->ops->unload(file)βlink_elf_obj_unload_file()atsys/kern/link_elf_obj.c:844. sys/kern/link_elf_obj.c:904βvm_map_remove(kernel_map, ef->address, ...): unmaps the module's kernel virtual address range. After this, any access totap_cloner(a static struct in if_tap.ko's .data section) ortap_cloner.ifc_name(aconst char *pointing into if_tap.ko's .rodata) causes a kernel page fault β panic.
The race window
The dangerous window is between if_clone_detach() LIST_REMOVE (if_clone.c:192)
and vm_map_remove() (link_elf_obj.c:904). This window spans:
1. The rest of the module's MOD_UNLOAD handler (e.g. tapdestroy loop, dev_ops cleanup).
2. module_release() + TAILQ_REMOVE from found_modules.
3. linker_file_sysuninit() (runs SYSUNINITs).
4. linker_file_unregister_sysctls().
5. Freeing deps, common symbols.
6. file->ops->unload(file) β vm_map_remove.
This is potentially milliseconds of work β a wide writer window. However, the
reader's vulnerable window per iteration is tiny: the time between
LIST_NEXT(prev_cloner, ifc_list) returning &tap_cloner and the subsequent
ifc->ifc_name read (a few instructions, ~50ns). Even though the reader
completes ~200k ioctls/sec/thread, the per-iteration vulnerable window is so
small that overlap with the writer's unload window is very rare.
The madvise(MADV_DONTNEED) technique in reader2.c attempts to widen the
reader's window by forcing copyout() to take a page fault, but the fault is
resolved quickly (the page is still in the page cache) and doesn't widen the
window enough to hit the race in practice.
Reproduction attempts (all on unpatched #0 kernel)
| Attempt | Reader | Threads | Duration | Reader iters | Writer cycles | Result |
|---|---|---|---|---|---|---|
| 1 | reader.c | 1 | 25s | 38.5M | 4598 (if_tap) | no panic |
| 2 | reader2.c (madvise) | 8 | 55s | 114.8M | 9411Γ3 (tap/vlan/gre) | no panic |
| 3 | reader2.c (madvise) | 16 | 170s | 151.3M | ~170sΓ5 modules | no panic |
| total | ~250s | ~265M | ~50k+ | 0 panics |
Why it doesn't reproduce
- Narrow reader window: ~50ns per iteration between getting a pointer to
the detaching
ifcand dereferencing its fields. - Writer requires root:
kldload/kldunloadneed root privilege, so the race is cooperative (unprivileged reader + root writer), not independently unprivileged. - Page unmap is late: the
vm_map_removethat actually makes theifcpointer dangerous happens after a long chain of cleanup work, by which point most reader iterations have already completed their traversal. - The
LIST_REMOVEitself doesn't zeroifc->ifc_list.le_next, so a reader already at the removed node can still follow the (stale but valid) next pointer to the next cloner β the UAF only bites if the module pages are unmapped while the reader is at the node.
This is consistent with the "Low" severity and "AC:H" (high attack complexity) in the CVSS vector.
Impact assessment
- If the race were hit: kernel panic (DoS) via UAF read of unmapped module memory during list traversal. No write primitive, no privilege escalation.
- Realistic trigger: requires an administrator to be actively loading/ unloading network interface modules while an unprivileged user hammers SIOCIFGCLONERS β a plausible but unusual sysadmin scenario.
- Not independently unprivileged: the writer side (kldload/kldunload) requires root.
Exploit chain
None. This is a race-condition read-side UAF with no write primitive.
There is no escalation path to uid=0. The only impact is DoS (panic) if the
race is hit, which requires root cooperation. Per the Phase 6 hard-blocker
criteria, this is a read-only UAF with no write primitive β no chain to
develop. The realistic impact ceiling is DoS.
Fix
fix.diff wraps all four functions' list/count accesses with
ifnet_lock()/ifnet_unlock():
- if_clone_attach(): moved LIST_FOREACH dup check + LIST_INSERT_HEAD +
count++ inside ifnet_lock() (merged with the existing lock for the
minifs loop). Bitmap allocation stays outside the lock (M_WAITOK, though
ifnet_lock is a sleep mutex so it would be safe either way).
- if_clone_detach(): wrapped LIST_REMOVE + kfree(ifc_units) + count--
with ifnet_lock().
- if_clone_list(): wrapped count read + LIST_FOREACH traversal + copyout
with ifnet_lock(). copyout under the sleep mutex is safe (consistent
with existing usage where if_clone_createif is called under ifnet_lock).
- if_clone_lookup(): wrapped LIST_FOREACH with ifnet_lock(). Callers
(if_clone_create at :67, if_clone_destroy at :116) do NOT hold
ifnet_lock at the call site, so acquiring it internally is safe.
Fix validation (Phase 8)
- Build:
make -j6 nativekernel KERNCONF=X86_64_GENERICβ rc=0 (clean compile). - Boot: installed
kernel.strippedβkernel, rebooted βkern.versionshows#1: Mon Jul 13 15:00:47 UTC 2026. - Functional test (no regression):
- SIOCIFGCLONERS returns the correct 7-cloner list (lagg, carp, lo, gif, usbus, tun, wlan).
- kldload/kldunload of all 5 cloner modules (if_tap, if_vlan, if_gre, if_bridge, if_wg) works.
- 60s race (16 reader threads + 5-module writer cycle): no panic, guest up.
- Code-path closure: verified by source inspection β all four functions
now hold
ifnet_lockduring list access.
The race was never observed to panic on the unpatched kernel, so the before/after behavioral contrast is "no panic / no panic". The fix validation is therefore functional (no regression) + code-path closure (verified by build + source inspection).
PoC changes
Created the entire evidence pack from scratch (the finding had no prior PoC
folder):
- reader.c β single-threaded SIOCIFGCLONERS hammer (unprivileged reader).
- reader2.c β multi-threaded version with madvise(MADV_DONTNEED) to widen
the copyout window via page faults.
- build.sh / run.sh β exact build/run commands.
- fix.diff β the verified fix (ifnet_lock wrapping all list accesses).
Fix verification
fixedVALIDATED via build+boot+functional test. Race never panicked on either kernel (too narrow). fix.diff applies+compiles+boots; SIOCIFGCLONERS returns correct 7-cloner list; kldload/kldunload OK; 60s race no panic. Code-path closure: all 4 functions now hold ifnet_lock.
baseline: 265M reader iters, 0 panics. patched: 8.5M reader iters, 0 panics. SIOCIFGCLONERS rc=0 total=7. kldload/kldunload all OK.
Confirmed kernel references
- sys/net/if_clone.c:43
- sys/net/if_clone.c:44
- sys/net/if_clone.c:147
- sys/net/if_clone.c:166
- sys/net/if_clone.c:167
- sys/net/if_clone.c:192
- sys/net/if_clone.c:193
- sys/net/if_clone.c:194
- sys/net/if_clone.c:207
- sys/net/if_clone.c:216
- sys/net/if_clone.c:219
- sys/net/if_clone.c:300
- sys/net/if.c:2017
- sys/net/if.c:2018
- sys/kern/link_elf_obj.c:904
Detail
Exploit chain
none -- read-only UAF with no write primitive. Writer requires root (not independently unprivileged). Impact ceiling: DoS panic if hit.
Evidence (decisive lines)
3 race attempts: 265M reader iters, 50k+ writer cycles, 0 panics. Code trace: if_clone_list:207/216/219-221 reads unlocked; if_clone_detach:192-194 writes unlocked; SIOCIFGCLONERS no caps_priv_check (if.c:2017-2018).
PoC changes
Created entire evidence pack from scratch. reader.c, reader2.c (multi-threaded+madvise widener), build.sh, run.sh, VERDICT.md, fix.diff (ifnet_lock all 4 functions), manifest.json.
Verified recommended fix
Wrap all if_cloners/if_cloners_count accesses with ifnet_lock/ifnet_unlock in all 4 functions. Full git-apply-able diff in findings/poc/DF-0726/fix.diff.
Verdict
NOT REPRODUCED. Race is REAL at source level -- all 4 functions access if_cloners list/count without ifnet_lock. Reader (SIOCIFGCLONERS) is unprivileged (no caps_priv_check). Writers are root-only (kldload/kldunload). 3 aggressive attempts (265M reader iters, 50k+ writer cycles) produced ZERO panics. Race too narrow (~50ns window). Cooperative race (writer requires root). No write primitive.
No comments yet.