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

dirfs_alloc_file openat error path leaks dirfs node and parent refcount

Summary

dirfs_subr.c:182 dnp=dirfs_node_alloc(mp). :186 name set. :187-188 dnp->dn_parent=pdnp dirfs_node_ref(pdnp). :193-199 if openat()==-1: dirfs_dropfd + return errno WITHOUT dirfs_node_free(dmp,dnp). Compare stat error path :202-210 correctly calls dirfs_node_free. Each failed openat leaks one dirfs_node (~200B+name) + permanently inflates parent refcount preventing free. Sustained: kernel heap exhaustion + unmount fail. Trigger: open O_CREAT in read-only/quota-exceeded dir. Fix: dirfs_node_free(dmp,dnp) before return on openat error.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0856 Β· 13 files
FileTypeDescriptionSize
trigger.c trigger-source Annotated source-level reproduction (no-op runtime; documents the openat error-path leak and the contrast with the sibling stat error path) 3.4 KB view raw
fix.diff suggested-fix One-line git-apply-able fix: dirfs_node_free(dmp,dnp) before return on openat error 355 B view raw
build.sh build-script Build the (no-op) trigger 199 B view raw
run.sh run-script Run the (no-op) trigger; bug verified by source trace 438 B view raw
build.log build-log Trigger build output (guest cc 8.3) 69 B view raw
run.log run-log Trigger run output (exit 0, no side effects) 506 B view raw
env.txt environment uname, cc version, kernel info 737 B view raw
baseline_vkernel64_dirfs_build.log build-log Custom VKERNEL64_DIRFS nativekernel build WITHOUT fix: dirfs fails to compile (11 dirfs_subr.c errors + 20+ in vnops/vfsops) 610.6 KB ↓ download
fix_vkernel64_dirfs_build.log build-log Custom VKERNEL64_DIRFS nativekernel build WITH fix: identical 11 dirfs_subr.c errors (only line numbers shift +1) β€” fix introduces zero new errors 610.8 KB ↓ download
VERDICT.md verdict Full narrative: mechanism, dead-code analysis, no-new-errors proof 8.3 KB ↓ raw
README.md readme Human overview + reproduce instructions 2.6 KB ↓ 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
README.md readme Human overview + reproduce instructions
↓ download raw

DF-0856 β€” dirfs_alloc_file openat error path leaks dirfs node + parent refcount

Finding

  • Severity: Low (CWE-401 Memory Leak)
  • CVSS: 3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
  • File: sys/vfs/dirfs/dirfs_subr.c lines 182–199
  • Function: dirfs_alloc_file

Summary

In dirfs_alloc_file, the openat(2) error path (lines 196–199) returns errno after only calling dirfs_dropfd, leaking the dirfs node allocated at line 182 and permanently inflating the parent's refcount (incremented at line 188). The sibling dirfs_node_stat error path at lines 203–209 correctly calls dirfs_node_free(dmp, dnp) first; the openat error path omits this.

Build

Not applicable β€” dirfs is optional dirfs in sys/platform/vkernel64/conf/files and is not present in the default VKERNEL64 config (and absent entirely from the running X86_64_GENERIC guest: nm /boot/kernel/kernel.debug | grep -i dirfs returns 0). The reproduction for this finding is source-level: see trigger.c for the annotated source trace, and VERDICT.md for the full mechanism.

To build the (no-op) trigger for completeness:

cc -o trigger trigger.c

Run / Expected

./trigger

trigger.c returns 0 with no side effects β€” it is a documented source trace, not a runtime trigger. The bug is verified by reading sys/vfs/dirfs/dirfs_subr.c:193-200 against the matching pattern at sys/vfs/dirfs/dirfs_subr.c:202-210.

To exercise the bug at runtime you would need to: 1. Build a vkernel64 with options dirfs enabled (test harness β€” the default VKERNEL64 config does not include it). 2. Boot that vkernel on a host directory backing the dirfs mount. 3. As a vkernel user, open(O_CREAT, ...) against a directory the vkernel process cannot write (read-only / quota-exceeded / no-permission) so the host openat(2) inside dirfs returns -1. 4. Observe leaked dirfs_node slab growth in the vkernel process and a dangling refcount on the parent node.

This is out of scope for the default-kernel threat model: dirfs only ships in vkernel64, and only when explicitly enabled.

Fix

fix.diff adds dirfs_node_free(dmp, dnp); immediately before the existing dirfs_dropfd / return errno on the openat failure path, mirroring the correct stat error path. dirfs_node_free (line 106) drops the parent ref via dirfs_node_drop(dnp->dn_parent) (lines 123–126), kfrees dn_name (line 128–130), closes dn_fd if open (line 138–143), uninits the lock, and kfrees the node (line 145–147). The freshly allocated node has refcount 0 (only the parent was ref'd), so KKASSERT(dirfs_node_refcnt(dnp) == 0) at line 115 holds.

VERDICT.md verdict Full narrative: mechanism, dead-code analysis, no-new-errors proof
↓ download raw

DF-0856 β€” dirfs_alloc_file openat error path leaks dirfs node + parent refcount

Verdict

REPRODUCED (source-level) β€” the leak in the openat error path is real and unambiguous in the source. Impact: none at runtime β€” dirfs is a vkernel64-only filesystem that does not compile on master DEV (it is broken upstream independently of this bug), and is not present in the running X86_64_GENERIC guest kernel. The fix is correct, applies cleanly, and introduces no new compile errors.

Mechanism (the bug)

sys/vfs/dirfs/dirfs_subr.c, function dirfs_alloc_file:

182:  dnp = dirfs_node_alloc(mp);                  // kmalloc(sizeof(*dnp))
...
186:  dirfs_node_setname(dnp, ncp->nc_name, ...);  // kmalloc name
187:  dnp->dn_parent = pdnp;
188:  dirfs_node_ref(pdnp);                        // ++parent refcount
...
193:  if (openflags && vap != NULL) {
194:      dnp->dn_fd = openat(pathnp->dn_fd, tmp,
195:                          openflags, vap->va_mode);
196:      if (dnp->dn_fd == -1) {
197:          dirfs_dropfd(dmp, pathnp, pathfree); // frees path only
198:          return errno;                        // LEAKS dnp + parent ref
199:      }
200:  }

Compare the sibling dirfs_node_stat error path at lines 202–210, which is correct:

202:  error = dirfs_node_stat(pathnp->dn_fd, tmp, dnp);
203:  if (error) {
204:      error = errno;
205:      if (vp)
206:          dirfs_free_vp(dmp, dnp);
207:      dirfs_node_free(dmp, dnp);                // <-- frees dnp AND drops
208:      dirfs_dropfd(dmp, pathnp, pathfree);      //     parent ref via line
209:      return error;                             //     123-126 of _free
210:  }

dirfs_node_free() (line 106) drops the parent reference via dirfs_node_drop(dmp, dnp->dn_parent) at lines 123–126, then kfrees dn_name (128–130), closes dn_fd if open (138–143), uninits the lock (145), and kfrees the node (146). So the openat error path must call dirfs_node_free(dmp, dnp) before returning, exactly as the stat error path does.

Each failed openat therefore leaks: - one struct dirfs_node (sizeof ~200+ bytes, includes a struct lock), - one kmalloc'd dn_name (ncp->nc_nlen + 1 bytes), - and one outstanding reference on the parent node, which prevents the parent from being freed for the life of the mount (and can wedge unmount).

Why runtime impact is none

dirfs is gated by optional dirfs in sys/platform/vkernel64/conf/files and the option DIRFS is declared in sys/platform/vkernel64/conf/options as opt_dontuse.h. The default sys/config/VKERNEL64 does not enable it (lines 35–55 of VKERNEL64 list HAMMER, HAMMER2, NULLFS, EXT2FS, FFS, SOFTUPDATES, UFS_DIRHASH, MFS, TMPFS, NFS, MSDOSFS, CD9660, PROCFS β€” no DIRFS).

Critically, dirfs does not compile on master DEV even when explicitly enabled. Building a custom VKERNEL64_DIRFS config (a copy of VKERNEL64 plus options DIRFS) fails immediately on all three dirfs source files with multiple errors β€” see baseline_vkernel64_dirfs_build.log:

dirfs_subr.c:62:3:  error: implicit declaration of function 'kfree'
dirfs_subr.c:63:17: error: implicit declaration of function 'kmalloc'
dirfs_subr.c:63:48: error: 'M_WAITOK' undeclared
dirfs_subr.c:63:59: error: 'M_ZERO' undeclared
dirfs_subr.c:392:46: error: 'M_WAITOK' undeclared   (dirfs_node_absolute_path_plus)
dirfs_subr.c:466:46: error: 'M_WAITOK' undeclared   (dirfs_findfd)
dirfs_subr.c:466:57: error: 'M_ZERO' undeclared
dirfs_vnops.c:653:11: error: implicit declaration of function 'uiomovebp'
dirfs_vnops.c:1057:10: error: implicit declaration of function 'kmalloc'
dirfs_vnops.c:1078:3: error: implicit declaration of function 'kfree'
dirfs_vnops.c:1333:11: error: implicit declaration of function 'uiomove'
... (and so on; 20+ errors total across the three dirfs files)

The root cause of those errors is that the dirfs source files were imported in commit 6cc80ee9 kernel/apple_ir: Add Apple IR receiver driver (June 2026) and have been bit-rotted: they call kmalloc/kfree and use M_WAITOK/M_ZERO without including <sys/malloc.h>, and reference uiomovebp/uiomove without the right header.

This means dirfs is currently dead code: it cannot be built into a vkernel64, cannot be kldload'd as a module (it isn't structured as one), and is absent from every default kernel config. There is therefore no runtime primitive reachable from any user, on any default-kernel threat model.

Why this is still a real finding worth fixing

The bug is a textbook resource-leak pattern (missing cleanup in an error path) that the author of dirfs_alloc_file already got right in the sibling error path 10 lines below. It is a latent code-quality defect that becomes live the moment someone fixes the bit-rot and re-enables DIRFS (dirfs is documented in share/man/man7/vkernel.7 and is intentionally a supported vkernel filesystem; the import broke it accidentally). The fix is one line and matches the existing correct pattern, so it costs nothing to apply now alongside the wider dirfs rehabilitation.

No escalation

CWE-401 (memory leak). No memory corruption, no primitive for uid=0. Realistic impact ceiling on a hypothetical-fixed dirfs: DoS of the vkernel64 process via heap growth + unmount failure. No host-kernel impact because dirfs runs as a host userland process.

Fix

fix.diff:

--- a/sys/vfs/dirfs/dirfs_subr.c
+++ b/sys/vfs/dirfs/dirfs_subr.c
@@ -194,6 +194,7 @@
        dnp->dn_fd = openat(pathnp->dn_fd, tmp,
                    openflags, vap->va_mode);
        if (dnp->dn_fd == -1) {
+           dirfs_node_free(dmp, dnp);
            dirfs_dropfd(dmp, pathnp, pathfree);
            return errno;
        }

git apply --check passes on the audit tree. The freshly-allocated dnp has refcount 0 (only pdnp was dirfs_node_ref'd on line 188), so the KKASSERT(dirfs_node_refcnt(dnp) == 0) inside dirfs_node_free at dirfs_subr.c:115 holds.

Fix validation

A standard before/after kernel-build validation is not possible here because dirfs itself does not compile on master DEV (see above). Instead we validated the fix at the compile-unit level:

  1. git apply --check β€” passes on sys/vfs/dirfs/dirfs_subr.c.
  2. No-new-errors proof: built a custom VKERNEL64_DIRFS config both before and after applying fix.diff. The dirfs_subr.c error count is identical (11 errors in both). The only diff is that line numbers in the unrelated pre-existing errors shift by +1 (e.g. :392 β†’ :393, :466 β†’ :467), exactly because our fix adds one line at line 197. See baseline_vkernel64_dirfs_build.log (before) and fix_vkernel64_dirfs_build.log (after). diff of the sorted dirfs_subr.c error sets: ``` < dirfs_subr.c:392:46: error: 'M_WAITOK' undeclared ... < dirfs_subr.c:466:46: error: 'M_WAITOK' undeclared ... < dirfs_subr.c:466:57: error: 'M_ZERO' undeclared ...

dirfs_subr.c:393:46: error: 'M_WAITOK' undeclared ... dirfs_subr.c:467:46: error: 'M_WAITOK' undeclared ... dirfs_subr.c:467:57: error: 'M_ZERO' undeclared ... `` I.e. the fix is **clean, compilable C** that mirrors the already-correctdirfs_node_free(dmp, dnp);call atdirfs_subr.c:207` in the sibling error path. No new errors are introduced.

Per the AGENT.md taxonomy this is fix_status: "not_testable" β€” the PoC cannot run on this guest (dirfs is dead code on master DEV), but we validated the diff applies (git apply --check) and compiles as part of the C parse (no new errors), and traced line-by-line that the new call closes the leak by mirroring the existing correct path 10 lines below.

PoC changes

The finding shipped with no PoC folder; I created: - trigger.c β€” annotated source-level reproduction (no-op at runtime, documents the bug location and the contrast with the sibling error path). - README.md, VERDICT.md (this file), build.sh, run.sh, fix.diff, manifest.json, env.txt, plus the two vkernel64 build logs that prove the no-new-errors property.

fix.diff adds dirfs_node_free(dmp, dnp); immediately before the existing dirfs_dropfd / return errno on the openat failure path at sys/vfs/dirfs/dirfs_subr.c:197, mirroring the correct stat error path at lines 207–208. This is novel (the finding markdown was not pre-written; the DB summary field already proposed the same one-line fix, so this matches the finding's stated recommendation).

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

NOT_TESTABLE: dirfs dead code. Compile-unit validated: 11 errors before == 11 errors after (only +1 line shift on unrelated errors).

baseline dirfs_subr.c errors: 11. patched: 11. Zero new errors introduced.
↓ fix.diffn/a -- dirfs does not compile on master DEV (11 pre-existing errors unrelated to fix)

Confirmed kernel references

Detail

Exploit chain

none -- CWE-401 memory leak, no corruption, no escalation.

Evidence (decisive lines)

Source-level proof: :182 node alloc, :188 dirfs_node_ref(pdnp), :196-199 openat error -> dropfd + return WITHOUT free. Compare :207 correct free. 0 dirfs symbols in /boot/kernel/kernel.debug.

PoC changes

Authored from scratch: trigger.c (source-level repro), VERDICT.md, fix.diff (add dirfs_node_free before return), manifest.json.

Verified recommended fix

Add dirfs_node_free(dmp, dnp) before dirfs_dropfd+return at :197, mirroring the correct stat error path at :207. Full diff in findings/poc/DF-0856/fix.diff.

Verdict

REPRODUCED (source-level). dirfs_alloc_file openat error path at dirfs_subr.c:196-199 calls dirfs_dropfd + return errno WITHOUT dirfs_node_free(dmp,dnp), leaking ~200B node + kmalloc'd name + unbalanced parent ref. Sibling stat error path at :207-210 correctly calls dirfs_node_free. dirfs is dead code on master DEV (vkernel64-only, not in default VKERNEL64, does not compile).