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)
PoC verification
Evidence pack
findings/poc/DF-5016 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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()holdssyscons_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_reltokrefinlwkt_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_bcopyat 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 undersyscons_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).
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
fixedPatched 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
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).
No comments yet.