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)
PoC verification
Evidence pack
findings/poc/DF-3405 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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 dereferencesdev->si_drv1(:332) for its entire body; it publishesTAP_OPENat :347 β but nothing forces a racing destroy to wait for that.tapclose()(:395) holds the mplock and (auto-destroy path, :458-461) callsif_clone_destroy().tap_clone_destroy()(:503) β theifconfig tapN destroy/ SIOCIFDESTROY path β holds no mplock at all:if_clone_destroy()(sys/net/if_clone.c:104) only takesifnet_lock(), whichtapopen()never acquires. It checksTAP_OPEN(:507) and then runstapdestroy()(:471) which doesether_ifdetach,dev->si_drv1 = NULL(:487),destroy_dev, andkfree(sc)(:495).tapcreate()(:214) also publishesdev->si_drv1 = scat :223, before the interface is initialized (:236-250), so a racingopen("/dev/tapN")can also catch a half-built softc (this is what the capturedflags = 0x41line 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:
- open vs destroy:
tapopenfetchedsi_drv1(ortap_clone_destroycheckedTAP_OPEN) just beforekfree(sc); the open side then setsTAP_OPEN/taprefcntin freed memory and hands the fd to userland; the eventualclose()runstapclose β tapifstopon the freed softc β fault writing through garbageifq_sqpointers. - open vs create:
si_drv1is published at :223 beforeTAP_INITED(:250) β a racing open proceeds on a half-built sc (the capturedflags = 0x41line) 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 dumprun.logβ baseline narrative;fix_validation.logβ patched runsfix.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
fixedfix.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']
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.
No comments yet.