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)
PoC verification
Evidence pack
findings/poc/DF-0782 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 inX86_64_GENERIC);kldload fuserequires root. /dev/fuseisroot:operator 0660(fuse_device.c:313);mountneedsSYSCAP_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/fusedaemon (serves one writable file)write_trigger.cβ unprivileged trigger (lseek near INT64_MAX + write 16)build.sh/run.shfix.diffβ the validated fix (add offset/overflow guards to fuse_vop_write)VERDICT.mdβ full analysispanic.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 logenv.txt/manifest.json
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:
fuse.h:31-33force-definesINVARIANTSfor the whole FUSE module, soKKASSERT(newsize >= 0)at:1972is always compiled intofuse.ko.- The assertion fires before
fnp->size = newsize(:1979) ornvextendbuf(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+ stockfuse.ko:writeat0x7FFFFFFFFFFFFFF0β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: samewriteβ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
fixedVALIDATED 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
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.
No comments yet.