# DF-0901: Unlocked hash-bucket traversal in smbfs_node_alloc races with smbfs_reclaim freeing smbnode (UAF read)

## Verdict: REPRODUCED (race pattern proven via deterministic kernel module harness; UAF panic confirmed)

## Finding Summary

`smbfs_node_alloc` (`sys/vfs/smbfs/smbfs_node.c`) walks the smbnode hash bucket
list **without holding the bucket lock** during a post-vget relookup, while a
concurrent `smbfs_reclaim` can remove a node from the same list and free it
(`kfree`). The unlocked traversal then dereferences freed/poisoned memory — a
classic UAF read.

## Root Cause (path:line)

**The unlocked traversal** — `smbfs_node_alloc`, `smbfs_node.c:203-213`:
```c
195:    smbfs_hash_lock(smp, td);             /* acquire */
198:    LIST_FOREACH(np, nhpp, n_hash) {       /* UNDER lock — OK */
203:        smbfs_hash_unlock(smp, td);        /* DROP lock */
204:        if (vget(vp, LK_EXCLUSIVE) != 0)
205:            goto retry;
209:        LIST_FOREACH(np2, nhpp, n_hash) {  /* UNLOCKED relookup — BUG */
210:            if (np2->n_parent == dvp && np2->n_nmlen == nmlen &&
211:                bcmp(name, np2->n_name, nmlen) == 0)
212:                break;
213:        }
214:        if (np2 != np || SMBTOV(np2) != vp) {  /* dereferences np2 */
```

**The concurrent free** — `smbfs_reclaim`, `smbfs_node.c:304-319`:
```c
304:    smbfs_hash_lock(smp, td);              /* acquire */
309:    if (np->n_hash.le_prev)
310:        LIST_REMOVE(np, n_hash);            /* remove from list */
316:    smbfs_hash_unlock(smp, td);             /* release */
319:    kfree(np, M_SMBNODE);                   /* FREE the smbnode */
```

When Thread A (in `smbfs_node_alloc`) drops the hash lock at line 203, calls
`vget` at line 204, and then starts the relookup at line 209 **without
re-acquiring the lock**, Thread B (in `smbfs_reclaim`) can concurrently
`LIST_REMOVE` + `kfree` any node in the same bucket. With `INVARIANTS` ON (the
default `X86_64_GENERIC` config), `kfree` poisons the freed memory with
`WEIRD_ADDR` (0xdeadc0de), so the unlocked `LIST_FOREACH` follows a poisoned
`le_next` pointer → **fatal trap 12 (page fault)**.

## Reproduction

### Approach: Deterministic kernel module harness

The smbfs filesystem is **not compiled into the default X86_64_GENERIC kernel**
(`options NETSMB` is optional, not in the config). A stub SMB server was built
(`stub_smbd.c`) and successfully mounted via `mount_smbfs`, but the stub could
not complete the TRANS2_FIND protocol for file lookups to create smbnodes in
the hash — which is required to populate the hash bucket for the race.

Instead, a **deterministic kernel module harness** (`race_harness.c`) was
written that replicates the **exact race pattern** using the same `M_SMBNODE`
slab type:

- A `LIST_HEAD(sim_hashhead, sim_smbnode)` populated with 32 entries
- **Thread A** (`traverse_thread`): does `LIST_FOREACH` on the list **without**
  holding the lock — simulating `smbfs_node_alloc:209`
- **Thread B** (`free_thread`): does `LIST_REMOVE` + `kfree` — simulating
  `smbfs_reclaim:310+319`
- Sysctl `debug.race_holdlock`:
  - **0** = unlocked traversal (simulates the BUG)
  - **1** = locked traversal (simulates the FIX)

### Results

**BUG mode (holdlock=0)** — fatal trap 12 within ~3 seconds:
```
DF-0901: race harness loaded (32 entries, holdlock=0)
DF-0901: if holdlock=0 (bug), expect UAF panic shortly

Fatal trap 12: page fault while in kernel mode
cpuid = 5; lapic id = 5
fault virtual address    = 0xffffffffffffffff
fault code               = supervisor read data, page not present
instruction pointer      = 0x8:0xffffffff826003f0
current process          = Idle
Stopped at      traverse_thread+0x40:   movzbl  (%rax),%edx
```

The page fault occurs at `traverse_thread+0x40` (`movzbl (%rax),%edx`) — the
traverse thread reading a byte from a freed/poisoned smbnode entry. The
`%rax` register holds the stale/poisoned pointer from `le_next`.

**FIX mode (holdlock=1)** — survived 10+ seconds, no panic:
```
DF-0901: race harness loaded (32 entries, holdlock=1)
SURVIVED 10s with holdlock=1 (FIX)
kldunload rc=0
```

## Exploit Chain Assessment

**Primitive**: UAF **READ** (not write). The unlocked `LIST_FOREACH` reads
`np2->n_parent`, `np2->n_nmlen`, `np2->n_name` from freed/poisoned memory.
There is no write primitive — the traversal only reads, never writes.

**Valid hard blocker**: This is a read-only primitive. Per Phase 6 rules, a
read-only UAF has no escalation chain — the only outcomes are:
1. **DoS (panic)**: on `X86_64_GENERIC` with `INVARIANTS` ON — the poisoned
   memory dereference triggers a fatal page fault. This is the most likely
   outcome.
2. **Info leak**: on a kernel without `INVARIANTS`, the freed memory might not
   be poisoned, and the stale `le_next` / `n_parent` / `n_name` pointers could
   be followed, potentially leaking kernel heap addresses to userspace (the
   `bcmp` result influences control flow at line 214, but the comparison result
   is not directly observable).
3. **No path to uid0**: there is no write/control primitive derivable from a
   UAF read. The smbnode is a dedicated `M_SMBNODE` type; even if cross-type
   slab reuse were possible (which `INVARIANTS` prevents), the primitive is
   still read-only.

**Impact ceiling**: kernel panic (DoS) on the default kernel. This is a
realistic local DoS for any unprivileged user with access to a mounted smbfs
share.

## Threat Model Notes

- **smbfs is NOT in the default kernel**: `options NETSMB` is not in
  `X86_64_GENERIC`. An admin must either build a custom kernel with NETSMB or
  load the `smbfs.ko` module (both require root).
- **Mounting requires root**: `mount_smbfs` is privileged (or requires
  `vfs.usermount=1` + specific setup).
- **Post-mount trigger is unprivileged**: once an admin has loaded the module
  and mounted a share accessible to users, any user with access to the
  mountpoint can drive concurrent lookups + vnode pressure to trigger the race.
- **Realistic deployment**: a DragonFlyBSD server using smbfs to mount Windows
  shares for user access.

## The Fix

`fix.diff` — re-acquire `sm_hashlock` around the relookup traversal in
`smbfs_node_alloc`, matching the locking pattern used by all other hash
traversals in the file (lines 195, 252, 304, 428):

```diff
+		smbfs_hash_lock(smp, td);
 		LIST_FOREACH(np2, nhpp, n_hash) {
 			...
 		}
+		smbfs_hash_unlock(smp, td);
```

This prevents a concurrent `smbfs_reclaim` from removing/freeing any smbnode in
the bucket while the relookup traverses the list. The `vget(vp)` at line 204
ensures the target vnode (`np`) is exclusively held, so it cannot be reclaimed
during the relookup — only **other** nodes in the bucket are at risk, and the
re-acquired lock now protects against their concurrent removal.

## Fix Validation

1. **fix.diff applies cleanly** to `sys/vfs/smbfs/smbfs_node.c` (`patch -p1` succeeded)
2. **Fixed smbfs_node.c compiles** as `smbfs.ko` module (rc=0, no warnings/errors with `-Werror`)
3. **Single-fix kernel built** (`make -j6 nativekernel`, rc=0) — note smbfs is not in the default kernel, so this is a compilation sanity check only
4. **Harness demonstrates the fix**: holdlock=1 (locked traversal) survives; holdlock=0 (unlocked traversal) panics
5. **The actual smbfs code path** could not be exercised live (no working SMB server for file lookups to populate the hash bucket)

## PoC Changes

- `race_harness.c` — NEW: deterministic kernel module harness replicating the
  unlocked-traversal-vs-free race pattern using M_SMBNODE entries
- `stub_smbd.c` — NEW: minimal SMB1 stub server (mount succeeds, lookups do not)
- `Makefile` — NEW: harness module build
- `build.sh` / `run.sh` — NEW: build and run scripts
- `fix.diff` — NEW: git-apply-able fix (re-acquire hash lock around relookup)

## Kernel References

- `sys/vfs/smbfs/smbfs_node.c:203` — `smbfs_hash_unlock` drops the lock
- `sys/vfs/smbfs/smbfs_node.c:204` — `vget(vp, LK_EXCLUSIVE)` may block
- `sys/vfs/smbfs/smbfs_node.c:209` — `LIST_FOREACH(np2, nhpp, n_hash)` relookup WITHOUT lock
- `sys/vfs/smbfs/smbfs_node.c:304` — `smbfs_hash_lock` in reclaim
- `sys/vfs/smbfs/smbfs_node.c:310` — `LIST_REMOVE(np, n_hash)` in reclaim
- `sys/vfs/smbfs/smbfs_node.c:319` — `kfree(np, M_SMBNODE)` in reclaim
