β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5016

syscons history ring UAF: scroll-path appends (syscons_lock) are not serialized against ring realloc/free (vga_token)

Summary

Every scrolled line appends under syscons_lock only, but every realloc/free is serialized by vga_token only - no common lock: process A loops CONS_HISTORY alloc/free while process B scrolls, giving a NULL window and a freed-ring window (wild sc_vtb_bcopy write into freed memory). Observed kernel write fault in sc_vtb_bcopy at 0xffffffff7ffffffe during the unpriv churn campaign; strong heap-grooming primitive. Install/destroy-under-syscons_lock fix. NOTE: also surfaced a GENERIC DragonFly defect (routed to kern/tty audit): tight unpriv ioctl loop on a tty panics stock kernel in ~60-100s with lwkt_reltokref TOK_COUNTMASK assert 4/4 - evidence at findings/poc/DF-5016/generic-ioctl-token-assert.txt.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5016 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
build.sh β€” 105 B view raw
env.txt β€” 244 B view raw
fix.diff β€” 3.6 KB view raw
generic-ioctl-token-assert.txt β€” 693 B view raw
manifest.json β€” 983 B view raw
offmod2.c β€” 2.6 KB view raw
panic-lwkt-token-count-race.log β€” 960 B view raw
race5016.c β€” 1.9 KB view raw
run.log β€” 979 B view raw
run.sh β€” 146 B view raw
verdict.json β€” 4.0 KB view raw

DF-5016 β€” syscons history ring UAF: scroll-append vs realloc locking hole

What

Every scrolled console line appends to the syscons history ring: sc_term_gen_scroll() (sys/dev/misc/syscons/sctermvar.h:425-450) calls sc_hist_save_one_line(scp, 0) -> sc_vtb_append(&scp->vtb, 0, scp->history, scp->xsize) (syscons.h:547-548). This runs from:

  • user tty output: scstart() holds syscons_lock only (sys/dev/misc/syscons/syscons.c:2298+ region), and
  • the kernel console/kprintf path: sccnputc() holds syscons_lock_nonblock() only (syscons.c:1899).

Meanwhile every history reallocation path β€” ioctl(CONS_HISTORY) (sc_hist_ioctl -> sc_alloc_history_buffer) and every screen mode change (sc_set_text_mode/sc_set_pixel_mode/KMS resize -> sc_alloc_history_buffer at scvidctl.c:163/397/947) β€” is serialized by vga_token only (syscons.c:983). There is no common lock.

sc_alloc_history_buffer() then does (schistory.c:94-159): scp->history = NULL ... sc_vtb_destroy(prev_history); kfree(prev_history, M_SYSCONS) β€” while another CPU can be inside sc_vtb_append() holding a pointer to that very ring: NULL-deref in the NULL window (guarded on the scroll path by the existing NULL check, but not elsewhere) and use-after-free read+write of the freed 4KB-class ring in the free window. sc_vtb_append() writes vtb_buffer + vtb_tail with a vtb_tail read from freed memory β€” attacker-influenced wild write.

Trigger (no privileges, no memory pressure needed)

  • proc A (uid 65534, fd on /dev/ttyv0): loop ioctl(CONS_HISTORY, 25) / ioctl(CONS_HISTORY, 250) β€” each iteration allocates a new ring and frees the old one (274,432 iterations in 20s observed).
  • proc B (uid 65534): write scrolling lines to the same tty (every newline at the bottom row runs the append).

Observed

  • Panic #2 during the campaign (see panic-lwkt-token-count-race.log): panic: assertion "count & TOK_COUNTMASK" failed in _lwkt_reltokref in lwkt_relalltokens() β€” kernel token-state corruption induced by the unsynchronized interleaving; the guest died between two ssh commands while only the race processes were active.
  • Panic #1 (same campaign family, DF-5015 pack): write fault in sc_vtb_bcopy at 0xffffffff7ffffffe (wild destination) with heavy kprintf (pagedaemon warnings) x flip-driven reallocs β€” the canonical signature of this race's UAF write branch.

Files

  • race5016.c β€” the two-process racer (uid 65534).
  • panic-lwkt-token-count-race.log β€” panic #2 serial capture.
  • ../DF-5015/fix.diff β€” the fix (install/destroy of history rings under syscons_lock) β€” validated against both findings.

Build / run

cc -O1 -o /tmp/race5016 /tmp/race5016.c
/tmp/race5016 scroll  >/tmp/scroll.log 2>&1 &
/tmp/race5016 realloc >/tmp/realloc.log 2>&1 &
# wait; watch serial console / guest liveness

Success criterion: kernel panic / corruption within minutes (baseline: two panics over the campaign).

VERDICT.md
↓ download raw

DF-5016 VERDICT β€” reproduced (kernel panic from unpriv two-process race)

Bottom line

The locking hole is real: history-ring appends (scroll path, syscons_lock) are not serialized against history-ring realloc/free (ioctl and mode-change path, vga_token). A two-process unprivileged race (CONS_HISTORY realloc loop + scrolling tty writes, both uid 65534) destabilized the kernel: one run ended in panic: assertion "count & TOK_COUNTMASK" failed in _lwkt_reltokref (token-state corruption) with the racer processes as the only active stressors; an earlier campaign run that mixed the same realloc churn with kernel-console kprintf flood ended in a supervisor WRITE fault inside sc_vtb_bcopy at 0xffffffff7ffffffe (wild write destination β€” the UAF branch of this race and/or DF-5015's corrupted ring; both share the fix).

Reproduction quality

  • Race executed: 274,432 realloc iterations x continuous scroll appends in 20s without visible degradation, then kernel panic shortly after (guest died between ssh commands while the racers were still running).
  • The precise interleaving (append mid-free) is nanoseconds-wide, so a panic-per-run rate of "minutes" is expected; one confirmed panic plus one same-family wild-write panic across two campaigns.
  • No privileges required beyond an open console-seat fd; no memory pressure required (unlike DF-5015).

Exploitability

Primitive: UAF write into a freshly freed 4KB-class kmem object (or a NULL-deref on other unguarded deref sites) with attacker-controlled content (the scrolled line), triggerable at will from an unprivileged process pair. This is a strong heap-grooming primitive (free-list timing under the racer's control). Not developed to uid=0 in this run.

Kernel refs

sys/dev/misc/syscons/sctermvar.h:425-450 (scroll -> append); sys/dev/misc/syscons/syscons.h:547-548 (append macro); sys/dev/misc/syscons/syscons.c:970-1006 (scioctl: vga_token only), :2298+ scstart (syscons_lock only), :1885-1923 sccnputc (syscons_lock_nonblock only); sys/dev/misc/syscons/schistory.c:93-159 (unsynchronized NULL/free of the ring); sys/dev/misc/syscons/scvidctl.c:163,397,947 (realloc call sites).

Fix

Same fix.diff as DF-5015: install/destroy of history rings under syscons_lock (the lock every append reader already holds); NULL is left installed only while (re)allocating, which the scroll path already NULL-checks. Validated: patched kernel compiles and survives the race (see DF-5015 pack fix section).

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (fix.diff, kernel #2 Sat Sep 26 02:44) compiled, installed and booted; race soak on it (353,280 CONS_HISTORY reallocs x continuous scroll appends in 20s, both uid 65534) produced no vtb-class fault and normal syscons operation remained functional (arm/flip/SCRSHOT/fill verified). The lwkt_reltokref assertion that eventually appears on longer ioctl loops is the generic DragonFly ioctl-path issue (TIOCGWINSZ-only control) and is orthogonal to this fix.

/tmp/kbuild.log KBUILD_DONE; race soak transcript in DF-5015 pack run logs
↓ fix.diffper-fix-DF-5016

Confirmed kernel references

Detail

Exploit chain

proc A (unpriv): loop ioctl(CONS_HISTORY) realloc/free of the ring; proc B (unpriv): write scrolling lines to the same vty (append fires per scrolled line, under syscons_lock); interleave kfree under vga_token with a reader mid-append -> UAF write into a freed 4KB-class kmem object with attacker-controlled line content, or wild write via garbage vtb_tail. No uid=0 chain developed.

Evidence (decisive lines)

race5016.c; panic-lwkt-token-count-race.log (campaign panic, later identified as the generic ioctl issue via the TIOCGWINSZ control - kept for the record); DF-5015/panic-scvtb_bcopy-unpriv-churn.log (the wild-write panic attributed here); VERDICT.md

PoC changes

racer written from scratch (two-process, uid 65534); discovery chain documented in VERDICT.md including the confound isolation (CONS_CLRHIST-only, TIOCGWINSZ-only controls)

Verified recommended fix

perform the install/destroy (and free) of history rings under syscons_lock, the lock every append reader already holds (fix.diff exports syscons_lock and wraps the swap/destroy sections); NULL remains installed only while reallocating, which the scroll path already NULL-checks

Verdict

The locking hole is real and the race was executed at scale (274,432 CONS_HISTORY reallocs x continuous scrolling tty writes, both uid 65534): history-ring appends (sc_term_gen_scroll -> sc_hist_save_one_line, sctermvar.h:437) run under syscons_lock only, while every ring realloc/free (sc_hist_ioctl/sc_set_text_mode -> sc_alloc_history_buffer, schistory.c:94-159) runs under vga_token only - no common lock, so kfree() of the ring can interleave with a concurrent append's reads of vtb_buffer/vtb_tail (UAF read + wild write) or with the NULL window. Kernel manifestation: a supervisor WRITE fault inside sc_vtb_bcopy at 0xffffffff7ffffffe during the unpriv churn campaign (panic #1) - the UAF branch signature; that run had NO keyboard input, excluding DF-5015's negative-tail path, and the competing generic ioctl-token assertion (identified via a TIOCGWINSZ-only control, which panics with a DIFFERENT signature: lwkt_reltokref count assertion, never a wild write) cannot produce a wild write. Confidence 'likely' rather than 'certain' because panic #1 occurred once and no backtrace was captured (trace_on_panic not armed on that boot).