# DF-5015 — syscons schistory.c geometry-mismatch heap OOB write

## What
`sc_alloc_history_buffer()` (sys/dev/misc/syscons/schistory.c) can leave a
stale-geometry history ring installed after a screen-mode change, because all
three mode-change call sites ignore its return value
(sys/dev/misc/syscons/scvidctl.c:163, 397, 947) and its own failure path
re-installs the OLD buffer (schistory.c:147-159) *after* the caller has
already updated `scp->xsize/ysize` (scvidctl.c:146-147/382-383).

When the ring is smaller than one screenful, the ring arithmetic goes
negative:

* `sc_hist_restore()` (schistory.c:230-232) computes
  `sc_vtb_pos(hist, tail, -xsize*ysize)` = `(tail - 4800 + 2000) % 2000`
  -> negative; `sc_vtb_seek()` stores it raw (`vtb_tail = pos % size`,
  scvtb.c:239-242).
* the next `sc_hist_save()`/`sc_hist_save_one_line()`
  (schistory.c:211-215, syscons.c:3851, sctermvar.h:437) runs
  `sc_vtb_append()` with the negative tail:
  `len = imin(count, vtb_size - vtb_tail)` exceeds the ring, and
  `sc_vtb_bcopy(screen, vtb_buffer + vtb_tail, ...)` writes up to one
  screenful of attacker-typed text BEFORE the ring buffer
  (scvtb.c:221-235).
* browsing also OOB-READS past the end of the ring via `sc_vtb_copy()`
  (schistory.c:254-256 -> scvtb.c:198-211 has no wraparound), leaking
  adjacent kernel heap to the console screen.

## Preconditions
* an open fd on a syscons vty (any console-login user; the PoC opens as
  root and setuid()s to nobody — all triggering ioctls run as uid 65534),
* `ioctl(CONS_HISTORY, 25)` on an 80x25 vty (ring = 25 rows),
* a mode change to a taller text mode (80x60 = mode 34) whose ring realloc
  (`kmalloc(..., M_NOWAIT)` at schistory.c:118/124 + scvtb.c:93) fails
  under kernel-memory pressure,
* scroll-lock browse in/out (keyboard) to arm the negative tail, then any
  scrolled line or mode change performs the OOB write.

## Files (this pack)
* `df5015.c`   — guest-side tool (arm/flip/flipwatch/shot/mark/fill/drain/
  pressure/vmburst/info); all triggering paths run as uid 65534 after open.
* `drive.py`, `round2.py` … `round6.py` — host drivers (QEMU-monitor key
  injection + ssh orchestration + in-kernel probe polling).
* `offmod2.c` — KLD evidence probe: `sysctl hw.df5015` prints the live
  history vtb state (cols/rows/size/buffer/tail/xsize/ysize) and counts
  Z-cells (0x075a) in the page BEFORE the ring buffer.
* `probe.c`, `off.c` — earlier /dev/kmem + offset experiments.
* `squeeze.c`, `watcher.sh` — root-side memory-pressure setup + in-guest
  mismatch watcher.
* `panic-scvtb_bcopy-unpriv-churn.log` — kernel WRITE fault in
  `sc_vtb_bcopy` at 0xffffffff7ffffffe during the unpriv trigger campaign
  (panic #1; see VERDICT.md for the attribution discussion — the DF-5016
  race in the same campaign is the competing cause).
* `fix.diff` — verified fix (also covers DF-5016 and DF-2045).

## Build
Guest: `cc -O1 -o /tmp/df5015 /tmp/df5015.c`
KLD probe: see offmod2/Makefile in the transcript (`make` with
bsd.kmod.mk against /usr/src/sys).

## Run (condensed; full story in VERDICT.md)
1. `/tmp/df5015 arm`                      (uid 65534 ioctls)
2. memory-pressure window (unpriv dd/vmburst churn, or root squeeze setup)
3. `/tmp/df5015 flip 100000 3000 60 &`    (CONS_SET 80x25 <-> 80x60)
4. detect: scroll-lock + End + CONS_SCRSHOT dump — MARKERX appearing 2-3x
   proves the 25-row ring is serving a 60-row screen (mismatch);
   in-kernel: `sysctl hw.df5015` shows `rows=25 size=2000 ... ysize=60`.
5. scroll-lock in/out cycles arm the negative tail (`sysctl hw.df5015`
   shows `tail=-N`) and fire the OOB write (`zprev` counts written cells).

## Expected result (what was observed)
* mismatch reproduced 3x via the marker-wrap artifact (rounds 1-3:
  marker rows [3,34,59], [28,59], [28,59]);
* in-kernel probe confirmed `rows=25 size=2000` rings on the shrunk vtys
  and the state machine live;
* kernel panic (write fault in sc_vtb_bcopy at 0xffffffff7ffffffe) during
  the unpriv churn campaign on the syscons vtb path.
