β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5846

Unguarded NULL nc_parent dereference in nlookup dotdot walk on topology-disconnected cwd - unprivileged reliable local DoS

  • File: sys/kern/vfs_nlookup.c:756 (deref fallout at :767)
  • Severity: Medium (unpriv reliable kernel panic)
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
  • CWE: CWE-476 NULL Pointer Dereference
  • Confidence: certain (3/3 panics ≀90s, crash-dump root-caused)
  • Found in pass 2 (GLM 5.3) β€” discovered while verifying DF-2717

Summary

In the nlookup ".." traversal, nctmp.ncp = nctmp.ncp->nc_parent (vfs_nlookup.c:756) runs without a NULL guard, and the resulting handle is dereferenced at :767 (NULL+0x84). A current working directory whose namecache node has been topology-disconnected (nc_parent=0, unhashed β€” produced by rmdir(cwd) deferred-zap completing under churn while fd_ncdir holds the last reference) feeds a NULL straight through. The intended NULL-parent handling at :831 runs after the deref.

Reproduction (from findings/poc/DF-2717/, storm bit-mix 0xf)

Unprivileged pthread storm: fchdir swapper between tmpfs/hammer2 mounts + volatile mkdir/chdir/rmdir/chdir-away "last-ref-drop dance" + relative-path open readers. Stock INVARIANTS kernel panicked 3/3 within 90 seconds: nlookup+0x667 Fatal trap 12 fault at NULL+0x84; kgdb on vmcore.0 shows nch.ncp=0x0, cwd ncp with nc_parent=0, nc_head=0, nc_name=0, nc_flag=0x1210, nc_refs=30 β€” the disconnected-node signature. Single-threaded rmdir(cwd)+ls .. is benign (fd ref blocks the zap); churn is required.

Threat

Any local user; reliable kernel panic (whole-system DoS). No memory-safety primitive beyond the NULL fixed-offset read.

NULL-guard the parent walk: if nctmp.ncp == NULL after the assignment, recover exactly like the existing jail-break case (nctmp = nd->nl_rootnch). Patched kernel survived 2Γ—150s of the exact storm that killed stock 3/3.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5846 Β· 10 files
FileTypeDescriptionSize
df2717_storm.c β€” 6.4 KB view raw
panic-stock.txt β€” 845 B view raw
run-stock.log β€” 992 B view raw
run-fixed.log β€” 1.9 KB view raw
run-fixed3-console.txt β€” 3.7 KB view raw
panic-fixedrun-hammer2-incidental.txt β€” 893 B view raw
stock-hammer2-attribution-run.txt β€” 758 B view raw
fix.diff β€” 967 B view raw
env.txt β€” 722 B view raw
VERDICT.md β€” 5.0 KB ↓ raw
VERDICT.md
↓ download raw

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

} 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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Stock baseline: 4/4 nlookup NULL-parent panics (3 historical DF-2717 + 1 fresh this run), all within 90s. Patched kernel (tri-fix build): the guard fired 2x then 46x across two full-mix runs with zero nlookup faults - the DF-5846 crash is gone. Honest caveat: the storm harness has a SECOND, independent kill path (hammer2_xop_start_except NULL+0x81554 read) that still takes down both stock and patched kernels after the DF-5846 crash would have fired; attribution runs on the patched kernel (mode 0xd, zero guard firings) and on pristine stock (mode 0xd) prove it is pre-existing and unrelated - it is a new candidate finding, not a fix failure. DF-5846-specific behavior eliminated.

run-fixed.log (both fixed-kernel storm runs + the two attribution runs); run-fixed3-console.txt (46 guard messages, crash at hammer2 site not nlookup); panic-stock.txt (baseline); fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Mon Oct 26 06:04:19 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (tri-fixed: DF-5846+DF-5847+DF-5848)

Confirmed kernel references

Detail

Exploit chain

none - fixed-offset kernel read at NULL+0x84 (nc_generation offset); no write, no leak, no lifetime hijack; impact ceiling is reliable unprivileged local DoS

Evidence (decisive lines)

panic-stock.txt (fresh stock panic, nlookup+0x66c VA 0x84, process df5846_storm); run-fixed.log + run-fixed3-console.txt (46 guard recoveries, no nlookup fault on fixed kernel); stock-hammer2-attribution-run.txt (hammer2 crash pre-exists on stock, guard-independent); fix.diff

PoC changes

trigger used verbatim from the DF-2717 pack (df2717_storm.c); no code changes needed; watchdog-wrapped 90/150/300s budgets; fix.diff extracted standalone (part B of the DF-2717 validated fix)

Verified recommended fix

NULL-guard the dotdot parent walk: recover with nctmp = nd->nl_rootnch when nctmp.ncp == NULL after the nc_parent assignment (see fix.diff)

Verdict

Fresh stock-kernel reproduction of the unprivileged nlookup dotdot NULL-parent panic (Fatal trap 12, VA 0x84, nlookup+0x66c from uid-1001 storm process, inside the 90s window; identical to the 3/3 DF-2717-era panics with kgdb root-cause). Fix validated by A/B on the rebuilt kernel: the NULL guard fired 2x then 46x across two full-mix runs with ZERO nlookup faults - every firing is an instant panic on stock. The storm can still kill the guest afterwards at a DIFFERENT site (hammer2_xop_start_except, NULL+0x81554); attribution runs prove that hammer2 crash is pre-existing on pristine stock (mode 0xd, guard never fires) and independent of this fix - flagged as a separate candidate finding. No escalation primitive: fixed-offset NULL read only.