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)
PoC verification
Evidence pack
findings/poc/DF-5198 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_doneor the adapter flagV_ADP_MODECHANGEclear β 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) whenget_mode_param(mode) == NULL(vga.c:888-893) β the same checkfire_initialisealready 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 sessionsenv.txtβ guest uname, dmesg (sc0/vga0), kldstat, module listtrigger.sh/run.sh/build.shβ exact commandsfix.diffβ guard the mode switch, bail out with ENODEV on failureVERDICT.md,manifest.json,verdict.json
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:
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_MODECHANGEis set once during adapter init (vga.c:684) and never cleared at runtime (only read, vga.c:888/1003/2007).vga_init_doneis only ever set at configure time.if (vga_get_info(adp, mode, &info)) return EINVAL;β vga.c:1005-1006;vga_get_info(vga.c:880) rejects the mode iffget_mode_param(mode) == NULLwhenV_ADP_MODECHANGEis 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.
kldload fire_saverβ KLDLOAD_OK βfire_initialise'sget_mode_infogate passed and the module registered as the LKM saver.vidcontrol -t 1 < /dev/ttyv0β ARMED; slept 5 s β the saver dispatched (~125 fire ticks: bottom-row seed, fade loop, 64000-byte blit each).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.- Second cycle:
-t 3, 8 s of fire, disarm β SECOND_CYCLE_DONE. 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).- 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_palettepasses a 768-byte table. memcpyis 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 singlescrn_timercallout undersyscons_lock(syscons.c:2182-2232). blanked <= 0/blanked = 0re-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_testablefix.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
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.
No comments yet.