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

tap(4) softc use-after-free: clone-destroy/create paths race open() with no common lock (kfree'd tap_softc used by tapopen/tapwrite/tapclose)

Summary

tapopen() dereferences dev->si_drv1 under the mplock and publishes TAP_OPEN, but the ifconfig destroy path SIOCIFDESTROY -> if_clone_destroy() -> tap_clone_destroy() checks TAP_OPEN and runs tapdestroy() -> kfree(sc) with no mplock; tapcreate() also publishes dev->si_drv1 before the softc/interface is initialized. An open() interleaved with create/destroy therefore operates on a freed or half-built tap_softc: it sets TAP_OPEN/taprefcnt in freed memory, returns a usable fd, and the subsequent tapwrite/tapioctl/tapclose run entirely on freed memory. REPRODUCED 2/2 panics within 43-90s of hammering (8 open/write/close threads vs create/destroy cycling): 'Fatal user address access from kernel mode', fault VA 0x84 supervisor WRITE, trace tapifstop <- tapclose <- dev_dclose <- close(fd), with TAPDEBUG showing torn flags impossible on a live tap; crash dump + kgdb backtrace captured. Fixed kernel survives 3x identical runs (~480s) with zero panics. Privileged trigger (0600 root:wheel + SYSCAP gates); with slab grooming RIP control plausible but pointless post-root. Fix validated (publish si_drv1 after init + ENXIO on NULL + mplock across clone_destroy).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3405 Β· 10 files
FileTypeDescriptionSize
taprace.c β€” 3.8 KB view raw
run.sh β€” 162 B view raw
README.md β€” 1.2 KB ↓ raw
VERDICT.md β€” 7.0 KB ↓ raw
run.log β€” 3.5 KB view raw
fix_validation.log β€” 1.0 KB view raw
panic.txt β€” 3.0 KB view raw
core.txt.0 β€” 121.4 KB ↓ download
fix.diff β€” 1.9 KB view raw
verdict.json β€” 6.0 KB view raw

DF-3405 β€” tap(4) softc use-after-free: clone-destroy/create vs open

tap_softc lifetime is protected only on some paths: tapopen/tapclose hold the mplock, but the ifconfig destroy path (SIOCIFDESTROY -> if_clone_destroy -> tap_clone_destroy -> tapdestroy -> kfree) takes no mplock, and tapcreate publishes dev->si_drv1 before the softc is initialized. An open() racing create/destroy operates on a freed or half-built tap_softc.

Reproduce (guest, as root)

cc -O2 -o taprace taprace.c -lpthread
sysctl -w debug.if_tap_debug=1
sysctl -w debug.debugger_on_panic=0   # so the panic text hits serial
./taprace 90

8 threads open/write/close /dev/tap0 in a tight loop while the main thread cycles SIOCIFCREATE/SIOCIFDESTROY on tap0.

Expected (stock kernel #0): kernel panic within ~90 s (tapclose -> tapifstop writing through the freed softc β€” fault VA 0x84, supervisor write), crash dump in /var/crash. Baseline hit 2/2 runs (43 s and ≀90 s).

Expected (kernel with fix.diff): no panic; guest stays up.

Fix

See fix.diff (publish si_drv1 late; ENXIO on NULL; mplock in tap_clone_destroy). Validated in-guest: see fix_validation.log.

VERDICT.md
↓ download raw

DF-3405 β€” VERDICT

Status: reproduced (baseline, stock INVARIANTS kernel #0). Fix: fixed (patched kernel #1 built in-guest; the UAF crash is gone). Impact ceiling: kernel use-after-free (privileged-trigger local DoS / memory corruption); no unprivileged trigger found (open + destroy both require root/cap).

Root cause (path:line, all against sys/net/tap/if_tap.c)

The tap softc's lifetime is governed by three unsynchronized entry paths:

  • tapopen() (:315) holds the mplock and dereferences dev->si_drv1 (:332) for its entire body; it publishes TAP_OPEN at :347 β€” but nothing forces a racing destroy to wait for that.
  • tapclose() (:395) holds the mplock and (auto-destroy path, :458-461) calls if_clone_destroy().
  • tap_clone_destroy() (:503) β€” the ifconfig tapN destroy / SIOCIFDESTROY path β€” holds no mplock at all: if_clone_destroy() (sys/net/if_clone.c:104) only takes ifnet_lock(), which tapopen() never acquires. It checks TAP_OPEN (:507) and then runs tapdestroy() (:471) which does ether_ifdetach, dev->si_drv1 = NULL (:487), destroy_dev, and kfree(sc) (:495).
  • tapcreate() (:214) also publishes dev->si_drv1 = sc at :223, before the interface is initialized (:236-250), so a racing open("/dev/tapN") can also catch a half-built softc (this is what the captured flags = 0x41 line proves happened).

Any interleave where tap_clone_destroy()'s TAP_OPEN check passes while a tapopen() (or the SIOCIFCREATE-side tapcreate()) is inside the window leaves the open path β€” and everything reachable from the returned fd (tapwrite, tapioctl, tapclose) β€” operating on a kfreed tap_softc.

How it was reproduced (baseline)

taprace.c: 8 threads loop open("/dev/tap0") β†’ write(64B) β†’ close() while the main thread loops SIOCIFCREATE tap0 β†’ 20-60ms β†’ SIOCIFDESTROY tap0. Built and run as root on the stock INVARIANTS kernel #0 (DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026).

  • Run 1 (90 s): kernel fault bcmp+0x3c (wild pointer read), guest dead in ddb.
  • Run 2 (150 s, debug.if_tap_debug=1, debug.debugger_on_panic=0): panic at 43 s uptime β€” full trace and crash dump captured (panic.txt, core.txt.0):
tap0: opened, minor = 0. Module refcnt = 1
tap0: writing, minor = 0
tap0: not ready, minor = 0, flags = 0x41      <- TAP_OPEN|TAP_MANUALMAKE
                                                 with TAP_INITED clear:
                                                 torn/freed softc state
                                                 (a live ifconfig-created
                                                 tap reads 0x53)
Fatal user address access from kernel mode from taprace at ffffffff82600cb8
Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0x84
fault code               = supervisor write data, page not present
current process          = 872 (taprace)
panic: page fault
tapifstop() at tapifstop+0x88        <- if_tap.c:710 IF_DRAIN(&sc->tap_devq)
tapclose()  at tapclose+0x1b5        <- if_tap.c:422
dev_dclose / devfs_spec_close / ... / kern_close(fd=4)

kgdb on the dump (core.txt.0): tapifstop (sc=0xfffff801179a9e00, clear_flags=1) at if_tap.c:710 β€” the closer thread is writing through subqueue pointers read out of the freed softc (fault VA 0x84; the dump's process table is also corrupt: Cannot access memory at 0x2b00001665). 2/2 completed baseline runs panicked within ≀90 s.

Why the crash is the UAF (not e.g. a half-created tap)

Both interleavings are the same missing-lock defect:

  1. open vs destroy: tapopen fetched si_drv1 (or tap_clone_destroy checked TAP_OPEN) just before kfree(sc); the open side then sets TAP_OPEN/taprefcnt in freed memory and hands the fd to userland; the eventual close() runs tapclose β†’ tapifstop on the freed softc β†’ fault writing through garbage ifq_sq pointers.
  2. open vs create: si_drv1 is published at :223 before TAP_INITED (:250) β€” a racing open proceeds on a half-built sc (the captured flags = 0x41 line) and/or races the concurrent destroy of that same unit.

Exploit chain (as far as the class permits)

Root-triggerable kernel UAF write. The freed tap_softc (~256 B, M_TAP kmalloc zone) is immediately re-usable by any same-size allocation; a reallocation with attacker-influenced content gives control of the ifq_sq/if_snd pointers that tapifstop/tapifstart write through, and of sc->tap_dev/sc->arpcom used by every later fd operation. On this guest no SMEP/SMAP/KASLR is present, so a patient root attacker could plausibly convert this to RIP control β€” but the trigger already requires root, so escalation adds nothing; the finding is filed at Low severity as privileged local DoS / memory corruption. No unprivileged trigger exists: /dev/tapN is 0600 root:wheel and tapuopen defaults to 0 (SYSCAP_RESTRICTEDROOT gate at :323-327); SIOCIFCREATE/SIOCIFDESTROY are SYSCAP_RESTRICTEDROOT (net/if.c:2012).

Fix validation

fix.diff (3 hunks, all in if_tap.c): 1. tapcreate: publish dev->si_drv1 only after the softc is fully initialized (TAP_INITED set); 2. tapopen: si_drv1 == NULL β†’ ENXIO (never resurrect a destroyed/pending device); 3. tap_clone_destroy: hold the mplock across the TAP_OPEN check and tapdestroy() β€” matching the lock discipline tapopen/ tapclose already use.

Built in-guest (make -j4 nativekernel KERNCONF=X86_64_GENERIC, make installkernel, kernel #1 Sat Sep 12 06:59:23 UTC 2026, both DF-3405+DF-3406 fixes). Re-ran the identical race three times (~480 s total hammering, same 8-thread pressure): zero panics, guest stable (stock crashed 2/2 within 90 s). Post-fix create/open/write/destroy cycles work (CYCLE_OK).

Residual, honestly noted (NOT covered by this fix)

On the patched kernel a destroy_dev() racing an in-flight open() can still lose the fd β€” the cdev is destroyed mid-open, the fd is never delivered, tapopen's taprefcnt++/TAP_OPEN are left set forever (zombie tap unit; kldunload if_tap blocked until reboot). No memory unsafety was observed in this state (the UAF is gone); fully closing it needs destroy_dev()-side synchronization (devfs drain of in-flight opens), which is outside this file. Reported in notes/recommended_fix.

Artifacts

  • taprace.c β€” trigger (8 openers vs SIOCIFCREATE/SIOCIFDESTROY)
  • run.sh β€” baseline procedure (sysctls + signature scan)
  • panic.txt β€” serial-console capture of the panic (run 2)
  • core.txt.0 β€” savecore output incl. kgdb backtrace of the dump
  • run.log β€” baseline narrative; fix_validation.log β€” patched runs
  • fix.diff β€” the validated fix

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (3 hunks: publish dev->si_drv1 only after tapcreate() fully initializes the softc; tapopen() returns ENXIO when si_drv1 is NULL instead of resurrecting a destroyed/pending device; tap_clone_destroy() holds the mplock across its TAP_OPEN check and tapdestroy(), matching tapopen/tapclose discipline). Built in-guest with make -j4 nativekernel KERNCONF=X86_64_GENERIC + make installkernel (kernel #1, Sat Sep 12 06:59:23 UTC 2026, combined with the DF-3406 hunk). Re-ran the identical race three times (~480s total, same 8-thread open/write/close vs SIOCIFCREATE/SIOCIFDESTROY pressure that panicked the stock kernel 2/2 within 43-90s): zero panics, guest stable, post-fix create/open/write/destroy cycle passes (CYCLE_OK). The UAF crash is gone. Residual (documented, outside this diff): destroy_dev() vs an in-flight open() can still lose the fd leaving taprefcnt=1/TAP_OPEN set (zombie tap unit until reboot) -- a devfs-level drain issue, no memory unsafety observed.

['findings/poc/DF-3405/fix_validation.log', 'findings/poc/DF-3405/fix.diff']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Sep 12 06:59:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root: 8 threads open+write+close /dev/tap0 while another thread cycles SIOCIFCREATE/SIOCIFDESTROY -> tapopen fetches dev->si_drv1 and continues while tapdestroy kfrees the 256-byte M_TAP softc (or catches tapcreate's half-built sc published at if_tap.c:223) -> open side writes TAP_OPEN/taprefcnt into freed memory and returns an fd -> tapwrite/tapioctl/tapclose run entirely on the freed softc -> tapclose->tapifstop writes through garbage ifq_sq pointers -> supervisor write page fault (VA 0x84), panic. The freed kmalloc(256) chunk is reallocation-groomable; on this no-SMEP/SMAP/KASLR guest a persistent root attacker could aim for RIP control, but the trigger is already root-only: escalation value none, filed Low/DoS. No unprivileged entry found.

Evidence (decisive lines)

["panic.txt: full serial-console panic -- 'Fatal user address access ... fault virtual address = 0x84 / supervisor write data' + trace 'tapifstop() at tapifstop+0x88 / tapclose() at tapclose+0x1b5', preceded by TAPDEBUG 'not ready, minor = 0, flags = 0x41' (torn softc flags)", "core.txt.0: savecore/kgdb dump session -- #6 tapifstop (sc=0xfffff801179a9e00, clear_flags=1) at /usr/src/sys/net/tap/if_tap.c:710, #7 tapclose at if_tap.c:422, via dev_dclose/devfs_spec_close/kern_close(fd=4); process table already corrupt ('Cannot access memory at 0x2b00001665')", 'run.log: baseline run1 (90s) kernel fault bcmp+0x3c, run2 (150s) panic at 43s uptime -- 2/2 completed runs crashed', 'fix_validation.log: patched kernel #1 (both fixes), 3 race runs ~480s total, 0 panics, guest up, functional create/open/write/destroy CYCLE_OK']

PoC changes

PoC written fresh for this finding (race harness). Improved mid-validation: destroy is now attempted every cycle (not only after a successful create) because a single EBUSY stalled the loop; that stall also exposed the residual devfs destroy_dev-vs-in-flight-open wedge (documented, not part of this finding's fix).

Verified recommended fix

Publish dev->si_drv1 only after full tapcreate() init; tapopen() returns ENXIO when si_drv1 is NULL; hold the mplock across tap_clone_destroy()'s TAP_OPEN check + tapdestroy() (see fix.diff); follow-up: drain in-flight opens in destroy_dev().

Verdict

tap_softc use-after-free reproduced on the stock INVARIANTS kernel: tapopen() (mplock) races the ifconfig destroy path tap_clone_destroy()->tapdestroy()->kfree(sc) (no mplock; only ifnet_lock, which tapopen never takes), and tapcreate() publishes dev->si_drv1 before the softc is initialized. 8-thread open/write/close vs SIOCIFCREATE/SIOCIFDESTROY hammering panicked the guest within 43-90s on 2/2 completed baseline runs: 'Fatal user address access from kernel mode', fault VA 0x84 supervisor WRITE, trace tapifstop() at if_tap.c:710 <- tapclose() <- dev_dclose <- close(fd), with the TAPDEBUG line 'not ready, flags = 0x41' proving the closer ran on torn/freed softc state (a live ifconfig-created tap reads 0x53); full crash dump in core.txt.0 (kgdb: tapifstop sc=0xfffff801179a9e00). Fixed in-guest (fix.diff: publish si_drv1 after full init; tapopen ENXIO on NULL; mplock across tap_clone_destroy) -- 3 re-runs ~480s total identical pressure, zero panics, normal create/open/write/destroy cycles pass. Trigger requires root on both sides (/dev/tapN 0600 + tapuopen=0 SYSCAP gate; SIOCIFDESTROY SYSCAP_RESTRICTEDROOT), so severity is Low (privileged local kernel memory corruption / DoS), memcorrupt bucket.