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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0726 Β· 13 files
FileTypeDescriptionSize
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
README.md readme summary + reproduce instructions
↓ download 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.
VERDICT.md verdict full analysis: mechanism trace, 3 race attempts, fix validation
↓ download raw

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)

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 β€” SIOCIFGCLONERS case in ifioctl(): calls if_clone_list() directly with no caps_priv_check (contrast SIOCIFCREATE at :2007 and SIOCIFDESTROY at :2013, which both require SYSCAP_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_count again (no lock, TOCTOU vs :207)
  • :219-221 LIST_FIRST + LIST_NEXT traversal (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_FOREACH traversal (no lock)
  • Called from if_clone_create() at :67 (before ifnet_lock() at :74) and if_clone_destroy() at :116 (after ifnet_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_FOREACH duplicate-name check (no lock)
  • :166 LIST_INSERT_HEAD(&if_cloners, ifc, ifc_list) (no lock β€” before ifnet_lock() at :169)
  • :167 if_cloners_count++ (no lock)
  • The ifnet_lock() at :169 only covers the minifs allocation 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. tapmodevent at sys/net/tap/if_tap.c:190-201), which calls if_clone_detach(&tap_cloner) at if_tap.c:194.
  • :556 file->ops->unload(file) β†’ link_elf_obj_unload_file() at sys/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 to tap_cloner (a static struct in if_tap.ko's .data section) or tap_cloner.ifc_name (a const 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

  1. Narrow reader window: ~50ns per iteration between getting a pointer to the detaching ifc and dereferencing its fields.
  2. Writer requires root: kldload/kldunload need root privilege, so the race is cooperative (unprivileged reader + root writer), not independently unprivileged.
  3. Page unmap is late: the vm_map_remove that actually makes the ifc pointer dangerous happens after a long chain of cleanup work, by which point most reader iterations have already completed their traversal.
  4. The LIST_REMOVE itself doesn't zero ifc->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.version shows #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_lock during 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

fixed
baseline no→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 15:00:47 UTC 2026 (sha256 26ef0aa5...)

Confirmed kernel references

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.