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**:

```c
/* 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)
```
