# DF-3015 VERDICT

**Finding**: ufs_symlink heap buffer overflow via unvalidated on-disk
`fs_maxsymlinklen` — `sys/vfs/ufs/ufs_vnops.c:1561-1566`, mount glue
`sys/vfs/ufs/ffs_vfsops.c:504,724`.

**Status: reproduced** (impact: memcorrupt — controlled kernel heap
overflow; two distinct kernel panics; attacker bytes disclosed inside a
live inode via stat(2)).

## How it was reproduced

1. `setup3015.sh` (root): 128MB UFS1 image via `newfs /dev/vn0`,
   `fs_maxsymlinklen` patched at superblock byte 9512 to `0x00007fff`,
   mounted at /mnt (no softdep/async).
2. Unprivileged `symlink()` with a 1023-byte target
   (`trigger3015.c`, uid 1001):
   `len < mnt_maxsymlinklen` ⇒ `bcopy(target, i_shortlink, 1023)` from
   `i_din+40` in a 304-byte object in the 320-byte slab class ⇒ ~919
   bytes past the allocation, fully attacker-controlled content
   (any non-NUL bytes), length selectable 61..1023.

### Evidence chain

* **Controlled content in a live kernel object, disclosed to userland**:
  after the trigger, `ls -la` reports the symlink's uid/gid as
  `1094795585` = `0x41414141` (`run.log`) — `di_uid/di_gid` at
  `i_din+112/+116` are inside the bcopy range.
* **Kernel panic #1 — freelist poisoning**: a 280-byte variant
  overwrote a *free* chunk's `c_Next` (first 8 bytes of a slab free
  chunk) with `0x5353535353535353`; the next inode allocation popped
  the poisoned list and tripped the INVARIANTS zone assertion
  (`panic.txt`).  On a production (non-INVARIANTS) kernel the same
  poisoning is the classic arbitrary-in-zone-allocation primitive —
  no mask assertion exists there.
* **Kernel panic #2 — live-object corruption**: the 1023-byte trigger
  smashed a live neighbouring inode's pointer fields; `ffs_update`
  dereferenced `0x4141...` ⇒ fatal trap 9 GPF (`panic2.txt`).
* **Deterministic offset proof**: grooming harness (`exp3015.c`)
  reached steady-state slab allocation after 150 filler pairs; the
  DF-0778 readlink leak of the chunk following a 280-byte symlink
  showed the *live neighbour's* `i_number` at exactly the predicted
  offset (`leak[144..152) == V ino 306`) — `run.log`.  The write can
  therefore be placed at a chosen field of a chosen neighbouring inode
  (neighbour field f ⇒ payload index 104+f; verified end-to-end).

## Exploitation analysis (uid=0)

The primitive is a strong memcorrupt: controlled content, controlled
length, deterministic placement into the neighbouring `struct inode`
after minimal grooming.  A complete unpriv→root chain was engineered
this far:

* Weaponised write into a neighbouring inode can set
  `di_mode = IFREG|ISUID|0755` (bytes `ED 89`) and leave `di_uid`
  (root) untouched by stopping the bcopy at payload offset 282 —
  the setuid-root exec primitive in two bytes.
* **Hard blocker**: the same write necessarily passes through the
  victim inode's header, destroying `i_fs` (VTOI(vp)->i_fs,
  needed by ffs_read/bmap), `i_devvp` (ufs_strategy) and `i_dev`
  (ufs_getattr→devid_from_dev during execve).  These kernel pointers
  cannot be rewritten through the string-based bcopy because their
  addresses contain 0x00 bytes (kernel heap objects are size-class
  aligned; leaked values confirmed: `k_fs=...8800`, `k_dev=...4f00f8..`,
  `k_devvp=...0f00` — see `run.log`).  Repairing them requires a
  kernel-written source (e.g. an allocator-forged overlapping inode —
  feasible via the demonstrated freelist poisoning, but every field
  layout I derived either zeroes the just-set `di_mode` or re-zeroes
  the repaired pointers; the colliding offsets are documented in the
  session analysis).
* Within this run's budget the chain therefore stops at: controlled
  heap corruption + reliable kernel panic + disclosed attacker bytes,
  with the two-byte setuid-mode write demonstrated as the final
  primitive component.

**Threat-model note**: with root mounting attacker-crafted media, the
mount itself already conveys root-chosen file ownership, so "uid0" is
not the meaningful boundary for this class (DF-0778/0887 were verified
the same way: panic/leak).  The real-world value of this bug is on
mounts that are *not* attacker-owned media — e.g. an image modified in
transit, a multi-tenant image store, or `vfs.usermount=1` boxes — where
an unprivileged user gets a controlled kernel heap overflow from a
single symlink(2).

## Fix validation

`fix.diff` clamps `mnt_maxsymlinklen` to `UFS1_MAXSYMLINKLEN` at both
mount-time assignment sites (ffs_vfsops.c) and adds a belt-and-braces
`len < UFS1_MAXSYMLINKLEN` guard in `ufs_symlink` itself.  Applied to
the guest's /usr/src, `make nativekernel` + installkernel, reboot:

* baseline (stock): symlink uid/gid = 0x41414141, panics above.
* patched: 1023-byte target takes the long-symlink path; uid/gid stay
  `maxx maxx`; repeated triggers (1023-byte and 280-byte) produce no
  panic, no corruption (`run.patched.log`).

Fix status: fixed.
