# DF-0841 — VERDICT

## Verdict
**REPRODUCED (deterministic code-level trace)** + **FIX VALIDATED**.

The bug is real and confirmed by a line-by-line trace of
`sys/vfs/msdosfs/msdosfs_lookup.c`. The live in-kernel panic requires an
NFS-exported msdosfs plus a namecache-eviction + filehandle-replay window
that the limited 16 MiB test guest cannot reproduce reliably (the
namecache holds a permanent `vhold` on any directory vnode that has a
cached ncp, so without genuine memory pressure or vnode reclaim the
`cache_fromdvp` fall-through to `vop_nlookupdotdot` never executes).
Per the audit procedure a deterministic code-level trace proving the
`VTODE(*vpp)`-on-unassigned-`*vpp` is an acceptable reproduction when the
live trigger is too narrow — that is what this run delivers.

## Mechanism (trigger → primitive → effect)

1. **Trigger (reachability)** — `sys/vfs/msdosfs/msdosfs_lookup.c:110`
   `msdosfs_lookup()`.  It is reached via `vop_old_lookup`, the default
   `vop_nresolve` (`sys/kern/vfs_default.c:96` → `:217`), and via
   `vop_compat_nlookupdotdot` (`sys/kern/vfs_default.c:259`).  Only the
   latter sets `CNP_ISDOTDOT` (`:279`), so the ISDOTDOT branch is entered
   exclusively when the kernel walks `..` for *topology reconstruction*
   purposes — i.e. when `cache_fromdvp()` calls `vop_nlookupdotdot` on a
   directory vnode that has no namecache entry.  `cache_fromdvp(.., makeit=1,..)`
   has exactly one user: `nfs_namei()` on the **NFS server** side
   (`sys/vfs/nfs/nfs_subs.c:1153`).  The realistic production vector is
   therefore: **NFS-exported FAT filesystem + a directory vnode whose ncp
   has been evicted/reclaimed** (e.g. busy NFS server after namecache
   churn).  Regular local `cd ..` / `stat ..` does NOT hit this path
   (`vop_compat_nresolve` clears `cn_flags`, `vfs_default.c:207`).

2. **The bug** — at `sys/vfs/msdosfs/msdosfs_lookup.c:148` `*vpp = NULL`.
   In the ISDOTDOT branch (`:540`), `:543` calls `deget(pmp, cluster,
   blkoff, &tdp)` which assigns the **local** `tdp` (a `struct denode *`).
   `*vpp` (the caller's out-parameter) is **never assigned** in this
   branch.  At `:557`:

   ```c
   error = msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp);
   ```

   `VTODE(*vpp)` expands (`sys/vfs/msdosfs/denode.h:223`) to
   `((struct denode *)(*vpp)->v_data)`.  With `*vpp == NULL` this reads
   the qword at address `offsetof(struct vnode, v_data)` from address 0.

3. **Primitive** — kernel-mode read of address `0 + offsetof(struct vnode,
   v_data)`.  The offset was probed on the running kernel via:

   ```
   gdb /boot/kernel/kernel.debug -ex 'print &((struct vnode*)0)->v_data'
   => (void **) 0x128
   ```

   i.e. the read is from address **0x128**, which is inside the unmapped
   zero page on x86-64 (the kernel never maps the bottom page).  This is
   a fixed-address NULL-deref read — a pure DoS, no write primitive, no
   escalation path.

4. **Effect** — `msdosfs_lookup_checker` (`msdosfs_lookup.c:70`) does
   `vp = DETOV(tdp);` which would dereference the garbage `tdp` further.
   In practice the fault fires at the `VTODE(NULL)` read itself → fatal
   page fault, trap 12, kernel panic.

5. **Why only line 557 is wrong** — the three sibling call sites that
   also reach `msdosfs_lookup_checker` (`:481`, `:512`, `:574`) all pass
   the **local** `tdp` directly.  Only the ISDOTDOT site passes
   `VTODE(*vpp)`.  FreeBSD's msdosfs has the equivalent of
   `*vpp = DETOV(tdp);` before the checker; the assignment was lost in
   the DragonFly fork.

## Escalation
Not applicable — this is a pure read-only NULL dereference at a fixed
address (0x128).  No write primitive, no content control, no
attacker-shaped bytes.  Ceiling is **local/remote DoS** (kernel panic,
single instruction, unprivileged trigger via the NFS vector).  CVSS
matches the finding's `AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H`.

## Fix
One-line change at `sys/vfs/msdosfs/msdosfs_lookup.c:557`:

```diff
-		error = msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp);
+		error = msdosfs_lookup_checker(pmp, vdp, tdp, vpp);
```

i.e. pass the freshly-degot `tdp` (matching the three sibling sites at
`:481`, `:512`, `:574`).  See `fix.diff` for the standalone git-apply-able
diff.  This **matches** (and confirms) the finding's `## Recommended fix`
proposal.

## Fix validation (Phase 8)

| Step | Result |
|------|--------|
| Reset to `with-src` baseline, confirm `#0` unpatched | ✓ `6.5-DEVELOPMENT #0` Thu Jul 2 06:02:54 UTC 2026 |
| Confirm buggy line in baseline source | ✓ `:557` = `VTODE(*vpp)` |
| Apply `fix.diff` to in-guest `/usr/src` | ✓ `Hunk #1 succeeded at 554` |
| Build single-fix kernel `make -j6 nativekernel` | ✓ `=== NK_DONE rc=0 ===` (Sat Jul 11 09:40:41 UTC 2026) |
| Install `kernel.stripped` → `/boot/kernel/kernel` | ✓ sha256 `a557e4142626...` |
| Reboot into patched kernel | ✓ `6.5-DEVELOPMENT #1` (today's ts) |
| Confirm patched line in source | ✓ `:557` = `msdosfs_lookup_checker(pmp, vdp, tdp, vpp)` |
| msdosfs sanity (mount FAT, `cat sub/file`, `cd ..` from subdir) | ✓ `hi` returned; `cd .. ok (no panic)` |
| Patched kernel boots cleanly, no panic, guest stays up | ✓ |

### Before / after contrast
- **Before (baseline `#0`)**: `sys/vfs/msdosfs/msdosfs_lookup.c:557`
  ```c
  error = msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp);
  ```
  → would read address `0x128` in the unmapped zero page → trap 12 panic
  whenever the ISDOTDOT branch is reached via `cache_fromdvp` (NFS-exported
  FAT after ncp eviction).
- **After (patched `#1`)**: `sys/vfs/msdosfs/msdosfs_lookup.c:557`
  ```c
  error = msdosfs_lookup_checker(pmp, vdp, tdp, vpp);
  ```
  → passes the just-degot local denode, identical to the 3 sibling sites.
  ISDOTDOT branch is now indistinguishable in correctness from the regular
  lookup paths.

### Caveats
The live in-kernel panic was **not** reproduced on either the unpatched
baseline or the patched kernel, because the trigger window is genuinely
narrow: `cache_fromdvp(makeit=1)` only falls through to
`vop_nlookupdotdot` when `TAILQ_FIRST(&dvp->v_namecache) == NULL`, and a
positive ncp keeps a `vhold` on its vnode that prevents reclaim.  In a
16 MiB loopback test image with no real memory pressure, subdir vnodes
keep their ncps indefinitely.  The bug is therefore validated at the
**source level**: the deterministic `trace.c` harness prints the exact
0x128 fault address the buggy line would read, the patched source no
longer contains the bug, and the patched kernel builds, boots, and
exercises a real FAT mount + `..` traversal without panic.  This matches
the procedure's "deterministic code-level trace is acceptable when the
live trigger is too narrow" allowance.

## PoC changes
- Wrote `trace.c` — userspace deterministic harness that prints the bug
  arithmetic (NULL + 0x128 → 0x128 → trap 12).  Independent of the
  running kernel.
- Wrote `trigger_nfs.sh` — realistic in-guest live trigger via
  loopback NFS export of a FAT image, exercising the NFS server path
  documented in the finding.  Noted in the script that the
  namecache-eviction window may not reproduce in a small guest.
- Wrote `fix.diff` — minimal one-line fix (pass `tdp` instead of
  `VTODE(*vpp)`), confirmed `git apply`-able against `sys/vfs/msdosfs/
  msdosfs_lookup.c` and validated on a built-and-booted kernel.
- Wrote `build.sh` / `run.sh` — exact repro commands.
