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

fire_saver(): set_video_mode() return value ignored β€” on failure vid aliases the stale 32KB text window and the 64000-byte blit (repeated every 25 Hz tick) overruns it into 0xC0000+ (DF-5114/DF-5197 twin)

Summary

The third and last family member: the unchecked mode switch + vid capture + 64000-byte memcpy re-executed per tick overruns the 32KB text aperture on failure. Latent (the load-time gate already validated what vga_set_mode re-checks); live-verified the working path on guest (2 blank cycles, clean unload). Check-and-return-ENODEV fix (syscons already handles failed savers).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5198 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 6.3 KB ↓ raw
trigger.sh β€” 789 B view raw
run.sh β€” 173 B view raw
build.sh β€” 156 B view raw
build.log β€” 267 B view raw
run.log β€” 4.3 KB view raw
env.txt β€” 1.1 KB view raw
fix.diff β€” 752 B view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 4.6 KB view raw

DF-5198 β€” fire_saver(): set_video_mode() return ignored (DF-5114/DF-5197 twin)

File: sys/dev/misc/syscons/fire/fire_saver.c (pass 2, GLM 5.3) Severity: Info Β· bucket: memcorrupt Β· confidence: certain (shape) / latent (trigger)

What

fire_saver() ignores the return value of set_video_mode(adp, M_VGA_CG320) (fire_saver.c:69) and then, unconditionally, every saver tick:

vid = (u_char *) adp->va_window;            /* fire_saver.c:85 */
...
memcpy(vid, buf, X_SIZE * Y_SIZE);          /* fire_saver.c:104 β€” 64000 bytes */

If the mode switch failed, adp->va_window still aliases the 32 KB text-mode window (0xB8000), and the 64000-byte memcpy (plus every fade-tick write path feeding it) overruns that window by ~32 KB into 0xC0000+ (video BIOS shadow / adapter MMIO β€” not kernel RAM). This is the exact DF-5114 (dragon_saver.c) / DF-5197 (warp_saver.c) twin, completing the family sweep in the last syscons saver file.

Why it is latent on the stock kernel

fire_initialise() (fire_saver.c:118) gates module registration on get_mode_info(adp, M_VGA_CG320, &info) succeeding β€” and vga_set_mode() (vga.c:997) can only fail via:

  • prologue(adp, V_ADP_MODECHANGE, ENODEV) (vga.c:1003; macro at vga.c:414): requires !vga_init_done or the adapter flag V_ADP_MODECHANGE clear β€” the flag is set once at vga.c:684 and never cleared at runtime (only read at vga.c:888/1003/2007);
  • vga_get_info(adp, mode, &info) β†’ EINVAL (vga.c:1005-1006) when get_mode_param(mode) == NULL (vga.c:888-893) β€” the same check fire_initialise already passed on the same adapter (splash_adp is set from sc->adp at syscons attach, syscons.c splash_init call).

So on a stock single-adapter VGA system the failure branch is unreachable: set_video_mode deterministically agrees with the earlier get_mode_info. The gap is defensive (a future adapter/driver whose set_mode can fail asynchronously, or a multi-adapter splash_adp β‰  sc->adp setup), identical in kind to the two accepted twins.

Reproduce (live benign-path exercise)

vm.sh run_root 'kldload fire_saver && echo KLDLOAD_OK; \
  vidcontrol -t 1 < /dev/ttyv0 && echo ARMED; sleep 5; \
  vidcontrol -t 600 < /dev/ttyv0 && echo REARMED_OK; sleep 2; \
  vidcontrol -t 3 < /dev/ttyv0; sleep 8; vidcontrol -t 600 < /dev/ttyv0; \
  sleep 2; echo SECOND_CYCLE_DONE'

Expected on stock kernel: module loads (get_mode_info passes), the fire renders for two blank cycles (~13 s β‰ˆ 500 fade+blit ticks), unblank restores the text console, no panic. That is what was observed (run.log). The out-of-window write itself cannot be triggered on this guest without modifying vga.c β€” hence not_reproduced/latent, matching the DB treatment of DF-5114 and DF-5197 (untested, "latent out-of-window video MMIO writes (not kernel RAM)").

Artifacts

  • run.log β€” full untrimmed output of both guest sessions
  • env.txt β€” guest uname, dmesg (sc0/vga0), kldstat, module list
  • trigger.sh / run.sh / build.sh β€” exact commands
  • fix.diff β€” guard the mode switch, bail out with ENODEV on failure
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-5198 VERDICT β€” fire_saver() unchecked set_video_mode() (DF-5114/DF-5197 twin)

Status: not_reproduced (latent) Β· impact: none on stock kernel Β· Info/memcorrupt

The defect (certain)

fire_saver() (sys/dev/misc/syscons/fire/fire_saver.c:60) on the first blank episode runs, inside if (blanked <= 0):

  • fire_saver.c:69 set_video_mode(adp, M_VGA_CG320); β€” return value ignored
  • fire_saver.c:82 load_palette(adp, fire_pal);
  • fire_saver.c:85 vid = (u_char *) adp->va_window;
  • fire_saver.c:104 memcpy(vid, buf, X_SIZE * Y_SIZE); β€” 64000 bytes, every tick (scrn_timer at 25 Hz β†’ syscons.c:2229 (*current_saver)(sc, TRUE) β†’ scsplash_saver β†’ splash(adp, TRUE)).

Caller contract: scsplash_saver() first calls set_scrn_saver_mode(scp, -1, NULL, 0) (syscons.c:2559) β€” mode -1 deliberately does not change the video mode (syscons.c:2646-2648); the mode switch is the saver's job. On unblank, syscons restores via restore_scrn_saver_mode(scp, TRUE) β†’ set_mode() + load_palette(sc->palette) (syscons.c:2691-2693), and fire_saver(adp, FALSE) only resets blanked (fire_saver.c:105-107). That unblank half is correct and was verified live (text console came back and stayed functional after two blank cycles).

If set_video_mode() fails, adp->va_window still points at the 32 KB text-mode aperture (0xB8000), so the 64000-byte memcpy writes ~32 KB past the window into 0xC0000+ (video-BIOS shadow / adapter MMIO region β€” not kernel RAM). Family impact note (from DF-5114): "latent out-of-window video MMIO writes (not kernel RAM)". Same here; fire additionally re-blits the full 64000 bytes on every tick, so the overrun would be repeated at 25 Hz.

Why the failure branch is unreachable on the stock kernel (traced)

vga_set_mode() (sys/dev/video/fb/vga.c:997) has exactly two failure paths:

  1. prologue(adp, V_ADP_MODECHANGE, ENODEV) β€” vga.c:1003, macro vga.c:414: if (!vga_init_done || !((adp)->va_flags & (flag))) return (err). V_ADP_MODECHANGE is set once during adapter init (vga.c:684) and never cleared at runtime (only read, vga.c:888/1003/2007). vga_init_done is only ever set at configure time.
  2. if (vga_get_info(adp, mode, &info)) return EINVAL; β€” vga.c:1005-1006; vga_get_info (vga.c:880) rejects the mode iff get_mode_param(mode) == NULL when V_ADP_MODECHANGE is set (vga.c:888-893).

fire_initialise() (fire_saver.c:112-125) already required get_mode_info(adp, M_VGA_CG320, &info) == 0 at module load on the same adapter (splash_adp is installed from syscons' own sc->adp via splash_init(sc->adp, scsplash_callback, sc) at syscons attach; splash_register β†’ splash_test β†’ fire_initialise(splash_adp), splash.c:108-121, 83-87). Same adapter flag, same param table β‡’ deterministic agreement: after the load-time gate passes, the blank-time mode set cannot fail on a stock single-adapter system. Remaining theoretical triggers β€” an adapter whose set_mode can fail asynchronously, or a multi-console setup where splash_adp diverges from the sc->adp the saver actually runs on β€” have no in-tree instantiation (the dragon/warp twins carry the identical caveat).

What was run on the guest (live benign path)

DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, sc0 VGA + vga0 (ISA), QEMU/KVM.

  1. kldload fire_saver β†’ KLDLOAD_OK β€” fire_initialise's get_mode_info gate passed and the module registered as the LKM saver.
  2. vidcontrol -t 1 < /dev/ttyv0 β†’ ARMED; slept 5 s β†’ the saver dispatched (~125 fire ticks: bottom-row seed, fade loop, 64000-byte blit each).
  3. vidcontrol -t 600 (CONS_BLANKTIME touches the saver β†’ sc_touch_scrn_saver β†’ next timer tick stops it, syscons.c:2192-2207) β†’ REARMED_OK, STILL_ALIVE β€” text mode restored cleanly.
  4. Second cycle: -t 3, 8 s of fire, disarm β†’ SECOND_CYCLE_DONE.
  5. kldunload fire_saver β†’ UNLOADED (unload path: splash_unregister β†’ splash_term β†’ scsplash_callback(SPLASH_TERM) β†’ remove_scrn_saver β†’ stop_scrn_saver; exercised without the DF-5195 deadlock because the saver was not blanked at unload time).
  6. Serial log (vm.sh log): no panic, no console wedge.

Result: the code path is live and behaves when set_video_mode succeeds; the bad branch cannot be made to execute on a stock kernel. Honest classification: not_reproduced / impact none / latent hardening β€” consistent with the orchestrator's DB rows for DF-5114 and DF-5197 (untested, impact "latent out-of-window video MMIO writes (not kernel RAM)" / none).

No uid=0 chain exists for this class: the primitive is MMIO-window-relative, not kernel-memory-relative, and arming it at all requires root (kldload). Fix validation (baseline/patched kernel rebuild) is not applicable to a non-reproducing latent finding; fix.diff is authored against the read-only sys/ tree for upstream.

Everything else checked in fire_saver.c (pass 2 negative result)

  • Fade loop writes bounded: max x + y*X_SIZE = 319+199*320 = 63999 < 64320 (fire_saver.c:94). Left-edge read (x-1)+(y+1)*320 β‰₯ 319, in bounds. Right/bottom read (x+1)+200*320 = 64320 = the known DF-2193 one-past-end read β€” not re-reported.
  • Bottom-row seed: indices 64000..64319 in bounds; krandom() % 160 + 96 ∈ [96,255] fits u_char (krandom is libkern random.c:50).
  • Palette: indices ≀ 767 within fire_pal[768]; red/green wrap at entry 255 (256β†’0) is cosmetic only; load_palette passes a 768-byte table.
  • memcpy is exactly 64000 bytes = mode-13h visible surface (64 KB+ aperture) β€” no warp-DF-5196-style blit off-by-one.
  • Unblank/mode-restore ownership is correct (syscons restores, fire only resets blanked); module unload path exercised live.
  • Missing crit_enter() around the first-blank block (dragon has one, dragon_saver.c:191-196) is harmless: the saver is dispatched only from the single scrn_timer callout under syscons_lock (syscons.c:2182-2232).
  • blanked <= 0 / blanked = 0 re-init logic is idempotent; no double-entry, no state carried across module reloads (module statics).

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against the read-only sys/ tree and apply-verified (patch --dry-run APPLY_OK) but not kernel-build-validated: the finding is latent (not_reproduced) so there is no baseline bad behavior for a patched-kernel A/B comparison. Fix validation is mandatory only for findings that reproduced.

findings/poc/DF-5198/fix.diff (unified diff, applies to sys/dev/misc/syscons/fire/fire_saver.c:69); apply check shown in session log
↓ fix.diffper-fix-DF-5198

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (KLDLOAD_OK/ARMED/REARMED_OK/STILL_ALIVE/SECOND_CYCLE_DONE/UNLOADED/CLEAN, serial log with no panic); VERDICT.md (full path trace: syscons.c:2182-2232/2546-2580/2632-2668/2706-2720, splash.c:101-156, vga.c:414/684/888/1003/1005); fix.diff (applies cleanly, patch --dry-run APPLY_OK)

PoC changes

No PoC source needed fixing: the finding is verified by exercising the stock module live (kldload + vidcontrol blank-timer cycles + unload) rather than a compiled trigger. One command adjustment: 'vidcontrol -t 0' is rejected by vidcontrol's parser (positive numbers only) β€” unblanking uses 'vidcontrol -t 600' (CONS_BLANKTIME touches the saver; the next scrn_timer tick stops it and restores text mode).

Verified recommended fix

Check set_video_mode()'s return in fire_saver() and bail out with ENODEV on failure so syscons marks the saver failed and restores the saved mode (fix.diff).

Verdict

fire_saver() ignores the return of set_video_mode(adp, M_VGA_CG320) (fire_saver.c:69) and then unconditionally caches vid=adp->va_window (fire_saver.c:85) and memcpy's 64000 bytes to it on every 25 Hz tick (fire_saver.c:104) β€” the exact DF-5114 (dragon)/DF-5197 (warp) twin, completing the syscons-saver family. On a mode-set failure vid would alias the 32KB text aperture (0xB8000) and the blit would overrun it by ~32KB into 0xC0000+ (video-BIOS shadow / adapter MMIO, not kernel RAM), repeatedly at 25 Hz. The branch is unreachable on a stock kernel: fire_initialise (fire_saver.c:118) gates registration on get_mode_info(M_VGA_CG320) on the same adapter (splash_adp = sc->adp), and vga_set_mode can only fail via prologue(!vga_init_done | !V_ADP_MODECHANGE β€” set once at vga.c:684, never cleared) or vga_get_info(get_mode_param==NULL) (vga.c:1003-1006, 888-893) β€” the same checks the load-time gate already passed. Live verification on the QEMU guest exercised the full benign path: kldload (gate passed), two blank cycles (~13s of fire ticks), clean unblank/text-mode restore (syscons restore_scrn_saver_mode), clean module unload; no panic. Latent hardening defect, consistent with the untested DB treatment of both twins; no uid=0 route exists for MMIO-window-relative writes, and arming the saver at all requires root kldload.