# DF-0781 — Kernel heap info leak via unvalidated `namelen` in `fuse_vop_readdir`

Reproduction + fix-validation evidence pack.

## What this proves

`fuse_vop_readdir()` (`sys/vfs/fuse/fuse_vnops.c:1012`) walks a FUSE
daemon's `FUSE_READDIR` reply as a sequence of `struct fuse_dirent`.  Its
only bounds check is `len < FUSE_NAME_OFFSET` (24) — it never validates
that `fde->namelen` fits in the remaining reply.  It then passes the
daemon-chosen `fde->namelen` (truncated to `uint16 d_namlen`) and
`fde->name` to `vop_write_dirent()`, which does
`bcopy(d_name, dp->d_name, d_namlen)` reading `d_namlen` bytes from
`fde->name` regardless of how many name bytes actually exist in the
reply.  When `d_namlen` exceeds the real name size, the bcopy reads past
the daemon's kmalloc'd `M_FUSE_BUF` reply buffer into adjacent kernel
heap, and `uiomove` then ships those bytes to the user's `getdents`
buffer — a kernel heap info leak.

After the leak, `len -= freclen` underflows (both `size_t`) and
`buf += freclen` advances to a wild pointer — the "wild ptr" half of the
title.  Whether the next iteration faults depends on slab layout; on
this INVARIANTS-ON guest with `fde->namelen = 32000` it did not fault
(mapped slab) so the guest survived, but on a system with a different
slab layout it would page-fault → panic (DoS).

The existing `fuse_audit_length()` in `fuse_device_write()` only checks
`len <= fri->size` for `FUSE_READDIR` (`fuse_util.c:132`, where
`fri->size = FUSE_BLKSIZE * 10 = 40960`) — it does NOT validate
`dirent.namelen` against the actual reply content, so the audit passes
and the IPC completes normally.

## Files

* `evil_daemon.c` — raw `/dev/fuse` malicious daemon.  Mounts a synthetic
  FUSE fs (`/mnt/fuse`) and answers every `FUSE_READDIR` with a single
  48-byte reply containing one dirent with `namelen = 32000` but only
  8 actual name bytes.
* `read_trigger.c` — unprivileged `getdents` consumer.  Pre-fills its
  64 KB buffer with `0x5a`, calls `getdents(2)`, then analyzes the buffer
  to prove the OOB read (kernel-touch high-water mark, honored
  `d_namlen`, non-zero leaked-byte count, kernel-pointer scan, leaked-
  window hexdump).
* `build.sh`  — `cc -O0 -g -o evil_daemon evil_daemon.c && cc -O0 -g -o read_trigger read_trigger.c`
* `run.sh`    — loads `fuse`, starts the daemon (root), then runs the
  `getdents` trigger as `maxx`.
* `fix.diff`  — consumer-side bound check in `fuse_vop_readdir`
  (`fde->namelen > NAME_MAX || FUSE_NAME_OFFSET + fde->namelen > len ||
  freclen > len` → `EINVAL`).
* `build.log` / `run.log` / `fix_run.log` / `fix_build.log` /
  `leak_sample.txt` / `env.txt` / `VERDICT.md` / `manifest.json`.

## Reproduce (info leak)

On the audit guest (unprivileged `maxx` exists; root needed only to load
the module, open `/dev/fuse`, and call `mount -t fuse`):

```sh
# as root, on the guest:
cp /usr/obj/usr/src/sys/X86_64_GENERIC/usr/src/sys/vfs/fuse/fuse.ko /root/fuse.ko
kldload /root/fuse.ko
# as maxx:
cd poc/DF-0781 && sh build.sh
# as root:
sh run.sh            # maxx getdents /mnt/fuse -> leak fires
```

Expected on the **unpatched** kernel + stock `fuse.ko`:
```
=== kernel wrote through user-buffer offset 32023 ===
=== honored d_namlen = 32000 (daemon claimed 32000) ===
=== 72 non-zero non-marker bytes in window [24..32024) ===
[LEAK-PROOF] kernel wrote 32024 bytes; ... => 32000 bytes were read from past the daemon's reply buffer
```
with kernel pointers (`0xfffff8008d62XXXX`) and ASCII kernel strings
(`rcng_dhcpd\0disabled`, `rcng_lvm`, `rcng_cryptdisks`, `rcng_initrandom`,
`rcng_fsck`, `running`, `disabled`) visible in the leaked window.

## Validate the fix

Because `fuse` is a loadable module (not compiled into `X86_64_GENERIC`),
the fix lives in `fuse.ko`.  Build the patched module, install, reload,
re-run:

```sh
scp fix.diff dfbsd:/root/fix.diff
vm.sh run_root 'cd /usr/src && patch -p1 --forward < /root/fix.diff'
vm.sh run_root 'cd /usr/src/sys/vfs/fuse && make obj && make && \
                cp /usr/obj/usr/src/sys/vfs/fuse/fuse.ko /root/fuse.ko'
vm.sh run_root 'kldunload fuse; kldload /root/fuse.ko'
sh run.sh           # maxx getdents -> EINVAL, NO leak, guest up
```

Expected on the **patched** `fuse.ko`: the bound check fires, the
dirent is rejected, and the user buffer's `0x5a` marker is fully
preserved:
```
=== kernel wrote through user-buffer offset -1 (bytes [0..-1]) ===
=== kernel did not write a full dirent header -> no leak ===
```

## Threat model / scope (read VERDICT.md for detail)

The trigger is **root-only on default DragonFly** (`kldload fuse` +
`/dev/fuse` `root:operator 0660` + `mount -t fuse` capability check all
require root), so the daemon author must be root or `operator`+helper.
The readdir consumer (the user whose `getdents` trips the leak) can be
unprivileged (`maxx` here).  This is a **root-controlled malicious
daemon → kernel info leak → unprivileged consumer** threat model — a
hardening gap / insider-abuse vector on a default system, and a real
info leak where unprivileged users are permitted to mount FUSE.  It is
**not** an unprivileged→root escalation on a default system.
