# DF-0895 — OOB heap read via unvalidated FUSE daemon reply size (INIT/STATFS/STATVFS)

## Claim (High)
`sys/vfs/fuse/fuse_vfsops.c` reads fixed-size reply structs
(`struct fuse_init_out` 64 B at `:216`, `struct fuse_statfs_out` 80 B at
`:399` and `:430`) out of the daemon's reply buffer via `fuse_out_data(fip)`
without checking that the reply buffer actually contains that many bytes.
`fuse_audit_length()` (`fuse_util.c:87`) is *supposed* to bound reply sizes,
but it validates the daemon-**claimed** `ohd->len` header field, not the
**actual** number of bytes the daemon wrote (`fip->reply.len` / `fb.len`).
A malicious FUSE daemon can therefore write a short buffer (just the 16-byte
`fuse_out_header`) while setting `ohd->len` to the struct-sized value the
audit expects; the audit passes, the IPC completes unconditionally
(`fuse_device.c:218` "Complete the IPC regardless of above result"), and the
kernel-side consumer dereferences the full fixed-size struct past the
allocation → **80-byte heap OOB read**.

For STATFS/STATVFS the leaked bytes flow back to userspace via the
`statfs(2)`/`statvfs(2)` syscalls (`sbp->f_blocks/f_bfree/f_bavail/f_files/
f_ffree`); for INIT they corrupt `fmp->{abi_major,abi_minor,max_write}`
(read from garbage, then checked by `fuse_cmp_version`).

## Threat model
FUSE is module-only on DragonFly; `/dev/fuse` is `root:operator 0660` and the
mount requires privilege (`caps_priv_check` for `nomount_fuse`). The PoC
therefore runs the malicious daemon **as root** (matching DF-0780/0781/0915).
Root→kernel is already game-over; the security-relevant impact is that the
OOB read is a **kernel heap disclosure primitive**: any local user who can
`statfs()` the mount point receives the leaked bytes, and on a KASLR-enabled
kernel the leaked pointers (`0xfffff800...` direct-map, `0xffffffff82...`
kernel-image) defeat KASLR. The short-reply trigger passes the existing audit
silently, so this is not caught by current defenses.

## Reproduce
```
./build.sh && ./run.sh          # as root on the DragonFly guest
```
`run.sh` loads `fuse.ko`, runs the malicious daemon (forks: child mounts +
calls `statfs()` 3×, parent answers FUSE_INIT/STATFS with a short reply).

### Expected (bug present)
`statfs()` returns non-zero kernel heap bytes in `f_blocks/f_bfree/f_bavail/
f_files/f_ffree` — e.g. `0xfffff8004f3308e0` (direct-map pointer) and
`0xffffffff82606ca0` (kernel-image pointer, stable across runs). These vary
byte-for-byte across runs/iterations (genuine heap OOB, not deterministic).

### Expected (fixed)
All `statfs()` fields are zero; the daemon's short write is rejected by the
kernel with `EPROTO` (daemon sees `write()=-1`), and the kernel-side consumer
aborts before dereferencing the too-short buffer.

## Files
- `fused_short.c` — malicious FUSE daemon (forks mount+statfs child; short
  replies). `./fused_short` = valid INIT + short STATFS (leak);
  `./fused_short init` = short INIT too (mount fails with EPROTONOSUPPORT).
- `build.sh` / `run.sh` — exact build/run.
- `fix.diff` — root-cause fix (validate actual reply len, propagate failure).
- `build.log` / `run.log` / `run_init.log` / `fix_build.log` / `fix_run.log`
  / `leak_sample.txt` / `env.txt` — full evidence.
- `VERDICT.md` — detailed analysis.
