# DF-3188 — pfsm RB-tree lookup-vs-final-remove race (UAF / double-free)

## What

`hammer_load_pseudofs()` (sys/vfs/hammer/hammer_inode.c:983-989) finds a
`struct hammer_pseudofs_inmem` in `hmp->rb_pfsm_root` with an unlocked
`RB_LOOKUP` + `hammer_ref()` pair, while `hammer_rel_pseudofs()`
(hammer_inode.c:1218-1227) drops the last reference with an unlocked
`hammer_rel()` + `hammer_norefs()` check + `RB_REMOVE()` + `kfree()`.
There is no lock (spin, token, or the `hammer_ref_interlock()` machinery
used for buffers) serializing the 0-ref transition against concurrent
lookups.

Interleaving:

```
T2 (load)                          T1 (final rel)
RB_LOOKUP finds pfsm
                                   hammer_rel()          refs 1->0
                                   hammer_norefs()       -> true
hammer_ref()                       refs 0->1
return pfsm
                                   RB_REMOVE(pfsm)   <- already reffed
                                   kfree(pfsm)
copyout(&pfsm->pfsd...) /          (T2 now owns freed memory)
hammer_rel() on freed object ->
  second RB_REMOVE of removed node + double kfree()
```

## Reachability (unprivileged)

`HAMMERIOC_GET_PSEUDOFS` is one of the two PFS ioctls whose
`caps_priv_check(cred, SYSCAP_NOVFS_IOCTL)` result is unconditionally
overwritten in the dispatcher (hammer_ioctl.c, `case HAMMERIOC_GET_PSEUDOFS`).
Calling it with a **nonexistent** pfs id runs the complete
kmalloc -> RB_INSERT -> ENOENT -> rel -> RB_REMOVE -> kfree cycle on every
invocation with no other state involved, so both sides of the race can be
sprayed flat-out by an unprivileged local user.

## Reproduce

guest# (root, once)
    truncate -s 10G /home/maxx/h1.img
    vnconfig -c -S 10g /dev/vn0 /home/maxx/h1.img
    newfs_hammer -f -L test /dev/vn0
    mount_hammer /dev/vn0 /mnt && chmod 755 /mnt

guest$ (unprivileged)
    cc -O -pthread -I/usr/src/sys/vfs/hammer -o race_getpfs race_getpfs.c
    ./race_getpfs /mnt 40000 480 8

expected on a winning interleave: kernel panic (double free / RB tree
corruption / hammer_rel on freed chunk KKASSERT) or silently corrupted
rb_pfsm_root; the racer prints periodic ENOENT rates, "SURVIVED" if no hit.

## Observed

8 threads, 6 vCPU guest: >400M full load/rel cycles at ~1M ioctls/s,
no winning interleave in the observation window.  The window is bounded by
the findable lifespan of the transient pfsm (RB_INSERT..rel in the ioctl
error path, tens of ns) times the ref-gap of the racing lookup (a few
instructions), which matches the empirical hit rate being very low.
The race is structural (no serialization exists); see VERDICT.md.
