dirfs_nrename NULL deref panic on over-length or unlinked rename paths
Summary
dirfs_vnops.c:977-981 tpath=dirfs_node_absolute_path_plus(...). fpath=dirfs_node_absolute_path_plus(...). rename(fpath,tpath) β NO NULL check. dirfs_node_absolute_path_plus (dirfs_subr.c:377-443) returns NULL when: cur==NULL :390 or count>MAXPATHLEN :433 false -> kfree+NULL :441 or dnp1==NULL parent unlinked :423. rename(NULL,...) derefs NULL in kernel = panic. Trigger: over-length path (5 levels * 250 char components >1024) or unlink parent race. Guest user dirfs mount. Fix: if(fpath==NULL||tpath==NULL) error=ENAMETOOLONG.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0808 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | faithful transcription of dirfs_node_absolute_path_plus + dirfs_nrename + live rename(NULL,...) EFAULT proof | 13.9 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 165 B | view raw |
| run.sh | run-script | ./harness | 95 B | view raw |
| build.log | build-log | final successful build, full output | 471 B | view raw |
| run.log | run-log | decisive run, full output incl EFAULT proof | 2.3 KB | view raw |
| run.2.log | run-log | stress run 2 (determinism check) | 2.3 KB | view raw |
| run.3.log | run-log | stress run 3 (determinism check) | 2.3 KB | view raw |
| env.txt | environment | uname, cc version, dirfs-not-in-kernel proof | 353 B | view raw |
| VERDICT.md | verdict | full analysis: false-positive for panic, real code defect for correctness | 6.7 KB | β raw |
| fix.diff | suggested-fix | git-apply-able fix: NULL check β ENAMETOOLONG in dirfs_nrename | 600 B | view raw |
| ../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-0808 β dirfs_nrename NULL deref panic on over-length or unlinked rename target
Verdict
NOT REPRODUCED β the finding's claimed impact (NULL deref panic) is a
FALSE POSITIVE for the security consequence. The underlying code defect
(missing NULL check on dirfs_node_absolute_path_plus return) IS real, but
rename(NULL, ...) returns EFAULT (errno 14), NOT a segfault/panic.
The kernel's copyinstr(NULL) catches the bad address and returns EFAULT
cleanly β proven by live call + ktrace on the guest.
The code defect β REAL but mischaracterized
The cited path is accurate. dirfs_nrename (sys/vfs/dirfs/dirfs_vnops.c:977-981):
977: tpath = dirfs_node_absolute_path_plus(dmp, tdnp,
978: tncp->nc_name, &tpathfree);
979: fpath = dirfs_node_absolute_path_plus(dmp, fdnp,
980: fncp->nc_name, &fpathfree);
981: error = rename(fpath, tpath); // NO NULL CHECK
dirfs_node_absolute_path_plus (dirfs_subr.c:377-443) returns NULL when:
- cur == NULL (line 390)
- assembled path > MAXPATHLEN (line 433 condition fails: dnp1 && count <= MAXPATHLEN)
- parent chain broken / unlinked (dnp1==NULL after loop break at line 423)
All three NULL-return paths are confirmed by faithful code transcription in
the harness. The missing NULL check between the path construction and the
rename() call IS a real code defect.
Why the claimed panic does NOT occur
The finding claims: "rename(NULL,...) derefs NULL in kernel = panic."
This is incorrect. In dirfs, rename is the standard libc rename()
(dirfs_vnops.c includes <unistd.h> at line 39; there is no #define rename
anywhere in sys/). libc's rename() is a thin syscall stub that passes the
raw pointers to the kernel. The kernel's kern_rename β nlookup β
copyinstr(NULL) catches the NULL address via the page-fault handler and
returns EFAULT, not a panic.
Proof β live on the guest (same libc + kernel as dirfs would use):
$ ktrace rename(NULL, "/tmp/x")
912:1 CALL rename(0,0x400b82)
912:1 RET rename -1 errno 14 Bad address
$ rename(NULL, "/tmp/x") β r=-1, errno=14 (Bad address)
$ rename("/tmp/f", NULL) β r=-1, errno=14 (Bad address)
$ rename(NULL, NULL) β r=-1, errno=14 (Bad address)
ALL DONE β process did not crash, exit 0
All three variants return EFAULT. The process does not crash. This is deterministic across 3 consecutive runs (run.log, run.2.log, run.3.log β identical output).
Why a live kernel trigger is not possible
dirfs is vkernel64-only:
- grep -c dirfs /usr/src/sys/conf/files β 0 (not in host-kernel file list)
- grep -c dirfs /usr/src/sys/platform/vkernel64/conf/files β 3 (optional dirfs)
- /boot/kernel/dirfs* β does not exist; kldstat | grep dirfs β none
- dirfs_vnops.c includes <stdio.h>, <unistd.h> (userspace headers) β it
literally cannot compile into the real host kernel
The running kernel is 6.5-DEVELOPMENT #0 (X86_64_GENERIC) which does not
include options DIRFS. There is no vkernel running on this guest. So the
rename(2) β dirfs_nrename β dirfs_node_absolute_path_plus path cannot
be exercised on a live kernel here.
However, the actual rename(NULL,...) behavior is testable on the guest
because dirfs uses the same libc rename(). The harness tests this live,
plus transcribes the dirfs_node_absolute_path_plus function faithfully.
Impact assessment
- Claimed (finding): NULL deref panic / DoS (Medium, CWE-476) β FALSE
- Actual:
rename()returns EFAULT (errno 14) instead of ENAMETOOLONG when the assembled absolute path exceeds MAXPATHLEN or the parent is unlinked. This is a POSIX correctness bug (wrong error code), not a security-relevant panic or DoS. - The error propagates cleanly:
dirfs_nrenamereturns EFAULT β VFS returns EFAULT β user'srename(2)returns EFAULT. No crash, no memory corruption, no information leak. - Impact severity: Info / Low (wrong error code, no security consequence).
Exploit chain
none (not applicable). This is not a memory-corruption primitive β it is a NULL pointer passed to a syscall that returns EFAULT. There is no corruption, no control flow hijack, and no escalation path. The NULL deref panic claimed by the finding does not occur.
The fix β fix.diff
Despite the false-positive impact, the missing NULL check IS a code defect
that should be fixed for correctness. The fix adds a NULL check after the
two dirfs_node_absolute_path_plus calls, returning ENAMETOOLONG instead
of silently getting EFAULT:
tpath = dirfs_node_absolute_path_plus(dmp, tdnp,
tncp->nc_name, &tpathfree);
fpath = dirfs_node_absolute_path_plus(dmp, fdnp,
fncp->nc_name, &fpathfree);
- error = rename(fpath, tpath);
- if (error < 0)
- error = errno;
+ if (fpath == NULL || tpath == NULL) {
+ error = ENAMETOOLONG;
+ } else {
+ error = rename(fpath, tpath);
+ if (error < 0)
+ error = errno;
+ }
The harness validates: with the fix, the over-length-path scenario returns
ENAMETOOLONG (63) instead of EFAULT (14). The cleanup path
(dirfs_dropfd(dmp, NULL, pathfree) at lines 1017-1018) is already safe
with NULL pathfree (checked at dirfs_subr.c:503).
Compile neutrality: both patched and unpatched dirfs_vnops.c compile
identically with the same warnings (filt_dirfswrite/filt_dirfsvnode
unused-function warnings, pre-existing). The fix introduces zero new
compile errors.
Applies cleanly: git apply --check RC=0; patch -p1 --dry-run hunk #1
succeeded at line 978.
Fix validation (Phase 8)
not_applicable β the finding's claimed impact (panic) does not reproduce
(rename(NULL,...) returns EFAULT), and dirfs is vkernel-only (not in the
host kernel, so no single-fix host kernel can include it). The fix is a
correctness improvement validated by:
1. Harness transcription (shows ENAMETOOLONG instead of EFAULT)
2. git apply --check RC=0
3. Compile-neutral (patched β‘ unpatched)
No kernel build/boot test is needed because there is no reproduced bug to validate against.
PoC changes
harness.cβ written from scratch (no pre-existing PoC). Faithful transcription ofdirfs_node_absolute_path_plus(dirfs_subr.c:377-443) anddirfs_nrename(dirfs_vnops.c:977-1020), plus liverename(NULL,...)calls proving EFAULT (not crash).build.sh/run.shβ standard build/run wrappers.fix.diffβ authored post-verification: NULL check β ENAMETOOLONG.
How to reproduce
ssh dfbsd-maxx # unprivileged (uid 1001) cd poc/DF-0808 ./build.sh && ./run.sh # expected: "rename(NULL,...) returns EFAULT" (no crash), proving the # finding's "NULL deref panic" claim is false
Fix verification
not_testablenot_applicable -- the finding's claimed impact (NULL deref panic) does not reproduce: rename(NULL,...) returns EFAULT (errno 14), not a crash (proven via ktrace + live call). dirfs is vkernel64-only (not in the host kernel: 0 entries in sys/conf/files, no dirfs.ko), so no single-fix host kernel can include it. The fix.diff is a correctness improvement (EFAULT -> ENAMETOOLONG) validated by harness transcription + git apply --check RC=0 + compile-neutral check (patched and unpatched dirfs_vnops.c compile identically with same warnings). No kernel build/boot test was performed because there is no reproduced bug to validate against.
Baseline (unpatched, live guest): rename(NULL, "/tmp/x") -> r=-1 errno=14 EFAULT, NO CRASH (run.log/run.2.log/run.3.log, 3 deterministic runs) Harness vulnerable transcription: dirfs_nrename returns error 14 (EFAULT) -- NO PANIC Harness fixed transcription: dirfs_nrename returns error 63 (ENAMETOOLONG) -- correct POSIX error No panic signature in boot.log (guest never crashed) git apply --check: RC=0; patch -p1 --dry-run: hunk #1 succeeded at 978
Confirmed kernel references
Detail
Exploit chain
none -- not a memory-corruption bug. rename(NULL,...) returns EFAULT (errno 14), not a NULL deref panic. The kernel's copyinstr catches the NULL address and returns EFAULT cleanly (proven via ktrace). There is no corruption primitive, no control-flow hijack, and no escalation path. The finding claims CWE-476 NULL Pointer Dereference but the dereference is caught by the kernel's fault handler before it reaches a crash -- it manifests as EFAULT, not a panic. No chain was or could be developed.
Evidence (decisive lines)
Live rename(NULL,...) test on guest (3 runs, deterministic):
rename(NULL, "/tmp/df0808_dst"): r=-1 errno=14 (Bad address) -> NO CRASH
rename("/tmp/df0808_testfile", NULL): r=-1 errno=14 (Bad address) -> NO CRASH
rename(NULL, NULL): r=-1 errno=14 (Bad address) -> NO CRASH
ALL DONE -- process did not crash, RUN_EXIT=0
ktrace confirmation:
912:1 CALL rename(0,0x400b82)
912:1 RET rename -1 errno 14 Bad address
Harness transcription (dirfs_node_absolute_path_plus):
Short path: result=/mnt/dirfs/subdir/myfile.txt (valid)
Over-length (5*200char+mountpath>1024): tpath=NULL, fpath=NULL (CONFIRMED NULL return)
Unlinked parent: returns NULL (CONFIRMED)
Vulnerable dirfs_nrename: returns error 14 (EFAULT) -- NO PANIC
Fixed dirfs_nrename: returns error 63 (ENAMETOOLONG) -- correct POSIX error
PoC changes
harness.c written from scratch (no pre-existing PoC folder existed). Faithful transcription of dirfs_node_absolute_path_plus (dirfs_subr.c:377-443) and dirfs_nrename (dirfs_vnops.c:977-1020), plus live rename(NULL,...) calls proving EFAULT (not crash). Two-pass node allocation in make_chain to avoid use-before-alloc parent pointer bug. build.sh/run.sh standard wrappers. fix.diff authored post-verification: NULL check -> ENAMETOOLONG after dirfs_node_absolute_path_plus returns.
Verified recommended fix
Add a NULL check after the two dirfs_node_absolute_path_plus calls in dirfs_nrename (dirfs_vnops.c:981): wrap 'error = rename(fpath, tpath)' in 'if (fpath == NULL || tpath == NULL) error = ENAMETOOLONG; else { ... }'. This returns the correct POSIX error (ENAMETOOLONG) instead of EFAULT when the assembled absolute path exceeds MAXPATHLEN or the parent is unlinked. The cleanup path (dirfs_dropfd at :1017-1018) is already safe with NULL pathfree. git apply --check RC=0; compile-neutral (patched == unpatched). Note: this fix addresses a correctness issue, not the claimed security panic (which does not occur).
Verdict
NOT REPRODUCED -- FALSE POSITIVE for the claimed security impact (NULL deref panic). The underlying code defect IS real: dirfs_nrename (dirfs_vnops.c:977-981) does not check the return of dirfs_node_absolute_path_plus for NULL before passing it to rename(). The function CAN return NULL (over-length path at dirfs_subr.c:433, unlinked parent at :423/:441, cur==NULL at :390) -- confirmed by faithful code transcription. HOWEVER, the finding's claim that 'rename(NULL,...) derefs NULL in kernel = panic' is FALSE: rename(NULL,...) is the standard libc rename() (dirfs_vnops.c includes
No comments yet.