# DF-3015 — ufs_symlink controlled heap overflow via unvalidated fs_maxsymlinklen

## What

`sys/vfs/ufs/ufs_vnops.c:1561-1566` (`ufs_symlink`):

```c
len = strlen(ap->a_target);
if (len < vp->v_mount->mnt_maxsymlinklen) {
        bcopy(ap->a_target, (char *)ip->i_shortlink, len);
```

`mnt_maxsymlinklen` is copied verbatim from the on-disk superblock at mount
(`ffs_vfsops.c:504` and `:724`) with **no validation** (not in DF-0820's list
either).  `i_shortlink` aliases `i_din.di_db` (+`di_ib`) — 60 bytes total
(`UFS1_MAXSYMLINKLEN`, dinode.h:114) — and `i_din` is the *last* member of
the 304-byte `struct inode`, allocated from the 320-byte slab chunk class.

A crafted image with `fs_maxsymlinklen = 0x7fff` (superblock byte offset
8192+1320 = 9512) turns every symlink with target length 61..1023 into a
**fully attacker-controlled linear heap overflow** of up to ~919 bytes past
the inode allocation.

## Threat model

Crafted FFS media mounted by root (same model as DF-0778 / DF-0820 /
DF-0887): the unprivileged trigger is a single `symlink(2)` in a
world-writable directory of the mounted filesystem.

## Reproduce

```
# root: build + mount the crafted image (patches fs_maxsymlinklen=0x7fff)
sh setup3015.sh

# maxx (uid 1001): the trigger
cc -O1 -o trigger3015 trigger3015.c && ./trigger3015 /mnt/p/l1
```

### Expected baseline (stock kernel)

* `ls -la` shows the symlink's own uid/gid = **1094795585 (0x41414141)** —
  attacker bytes inside a live kernel inode disclosed to userland
  (`di_uid`/`di_gid` at `i_din+112/116` are inside the bcopy range).
* Then one of two kernel panics, depending on what follows the inode chunk:
  - `panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0"
    failed in chunk_mark_allocated` — freelist poisoning: a free chunk's
    `c_Next` was overwritten with 0x53 bytes (`panic.txt`)
  - `Fatal trap 9: general protection fault` in `ffs_update+0x3e` —
    a live neighbouring inode's `i_fs`/`i_devvp` were overwritten
    (`panic2.txt`)
* With grooming (`exp3015.c`) the write lands at a *chosen* victim inode:
  the calibration leak verified `leak[144..152) == victim i_number`,
  proving the exact offset model (chunk stride 320, `i_din`@176,
  `di_db`@+40 ⇒ neighbor field f at payload index 104+f).

### Patched kernel (fix.diff applied)

* No overflow: 1023-byte target takes the long-symlink path; `ls -la`
  shows `maxx maxx` uid/gid; no panic after repeated triggers.

## Files

* `trigger3015.c`  — minimal unprivileged trigger (1023-byte target)
* `spray3015.c`    — inode spray used in the first blind reproduction
* `exp3015.c`      — grooming/leak/weapon harness (calibration log in
  `run.calibration.log`: adjacency verification + leaked mount-constant
  pointers)
* `demo3015.c`     — controlled-write demo skeleton (see VERDICT.md for
  the observation caveat: stat on a smashed inode faults first)
* `setup3015.sh`   — crafted-image builder/mounter (root)
* `inopatch.c`     — on-disk dinode patcher (helper, FFS fragment math)
* `panic.txt`, `panic2.txt`, `run.blind.log`, `env.txt`, `fix.diff`
