# DF-0915 — fuse_device_write trusts daemon `ohd->len` over actual write size

**Verdict: REPRODUCED — heap overflow write (CWE-787), confirmed by deterministic harness + live kernel panic; fix VALIDATED (panic → no panic).**

## The bug

`fuse_device_write` (`sys/vfs/fuse/fuse_device.c`) allocates the daemon's reply
buffer from the **actual** number of bytes written (`uio_resid`) and never clamps
the stored length to the **claimed** length the daemon puts in the FUSE out
header:

- `:182` `fuse_buf_alloc(&fb, uio->uio_resid)` → `fb.len = uio_resid`
  (the ACTUAL bytes the daemon wrote);
- `:183` `uiomove(fb.buf, uio->uio_resid, uio)` copies the daemon's bytes in;
- `:188` `ohd = fb.buf` — the daemon controls `ohd->len` (the CLAIMED length);
- `:205` `fip->reply = fb` — stores the **unclamped** buffer, so
  `fip->reply.len = uio_resid`;
- `:212` `fuse_audit_length(ihd, ohd)` validates `ohd->len` (CLAIMED) against the
  request — **never** `fb.len` (ACTUAL);
- `:218` completes the IPC **regardless** of the audit result, so even an
  audit failure cannot stop the consumer.

Consumers use `fuse_out_data_size(fip) = fip->reply.len - 16 = uio_resid - 16`
(`fuse.h:271-274`).  So a daemon that writes more bytes than it claims drives a
memcpy past the consumer's destination buffer.

The most dangerous consumer is the FUSE_READ path
(`fuse_vnops.c:fuse_io_execute`, lines 2043-2055):

```c
case BUF_CMD_READ:
    ...
    fri->size = bp->b_bcount;          /* request size, e.g. 4096 or 8192   */
    ...
    memcpy(bp->b_data, fuse_out_data(fip),
           fuse_out_data_size(fip));    /* = uio_resid - 16  (ACTUAL, unclamped) */
```

The FUSE_READ audit case is `ohd->len - 16 <= fri->size`
(`fuse_util.c:132-134`), so a daemon that CLAIMS `ohd->len = 16 + fri->size`
passes the audit while ACTUALLY writing far more → the consumer memcpy
overflows `bp->b_data` (sized to the request) by the difference.

## The primitives (both confirmed)

1. **Heap overflow WRITE** (the real, dangerous primitive — CWE-787).  Daemon
   replies to `FUSE_READ(fri->size=4096)` CLAIMING `ohd->len=4112` but writing
   12288 bytes.  Audit passes (`4112-16=4096 <= 4096`); consumer does
   `memcpy(bp->b_data[4096], ..., 12288-16=12272)` → **8176-byte heap overflow
   write**, content fully daemon-controlled.  Confirmed live: the overflow
   page-faults in `memcpy` (corrupts an adjacent `vm_object` pointer →
   `panic: assertion "obj != NULL" failed in vm_object_hold_shared`).

2. **OOB read on reply buffer** — the finding's secondary framing.  With the
   current code `fb.len = uio_resid` always matches the allocation, so the
   daemon cannot make the consumer read past its own reply allocation via
   `ohd->len` alone; the exploitable direction is the overflow-write above.  (A
   daemon that CLAIMS `ohd->len > uio_resid` would, post-fix, be rejected with
   `EINVAL`.)

## Reproduction

- **Deterministic harness** (`harness.c`) — transcribes the exact vulnerable
  path (`fuse_buf_alloc` + unclamped `fip->reply` + `fuse_out_data_size` +
  consumer memcpy) and prints the overflow extent: **8176 bytes**, all
  daemon-controlled.
- **Live FUSE daemon** (`fused.c`) — opens `/dev/fuse`, mounts `/mnt/fuse`
  (handles FUSE_INIT/STATFS/LOOKUP/GETATTR/OPEN), then on FUSE_READ replies
  with 12288 actual bytes claiming `ohd->len=4112`.  A child process opens
  `/mnt/fuse/pwned` and `read()`s it, driving the buffer-cache → strategy →
  `fuse_io_thread` → `fuse_io_execute` → `memcpy` overflow.  Result on the
  unpatched #0 GENERIC kernel:

  ```
  panic: assertion "obj != NULL" failed in vm_object_hold_shared at vm_object.c:330
  Trace: vm_object_hold_shared -> vm_fault -> trap_pfault -> trap -> memcpy+0xfa
  ```

  (page fault *during* the overflow memcpy: `bp->b_data + 8176` overran into a
  corrupted `vm_object` pointer; VM down at the `db>` prompt).

## Threat model / reachability / Phase 6 escalation assessment

- `/dev/fuse` is `crw-rw---- root:operator` and `mount("fuse",...)` requires
  `caps_priv_check(SYSCAP_NOMOUNT_FUSE)` which needs `uid == 0`
  (`fuse_vfsops.c:155`, `kern_caps.c:311`).  `vfs.usermount=0` on this guest
  and `maxx` is NOT in the `operator` group, so **the FUSE daemon (which
  controls the overflow content/size) must be started by root on the default
  GENERIC config**.  An unprivileged consumer (`maxx`) CAN trigger the
  `FUSE_READ` (if root mounts with `allow_other`), but the overflow BYTES are
  authored by the root daemon, not by `maxx`, so `maxx` cannot shape the
  corruption to escalate himself.
- This is a **root→kernel heap overflow** (a robustness/hardening gap + DoS)
  on the default config.  It is NOT an unprivileged→root escalation on default
  GENERIC: the valid hard blocker "the write is reachable only from an
  already-root context" applies (root daemon authors the overflow; root→kernel
  is game-over by definition; there is no privilege boundary for the daemon to
  cross).
- Conditional escalation path (NOT default config): IF an admin set
  `vfs.usermount=1` AND added the user to `operator`, an unprivileged user
  could run the daemon and gain a full heap-overflow-write primitive
  (content + size attacker-controlled, no SMAP/SMEP/KASLR on this guest).
  On GENERIC (`options INVARIANTS`) slab grooming would very likely trip a
  `chunk_mark_*`/`WEIRD_ADDR` KASSERT before a clean `uid0` lands; on a
  non-default INVARIANTS-OFF build the primitive would be directly
  weaponizable.  These are documented as conditional, not default-GENERIC.

**Outcome:** primitive fully characterized (8176-byte heap overflow write,
daemon-controlled); escalation blocked by the root-only-daemon hard blocker on
default GENERIC.  Impact on default GENERIC = **memory corruption / panic
(DoS)** from a root-started (or conditionally-unprivileged) malicious daemon.

## The fix

`sys/vfs/fuse/fuse_device.c`, right after `ohd = fb.buf;` (line 188):

```c
if (ohd->len < sizeof(*ohd) || ohd->len > fb.len) {
    fuse_buf_free(&fb);
    return EINVAL;          /* daemon claimed more than it wrote */
}
fb.len = ohd->len;          /* clamp so consumers see the claimed, audit-validated size */
```

- rejects a daemon that claims `ohd->len > uio_resid` (would be an OOB read);
- clamps `fb.len` to `ohd->len` so `fuse_out_data_size = ohd->len - 16` (the
  audit-validated length), closing the overflow-write for every consumer.

**Validated:** unpatched kernel panics in `memcpy` on the same PoC; patched
`fuse.ko` (identical code path) returns `read()=4096` and the VM stays up.  See
`fix.diff` and `fix_run.log`.

## How to reproduce

```
ssh dfbsd-maxx 'cd poc/DF-0915 && cc -O2 -o harness harness.c && ./harness'   # deterministic proof
# live kernel trigger (as root):
ssh dfbsd 'kldload fuse && mkdir -p /mnt/fuse && cd /home/maxx/poc/DF-0915 && cc -O2 -o fused fused.c && ./fused'
```
