# DF-5846 — VERDICT

**Status: REPRODUCED** (unprivileged kernel panic, stock INVARIANTS
kernel, fresh run 2026-10-26; 3/3 historical in the DF-2717 pack with
crash-dump root-cause). **Impact: local DoS (panic) only — no escalation
primitive** (fixed-offset NULL read).

## 1. Fresh stock reproduction (this run)

- Guest: DragonFly 6.5-DEVELOPMENT `#0: Thu Jul 2 06:02:54 UTC 2026`
  (stock snapshot, INVARIANTS), 6 vCPU QEMU/KVM.
- Trigger: `df2717_storm 90 3 0xf` as unprivileged `maxx` (uid 1001) —
  pthread storm: fchdir swapper between tmpfs `/tmp/df2717/a` and
  hammer2 `/home/maxx/df2717/b`, relative `".."` chdir swapper across the
  mount boundary, volatile mkdir/chdir/rmdir/chdir-away dance on 64
  rotating directories, and 3 threads of relative-path open() readers.
- Result (`panic-stock.txt`): within the 90 s window —
  `Fatal trap 12: page fault`, `fault virtual address = 0x84`,
  `Stopped at nlookup+0x66c: movl 0x84(%rax),%edx`,
  `current process = 904` (df5846_storm, uid 1001), CPU3.
  Signature byte-for-byte identical to the DF-2717-era captures
  (nlookup+0x66c, VA 0x84, kern_chdir caller).

## 2. Root cause (source-verified, path:line)

1. `rmdir(cwd)` defers the namecache zap (`NCF_DEFEREDZAP`) because the
   fd-table reference is held (`sys/kern/vfs_cache.c`). Under chdir
   churn the deferred zap completes while `fd_ncdir` still points at the
   node: it becomes **topology-disconnected** — `nc_parent = NULL`,
   unhashed, unnamed, but alive and referenced. Proven by kgdb on
   /var/crash/vmcore.0 in the DF-2717 run: cwd ncp had `nc_parent=0,
   nc_head=0, nc_name=0, nc_flag=0x1210, nc_refs=30`, path
   `"../../../tmp/df2717/a"`, element `".."`.
2. In `nlookup()`'s `".."` handler the NULL-parent case is handled ONLY
   for the mount-point walk (`sys/kern/vfs_nlookup.c:741-756`,
   `mnt_ncmounton.ncp == NULL` → "broke jail" recovery). The normal
   parent step `sys/kern/vfs_nlookup.c:758`
   (`nctmp.ncp = nctmp.ncp->nc_parent`) has **no NULL guard**, even
   though the intended generic NULL-parent handling exists further down
   (:831+).
3. The resulting `nctmp.ncp = NULL` survives `cache_copy()` (which is
   NULL-safe, `sys/kern/vfs_cache.c:996-1026`) into `nch`, and
   `nch.ncp->nc_generation & ~3` at `sys/kern/vfs_nlookup.c:767`
   dereferences NULL+0x84 — the observed trap.

Single-threaded `rmdir(cwd); ls ..` is benign (the fd reference blocks
the zap); the multi-actor storm is what completes the deferred zap while
walkers keep adopting the disconnected node as their walk base.

## 3. Fix and A/B validation

`fix.diff` = the NULL guard (Part B of the DF-2717 pack's validated fix;
the FD-spinlock hardening in that pack belongs to DF-2717 and is NOT
needed for this crash):

```c
} else {
	nctmp.ncp = nctmp.ncp->nc_parent;
}
+ if (nctmp.ncp == NULL) {
+	kprintf("vfs_nlookup: '..' traverse disconnected: ...");
+	nctmp = nd->nl_rootnch;
+ }
```

- Validation kernel: tri-fixed build `#2: Mon Oct 26 06:04:19 UTC 2026`
  (vfs_nlookup NULL guard + DF-5847 nd6 fix + DF-5848 m_dup_data fix;
  the two others are in disjoint subsystems), RC=0 under -Werror.
- A/B (the exact full-mix storm `90/150 3 0xf`): stock baseline =
  **4/4 nlookup NULL-parent panics** (3 historical + 1 fresh, all ≤90 s).
  On the fixed kernel the guard **fired 2× then 46×** across two runs —
  every firing is a walk that is an instant panic on stock — with
  **zero nlookup faults**. The DF-5846 crash is gone.
- Honest caveat / separate bug: the storm harness has a SECOND kill
  path — `hammer2_xop_start_except+0x25` (NULL+0x81554 read) — that
  eventually takes down BOTH stock and fixed kernels after the DF-5846
  crash would already have fired. Attribution: on the FIXED kernel with
  mode 0xd (no cross-mount `".."` walks) the guard fired **0 times**
  and the hammer2 crash still occurred; on PRISTINE STOCK with mode 0xd
  (which has no guard at all) the identical crash occurred. The hammer2
  panic is therefore pre-existing and independent of this fix — it is a
  **new candidate finding** (transcripts: run-fixed3-console.txt,
  panic-fixedrun-hammer2-incidental.txt,
  stock-hammer2-attribution-run.txt).
- The `'..' traverse disconnected` recovery message firing 46× in one
  run confirms the guard handles the exact stock crash condition at
  volume without adverse effects (no leak, no wrong behavior).

## 4. Impact ceiling / privesc assessment

The primitive is a kernel read at a fixed NULL offset (0x84) — a trap,
not a corruption. No attacker-controlled value reaches memory; there is
no write, no leak, no lifetime hijack. **No path to uid=0.** Ceiling =
reliable unprivileged local DoS, exactly as filed (Medium,
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H).

## 5. Notes

- The DF-2717 unlocked-read sites are the delivery vehicle that makes
  walkers adopt the disconnected base quickly, but a correctly-locked
  reader would copy the same legitimate handle — the missing NULL guard
  is the root cause and the fix stands alone.
- Guest returned to a clean snapshot after validation.
