# DF-0887 — Heap overflow in `ffs_truncate` symlink fast-path (unbounded `bzero`)

## Bug

`sys/vfs/ufs/ffs_inode.c:159-168` — `ffs_truncate` fast path is entered for
`VLNK` inodes when `i_size < mnt_maxsymlinklen` **OR** `i_din.di_blocks == 0`.
The `di_blocks == 0` arm enters **regardless of `i_size`**. The fast path then
does:

```c
bzero((char *)&oip->i_shortlink, (uint)oip->i_size);
```

`i_shortlink` aliases `i_din.di_shortlink` aliases `i_din.di_db` (`inode.h:126`,
`dinode.h:111-112`), which is `ufs_daddr_t[12]` = **48 bytes** (`dinode.h:83`).
`i_size` is the on-disk `di_size` copied verbatim into the in-memory inode at
`ffs_vfsops.c:1147`. A crafted UFS1 inode with `di_mode=S_IFLNK`,
`di_blocks=0`, `di_size=4096`, `di_nlink<=0` therefore causes a 4048-byte
zero-write past the 48-byte `di_db` buffer into the `M_FFSNODE` slab heap and
beyond. (Even `UFS1_MAXSYMLINKLEN=60 > 48B` makes the size arm inherently
overflow by up to 12 bytes.)

Trigger path: mount crafted image, `stat` the symlink ->
`ufs_iget` reads the forged dinode -> `ufs_vinit` sets `vp->v_type=VLNK`
(`ufs_vnops.c:1962`) -> on vnode release, `ufs_inactive` (`ufs_inode.c:62`)
sees `i_nlink<=0` and calls `ffs_truncate(vp, 0)` (`ufs_inode.c:83`) ->
fast path -> unbounded `bzero`.

## Build / run / expected

```sh
# in guest as root
sh ./build.sh
sh ./run.sh        # expect: harness shows 4048B OOB; live mount+stat panics
```

Expected on the **unpatched #0 GENERIC** kernel (INVARIANTS ON):
- `harness 4096 0` prints `OOB WRITE of 4048 bytes past the 48-byte i_shortlink buffer`.
- Live trigger: kernel panic — either a slab-corruption INVARIANTS trap, or a
  page fault when the bzero runs past the slab page into an unmapped page. The
  panic signature is captured in `panic.txt` (from `dfbsd-qemu/boot.log`).

Expected on the **patched #1 single-fix kernel**: harness output unchanged
(it transcribes the *unpatched* code), but the live `stat` returns cleanly,
**no panic**, guest stays up.

## Files

| File           | Purpose                                                          |
|----------------|------------------------------------------------------------------|
| `craft_img.c`  | UFS1 inode patcher: computes on-disk inode offset from sb geometry, forges `di_size/di_blocks/di_nlink/di_mode`. |
| `harness.c`    | Deterministic primitive characterizer (transcribes the bzero verbatim with sentinels). |
| `reproduce.sh` | full live trigger: newfs -> symlink -> patch -> mount -> stat.   |
| `build.sh`     | `cc -o craft_img craft_img.c; cc -o harness harness.c`.          |
| `run.sh`       | calls `reproduce.sh`.                                            |
| `fix.diff`     | the verified single-fix patch (bounds the bzero).                |

## Impact ceiling

UFS is **not** user-mountable on DragonFly (`get_fscap` ->
`SYSCAP_RESTRICTEDROOT` for UFS; same blocker documented in DF-0820). So this
is a **root-context mount of attacker-supplied media -> kernel heap
corruption / panic** (a hardening gap / DoS), **not** an LPE. On default
GENERIC (INVARIANTS ON) the unbounded write lands in the `M_FFSNODE` slab and
is caught as slab corruption or runs into an unmapped page -> panic before any
grooming can land. On INVARIANTS-OFF builds the unbounded zero-write still
page-faults synchronously inside the `mount`/`stat` syscall before the caller
can observe or convert the corruption.
