# DF-4542 — VERDICT

**Status: reproduced** (impact: panic — reliable local kernel DoS from
console-reachable ioctls; underlying class is a heap UAF/double-free
window).  **Fix validated** on the guest (v2 patch: panic gone across
all previously-fatal stress configurations).

## Reproduced

Stock `DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026 X86_64_GENERIC` guest
(QEMU/KVM, 6 vCPU, std VGA text console, no VESA/KMS):

* 3/3 panics in ≤60 s whenever CONS_SET text-mode cycling runs
  concurrently with `write()` traffic on the same /dev/ttyvN:
  ```
  panic with 1 spinlocks held
  panic: assertion "count & TOK_COUNTMASK" failed in _lwkt_reltokref
         at /usr/src/sys/kern/lwkt_token.c:458
  cpuid = 2/5/5      (run.panic{1,2,3}_bootlog.txt)
  ```
* Isolation matrix proves the pair:
  * ioctl churn alone (2.9M ioctls) — clean
  * writers alone (1.3M writes) — clean
  * **CONS_SET + writers — panic** (mode 10; also full-mix modes 3/7)
* fix v1 (serialize only the buffer swaps) **changed the manifestation
  into the raw heap double-free**:
  ```
  panic: BADFREE2
  _kfree() <- sc_vtb_destroy() <- sc_alloc_scr_buffer()
            <- sc_set_text_mode() <- sc_vid_ioctl()
  ```
  (fixv1.BADFREE2_bootlog.txt) — direct confirmation that the mode-change
  path frees heap buffers that are still being managed concurrently.

## Root cause (path:line)

* `sc_set_text_mode()` scvidctl.c:54-184 holds only `vga_token`
  (scvidctl.c:64) across the entire scp mutation + buffer reallocation.
* `sc_alloc_scr_buffer()` syscons.c:3560-3587 frees the old `scp->vtb`
  and kmallocs a new one with `wait=TRUE` → **kmalloc(M_WAITOK)**:
  when the ioctl thread sleeps, LWKT drops `vga_token`
  (lwkt_relalltokens in lwkt_switch), so *neither* vga_token nor any
  other lock excludes:
  * the tty output path `scstart()` → `sc_puts()` (syscons.c) which
    writes `scp->vtb` and pushes scroll lines into `scp->history`
    under **`syscons_mtx` only** (a spin lock, `syscons_lock()`
    syscons.c:244) — **no common lock**;
  * a second concurrent CONS_SET (serialized by vga_token only while
    nobody sleeps).
* `sc_alloc_history_buffer()` schistory.c:72-173 is completely
  lock-free (read-NULL-out / install / free of `scp->history`), racing
    the same writers.
* Result: emulator writes into freed M_SYSCONS slabs (UAF write) and
  torn `scp` geometry (OOB write window); observed kernel-synchronization
  corruption → deterministic panic.

## Exploitability

* Reach: any process with a writable fd on /dev/ttyvN — `sc_vid_ioctl`
  does no privilege check (syscons.c:983-986 forwards directly; the only
  gate is device permission — 0600 root on the ssh-only audit guest, but
  a real console-login session holds such an fd; `vidcontrol`/X startup
  issue CONS_SET routinely while getty/init write).
* The verified product is the reliable panic (DoS).  The UAF-write
  primitive (attacker-controlled glyph/attribute u_shorts written by
  the emulator into a freed slab, race-window dependent, M_SYSCONS
  grooming) was NOT weaponized to uid=0 in this run — the win requires
  deterministic race timing; recorded as the escalation route.

## Fix validation

* v1 (swap under syscons_mtx only): BADFREE2 remained (CONS_SET-vs-CONS_SET
  interleave via token-drop-at-sleep) — valuable negative result.
* v2 (`fix.diff`, in this pack): allocate the new vtb first with
  M_NOWAIT; commit meta fields + vtb swap + `sc_init_emulator()` +
  cut-buffer under the reentrant `syscons_mtx` spinlock; free old
  buffers outside the spinlock; history detach/install/fail-restore
  under `syscons_mtx` in `sc_alloc_history_buffer()`; `sc_alloc_scr_buffer()`
  hardened the same way + refuses to install an invalid buffer.
* Patched kernel (#1 Mon Sep 21 10:17:20): three consecutive 60 s rounds
  of the exact fatal configurations (mode 10 ×2, mode 7 ×1; ~1M CONS_SET,
  ~3M writes, VT cycling) — all clean, guest alive (run.log).

fix_status: **fixed** (baseline reproduced 3/3; patched reproduced 0/3).

## Residual (noted for upstream)

The async renderer's `draw` phase runs without `syscons_mtx`
(scrn_update drops it, syscons.c) and without `vga_token` for most of
its vtb reads; a theoretical render-vs-mode-change read race remains
(active console only).  Upstream may want vga_token (or a dedicated
lock) around the vgarndr text draw loops as follow-up hardening.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
