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

Integer overflow in fuse_vop_write at offset near INT64_MAX triggers KKASSERT panic via negative newsize

Summary

fuse_vop_write :1469 newsize=uio->uio_offset+uio_resid off_t(signed)+size_t(unsigned) overflow at offset near INT64_MAX. :1470 if(newsize<oldsize) clamps to oldsize so FUSE_MAXFILESIZE/RLIMIT checks pass. But inside loop :1529 (uio->uio_offset+len) re-computed off_t+size_t unsigned >= INT64_MAX if size>offset -> resize branch :1531 fuse_reg_resize(vp, uio_offset+len). :1970 KKASSERT(newsize>=0) β€” fuse.h:32 FORCE-DEFINES INVARIANTS so always compiled in. uio_offset=0x7FFFFFFFFFFFFFF0 len=16 -> newsize=INT64_MIN KKASSERT panic. fuse_vop_read HAS if(uio_offset<0) return EINVAL :1338 write does NOT. Trigger: unprivileged lseek(fd,0x7FFFFFFFFFFFFFF0) write(fd,buf,16). Fix: if(uio_offset<0) return EINVAL + if(uio_offset>OFF_MAX||uio_offset+resid>OFF_MAX) return EFBIG.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0782 Β· 16 files
FileTypeDescriptionSize
fuse_daemon.c trigger-source benign raw /dev/fuse daemon serving one writable regular file (bug is in kernel offset arithmetic, not daemon data) 11.9 KB view raw
write_trigger.c trigger-source unprivileged trigger: lseek to 0x7FFFFFFFFFFFFFF0 + write 16 bytes 2.8 KB view raw
build.sh build-script cc -O0 -g -o fuse_daemon + write_trigger 248 B view raw
run.sh run-script kldload fuse, start daemon (mounts /mnt/fuse) as root, then su maxx runs write_trigger 1.4 KB view raw
build.log build-log final successful build output 127 B view raw
run.log run-log baseline run #1 (unpatched): panic signature 908 B view raw
run.3.log run-log baseline run #3 (unpatched): panic, 3/3 deterministic 375 B view raw
panic.txt panic-signature KKASSERT newsize>=0 in fuse_reg_resize at fuse_vnops.c:1970 1.5 KB view raw
fix.diff suggested-fix add uio_offset<0->EINVAL + uio_offset>FUSE_MAXFILESIZE-resid->EFBIG guards to fuse_vop_write 855 B view raw
fix_build.log build-log single-fix fuse.ko module build (rc=0, ~20s) 10.3 KB view raw
fix_run.log run-log patched runs 1-3: write returns EFBIG errno 27, no panic, guest up; normal write sanity ok 1.2 KB view raw
env.txt environment uname, kern.version, cc, fuse.ko, vfs.usermount, maxx id 470 B view raw
VERDICT.md verdict full narrative analysis 8.6 KB ↓ raw
README.md readme human reproduction guide 3.3 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 reproduction guide
↓ download raw

DF-0782 β€” PoC & Reproduction

Integer overflow in fuse_vop_write at offset near INT64_MAX β†’ KKASSERT panic via negative newsize (sys/vfs/fuse/fuse_vnops.c:1469 / :1529 / fuse_reg_resize :1970)

Medium severity. CWE-190 Integer Overflow. CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

What the bug is

fuse_vop_write() computes newsize = uio->uio_offset + uio->uio_resid (:1469) where uio_offset is off_t (signed int64) and uio_resid is size_t (unsigned). For an offset near INT64_MAX the sum wraps to 0x8000000000000000 (INT64_MIN). A subsequent clamp (if (newsize < oldsize) newsize = oldsize, :1470) masks it, so the FUSE_MAXFILESIZE/RLIMIT checks pass. Inside the write loop the size is recomputed without the clamp (:1529) and fuse_reg_resize(vp, INT64_MIN, 0) is called, hitting KKASSERT(newsize >= 0) (:1972) β€” which is always compiled in because fuse.h:31-33 force-#defines INVARIANTS for the whole FUSE module. Result: deterministic kernel panic (DoS).

fuse_vop_read() has if (uio->uio_offset < 0) return EINVAL; at :1338; fuse_vop_write() does not β€” the asymmetry this finding reports.

Impact

Local DoS (kernel panic), reachable from an unprivileged user who can write a file on a root-mounted FUSE filesystem (lseek near INT64_MAX + write). No surviving memory corruption (the KKASSERT halts before any write with the overflowed value). No escalation chain.

How to reproduce

./build.sh          # cc -O0 -g -o fuse_daemon fuse_daemon.c ; cc -O0 -g -o write_trigger write_trigger.c
./run.sh            # as root: kldload fuse, start daemon, mount; then as maxx: write at 0x7FFFFFFFFFFFFFF0

run.sh is root-driven: it loads fuse.ko, starts fuse_daemon (which opens /dev/fuse and mounts /mnt/fuse), then su -m maxx runs write_trigger /mnt/fuse/target. Expected on the unpatched kernel: the write() never returns; the guest panics:

panic: assertion "newsize >= 0" failed in fuse_reg_resize at fuse_vnops.c:1970
fuse_reg_resize() at fuse_reg_resize+0xd9
fuse_vop_write() at fuse_vop_write+0x322

On the patched kernel (apply fix.diff, rebuild fuse.ko): write returns EFBIG (errno 27, "File too large"), no panic.

Preconditions

  • FUSE is module-only (optional fuse, not in X86_64_GENERIC); kldload fuse requires root.
  • /dev/fuse is root:operator 0660 (fuse_device.c:313); mount needs SYSCAP_NOMOUNT_FUSE (root). So the daemon/mount setup is root-only (matches the finding's stated gates, same as DF-0780/0781).
  • The trigger (the actual overflow) is an unprivileged lseek+write β€” realistic model: admin has mounted a FUSE filesystem; any user with write access to a file on it can DoS the kernel.

Files

  • fuse_daemon.c β€” benign raw /dev/fuse daemon (serves one writable file)
  • write_trigger.c β€” unprivileged trigger (lseek near INT64_MAX + write 16)
  • build.sh / run.sh
  • fix.diff β€” the validated fix (add offset/overflow guards to fuse_vop_write)
  • VERDICT.md β€” full analysis
  • panic.txt β€” kernel panic signature (proof)
  • run.log/run.3.log β€” baseline (unpatched) runs (panic)
  • fix_run.log β€” patched runs (EFBIG, no panic)
  • fix_build.log β€” single-fix fuse.ko build log
  • env.txt / manifest.json
VERDICT.md verdict full narrative analysis
↓ download raw

DF-0782 β€” Verdict

Integer overflow in fuse_vop_write at offset near INT64_MAX triggers KKASSERT panic via negative newsize

Verdict: REPRODUCED (deterministic kernel panic / DoS); fix VALIDATED

field value
status reproduced
reproduced yes (deterministic, 3/3 fresh-reset runs)
impact panic (DoS β€” kernel panic via KKASSERT(newsize >= 0))
confidence certain
class integer overflow (CWE-190) β†’ panic / latent memory corruption
severity (audit) Medium
fix_status fixed (single-fix fuse.ko, 3/3 patched runs clean)

1. The bug β€” confirmed at source and at runtime

fuse_vop_write() (sys/vfs/fuse/fuse_vnops.c:1428) handles writes on a FUSE filesystem. At the top it computes the prospective new file size:

/* fuse_vnops.c:1469 */
oldsize = fnp->size;
newsize = uio->uio_offset + uio->uio_resid;

The operand types (verified in sys/sys/_uio.h) are: - uio->uio_offset β€” off_t (int64_t, signed) - uio->uio_resid β€” size_t (uint64_t, unsigned)

C usual arithmetic conversions promote the signed operand to unsigned, so for uio_offset = 0x7FFFFFFFFFFFFFF0 and uio_resid = 16 the sum is 0x7FFFFFFFFFFFFFF0 + 0x10 = 0x8000000000000000 (uint64), which when assigned to off_t newsize is INT64_MIN (negative).

This negative newsize is then masked by the subsequent clamp:

/* fuse_vnops.c:1470 */
if (newsize < oldsize)
    newsize = oldsize;          /* newsize becomes oldsize (e.g. 0) */

so the FUSE_MAXFILESIZE check at :1478 (if (newsize > FUSE_MAXFILESIZE), where FUSE_MAXFILESIZE = 0x7FFFFFFFFFFFFFFFLL, fuse.h:67) and the RLIMIT_FSIZE check at :1489 both pass β€” the overflow is invisible to them.

Inside the write loop the size is recomputed without the clamp:

/* fuse_vnops.c:1529 */
if ((uio->uio_offset + len) > fnp->size) {
    trivial = (uio->uio_offset <= fnp->size);
    error = fuse_reg_resize(vp, uio->uio_offset + len, trivial);
                                  /* ^^^ wraps again to 0x8000000000000000 */

so fuse_reg_resize() receives newsize = 0x8000000000000000 (INT64_MIN as a signed off_t). fuse_reg_resize() (fuse_vnops.c:1960) begins:

/* fuse_vnops.c:1968-1972 */
#ifdef INVARIANTS
    KKASSERT(vp->v_type == VREG);
    KKASSERT(newsize >= 0);     /* <-- FAILS: newsize == INT64_MIN */
#endif

Crucially, fuse.h:31-33 unconditionally #define INVARIANTS for the entire FUSE module (verified):

/* fuse.h:31 */
#ifndef INVARIANTS
#define INVARIANTS
#endif

so the KKASSERT(newsize >= 0) is always compiled into fuse.ko β€” even on a kernel built without options INVARIANTS β€” and fires immediately, halting before fnp->size = newsize or nvextendbuf() execute.

The asymmetry that makes this a bug

fuse_vop_read() (fuse_vnops.c:1338) has an early guard:

if (uio->uio_offset < 0)
    return EINVAL;

fuse_vop_write() has no such guard β€” that is the root cause. A write at an offset near INT64_MAX reaches the overflow arithmetic unchecked.

2. Live reproduction

The trigger is an unprivileged write at an offset near INT64_MAX:

lseek(fd, 0x7FFFFFFFFFFFFFF0, SEEK_SET);
write(fd, buf, 16);

write_trigger.c does exactly this. The FUSE mount itself requires root (kldload fuse + /dev/fuse is root:operator 0660 + mount needs SYSCAP_NOMOUNT_FUSE), so run.sh starts the benign daemon fuse_daemon.c as root (it merely serves one writable regular file target, reporting size 0 so oldsize = 0 and the clamp masks the overflow). The triggering write is then issued as the unprivileged user maxx (uid 1001).

Result on the unpatched #0 kernel + stock fuse.ko, deterministic 3/3 fresh-reset runs β€” the write() never returns; the guest dies:

panic: assertion "newsize >= 0" failed in fuse_reg_resize at /usr/src/sys/vfs/fuse/fuse_vnops.c:1970
cpuid = 2
fuse_reg_resize() at fuse_reg_resize+0xd9
fuse_vop_write() at fuse_vop_write+0x322
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>

The panic names the exact assertion and source line the finding cites, and the call chain shows it reached fuse_reg_resize from fuse_vop_write (i.e. the write path, exactly as claimed). This is the bug, not an unrelated crash.

3. Impact & escalation assessment (Phase 6) β€” DoS, NOT surviving corruption

This is a deterministic kernel panic (DoS), not a surviving memory-corruption primitive, and there is no escalation chain to develop. The reason is structural:

  1. fuse.h:31-33 force-defines INVARIANTS for the whole FUSE module, so KKASSERT(newsize >= 0) at :1972 is always compiled into fuse.ko.
  2. The assertion fires before fnp->size = newsize (:1979) or nvextendbuf(vp, oldsize, newsize, ...) (:1996) β€” i.e. before any kernel memory is written with the overflowed value.

So the overflow cannot land as heap/stack corruption on any stock fuse.ko build; the only observable effect is the panic. The "OOB / corruption" the finding speculatively mentions is latent and unreachable through the shipped module. There is no write primitive to groom, no corrupted object to convert, no UAF to reclaim β€” the assertion halts at the gate. This is the valid non-corruption case: document the realistic impact ceiling (local DoS).

Reachability of the trigger (the realism test): the FUSE mount setup is root-only (kldload, /dev/fuse perms, mount capability), which matches the finding's stated preconditions and the sibling findings DF-0780/0781. But the trigger itself β€” lseek to a near-INT64_MAX offset and write β€” needs no privilege beyond write access to a file on an already-mounted FUSE filesystem. This is the "an admin has mounted a filesystem image and chowned it to the user" acceptable-precondition model: once root mounts a FUSE filesystem (a legitimate use), any unprivileged user who can write a file on it can panic the kernel. So the realistic impact is a local DoS reachable from an unprivileged user against a root-configured FUSE mount β€” correctly rated Medium.

(For completeness: root β†’ kernel panic is game-over-by-definition; the security boundary that matters here is unprivileged-user β†’ kernel DoS via a file write, which is real.)

4. The fix (validated)

fix.diff adds two guards at the top of fuse_vop_write(), immediately after the FUSE_WRITE nosys check, mirroring fuse_vop_read() and the established DragonFly overflow-safe pattern (vfs_syscalls.c:5484, if (offset > OFF_MAX - len) return EFBIG;):

if (uio->uio_offset < 0)
    return (EINVAL);
if (uio->uio_offset > FUSE_MAXFILESIZE - uio->uio_resid)
    return (EFBIG);

These fire before the overflowing newsize = uio_offset + uio_resid arithmetic at :1469, so the masked-clamp / FUSE_MAXFILESIZE bypass and the in-loop re-overflow at :1529 can no longer occur. Minimal, targeted at the root cause (missing offset/overflow guard on the write path), one logical change. This matches the finding's proposed fix (if(uio_offset<0) return EINVAL + if(uio_offset>OFF_MAX || uio_offset+resid>OFF_MAX) return EFBIG); it uses FUSE_MAXFILESIZE (= OFF_MAX = INT64_MAX, fuse.h:67) which is already in scope.

Since FUSE is a loadable module (not compiled into X86_64_GENERIC), the single-fix validation rebuilt only fuse.ko (cd sys/vfs/fuse && make, ~20 s, rc=0), swapped it into /boot/kernel/fuse.ko, and kldloaded it β€” no kernel rebuild or reboot required.

Before / after (single-fix module)

  • before β€” #0 + stock fuse.ko: write at 0x7FFFFFFFFFFFFFF0 β‡’ panic: assertion "newsize >= 0" failed in fuse_reg_resize at fuse_vnops.c:1970 (3/3 fresh-reset runs, guest dies).
  • after β€” patched fuse.ko: same write β‡’ write returned -1 (errno=27 File too large) (EFBIG), no panic, guest stays up (3/3 runs). Legitimate writes unaffected (dd of=/mnt/fuse/target bs=5 β‡’ 5 bytes written, rc=0).

fix_status = fixed.

5. PoC changes vs. the as-filed package

There was no prior PoC package for DF-0782 (the finding folder did not exist). I authored the entire evidence pack from scratch: - fuse_daemon.c β€” a benign raw /dev/fuse daemon (the bug is in the kernel's offset arithmetic, not in daemon-controlled data, so the daemon need not misbehave β€” it only serves one writable file so the trigger can reach fuse_vop_write); - write_trigger.c β€” the unprivileged trigger (lseek to 0x7FFFFFFFFFFFFFF0 + write of 16 bytes); - build.sh / run.sh, fix.diff, and this VERDICT.md + logs.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix. fuse_vop_write's missing offset/overflow guard (the asymmetry vs fuse_vop_read:1338) is the root cause; fix.diff adds if (uio->uio_offset < 0) return (EINVAL); and if (uio->uio_offset > FUSE_MAXFILESIZE - uio->uio_resid) return (EFBIG); before the overflowing arithmetic. BEFORE (unpatched #0 + stock fuse.ko): write at 0x7FFFFFFFFFFFFFF0 -> panic assertion "newsize >= 0" failed in fuse_reg_resize at fuse_vnops.c:1970 (3/3, guest dies). AFTER (patched fuse.ko, same trigger): write returns EFBIG (errno 27, 'File too large'), no panic, guest stays up (3/3); legitimate writes unaffected (dd 5 bytes -> rc=0). The overflow can no longer reach fuse_reg_resize with a negative newsize. fix closes the bug.

before (unpatched): panic: assertion "newsize >= 0" failed in fuse_reg_resize at fuse_vnops.c:1970 / fuse_vop_write+0x322 / Stopped at Debugger (guest down, 3/3)
after (patched fuse.ko): [trigger] write returned -1 (errno=27 File too large) / guest up, 0 panics in boot.log (3/3)
sanity: printf hello | dd of=/mnt/fuse/target bs=5 -> 5 bytes transferred, rc=0
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel unchanged; FUSE is module-only -- the single-fix validation rebuilt ONLY fuse.ko via `cd /usr/src/sys/vfs/fuse && make` (~20s, rc=0), swapped it into /boot/kernel/fuse.ko, and kldload-ed it; no kernel rebuild or reboot required)

Confirmed kernel references

Detail

Exploit chain

DoS-class finding, no escalation chain to develop (valid non-corruption case). The primitive that fires is a KKASSERT panic, NOT a surviving memory-corruption write: fuse.h:31-33 force-#defines INVARIANTS for the entire FUSE module, so KKASSERT(newsize >= 0) at fuse_vnops.c:1972 is always compiled into fuse.ko and halts execution BEFORE fnp->size = newsize (:1979) or nvextendbuf(vp, oldsize, newsize, ...) (:1996) can write the overflowed value into kernel memory. Therefore no heap/stack corruption ever lands on a stock fuse.ko build -- the 'OOB/corruption' the finding speculatively mentions is latent and unreachable. There is no write primitive to groom, no corrupted object to convert. Reachability of the trigger: the FUSE mount setup (kldload + /dev/fuse root:operator 0660 + SYSCAP_NOMOUNT_FUSE) is root-only (matches the finding's stated gates and sibling DF-0780/0781), but the trigger itself (lseek near INT64_MAX + write) needs no privilege beyond write access to a file on an already-mounted FUSE filesystem -- the 'admin has mounted a filesystem' acceptable-precondition model. Realistic impact: local DoS (kernel panic) from an unprivileged user against a root-configured FUSE mount, correctly rated Medium.

Evidence (decisive lines)

BASELINE (unpatched #0 + stock fuse.ko), maxx write at 0x7FFFFFFFFFFFFFF0, 3/3:
panic: assertion "newsize >= 0" failed in fuse_reg_resize at /usr/src/sys/vfs/fuse/fuse_vnops.c:1970
cpuid = 2
fuse_reg_resize() at fuse_reg_resize+0xd9 0xffffffff82602699
fuse_vop_write() at fuse_vop_write+0x322 0xffffffff82604cf2
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>  (guest down; write() never returned)

PATCHED (fuse.ko rebuilt with fix.diff), same trigger, 3/3:
[trigger] write returned -1 (errno=27 File too large)
write: File too large   (guest up, zero panics in boot.log)
Normal-write sanity: dd of=/mnt/fuse/target bs=5 -> 5 bytes, rc=0 (legitimate I/O unaffected)

PoC changes

No prior PoC package existed (the finding folder did not). Authored the entire evidence pack from scratch: fuse_daemon.c (a BENIGN raw /dev/fuse daemon -- the bug is in the kernel's offset arithmetic, not daemon-controlled data, so the daemon need not misbehave; it only serves one writable regular file 'target' reporting size 0 so oldsize=0 and the clamp masks the overflow); write_trigger.c (unprivileged trigger: lseek to 0x7FFFFFFFFFFFFFF0 + write 16 bytes); build.sh/run.sh; fix.diff; VERDICT.md; manifest.json; all logs.

Verified recommended fix

In fuse_vop_write (sys/vfs/fuse/fuse_vnops.c), immediately after the if (fuse_test_nosys(fmp, FUSE_WRITE)) return EOPNOTSUPP; check (and before the overflowing newsize = uio->uio_offset + uio->uio_resid at :1469), add: if (uio->uio_offset < 0) return (EINVAL); (mirroring fuse_vop_read:1338) and if (uio->uio_offset > FUSE_MAXFILESIZE - uio->uio_resid) return (EFBIG); (overflow-safe, matching the established DragonFly pattern at vfs_syscalls.c:5484, using FUSE_MAXFILESIZE == OFF_MAX == INT64_MAX from fuse.h:67). These fire before the masked-clamp/FUSE_MAXFILESIZE bypass and the in-loop re-overflow at :1529 can occur. Matches the finding's proposed fix (offset<0 -> EINVAL; offset+resid>OFF_MAX -> EFBIG); uses FUSE_MAXFILESIZE which is already in scope. The full git-apply-able diff lives in findings/poc/DF-0782/fix.diff.

Verdict

REPRODUCED (deterministic 3/3). The bug is real: fuse_vop_write (sys/vfs/fuse/fuse_vnops.c:1469) computes newsize = uio->uio_offset + uio->uio_resid where uio_offset is off_t (signed int64) and uio_resid is size_t (unsigned int64, confirmed in sys/sys/_uio.h:78); C usual arithmetic conversions promote the signed operand to unsigned, so offset 0x7FFFFFFFFFFFFFF0 + resid 16 wraps to 0x8000000000000000 (INT64_MIN). The clamp if (newsize < oldsize) newsize = oldsize at :1470 masks the negative value, so the FUSE_MAXFILESIZE (:1478) and RLIMIT_FSIZE (:1489) checks pass. Inside the write loop the size is recomputed WITHOUT the clamp at :1529 (uio->uio_offset + len re-wraps), so fuse_reg_resize(vp, INT64_MIN, 0) is called (:1531). fuse_reg_resize begins with KKASSERT(newsize >= 0) (:1972) which is ALWAYS compiled in because fuse.h:31-33 unconditionally #defines INVARIANTS for the whole FUSE module (verified) -- so the assertion fires and the kernel panics. fuse_vop_read has if (uio->uio_offset < 0) return EINVAL at :1338; fuse_vop_write does NOT -- the asymmetry the finding reports. Confirmed by the exact panic signature: panic: assertion "newsize >= 0" failed in fuse_reg_resize at fuse_vnops.c:1970, call chain fuse_reg_resize<-fuse_vop_write, triggered by maxx (uid 1001) doing lseek(0x7FFFFFFFFFFFFFF0)+write(fd,buf,16) on a root-mounted FUSE file. Guest dies (ssh dies, vm status down) -- 3/3 fresh-reset runs.