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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0808 Β· 12 files
FileTypeDescriptionSize
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
VERDICT.md verdict full analysis: false-positive for panic, real code defect for correctness
↓ download 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_nrename returns EFAULT β†’ VFS returns EFAULT β†’ user's rename(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 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).
  • 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_testable
baseline no→ patch + rebuild →patched clean

not_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
↓ fix.diffper-fix-DF-0808

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 :39, no #define rename in sys/), which passes the raw pointer to the host kernel. The kernel's kern_rename -> nlookup -> copyinstr(NULL) catches the bad address via the page-fault handler and returns EFAULT (errno 14), NOT a panic. Proven definitively via ktrace (CALL rename(0,...) -> RET -1 errno 14) and live rename(NULL,...) calls on the guest -- all three variants (NULL oldpath, NULL newpath, both NULL) return EFAULT cleanly, process exits 0, no crash. Deterministic across 3 consecutive runs. dirfs is vkernel64-only (not in sys/conf/files, only in sys/platform/vkernel64/conf/files as 'optional dirfs'; no dirfs.ko in /boot/kernel), so a live kernel trigger is not possible on this guest, but the rename(NULL,...) behavior is testable because dirfs uses the same libc rename(). Actual impact: rename returns EFAULT instead of ENAMETOOLONG -- a POSIX correctness bug (wrong error code), not a security-relevant panic/DoS.