vga_save_font()/vga_load_font() accept unvalidated glyph index and count β latent OOB-write API one caller away from a real overflow (user-influenced mouse_char already flows in)
Summary
Both font functions validate only page; ch, count and their product with fontsize feed ch*32 straight into bcopy_toio/bcopy_fromio. All current callers are kernel-internal and clamped (syscons ch=0/count=256; the user-settable mouse_char pre-clamped to <=251), so no trigger exists today; one future caller forwarding an unchecked value gets an int-overflowed ch*32 sign-extended into a wild KVA write. Guest hostile-input run proves every currently user-reachable vga.c sink is bounds-checked (modes/palette rejected, unpriv EPERM, /dev/vga absent). Glyph-range validation fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4158 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 3.0 KB | β raw | |
| vgafuzz.c | β | 5.0 KB | view raw | |
| build.sh | β | 99 B | view raw | |
| run.sh | β | 144 B | view raw | |
| build.log | β | 69 B | view raw | |
| run.log | β | 2.6 KB | view raw | |
| env.txt | β | 604 B | view raw | |
| fix.diff | β | 968 B | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-4158 β vga_save_font/vga_load_font have no internal bounds on (ch, count, fontsize) + guest proof the user-reachable vga.c surface is bounds-checked
File: sys/dev/video/fb/vga.c (pass 2, GLM 5.3)
What (the hardening gap)
vga_save_font() / vga_load_font() (vga.c:1189-1233, 1241-1286)
validate page (0..7) but perform zero validation of the glyph index
ch, glyph count, or their combination with fontsize:
if (fontsize == 32) {
bcopy_toio(data, (uintptr_t)segment + ch*32, fontsize*count);
} else {
for (c = ch; count > 0; ++c, --count) {
bcopy_toio(data, (uintptr_t)segment + c*32, fontsize);
Every caller today is kernel-internal and clamped (syscons: ch=0,count=256;
scmouse.c:888 passes the user-influenced sc->mouse_char as ch, which
MOUSE_MOUSECHAR pre-clamps to β€ (unsigned char)-1-4, scmouse.c:881), so
this is currently a latent API hazard: one future caller that forwards an
unchecked ch/count gets an OOB write through ch*32 (int overflow β
negative β sign-extended uintptr_t) into arbitrary KVA, or a OOB read of
the data source. Filed as Info/defense-in-depth.
Guest verification (negative-space proof for the rest of the file)
vgafuzz.c exercises every user-reachable vga.c sink reachable through
syscons (/dev/ttyvN β sc_vid_ioctl β vga_dev_ioctl/fb_commonioctl)
with hostile scalars. Results on the live guest (run.log):
- FBIO_SETMODE with {-2^31, -1, 29, 64, 256, 65535, 2^30, 2^31-1} β all
rejected (mode walk via
get_mode_param()β NULL β EINVAL,vga.c:533,vga.c:893). - FBIO_MODEINFO with vi_mode {-1, 29, INT_MAX} β rejected.
- FBIO_GET/SETPALETTE, FBIOGET/FBIOPUTCMAP with index=-1/300/INT_MAX,
count=-1/INT_MAX/2^30, index+count>256 β all rejected by the
get_palette/set_palettegate (vga.c:1853, 1885). - FBIO_SETDISPSTART INT_MAX/INT_MIN and FBIO_SETLINEWIDTH INT_MAX/-1/0 are
accepted but land only in CRTC register bytes and the
va_line_widthint (vga.c:750-752,786-800) β display corruption by the console user on themselves, no kernel-memory sink (checked all consumers:planar_fill/genfbreaduse window_size/maxsize, not line_width multiplies that reach memory ops unclamped). - Guest stayed up after the full battery (dmesg tail clean).
- Permission model: /dev/ttyv0..N are 0600 root:wheel (console-login users
get the tty chowned β the historical syscons model);
/dev/vga0 does not exist (FB_INSTALL_CDEV is not compiled into
X86_64_GENERIC; when compiled it is 02660 root:wheel,
vga_isa.c:156). - State save/restore is NOT user-reachable at all: no FBIO_*STATE ioctls
exist;
vga_save_state/load_stateare reached only fromisavga_suspend/resumewith kernel-sized buffers (vga_isa.c:190-207), andvga_save_stateitself enforcessize < sizeof(adp_state_t)β EINVAL (vga.c:1432).
Fix
fix.diff: validate ch/count against the 512-glyph (0x4000/32) font
page in both functions. Applies cleanly with git apply.
DF-4158 VERDICT
status: not_reproduced (by design β no user-reachable trigger exists today) | impact: none (hardening)
Was it reproduced?
Attempted on the live guest and no impact could be produced, because the missing bounds check is not reachable with attacker-chosen values on this kernel:
vga_load_font/vga_save_font(vga.c:1189,1241) have no FBIO ioctl anywhere in the tree (grep across sys/: no FBIOPUTFONT handler;fb.c:741-765returns ENODEV for font/palette-adjacent unknowns via the default arm).- All call sites are kernel-internal with clamped arguments:
-
syscons.c:1653/1691/1727/3403-3417/4201-4207,scvidctl.c:674-678: ch=0, count=256, fontsizeβ{8,14,16} β max byte offset 8192 = the 0x4000-byte... (256 glyphs Γ 32 B = 0x2000) well inside the font page. -scvgarndr.c:380: ch=sc->mouse_char, count=4, fontsize=32 β max offset (mouse_char+3)32+128 β€ 25132+128 = 8160 < 0x4000.mouse_charis user-settable via MOUSE_MOUSECHAR but pre-clamped to β€ (unsigned char)-1-4 = 251 (scmouse.c:881). -scmouse.c:888: same clamped ch, count=4. vgafuzz(run.log) additionally proves the actually reachable vga.c sinks reject or safely consume every hostile scalar: modes, palette index/count, dispstart, linewidth. FBIO_SETLINEWIDTH INT_MAX is accepted but lands only in one CRTC byte + theva_line_widthint β I traced every consumer ofva_line_width(set_display_start arithmetic β CRTC bytes; planar/packed_fill length β window_size-bounded loops; genfbread β window_size/modulo) and none turns it into an unclamped memory op. FBIO_SETDISPSTART likewise only programs CRTC/ATC bytes.
Guest survived the whole battery (dmesg tail clean, subsequent ssh runs
fine). /dev/vga0 absent (FB_INSTALL_CDEV not compiled; would be 02660
root:wheel when compiled β vga_isa.c:156). /dev/ttyv* 0600 root:wheel;
unprivileged maxx gets EPERM on open (run.log); the historical model is
that a vty is chowned to the user who logs in on it.
Why file it at all
This is the classic shape that later becomes a real overflow the day one
new caller forwards an unchecked value (user-influenced scalars like
mouse_char already flow into this API). One-line clamp per function makes
the invariant local. Info severity, no demonstrated impact β consistent
with not_reproduced/impact none.
Fix validation
Not meaningful (nothing to observe pre/post on this kernel). fix.diff
applies cleanly (git apply --check); it validates
ch β₯ 0 && count β₯ 0 && ch+count β€ 512 (0x4000-byte page / 32-byte glyph)
in both functions.
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_testableNothing observable to diff on this kernel (no userland path can reach the font functions with unchecked values), so baseline/patched comparison is not meaningful. fix.diff applies cleanly (git apply --check) and adds the ch/count bounds gate to both functions.
findings/poc/DF-4158/fix.diff (git apply --check clean, two hunks: save_font + load_font)
Confirmed kernel references
Detail
Evidence (decisive lines)
vga.c:1189-1233+1241-1286 (missing ch/count validation), scmouse.c:881-889 (user-influenced mouse_char flows in, clamped upstream), run.log (vgafuzz results + unpriv EPERM + /dev/vga absent), build.log
PoC changes
authored fresh exerciser (no seed); expected-accept labels adjusted after observing FBIO_SETLINEWIDTH INT_MAX lands only in hw regs
Verified recommended fix
Validate ch/count against the 512-glyph (0x4000/32-byte) font page in vga_save_font and vga_load_font
Verdict
vga_save_font/vga_load_font perform no internal validation of the glyph index/count, making them a latent OOB-write/OOB-read API (ch*32 int overflow -> wild KVA via bcopy_toio/bcopy_fromio) that one unshackled future caller could trigger; today every caller is kernel-internal and clamped (mouse_char pre-clamped to <=251 by MOUSE_MOUSECHAR), so no user-reachable trigger exists and none could be produced on the live guest. The accompanying vgafuzz run on the guest (root on /dev/ttyv0) proves the entire user-reachable vga.c ioctl surface rejects or safely consumes hostile scalars: all hostile FBIO_SETMODE/FBIO_MODEINFO values rejected (get_mode_param NULL gate), all hostile palette index/count combinations rejected (base/count gate), FBIO_SETDISPSTART/FBIO_SETLINEWIDTH extremes accepted but landing only in CRTC register bytes and the va_line_width int with no unclamped memory-op consumer. Guest stayed up throughout.
No comments yet.