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

autofs_node_vn loses a create race panicking kernel on concurrent first-lookup

Summary

autofs_vnops.c:571 mtx_lock(an_vnode_lock). :573 vp=an_vnode. :589 mtx_unlock BEFORE getnewvnode. :591 getnewvnode (can sleep). :597 KASSERT(anp->an_vnode==NULL lost race) fires when two threads both observe NULL. :598 anp->an_vnode=vp unsynchronized. Callers (nresolve:225/nmkdir:265) unlock am_lock BEFORE calling. With INVARIANTS = panic. Without = vnode leak + reclaim confusion. Any unprivileged user on autofs box. Fix: hold an_vnode_lock across getnewvnode.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0886 Β· 14 files
FileTypeDescriptionSize
df0886_race.c trigger-source kernel-module harness: 4 CPU-pinned kthreads race real autofs_node_vn() 5.5 KB view raw
stress_autofs_race.c trigger-source userspace concurrent-stat stress (probabilistic) 5.4 KB view raw
Makefile build-config kmod makefile for df0886_race 227 B ↓ download
build.sh build-script builds stress + kmod harness 910 B view raw
run.sh run-script kldloads the race harness 1.1 KB view raw
VERDICT.md verdict full analysis: mechanism, reachability, fix validation 7.5 KB ↓ raw
README.md readme quickstart reproduction guide 1.3 KB ↓ raw
fix.diff suggested-fix hold an_vnode_lock across getnewvnode() (removes race) 1014 B view raw
panic.txt panic-signature panic 'lost race' at autofs_node_vn+0x2c4 (unpatched) 743 B view raw
fix_run.log run-log 3 fixed-module runs: all racers rc=0, no panic 1.9 KB view raw
fix_build.log build-log autofs.ko module build with fix applied 8.2 KB view raw
env.txt environment uname, cc version, INVARIANTS=ON, module hashes 422 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 quickstart reproduction guide
↓ download raw

DF-0886 β€” autofs_node_vn create race

Bug: autofs_node_vn() (sys/vfs/autofs/autofs_vnops.c:564-602) drops an_vnode_lock (line 589) before sleeping in getnewvnode() (line 591) and assigns anp->an_vnode = vp (line 598) without re-taking the lock. The KASSERT at line 597 catches the create race; with INVARIANTS (default GENERIC) the kernel panics.

Impact: Medium β€” local kernel DoS (panic) on any autofs-configured box. Any unprivileged user can trigger via stat()/ls under the mountpoint. No escalation path (vnode-pointer-to-vnode-pointer overwrite, no attacker-controlled content).

Reproduce

Prerequisites (root, once)

kldload autofs
mkdir -p /autofs_test
mount_autofs -f autofs_race "" /autofs_test

Build

./build.sh     # builds stress_autofs_race + df0886_race.ko

Run (deterministic harness)

./run.sh       # kldload df0886_race.ko
# UNPATCHED: panic "lost race" in autofs_node_vn
# FIXED:     all 4 racers complete, no panic

Racer output goes to the serial console (/var/run/dmesg.boot or dmesg).

Run (userspace stress, probabilistic)

./stress_autofs_race /autofs_test 16 30
# May panic under vnode pressure; window is ~us otherwise.

Fix

See fix.diff β€” hold an_vnode_lock across getnewvnode() (mtx_t is a sleeping mutex in DragonFly, so this is safe).

VERDICT.md verdict full analysis: mechanism, reachability, fix validation
↓ download raw

DF-0886 β€” autofs_node_vn create race (lost-race KASSERT panic)

Verdict: REPRODUCED (panic, then FIXED)

The race in autofs_node_vn() is real and deterministically panics the default GENERIC kernel (INVARIANTS ON). The fix β€” holding an_vnode_lock across the sleepable getnewvnode() call β€” eliminates the race; validated on a single-fix autofs.ko module with 3 consecutive race runs, no panic.


The bug

File: sys/vfs/autofs/autofs_vnops.c:564-602 Function: autofs_node_vn()

retry:
    KKASSERT(mtx_notlocked(&anp->an_mount->am_lock));
    mtx_lock_ex_quick(&anp->an_vnode_lock);          // 571  LOCK

    vp = anp->an_vnode;                               // 573  read
    if (vp != NULL) {
        vhold(vp);
        mtx_unlock_ex(&anp->an_vnode_lock);           // 576  fast path unlock
        error = vget(vp, flags | LK_RETRY);
        ...
        return (0);
    }

    mtx_unlock_ex(&anp->an_vnode_lock);               // 589  *** UNLOCK ***

    error = getnewvnode(VT_AUTOFS, mp, &vp,           // 591  SLEEPS
        VLKTIMEOUT, LK_CANRECURSE);
    if (error)
        return (error);
    vp->v_type = VDIR;
    vp->v_data = anp;

    KASSERT(anp->an_vnode == NULL, ("lost race"));    // 597  KASSERT
    anp->an_vnode = vp;                               // 598  assign (UNLOCKED)

The lock is dropped at line 589 before getnewvnode() (line 591), which can sleep arbitrarily long (vnode allocation, vnlru reclaim). The assignment anp->an_vnode = vp at line 598 is done without re-taking the lock. The KASSERT at line 597 catches the lost race.

Race scenario

  1. Thread A: locks an_vnode_lock (571), reads an_vnode == NULL (573), drops lock (589), enters getnewvnode() (591) β€” sleeps.
  2. Thread B: locks an_vnode_lock (571), reads an_vnode == NULL (573, because A hasn't assigned yet), drops lock (589), enters getnewvnode() (591) β€” sleeps.
  3. Thread A wakes: KASSERT(597) passes (an_vnode still NULL), assigns an_vnode = vp (598), returns.
  4. Thread B wakes: KASSERT(597) fails (an_vnode != NULL) β†’ panic: lost race.

Callers (reachability)

autofs_node_vn() is called from: - autofs_nresolve() (autofs_vnops.c:232) β€” VOP_NRESOLVE, triggered by any stat(), ls, open(), or pathname resolution on the autofs mountpoint. Any unprivileged user can trigger this. - autofs_nmkdir() (autofs_vnops.c:268) β€” called by automountd. - autofs_root() (autofs_vfsops.c:271) β€” root vnode access.

Both nresolve and nmkdir release am_lock before calling autofs_node_vn (line 225 / 265), so concurrent callers can both find the same child node and race in autofs_node_vn().

Preconditions

  • autofs module loaded (kldload autofs) β€” root-only.
  • An autofs mount exists (mount_autofs) β€” root-only setup.
  • Once the admin has set up autofs, any unprivileged user triggers the race via stat()/ls of a path under the mountpoint.

Reproduction

Userspace stress (probabilistic)

stress_autofs_race.c forks N processes that all stat() the autofs mountpoint root concurrently, in a mount/unmount loop. The race window is the duration of one getnewvnode() call (~us under no pressure), so this approach is probabilistic and did not fire in 30 loops Γ— 16 racers on an idle guest. Under vnode-table pressure (low kern.maxvnodes), the window widens.

Kernel-module harness (deterministic) β€” USED FOR VALIDATION

df0886_race.c is a kldload module that: 1. Finds the first mounted autofs filesystem via mountlist. 2. NULLs the root node's an_vnode (simulating a reclaim, as autofs_vnops.c:425-428 does). 3. Spawns 4 kthreads on different CPUs (kthread_create_cpu) that all spin-wait on a barrier, then simultaneously call the REAL autofs_node_vn() on the same node.

Because getnewvnode() takes several microseconds, overlapping calls from different CPUs guarantee the race fires. Module-only (root kldload); the bug IS userspace-reachable via stat(), just too tight to fire reliably from userspace without pressure.

Panic signature (unpatched #0 kernel, original autofs.ko)

DF-0886: releasing 4 racers -- expect panic
panic: lost race
Trace beginning at frame 0xfffff80117c0fa20
autofs_node_vn() at autofs_node_vn+0x2c4 0xffffffff82603cd4
autofs_node_vn() at autofs_node_vn+0x2c4 0xffffffff82603cd4
racer_thread() at racer_thread+0x46 0xffffffff82659046
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>

The double autofs_node_vn in the stack trace is the smoking gun: two threads are inside autofs_node_vn concurrently; the one deeper in the stack hit the KASSERT after the shallower one won the assignment.


Impact

  • Default GENERIC (INVARIANTS ON): panic: lost race β†’ kernel DoS. Any unprivileged user on an autofs-configured box can crash the kernel by racing lookups of a path under the mountpoint.
  • Without INVARIANTS: the KASSERT is compiled out; both threads assign anp->an_vnode, leaking one vnode and corrupting reclaim state (the lost vnode's v_data still points at the autofs node, leading to use-after-free on unmount).
  • No escalation path: this is a pure race-condition DoS / memory corruption. The write (anp->an_vnode = vp) overwrites a vnode pointer with another vnode pointer β€” both are valid kernel objects, so there is no attacker-controlled content primitive. No path to uid=0.

CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H (Medium)


Fix

fix.diff β€” hold an_vnode_lock across getnewvnode().

mtx_t in DragonFly is a sleeping mutex (_mtx_lock_ex blocks via tsleep, confirmed in sys/sys/mutex2.h:153), so holding it across the sleepable getnewvnode() is legal. The fix:

  1. Removes the mtx_unlock_ex at line 589 (the unlock before getnewvnode).
  2. Adds mtx_unlock_ex on the error return path of getnewvnode.
  3. Adds mtx_unlock_ex after anp->an_vnode = vp (line 598).
  4. Removes the now-dead KASSERT(anp->an_vnode == NULL, ...) at 597.

The second concurrent caller blocks on mtx_lock_ex_quick at line 571 until the first finishes and sets an_vnode != NULL; it then takes the fast path (line 574). No double-create, no KASSERT, no vnode leak.

Fix validation (single-fix autofs.ko)

Built the fixed autofs.ko module from /usr/src/sys/vfs/autofs/ with the fix applied, installed it at /boot/kernel/autofs.ko, loaded it, mounted autofs, and ran the race harness 3 times:

Run autofs.ko Result
baseline original (bc43bb…) panic: lost race at autofs_node_vn+0x2c4
fix-1 fixed (68eec6…) 4/4 racers rc=0, same vp, no panic
fix-2 fixed (68eec6…) 4/4 racers rc=0, same vp, no panic
fix-3 fixed (68eec6…) 4/4 racers rc=0, same vp, no panic

The fix is deterministic: all 4 racers always observe the same vp (serialized by the lock), and no panic occurs. INVARIANTS is ON (confirmed: grep -c INVARIANTS sys/config/X86_64_GENERIC = 1).


PoC changes

  • df0886_race.c + Makefile β€” new deterministic kernel-module harness (not in the original PoC folder, which didn't exist). Calls the real autofs_node_vn() from 4 CPU-pinned kthreads.
  • stress_autofs_race.c β€” userspace concurrent-stat stress (probabilistic; documents the userspace reachability path but the window is too tight to fire reliably without vnode pressure).
  • fix.diff β€” the verified fix (hold an_vnode_lock across getnewvnode).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: the race harness panicked the unpatched baseline autofs.ko (bc43bb10) with 'panic: lost race' at autofs_node_vn+0x2c4 (2 concurrent autofs_node_vn frames in the trace). After replacing autofs.ko with the fixed version (68eec66c, fix.diff applied) and kldload-ing it, the SAME harness ran 3 times with all 4 racers completing rc=0 and observing the same vp (serialized by the lock) -- NO panic. The fix holds an_vnode_lock across getnewvnode, serializing vnode creation and eliminating the race window.

BASELINE (bc43bb10): panic: lost race / autofs_node_vn() at autofs_node_vn+0x2c4 / autofs_node_vn() at autofs_node_vn+0x2c4 / racer_thread() at racer_thread+0x46 / db>  ||  FIXED (68eec66c, run 1): racer[0] rc=0 vp=...c380 / racer[3] rc=0 vp=...c380 / racer[2] rc=0 vp=...c380 / racer[1] rc=0 vp=...c380 / no panic  ||  FIXED run 2: all rc=0 vp=...c500 / no panic  ||  FIXED run 3: all rc=0 vp=...c800 / no panic
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel unchanged; fix applied to autofs.ko module: original bc43bb10 -> fixed 68eec66c)

Confirmed kernel references

Detail

Exploit chain

none -- this is a pure race-condition DoS. The write (anp->an_vnode = vp at :598) overwrites a vnode pointer with another vnode pointer; both are valid kernel objects, so there is no attacker-controlled content primitive and no path to uid=0. With INVARIANTS ON (default GENERIC): kernel panic (DoS). Without INVARIANTS: silent vnode leak + reclaim-state corruption (the lost vnode's v_data still points at the autofs node, leading to UAF on unmount). No escalation chain was developed because the primitive is not a write-capable corruption with attacker-controlled content.

Evidence (decisive lines)

BASELINE (unpatched autofs.ko bc43bb10): DF-0886: releasing 4 racers -- expect panic / panic: lost race / Trace: autofs_node_vn() at autofs_node_vn+0x2c4 / autofs_node_vn() at autofs_node_vn+0x2c4 / racer_thread() at racer_thread+0x46 / Stopped at Debugger+0x7c / db>  ||  FIXED (autofs.ko 68eec66c, 3 runs): DF-0886 racer[0]: rc=0 vp=0xfffff8008f53c380 / racer[3]: rc=0 vp=0xfffff8008f53c380 / racer[2]: rc=0 vp=0xfffff8008f53c380 / racer[1]: rc=0 vp=0xfffff8008f53c380 / all racers done (no panic). All 4 racers observe the SAME vp -- serialized by the fix.

PoC changes

Created the entire PoC folder from scratch (only a DB row existed). Added: df0886_race.c -- deterministic kernel-module harness that finds the autofs mount via mountlist, NULLs the root node's an_vnode (simulating reclaim), and spawns 4 CPU-pinned kthreads (kthread_create_cpu) that spin-barrier then simultaneously call the REAL autofs_node_vn(). stress_autofs_race.c -- userspace concurrent-stat stress documenting the unprivileged reachability path. Makefile, build.sh, run.sh, VERDICT.md, README.md, fix.diff, manifest.json, panic.txt, fix_run.log, fix_build.log, env.txt.

Verified recommended fix

Hold an_vnode_lock across the sleepable getnewvnode() call in autofs_node_vn() (sys/vfs/autofs/autofs_vnops.c:589-598). Remove the mtx_unlock_ex at :589, add mtx_unlock_ex on the getnewvnode error-return path, add mtx_unlock_ex after the anp->an_vnode=vp assignment at :598, and remove the now-dead KASSERT at :597. mtx_t is a sleeping mutex in DragonFly (sys/sys/mutex2.h:153), so holding it across getnewvnode is safe. This is an original fix (the finding folder had no prior fix.diff); it matches the finding markdown's stated intent ('Fix: hold an_vnode_lock across getnewvnode').

Verdict

REPRODUCED. The race in autofs_node_vn() (sys/vfs/autofs/autofs_vnops.c:564-602) is real: the function drops an_vnode_lock at line 589 before sleeping in getnewvnode() (line 591) and assigns anp->an_vnode = vp at line 598 without re-taking the lock. The KASSERT at line 597 ('lost race') catches concurrent callers. A deterministic kernel-module harness (df0886_race.c) that spawns 4 CPU-pinned kthreads simultaneously calling the REAL autofs_node_vn() on the same NULL'd node panics the default GENERIC kernel (INVARIANTS ON): 'panic: lost race' at autofs_node_vn+0x2c4, with two autofs_node_vn frames in the stack trace proving the concurrent entry. The bug is userspace-reachable (autofs_nresolve at :232 is a VOP triggered by any stat()/ls under the mountpoint, called after am_lock is released at :225) but the race window is ~1 getnewvnode() call (~us), so a probabilistic userspace stress needs vnode-table pressure to widen it. The module harness is the deterministic proof.