# DF-3156 — dangling RRIP NM-CONTINUE chain makes the `defname` fallback write the ISO name out of bounds (kernel stack in lookup, kernel heap in readdir) — verified to unprivileged uid=0

## What (root cause)

`cd9660_rrip_altname()` (sys/vfs/isofs/cd9660/cd9660_rrip.c:226-284) advances
`ana->outbuf` by every NM fragment (`:276-277`) and only rewinds it on the
`> ana->maxlen` rejection (`:271`). A fragment whose FLAGS byte has
CONTINUE set never completes the name — it returns 0 **without** setting the
`ISO_SUSP_ALTNAME` result bit (`:279-283`).

If the SUSP walk then ends *without* a terminating NM fragment — trivially
arranged: the last field of the walk is a CONTINUE-flagged NM in a CE
continuation area — `cd9660_rrip_loop()`'s default-value fallback
(`:581-584`) invokes `cd9660_rrip_defname()`, which runs

```c
isofntrans(isodir->name, isonum_711(isodir->name_len),
	   ana->outbuf, ana->outlen, ...);			/* rrip.c:289-292 */
```

at the **stale advanced pointer** `base + N` where N is the accumulated
fragment length (up to 255, reached via CE continuations), writing the
record's translated ISO name (up to ~183 bytes, attacker-chosen content,
copied verbatim at joliet_level 0 / original=1) — up to **183 bytes past the
end of a 255-byte output buffer**:

* `cd9660_lookup()` (cd9660_lookup.c:109 `char altname[NAME_MAX]`) —
  **kernel stack**; OOB bytes 22..29 = saved RBP, 30..37 = saved RIP
  (measured: `frame pointer = 0x444342415a595857` = our cycling A..Z
  pattern, `Stopped at cd9660_lookup+0x461: ret`).
* `cd9660_readdir()` (cd9660_vnops.c:542, buffer = `idp->current.de.d_name`
  inside the kmalloc'd `struct isoreaddir`) — **kernel heap**; the write
  smashes the isoreaddir tail: `curroff`, **`uio` pointer**, `uio_off`,
  `eofflag`, `cookies`, `ncookies` (measured: `Fatal trap 9 ...
  iso_uiodir+0x25: cmpq %rax,0x18(%r8)` — the corrupted `idp->uio`
  dereferenced).

All fragment lengths are individually valid (>= 5) and each passes the
`> maxlen` check — this is **not** the DF-3146 negative-wlen underflow and
**not** the DF-0878 entry-length OOB-read family; the defect is the missing
output-buffer reset on the default-name fallback path.

## Verified impact (guest 6.5-DEVELOPMENT #0, stock INVARIANTS kernel)

1. **Unprivileged kernel memory corruption** — `getdents`/`ls` (heap) or
   any `stat`/path resolution (stack) under the mount, as any user.
2. **Full unprivileged → uid=0 chain** (`exploit.c` + `df3156_exploit.iso`):
   * the heap variant corrupts `idp->uio` to point at a **fake `struct uio`
     + `struct iovec` in the (unprivileged) process address space**;
   * `iso_uiodir()` → `uiomove(dirent, 152, fake_uio)` with
     `uio_segflg=UIO_SYSSPACE` `bcopy`s the dirent (16-byte header + the
     NM-chain bytes, all attacker data) to the fake `iov_base` — an
     **arbitrary kernel write** with no SMAP to stop it;
   * the write targets `&sysent[71]-16`, hijacking the obsolete syscall 71
     (`old mmap`) `sy_call` to a ring0 stub at a fixed user address (no
     SMEP), while restoring every live sysent entry it touches
     (munmap/mprotect/madvise/sbrk-neighbors) with their original values;
   * `syscall(71)` executes the stub in ring0:
     `%gs:0 → gd_curthread(+8) → td_proc(+168) → p_ucred(+16)` then
     `cr_uid=cr_ruid=cr_svuid=0`, `cr_ngroups=1`, `cr_caps=~0`;
   * the syscall returns through the intact syscall path; the process is
     now root — **`uid after: 0 euid: 0`**, reads the root-only
     `/root/df3156_rootproof` (mode 0400). Guest stays up.
3. The stack variant gives direct saved-RIP/RBP control (panic evidence in
   `panic_lstat.txt`) — an alternative escalation route not needed for the
   demonstrated chain.

Threat model: crafted RRIP ISO mounted by root (same setup as the
DF-0878/0879/DF-3124/DF-3146 crafted-media family); after the mount ANY
unprivileged user with read access triggers via getdents/stat/ls.

## Reproduce

Host: `python3 craft_dangle.py pat184 out.iso` (pattern trigger),
`python3 craft_exploit.py df3156_exploit.iso` (full chain).
Stage the pack into the guest and:

```
cc -O -o /tmp/exploit /root/exploit.c ; cc -O -o /tmp/trigger /root/trigger.c
echo "SECRET-ROOT-ONLY-DF3156-$(date +%s)" > /root/df3156_rootproof
chmod 400 /root/df3156_rootproof ; mkdir -p /mnt/df3156
vnconfig -c -S 2048 /dev/vn0 /root/df3156_exploit.iso
mount_cd9660 -o ro /dev/vn0 /mnt/df3156
su -m nobody -c "/tmp/exploit /mnt/df3156"     # -> uid after: 0 + secret
# stack-smash proof (panics the box):
vnconfig -u /dev/vn0 ; vnconfig -c -S 2048 /dev/vn0 /root/df3156_pat183.iso
mount_cd9660 -o ro /dev/vn0 /mnt/df3156
su -m nobody -c "/tmp/trigger lstat /mnt/df3156"
```

## Fix

`fix.diff` — rewind `ana->outbuf`/`*ana->outlen` to the buffer base at the
top of `cd9660_rrip_defname()` (no-op when no NM fragment was seen).
Validated on a rebuilt kernel (#1): both triggers become clean fallback
names (`name='A'`, pattern name at base), no panic, no uid0
(`fix_run_exploit.log`).
