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)
PoC verification
Evidence pack
findings/poc/DF-0137 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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, ©->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, traversingp1->p_varsymsetwithoutvx_lock, bumpingvs_refsnon-atomically and sleeping invarsymdup'skmalloc(M_WAITOK). - LWP B (churner): in
varsym_set(VARSYM_PROC)βvarsymmake, holdingvx_lock, freeing entries and atomically decrementingvs_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/thevarsymdropKKASSERTtrap the refcount underflow before any silent reclamation. - Escalation to
uid=0is 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
fixedvalidated
see evidence pack
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.
No comments yet.