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

warp_update() wrap off-by-one: star coordinate resting at exactly SCRW*SCRH is drawn and erased at vid[64000], one byte past the 64000-byte M_VGA_CG320 framebuffer (`>` must be `>=`)

Summary

The value exactly 64000 misses the > wrap - drawn+erased one byte past the logical framebuffer; 706 hits at exactly 64000 in a 100k-update model, never beyond. Benign on vga.c (64KB window, offscreen VRAM); a real OOB MMIO only against a <=64000-byte window. One-character >= fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5196 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 3.8 KB ↓ raw
warp_model.py β€” 1.8 KB view raw
model_output.txt β€” 383 B view raw
verdict.json β€” 3.1 KB view raw
manifest.json β€” 1.2 KB view raw

DF-5196 β€” warp_saver star wrap off-by-one: vid[SCRW*SCRH] written (1 byte past the 64000-byte mode-13h framebuffer)

File: sys/dev/misc/syscons/warp/warp_saver.c:70-77

What

warp_update() advances each star by i (1..4) and wraps with

star[k] += i;
if (star[k] > SCRW*SCRH)     /* warp_saver.c:74 β€” `>`, must be `>=` */
    star[k] -= SCRW*SCRH;
vid[star[k]] = i;            /* warp_saver.c:76 */

star[k] is initialized to krandom() % (SCRW*SCRH) ∈ [0,63999] (warp_saver.c:121) and the arithmetic keeps it in [0,64000]: every value greater than 64000 is wrapped, but the value exactly equal to 64000 is not. A star resting at 63996..63999 (depending on speed class i=1..4) advances to exactly 64000 and is drawn there (vid[64000] = i); on the next update it is erased there (vid[star[k]] = 0, warp_saver.c:72) before wrapping normally.

Proof (deterministic, host-side)

warp_model.py in this directory is a 1:1 arithmetic model of the loop (same constants, same statement order). Over 100,000 updates with the same init distribution:

max index written    = 64000  (framebuffer = 64000 bytes)
writes at vid[64000] = 706    (byte past framebuffer)
writes >= 64000      = 706    <- never beyond 64000

Also proves the companion invariant: k terminates at exactly STARS-1 (120+60+30+15 = 225 = STARS) β€” star[] itself is never indexed out of bounds, and unlike the snake saver (DF-5194) there is no runaway: 64000 is the maximum index ever touched.

Why Info (not higher)

On the only in-tree adapter that serves M_VGA_CG320 (vga.c), a successful mode switch points va_window at the 64 KB A0000 aperture (va_window_size = GRAPHICS_BUF_SIZE = 0x10000, sys/dev/video/fb/vga.c:581, sys/dev/video/fb/vgareg.h:43-44). Offset 64000 (0xFA00) therefore lands inside the mapped VGA window, in offscreen video RAM β€” no kernel memory is touched, nothing is displayed, no fault occurs. The defect is a latent bounds bug + hardening opportunity (any future adapter exposing M_VGA_CG320 with a 64000-byte or tightly-sized window would take a real OOB MMIO write; the value written is attacker-uncontrolled but loop-chosen: 0..4).

Fix

One character: > β†’ >= (keeps the invariant star[k] ∈ [0,63999]; 63999+4=64003 wraps to 3 exactly as before, 64000 now wraps to 0).

Reproduce the model (host, no guest needed)

python3 warp_model.py        # see model_output.txt for recorded output

Phase V (guest PoC) skipped per contract: Info hardening finding, no demonstrable kernel-memory impact on any in-tree adapter.

VERDICT.md
↓ download raw

DF-5196 β€” VERDICT

Status: untested (Phase V skipped β€” Info hardening finding, no memory-safety impact on any in-tree adapter). Impact: none. Confidence: certain (defect proven by exhaustive arithmetic model).

The defect is real and proven

warp_update() (sys/dev/misc/syscons/warp/warp_saver.c:70-77) wraps star coordinates with if (star[k] > SCRW*SCRH) (line 74). The loop invariant from init (krandom() % (SCRW*SCRH) ∈ [0,63999], line 121) plus += i (i ∈ [1,4]) means the coordinate after increment lies in [1,64003]. The wrap subtracts 64000 for values >64000 but not for the value exactly 64000, so vid[star[k]] = i (line 76) writes at vid[64000] β€” one byte past the 64000-byte mode-13h (M_VGA_CG320) framebuffer β€” and the erase vid[star[k]] = 0 (line 72) hits the same slot on the following update.

warp_model.py (this directory) replays the exact loop 100,000 times: max index written = 64000, hits at 64000 = 706, indices >64000 = 0. Recorded in model_output.txt. This also proves the two companion bounds the prompt's hunt asked about: k terminates at exactly STARS-1 (120+60+30+15 = 225 = STARS, so star[] is never indexed OOB), and there is no runaway of the DF-5194 (snake) shape β€” 64000 is the maximum index ever touched.

Why it is not exploitable on in-tree code

The only driver that implements M_VGA_CG320 is vga.c; after a successful set_video_mode it sets va_window = 0xA0000 with va_window_size = GRAPHICS_BUF_SIZE = 0x10000 (64 KB) β€” sys/dev/video/fb/vga.c:581 (update_adapter_info) and sys/dev/video/fb/vgareg.h:43-44. Offset 64000 (0xFA00) is inside that 64 KB aperture: the byte lands in offscreen video RAM (unused in mode 13h), no kernel or host memory is affected, nothing is displayed. The (vesa) layer, when loaded, routes mode 13h to the same A0000/64KB window. KMS/fb consoles never reach warp_init (their adapters fail get_mode_info(M_VGA_CG320) β†’ ENODEV at module load, warp_saver.c:114-117).

Severity is therefore Info: hardening / latent bounds defect. It becomes a real OOB MMIO write only for a hypothetical future adapter that advertises M_VGA_CG320 with a ≀64000-byte window.

Why Phase V was skipped

Contract: Low/Info hardening findings are explicitly exempt from guest PoC. Additionally there is no observable symptom to reproduce β€” the bad byte is invisible by construction (offscreen VRAM) β€” and the guest does not ship saver_warp.ko in /boot/kernel (module is built from sys/dev/misc/syscons/warp/Makefile on demand; loading requires root kldload, after which the loop runs on every saver tick). Guest was inspected read-only (uname, dmesg: sc0: VGA <16 virtual consoles>, vga0: <Generic ISA VGA> at iomem 0xa0000-0xbffff β€” the code path is live on this guest's console hardware); no state was changed, guest_dirty=0.

Fix

--- a/sys/dev/misc/syscons/warp/warp_saver.c
+++ b/sys/dev/misc/syscons/warp/warp_saver.c
@@ -71,7 +71,7 @@ warp_update(void)
        vid[star[k]] = 0;
        star[k] += i;
-       if (star[k] > SCRW*SCRH)
+       if (star[k] >= SCRW*SCRH)
        star[k] -= SCRW*SCRH;
        vid[star[k]] = i;
        }

Invariant after fix: star[k] ∈ [0,63999] strictly; 63996+4=64000 now wraps to 0; all other wrap arithmetic unchanged. No fix validation build was performed (no reproduced misbehavior to differentiate; the change is provably semantics-preserving for every coordinate except exactly 64000).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix validation not applicable: no misbehavior reproduces on in-tree adapters (the stray byte is invisible offscreen VRAM); the one-character change is provably semantics-preserving except at exactly 64000, which becomes 0.

See VERDICT.md 'Fix' section for the diff and invariant argument.
per-fix-DF-5196

Confirmed kernel references

Detail

Evidence (decisive lines)

warp_model.py + model_output.txt (max index 64000, 706 writes at vid[64000], 0 beyond); source: warp_saver.c:70-77 (loop), :74 (the `>`), :121 (init bound); window size proof: sys/dev/video/fb/vga.c:581, sys/dev/video/fb/vgareg.h:43-44; guest reachability (read-only): dmesg sc0+vga0 iomem 0xa0000-0xbffff, saver_waver.ko absent from /boot/kernel (built from sys/dev/misc/syscons/warp/Makefile, kldload is root-only)

PoC changes

n/a β€” no kernel PoC built (Info hardening; Phase V skipped per contract). Host-side deterministic arithmetic model included instead.

Verified recommended fix

warp_saver.c:74: change star[k] > SCRW*SCRH to star[k] >= SCRW*SCRH (one char; preserves star[k] in [0,63999]).

Verdict

Off-by-one wrap in warp_update(): if (star[k] > SCRW*SCRH) (warp_saver.c:74) fails to wrap the coordinate that lands on exactly SCRW*SCRH, so a star is drawn and later erased at vid[64000] β€” one byte past the 64000-byte M_VGA_CG320 framebuffer. Proven by a 100,000-update arithmetic model (warp_model.py: 706 hits at vid[64000], max index 64000, never beyond; k terminates at exactly STARS-1 so star[] is never indexed OOB and there is no DF-5194-style runaway). Impact is none on in-tree code: the only M_VGA_CG320 provider (vga.c) exposes a 64KB A0000 window (va_window_size=0x10000, vga.c:581 + vgareg.h:43-44), so the byte lands in offscreen VRAM. Info hardening finding β€” guest PoC skipped per contract; the byte is invisible by construction.