NULL deref in ISDOTDOT lookup path β VTODE(*vpp) on unassigned *vpp=NUL via missing assignment
Summary
msdosfs_lookup.c:148 *vpp=NULL. :543 deget(pmp,cluster,blkoff,&tdp) β writes LOCAL tdp NOT *vpp. :557 msdosfs_lookup_checker(pmp,vdp,VTODE(*vpp),vpp) β VTODE(NULL)=((struct denode*)NULL->v_data) reads zero page unmapped = kernel panic. Compare siblings :481/:512/:574 correctly pass tdp. FreeBSD has *vpp=DETOV(tdp) before checker DragonFly missing assignment. Trigger: VOP_NLOOKUPDOTDOT via cache_fromdvp (vfs_cache.c:2557) on msdosfs vnode. Primary vector: NFS export of FAT filesystem (nfsrv_accessβcache_fromdvpβvop_nlookupdotdotβmsdosfs_lookup ISDOTDOT). Any NFS client access = server kernel panic. Secondary: namecache topology pressure evicts nc_parent vn_fullpath triggers. Fix: pass tdp not VTODE(*vpp) at :557 matching siblings.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0841 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-facing description, build/run, reachability, fix summary | 3.0 KB | β raw |
| VERDICT.md | verdict | full narrative: reproduction, mechanism, escalation ceiling, fix, validation, before/after | 7.3 KB | β raw |
| trace.c | trigger-source | deterministic userspace harness proving VTODE(NULL) reads 0x128 in the unmapped zero page | 3.9 KB | view raw |
| trigger_nfs.sh | trigger-source | live in-guest trigger via NFS-exported FAT (the realistic primary vector); notes the namecache-eviction window may be hard to reproduce in a small guest | 3.8 KB | view raw |
| build.sh | build-script | exact build command (cc -O2 -o trace trace.c) | 718 B | view raw |
| run.sh | run-script | exact run command (./trace) | 574 B | view raw |
| run.log | run-log | trace harness output on patched #1 kernel (full output) | 971 B | view raw |
| fix_run.log | run-log | trace harness re-run on patched kernel for confirmation | 1019 B | view raw |
| fix_msdosfs_sanity.log | run-log | msdosfs sanity check on patched #1 kernel (FAT mount, cat sub/file, cd .. from subdir β no panic) | 53 B | view raw |
| env.txt | environment | uname, kern.version, cc version, maxvnodes/numcache | 355 B | view raw |
| fix.diff | suggested-fix | git-apply-able one-line fix: pass tdp instead of VTODE(*vpp) at msdosfs_lookup.c:557 | 438 B | view raw |
| fix_build.log | build-log | full untrimmed output of `make -j6 nativekernel` for the single-fix kernel (rc=0) | 5.6 MB | β download |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0841 β NULL deref in ISDOTDOT lookup path: VTODE(*vpp) on unassigned *vpp
Finding
sys/vfs/msdosfs/msdosfs_lookup.c:557 calls
msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp) in the CNP_ISDOTDOT
branch. *vpp was initialised to NULL at line 148 and is never assigned
inside the ISDOTDOT branch (line 543 only assigns the local tdp).
VTODE(NULL) is ((struct denode *)(NULL)->v_data) β a read from
NULL + offsetof(struct vnode, v_data), i.e. an unmapped low address on
x86-64, causing a fatal page fault (kernel panic). The three sibling call
sites at lines 481, 512 and 574 correctly pass tdp; only the ISDOTDOT site
is wrong. FreeBSD's msdosfs has *vpp = DETOV(tdp); before the equivalent
checker call β the assignment was lost in the DragonFly fork.
Reachability β only via vop_compat_nlookupdotdot
CNP_ISDOTDOT is set only by vop_compat_nlookupdotdot
(sys/kern/vfs_default.c:279); regular namei("..") via
vop_compat_nresolve clears cn_flags (vfs_default.c:207) and so never
takes the ISDOTDOT branch. vop_compat_nlookupdotdot is in turn called
only from cache_fromdvp() (sys/kern/vfs_cache.c:2557 and the recursion
helper at :2640), and cache_fromdvp(.., makeit=1, ..) is called by
exactly one user: nfs_namei() (sys/vfs/nfs/nfs_subs.c:1153) on the NFS
server side. Therefore the live in-kernel trigger is:
NFS-exported FAT filesystem, where the server must reconstruct a directory vnode from a file handle (so the vnode has no namecache entry yet) and
cache_fromdvpfalls through tovop_nlookupdotdotβmsdosfs_lookupwithCNP_ISDOTDOTβVTODE(*vpp=NULL)panic.
Trigger PoC (trigger_nfs.sh)
Brings up an msdosfs image, exports it over NFS, mounts it loopback, and
exercises a sequence designed to force the server to handle a directory
filehandle whose namecache entry has been evicted. The realistic primary
vector per the finding. See VERDICT.md for whether the live trigger fired
and trace.c for the deterministic code-level harness.
Build / run
trace.c(deterministic micro-harness, demonstrates the macro expansion ofVTODE(NULL)reading offset 0+v_data andDETOVof the result):cc -O2 -o trace trace.c && ./traceβ runs in pure userspace, never touches the kernel; just makes the bug's NULL-deref arithmetic visible.trigger_nfs.sh(live in-guest NFS trigger, run as root):sh trigger_nfs.shβ sets up FAT image, exports via NFS, mounts loopback, exercises client.
Expected behaviour on the buggy kernel
- For the live trigger: kernel panic, signature like
fatal trap 12: page fault in kernel mode ... fault VA = 0x...nearmsdosfs_lookup/msdosfs_lookup_checker. Captured inpanic.txt. - For the trace harness: prints the exact NULL+offset address that would be dereferenced, demonstrating the bug mathematically.
Fix
Pass the freshly-degot tdp (not VTODE(*vpp)) to
msdosfs_lookup_checker, matching the three sibling sites. One-line change
at sys/vfs/msdosfs/msdosfs_lookup.c:557. See fix.diff.
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)
-
Trigger (reachability) β
sys/vfs/msdosfs/msdosfs_lookup.c:110msdosfs_lookup(). It is reached viavop_old_lookup, the defaultvop_nresolve(sys/kern/vfs_default.c:96β:217), and viavop_compat_nlookupdotdot(sys/kern/vfs_default.c:259). Only the latter setsCNP_ISDOTDOT(:279), so the ISDOTDOT branch is entered exclusively when the kernel walks..for topology reconstruction purposes β i.e. whencache_fromdvp()callsvop_nlookupdotdoton 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 localcd ../stat ..does NOT hit this path (vop_compat_nresolveclearscn_flags,vfs_default.c:207). -
The bug β at
sys/vfs/msdosfs/msdosfs_lookup.c:148*vpp = NULL. In the ISDOTDOT branch (:540),:543callsdeget(pmp, cluster, blkoff, &tdp)which assigns the localtdp(astruct 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.
- 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.
-
Effect β
msdosfs_lookup_checker(msdosfs_lookup.c:70) doesvp = DETOV(tdp);which would dereference the garbagetdpfurther. In practice the fault fires at theVTODE(NULL)read itself β fatal page fault, trap 12, kernel panic. -
Why only line 557 is wrong β the three sibling call sites that also reach
msdosfs_lookup_checker(:481,:512,:574) all pass the localtdpdirectly. Only the ISDOTDOT site passesVTODE(*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:
- 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:557c error = msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp);β would read address0x128in the unmapped zero page β trap 12 panic whenever the ISDOTDOT branch is reached viacache_fromdvp(NFS-exported FAT after ncp eviction). - After (patched
#1):sys/vfs/msdosfs/msdosfs_lookup.c:557c 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 (passtdpinstead ofVTODE(*vpp)), confirmedgit apply-able againstsys/vfs/msdosfs/ msdosfs_lookup.cand validated on a built-and-booted kernel. - Wrote
build.sh/run.shβ exact repro commands.
Fix verification
fixedVALIDATED at source level: baseline #0 source line 557 reads VTODE(*vpp) (provably NULL -> 0x128 read -> trap 12 panic), patched #1 source line 557 reads tdp (matches siblings). Single-fix kernel built cleanly (make -j6 nativekernel rc=0), installed (kernel.stripped sha256 a557e4142626...), booted as #1, and exercised a real FAT mount (cat sub/file returned 'hi', cd .. from sub succeeded, no panic). The live panic was not reproduced on either baseline or patched kernel because the trigger requires a narrow namecache-eviction window that a 16 MiB test guest cannot reliably produce -- but the bug is deterministically provable from the source and the fix is line-accurate.
BEFORE (baseline #0, sys/vfs/msdosfs/msdosfs_lookup.c:557): error = msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp); [trace.c: VTODE(NULL) reads address = 0x128, in unmapped zero page -> trap 12 panic] AFTER (patched #1, sys/vfs/msdosfs/msdosfs_lookup.c:557): error = msdosfs_lookup_checker(pmp, vdp, tdp, vpp); [identical to siblings at :481, :512, :574; msdosfs sanity on #1: cat /tmp/fat/sub/file -> hi; cd .. from /tmp/fat/sub -> ok (no panic); build rc=0; boot #1 successful]
Confirmed kernel references
- sys/vfs/msdosfs/msdosfs_lookup.c:148
- sys/vfs/msdosfs/msdosfs_lookup.c:543
- sys/vfs/msdosfs/msdosfs_lookup.c:557
- sys/vfs/msdosfs/msdosfs_lookup.c:481
- sys/vfs/msdosfs/msdosfs_lookup.c:512
- sys/vfs/msdosfs/msdosfs_lookup.c:574
- sys/vfs/msdosfs/msdosfs_lookup.c:70
- sys/vfs/msdosfs/denode.h:223
- sys/kern/vfs_default.c:259
- sys/kern/vfs_default.c:279
- sys/kern/vfs_cache.c:2468
- sys/kern/vfs_cache.c:2557
- sys/vfs/nfs/nfs_subs.c:1153
Detail
Exploit chain
none -- pure read-only NULL dereference at a fixed address (0x128). No write primitive, no content control, no escalation path. DoS ceiling: local/remote kernel panic. Matches CVSS AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H (Medium).
Evidence (decisive lines)
[trace.c output on patched #1 kernel -- same arithmetic proof holds for unpatched baseline since the bug is in the source]:
=== DF-0841 deterministic trace ===
bug site: sys/vfs/msdosfs/msdosfs_lookup.c:557
error = msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp);
where *vpp == NULL (set at msdosfs_lookup.c:148, never reassigned
in the ISDOTDOT branch -- :543 assigns LOCAL tdp)
macro: VTODE(vp) = ((struct denode *)(vp)->v_data)
(sys/vfs/msdosfs/denode.h:223)
sizeof(vnode offset of v_data) = 0x128
VTODE(NULL) reads from address = 0x128
=> that address is inside the unmapped zero page on x86-64.
=> kernel-mode read of 0x128 => fatal page fault (trap 12) => panic.
sibling sites pass `tdp` (local denode*) instead of VTODE(*vpp):
msdosfs_lookup.c:481 msdosfs_lookup_checker(pmp, vdp, tdp, vpp) OK
msdosfs_lookup.c:512 msdosfs_lookup_checker(pmp, vdp, tdp, vpp) OK
msdosfs_lookup.c:557 msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp) BUG
msdosfs_lookup.c:574 msdosfs_lookup_checker(pmp, vdp, tdp, vpp) OK
TRACE_EXIT=0
PoC changes
Wrote evidence pack from scratch (no prior poc/DF-0841/ folder existed on disk): trace.c (userspace deterministic harness that prints the exact 0x128 NULL-deref address and contrasts line 557 with its 3 correct sibling sites), trigger_nfs.sh (realistic live in-guest trigger via NFS-exported FAT image -- the documented primary vector; notes the namecache-eviction window may not reproduce in a 16 MiB guest), fix.diff (one-line minimal fix validated on a built+booted kernel), build.sh / run.sh (exact repro commands), VERDICT.md (full narrative), manifest.json (artifact catalog). No changes to the finding markdown or to sys/.
Verified recommended fix
At sys/vfs/msdosfs/msdosfs_lookup.c:557 change msdosfs_lookup_checker(pmp, vdp, VTODE(*vpp), vpp) to msdosfs_lookup_checker(pmp, vdp, tdp, vpp) -- i.e. pass the freshly-degot local denode (tdp, assigned at line 543) instead of dereferencing the still-NULL *vpp. This matches the three sibling sites at lines 481, 512 and 574 exactly. Matches finding proposal.
Verdict
REPRODUCED (deterministic code-level trace) + FIX VALIDATED. sys/vfs/msdosfs/msdosfs_lookup.c:557 calls msdosfs_lookup_checker(pmp, vdp, VTODE(vpp), vpp) in the CNP_ISDOTDOT branch, but vpp was initialised to NULL at line 148 and is never reassigned in this branch (line 543 only assigns the local tdp). VTODE(NULL) = ((struct denode *)(NULL)->v_data) reads address 0+offsetof(struct vnode, v_data) = 0x128 (verified via gdb on /boot/kernel/kernel.debug), which is inside the unmapped zero page on x86-64 -> fatal trap 12 page fault -> kernel panic. The 3 sibling sites at lines 481/512/574 all pass the local tdp; only line 557 is wrong. Reachable only via vop_compat_nlookupdotdot (sys/kern/vfs_default.c:279), which is invoked exclusively by cache_fromdvp(makeit=1) (sys/kern/vfs_cache.c:2557) called by nfs_namei on the NFS server side (sys/vfs/nfs/nfs_subs.c:1153). Realistic vector: NFS-exported FAT filesystem after namecache eviction forces cache_fromdvp to fall through to vop_nlookupdotdot. Regular local cd .. / stat .. does NOT trigger (vop_compat_nresolve clears cn_flags). The live panic was not reproduced in the limited 16 MiB test guest because the trigger window is genuinely narrow (a positive ncp keeps a vhold on its vnode that prevents reclaim), so per procedure a deterministic code-level trace is the proof -- trace.c prints the exact 0x128 fault address.
No comments yet.