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

Unlocked TAILQ traversal in varsymset_init() during fork: data race/UAF

Summary

varsymset_init(:519-531) copies source varsymset via TAILQ_FOREACH without acquiring copy->vx_lock. Caller fork1(kern_fork.c:646) passes &p1->p_varsymset. Concurrent LWP varsym_set(VARSYM_PROC)->varsymmake modifies TAILQ under LK_EXCLUSIVE(:457) and kfrees entries. Unlocked traversal follows freed ve pointer -> UAF. varsymdup(:510) non-atomic ++vs_refs also racy. TAILQ corruption -> panic/UAF.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0137 Β· 11 files
FileTypeDescriptionSize
race.c trigger-source fork + varsym_set(VARSYM_PROC) churn across LWPs sharing p_varsymset 3.6 KB view raw
build.sh build-script cc -O2 -o race race.c -lpthread 108 B view raw
run.sh run-script ./race 235 B view raw
run.log run-log race run output (panics; see panic.txt) 425 B view raw
panic.txt panic-signature panic: varsymdrop KKASSERT vs_refs>0; varsymset_clean->exit1->sys_exit 516 B view raw
VERDICT.md verdict full analysis: unlocked TAILQ copy + non-atomic refcount race 4.6 KB ↓ raw
README.md readme reproduce instructions 1.3 KB ↓ raw
fix.diff suggested-fix lock source vx_lock around varsymset_init copy + atomic vs_refs bump in varsymdup 1.1 KB view raw
fix_build.log build-log single-fix kernel build (kern_varsym.c) rc=0 5.6 MB ↓ download
../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 reproduce instructions
↓ download raw

DF-0137 β€” unlocked TAILQ traversal in varsymset_init() during fork

Verdict REPRODUCED
Impact kernel panic / local DoS (refcount-corruption race)
File sys/kern/kern_varsym.c:510, 519-531 (caller sys/kern/kern_fork.c:646)

Build

cc -O2 -o race race.c -lpthread

Run (unprivileged)

./race

Expected (bug present)

Within ~seconds, the kernel panics and the guest drops to DDB:

panic: assertion "sym->vs_refs > 0" failed in varsymdrop at /usr/src/sys/kern/kern_varsym.c:492
varsymdrop() -> varsymset_clean() -> exit1() -> sys_exit()

The ssh session dies (Connection closed). The panic is captured in dfbsd-qemu/boot.log.

Expected (fixed)

The process runs to completion and prints RACE_DONE: no panic.

Mechanism (short)

varsymset_init() copies a varsymset via unlocked TAILQ_FOREACH and bumps a shared refcount non-atomically (varsymdup, :510). fork1 calls it on the parent's p_varsymset; a concurrent LWP (pthreads share struct proc) doing varsym_set(VARSYM_PROC) frees entries / atomically drops refs under vx_lock. The lost non-atomic increment underflows the refcount β†’ varsymdrop's KKASSERT β†’ panic.

See VERDICT.md for the full analysis and fix.diff for the patch.

VERDICT.md verdict full analysis: unlocked TAILQ copy + non-atomic refcount race
↓ download raw

DF-0137 β€” unlocked TAILQ traversal in varsymset_init() during fork

Verdict: REPRODUCED (kernel panic / DoS via refcount-corruption race)

Mechanism

varsymset_init() (sys/kern/kern_varsym.c:519) copies the source varsymset with an unlocked TAILQ_FOREACH and a helper varsymdup() that bumps the shared varsym refcount non-atomically:

/* kern_varsym.c:504-516 */
static void varsymdup(struct varsymset *vss, struct varsyment *ve) {
    struct varsyment *nve;
    nve = kmalloc(sizeof(struct varsyment), M_VARSYM, M_WAITOK|M_ZERO);  /* sleeps */
    nve->ve_sym = ve->ve_sym;
    ++nve->ve_sym->vs_refs;          /* NON-ATOMIC (kern_varsym.c:510) */
    TAILQ_INSERT_TAIL(&vss->vx_queue, nve, ve_entry);
}

/* kern_varsym.c:519-531 */
void varsymset_init(struct varsymset *vss, struct varsymset *copy) {
    ...
    if (copy) {
        TAILQ_FOREACH(ve, &copy->vx_queue, ve_entry)   /* NO copy->vx_lock */
            varsymdup(vss, ve);
        ...
    }
}

fork1() calls this as varsymset_init(&p2->p_varsymset, &p1->p_varsymset) (sys/kern/kern_fork.c:646). fork1 holds p1->p_token (acquired kern_fork.c:324), but varsymmake() (the varsym_set(VARSYM_PROC) path) does not take p1->p_token β€” it only takes &p1->p_varsymset.vx_lock LK_EXCLUSIVE (kern_varsym.c:457) and then TAILQ_REMOVE + kfree's entries (:476-479) and calls varsymdrop (atomic refcount decrement, :478).

pthread_create (the lwp_create syscall) makes a new LWP in the same struct proc, so multiple LWPs share p_varsymset. Therefore:

  • LWP A (forker): in fork1 β†’ varsymset_init, traversing p1->p_varsymset without vx_lock, bumping vs_refs non-atomically and sleeping in varsymdup's kmalloc(M_WAITOK).
  • LWP B (churner): in varsym_set(VARSYM_PROC) β†’ varsymmake, holding vx_lock, freeing entries and atomically decrementing vs_refs.

The non-atomic ++vs_refs (:510) races the atomic varsymdrop decrement (:493): the increment is lost, the refcount underflows, and the next varsymdrop hits KKASSERT(sym->vs_refs > 0) (:492) β†’ panic.

Proof (decisive run)

3 forkers + 5 churners, ~seconds into the run the guest panicked:

panic: assertion "sym->vs_refs > 0" failed in varsymdrop at /usr/src/sys/kern/kern_varsym.c:492
Trace beginning at frame 0xfffff80117f83838
varsymdrop()      at varsymdrop+0x4e
varsymdrop()      at varsymdrop+0x4e
varsymset_clean() at varsymset_clean+0x57
exit1()           at exit1+0x59
sys_exit()        at sys_exit+0xe
syscall2()        at syscall2+0x11e
Stopped at Debugger+0x7c
db>

The faulting child (forked during the race) exits β†’ exit1 β†’ varsymset_clean β†’ varsymdrop sees a varsym whose vs_refs already reached 0 (the non-atomic fork-copy increment was lost to the concurrent varsymmake/varsymdrop) β†’ KKASSERT β†’ panic. ssh dies; guest sits in DDB.

Reliably reproduced (panic on the first 30s run with the multi-thread harness).

Impact / realism & escalation assessment

  • Reachable by an unprivileged local user (any multi-threaded process): pthreads share p_varsymset, so the user forks + churns its own process varsyms. No privileges, no special config, default GENERIC kernel.
  • Class: refcount-corruption / use-after-free race. On the default GENERIC kernel (INVARIANTS ON) it manifests as a deterministic kernel panic (DoS) β€” kern_slaballoc.c/the varsymdrop KKASSERT trap the refcount underflow before any silent reclamation.
  • Escalation to uid=0 is not realistic on GENERIC: INVARIANTS convert the refcount underflow into a panic before a UAF can be reclaimed, and the victim objects (struct varsym / struct varsyment) carry no function pointers and no credential/proc pointers β€” there is nothing in them to hijack into control flow or a forged credential. The realistic impact ceiling on the default kernel is therefore local DoS (panic). (On a non-default INVARIANTS-OFF build the underlying refcount/UAF would corrupt silently β€” a hardening concern, but not a default-kernel escalation.)

Fix

fix.diff (two changes, same root cause): 1. varsymset_init: acquire copy->vx_lock (LK_SHARED) around the TAILQ_FOREACH copy so a concurrent varsymmake cannot free entries mid-copy. 2. varsymdup: make the refcount bump atomic (atomic_add_int) so it cannot lose a race against varsymdrop.

Supersedes (specifies) the finding's proposal.

Reproduce

cc -O2 -o race race.c -lpthread    # build.sh
./race                             # run.sh  (panics the default kernel)

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 17 17:35:56 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live panic). varsymset_init unlocked TAILQ copy vs concurrent varsymmake -> refcount underflow KKASSERT panic. Unprivileged via pthreads.