# 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:

```c
/* 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:

```c
/* 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:

```c
/* 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:

```c
/* 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):

```c
/* 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:

```c
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;`):

```c
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 `kldload`ed 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.
