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

sys_fhopen returns spurious success (fd 0) on VREG-without-VM-object invariant violation

Field Value
ID DF-0002
Status new
Severity Info
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:N
CWE CWE-388 7PK - Errors
File sys/kern/vfs_syscalls.c
Lines 4933-4938
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

In sys_fhopen, when VOP_OPEN succeeds for a VREG vnode that unexpectedly has no VM object, the code branches to the error-cleanup label without first setting error. Because error is still 0 from the successful VOP_OPEN, the syscall returns 0 (success) to the caller while the result fd (sysmsg_result, initialized to 0 at the syscall entry) is never assigned a real descriptor and the reserved slot is freed. This is a correctness bug, reported as Info because fhopen is gated by SYSCAP_RESTRICTEDROOT (root-only).

Root cause

sys/kern/vfs_syscalls.c:4933-4938:

if (vp->v_type == VREG && vp->v_object == NULL) {
    kprintf("fhopen: regular file did not have VM object: %p\n", vp);
    goto bad_drop;          /* error is still 0 here from VOP_OPEN */
}

The bad_drop path (4986-4988) clears the reserved fd slot (fsetfd(fdp, NULL, indx)) and fdrop()s fp, then falls into bad (vput(vp)) and done (mount_drop(mp)), finally return (error); with error == 0. sysmsg->sysmsg_result is only written on the true success path at line 4981, so it retains the syscall-entry default of 0. The caller therefore sees "success, fd 0" (its own stdin) even though no descriptor was allocated for the fhopen result.

Threat model & preconditions

  • Attacker position: root-equivalent only (caps_priv_check_td(td, SYSCAP_RESTRICTEDROOT) at sys/kern/vfs_syscalls.c:4832).
  • Privileges gained or impact: none (no privilege-boundary crossing). Practical effect is limited to caller confusion (operating on fd 0 instead of a new fd).
  • Required config or capabilities: root. Trigger requires a VREG vnode whose v_object is still NULL after a successful VOP_OPEN, i.e. an internal FS invariant violation (e.g. a buggy/out-of-tree/fuse filesystem).
  • Reachability: fhopen(2) on such a vnode. On normal filesystems the invariant holds and the path is unreachable.

Proof of concept

PoC source: findings/poc/DF-0002/fhopen_spur.c

Build & run

cc -o fhopen_spur findings/poc/DF-0002/fhopen_spur.c
./fhopen_spur /some/target   # as root, on a filesystem that violates the invariant

Expected output

fhopen returned 0 (success)
BUG: fd 0 is stdin, no real descriptor was allocated

(Fixed: fhopen failed as expected: Invalid argument.)

Impact

None security-wise β€” root-only. Reported for correctness/hardening: an FS invariant violation should not present itself to userspace as a silent success returning a recycled descriptor.

Set error before the goto.

--- a/sys/kern/vfs_syscalls.c
+++ b/sys/kern/vfs_syscalls.c
@@ -4933,6 +4933,7 @@
            kprintf("fhopen: regular file did not "
                "have VM object: %p\n",
                vp);
+           error = EINVAL;
            goto bad_drop;
        }

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/vfs_syscalls.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0002 Β· 13 files
FileTypeDescriptionSize
fhopen_spur.c trigger-source User-space PoC: mount tmpfs, open a file, null its v_object via the KLD, then fhopen() the handle. 3.4 KB view raw
df0002_trigger.c exploit-chain KLD exposing debug.df0002.corrupt_fd β€” saves-and-nulls a vnode's v_object (restores on fd=-1 or unload). Recreates the FS-invariant violation that no in-tree FS produces. 3.9 KB view raw
Makefile build-script Builds df0002_trigger.ko against /usr/src/sys via bsd.kmod.mk. 68 B ↓ download
build.sh build-script Builds fhopen_spur + df0002_trigger.ko. 627 B view raw
run.sh run-script kldload df0002_trigger.ko && ./fhopen_spur. 484 B view raw
VERDICT.md verdict Full narrative: mechanism, trigger, before/after, fix validation. 5.8 KB ↓ raw
README.md readme How to build, run, and what to expect. 2.4 KB ↓ raw
fix.diff suggested-fix git-apply-able one-line fix: add `error = EINVAL;` before goto bad_drop at vfs_syscalls.c:4937. Matches the finding markdown proposal exactly. 268 B view raw
run.baseline.log run-log Unpatched baseline #0 output: BUG fires (spurious fd 0). 896 B view raw
fix_build.log build-log Full 35512-line nativekernel build of the single-fix kernel (rc=0). 5.6 MB ↓ download
env.txt environment guest uname, modules, HW-gate note 629 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
README.md readme How to build, run, and what to expect.
↓ download raw

DF-0002 β€” PoC

sys_fhopen() at sys/kern/vfs_syscalls.c:4933-4938 has a missing error = EINVAL before goto bad_drop when a VREG vnode has v_object == NULL after VOP_OPEN. The function returns 0 (success) while the reserved fd slot is freed and sysmsg_result is never written, so the caller sees "fd 0" (its own stdin) β€” no real descriptor allocated.

Info-severity correctness bug. fhopen is gated by SYSCAP_RESTRICTEDROOT (root-only) and the trigger requires an FS-invariant violation (a VREG vnode without a VM object after VOP_OPEN), which no in-tree filesystem produces. There is no security impact; reported for correctness/hardening.

Files

File Purpose
fhopen_spur.c User-space PoC: mount tmpfs, open a file, get its handle, ask the KLD to null v_object, then fhopen(2) the handle.
df0002_trigger.c Tiny KLD exposing debug.df0002.corrupt_fd. Writing a positive fd saves-and-nulls that vnode's v_object; writing -1 restores.
Makefile Builds df0002_trigger.ko against /usr/src/sys.
build.sh / run.sh Exact build/run commands.
VERDICT.md Full narrative + before/after evidence.
fix.diff git apply-able one-line fix (matches finding proposal).
manifest.json Site-renderable catalog.

Build

./build.sh    # builds fhopen_spur + df0002_trigger.ko (guest, root, /usr/src present)

Run

./run.sh      # kldload df0002_trigger.ko && ./fhopen_spur

Expected output

Bug present (unpatched #0):

[*] v_object nullified on vnode for fd 3 (/tmp/df0002_tmpfs/file)
fhopen returned 0 (success)
BUG: fd 0 is stdin, no real descriptor was allocated

Fixed (single-fix #1):

[*] v_object nullified on vnode for fd 3 (/tmp/df0002_tmpfs/file)
fhopen failed as expected: Invalid argument (errno=22)

Normal usage (no KLD, normal FS) is unaffected on either kernel: ./fhopen_spur on a UFS file returns fhopen returned 3 (success) β€” a real descriptor.

Preconditions (realistic-threat model)

The KLD is root-only (kldload) and is just a tool to demonstrate the buggy code path fires; it is not part of any escalation chain. The bug itself is also gated by SYSCAP_RESTRICTEDROOT, so there is no privilege boundary being crossed. The realistic in-tree trigger is the FS-invariant violation alone (e.g. a buggy/out-of-tree/fuse filesystem) β€” without the KLD, the bug requires that violation to manifest.

VERDICT.md verdict Full narrative: mechanism, trigger, before/after, fix validation.
↓ download raw

DF-0002 β€” REPRODUCED + FIX VALIDATED

Verdict: REPRODUCED (logic/correctness bug, root-only) β€” and FIX VALIDATED (single-fix kernel boots, PoC now returns EINVAL instead of spurious success).

Mechanism

sys_fhopen at sys/kern/vfs_syscalls.c:4814 opens a vnode located via a file handle. After VOP_OPEN succeeds, the defensive check at lines 4933–4938 fires when the vnode is a regular file (VREG) that unexpectedly has no VM object (vp->v_object == NULL):

4933:   if (vp->v_type == VREG && vp->v_object == NULL) {
4934:       kprintf("fhopen: regular file did not "
4935:           "have VM object: %p\n",
4936:           vp);
4937:       goto bad_drop;            /* <-- error stays 0 from VOP_OPEN */
4938:   }

The goto bad_drop skips the only success-path assignment to sysmsg->sysmsg_result at line 4981. The bad_drop label (4986–4988) clears the reserved fd slot via fsetfd(fdp, NULL, indx) and fdrop()s fp, then falls into bad: (vput(vp)) β†’ done: (mount_drop(mp)) β†’ done2: return (error);. Because error is still 0 from the successful VOP_OPEN, the syscall returns 0 (success), and because sysmsg->sysmsg_result was never written, it retains the syscall-entry default of 0 β€” i.e. the caller's stdin (fd 0).

The caller therefore sees fhopen() "succeed" with fd 0, but no descriptor was actually allocated for the fhopen result. The reserved slot was freed, so the caller is left operating on its own stdin as if it were the requested file.

Why this is reachable only via an FS-invariant violation

v_object is initialized by vinitvmio() during vnode creation in every in-tree filesystem that supports fhopen (UFS, HAMMER, HAMMER2, msdosfs, cd9660, udf, ext2, tmpfs, …). After VOP_OPEN, v_object is always non-NULL for VREG vnodes. The buggy branch can therefore fire only on an out-of-tree filesystem that violates the VREG-has-VM-object invariant.

Demonstration (the bug DOES fire when the invariant is violated)

We mount a tmpfs, create a regular file, get its handle, and then ask a small KLD (df0002_trigger.ko) to null the vnode's v_object β€” recreating the invariant violation. Then we call fhopen(2) on the handle. tmpfs_open() does NOT re-create v_object (unlike ufs_open), so vp->v_object stays NULL when the defensive check at vfs_syscalls.c:4933 fires.

The KLD is root-only (kldload) and the bug is itself gated by SYSCAP_RESTRICTEDROOT (sys/kern/vfs_syscalls.c:4832). There is no privilege boundary being crossed β€” root can already open any descriptor β€” so this is reported as a correctness/hardening bug, Info severity, no security impact. The KLD is just a tool to demonstrate the buggy code path fires; it is not part of any escalation chain.

Unpatched baseline (#0) β€” single run:

$ ./fhopen_spur
[*] v_object nullified on vnode for fd 3 (/tmp/df0002_tmpfs/file)
fhopen returned 0 (success)
BUG: fd 0 is stdin, no real descriptor was allocated

dmesg:

DF-0002: corrupted vnode 0xfffff80118539080 (cleared v_object, was 0xfffff8008fad4040, type=VREG)
fhopen: regular file did not have VM object: 0xfffff80118539080     <-- the buggy kprintf at vfs_syscalls.c:4934
                                                                    <-- BUT no error is set; sysmsg_result retains the syscall-entry default of 0

Normal usage (no corruption, no KLD) is unaffected: ./fhopen_spur on a normal UFS file returns fhopen returned 3 (success) β€” a real descriptor, no bug.

Impact

None, security-wise. fhopen is root-only (SYSCAP_RESTRICTEDROOT), and the trigger requires an FS-invariant violation. A maintainer-facing concern only: an FS invariant violation should not present itself to userspace as a silent success returning the caller's stdin. Reported as Info for correctness.

Fix

fix.diff adds a single line β€” error = EINVAL; β€” before the goto bad_drop at vfs_syscalls.c:4937. This matches the finding markdown's ## Recommended fix proposal exactly (no divergence). The patch is git apply-able; it builds clean with make -j6 nativekernel KERNCONF=X86_64_GENERIC (rc=0, no errors).

Fix validation (Phase 8)

Built the single-fix kernel from the with-src baseline by applying ONLY fix.diff:

Step Result
with-src baseline (unpatched) kern.version = 6.5-DEVELOPMENT #0
apply fix.diff to /usr/src Hunk #1 succeeded at 4934, PATCH_RC=0
make -j6 nativekernel === NK_DONE rc=0 === (35512-line build log saved)
cp kernel.stripped β†’ /boot/kernel/kernel sha256 7e62d83b27d6f06663302ca7acd4894ee2dde3a6357fd14c1abe536a44ebc4fa
reboot kern.version = 6.5-DEVELOPMENT #1 (today's ts)
re-run ./fhopen_spur fhopen failed as expected: Invalid argument (errno=22) βœ“

Before/after contrast (same PoC, same KLD, same trigger condition):

BEFORE (unpatched #0):
  fhopen returned 0 (success)
  BUG: fd 0 is stdin, no real descriptor was allocated
  dmesg: fhopen: regular file did not have VM object: 0xfffff80118539080

AFTER  (single-fix #1, sha256 7e62d83b…ebc4fa):
  fhopen failed as expected: Invalid argument (errno=22)
  dmesg: fhopen: regular file did not have VM object: 0xfffff80117251e80
         (kprintf still fires β€” the path is hit β€” but the syscall now returns
          EINVAL instead of spurious success.)

The kprintf at line 4934 still fires on the patched kernel (the defensive path is still reached); the only change is that the syscall now correctly returns EINVAL instead of leaking error == 0 to the caller.

Final guest state: reset to with-src (unpatched #0) so the next runner starts clean.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline spurious success fd 0; patched EINVAL. Normal UFS unaffected.

BEFORE: fhopen 0 (success) BUG fd=stdin. AFTER: fhopen EINVAL. Normal: fd=3.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 16:50:26 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- Info-level logic bug, root-only (SYSCAP_RESTRICTEDROOT). No escalation.

Evidence (decisive lines)

BEFORE: fhopen returned 0 (success), BUG fd 0 is stdin. AFTER: fhopen failed EINVAL (errno=22). Normal UFS fhopen still works (fd=3).

PoC changes

Authored: df0002_trigger.c+Makefile (KLD nulls v_object), fhopen_spur.c (fhopen PoC), fix.diff (error=EINVAL before goto bad_drop), VERDICT.md, manifest.json.

Verified recommended fix

Add error=EINVAL before goto bad_drop at vfs_syscalls.c:4937. Matches finding proposal exactly. Full diff in findings/poc/DF-0002/fix.diff.

Verdict

REPRODUCED (logic, root-only kld trigger). sys_fhopen vfs_syscalls.c:4933-4938 missing error=EINVAL before goto bad_drop -> error stays 0 from VOP_OPEN -> spurious success fd 0 (stdin). KLD nulls tmpfs v_object to reach defensive kprintf branch. No in-tree FS produces this condition.