vga_probe_unit() returns success without initializing the caller's video_adapter_t β uninit kernel-stack read programmed into world-readable devinfo resource ranges (structurally unreachable today: both sources gated on one 'optional vga')
Summary
vga_probe_unit returns 0 while leaving the caller's buffer untouched when no video switch is registered; the caller then programs adp.va_io_base/va_io_size/va_mem_base/va_mem_size - four stale kernel-stack ints - into the resource list, world-readable via devinfo -rv (channel confirmed live). Unreachable in every shipped config (vga_isa.c and vga.c gate on the same 'optional vga'), one Makefile split away from a kernel-stack info leak + garbage rman claims. bzero+ENXIO one-liners fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5591 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| trigger.sh | β | 737 B | view raw | |
| build.sh | β | 91 B | view raw | |
| run.sh | β | 89 B | view raw | |
| run.log | β | 663 B | view raw | |
| env.txt | β | 574 B | view raw | |
| fix.diff | β | 533 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-5591 β vga_probe_unit() "succeeds" without filling the caller's buffer:
one file-split away from a kernel-stack read programmed into the resource list
What this is
Evidence pack for the pass-2 finding on sys/bus/isa/vga_isa.c (hardening /
Info):
isavga_probe() declares video_adapter_t adp; uninitialized on its
stack (sys/bus/isa/vga_isa.c:108) and passes &adp to
vga_probe_unit(). vga_probe_unit() returns 0 ("success") without
touching buf when vid_get_switch(VGA_DRIVER_NAME) is NULL
(sys/dev/video/fb/vga.c:76-78). Back in isavga_probe, error == 0
then programs adp.va_io_base, adp.va_io_size, adp.va_mem_base,
adp.va_mem_size β four ints of uninitialized kernel stack β into
the device resource list via bus_set_resource() (vga_isa.c:118-121).
Those values are user-visible: devinfo -rv dumps per-device resource
ranges to any unprivileged user (verified live on the audit rig).
Why this is filed Info and not higher
The path is structurally unreachable in every shipped configuration:
vga_isa.c and vga.c are both gated on the same optional vga kernel
option (sys/platform/pc64/conf/files:72 and :199), so whenever the
isavga driver is present, the VIDEO_DRIVER(vga, ...) linker-set entry
(vga.c:244) is in the same kernel image and vid_get_switch("vga")
cannot return NULL. Live proof: the guest's vga0 windows are exactly the
statically initialized values (IO_VGA=0x3c0/32 and
EGA_BUF_BASE=0xa0000/EGA_BUF_SIZE=0x20000, vga.c:175-179), i.e. the
initialized path ran, not garbage.
The trap is one Makefile/file-split away from activation (e.g. moving vga.c into its own KLD, or any future glue file reusing vga_probe_unit), at which point it becomes a kernel-stack info leak into world-readable devinfo output plus garbage rman claims. Hence hardening, confidence certain about the code shape, no demonstrated impact today.
Reproduce
Nothing to compile:
./run.sh # from the repo root (uses dfbsd-qemu/vm.sh)
Collects the live negative evidence: vga0's exact window values, the unprivileged readability of devinfo, and (host-side) the config-file gate that makes the NULL path unreachable.
Expected output
vga0: <Generic ISA VGA> at port 0x3c0-0x3df iomem 0xa0000-0xbffff on isa1
0x3c0/32 and 0xa0000/0x20000 are precisely IO_VGA, 32, EGA_BUF_BASE,
EGA_BUF_SIZE from the static initializer at sys/dev/video/fb/vga.c:175-179
β proving the buffer was written by vga_probe()'s bcopy(adp, buf, ...)
(vga.c:82), not left uninitialized.
DF-5591 VERDICT β not_reproduced (by design: path structurally unreachable; negative result is the deliverable)
Finding (hardening/Info): isavga_probe() reads adp.va_io_base/
va_io_size/va_mem_base/va_mem_size from a stack buffer that
vga_probe_unit() may have left untouched, because that helper returns 0
("success") when no video switch is registered.
Why it does NOT fire on any shipped kernel β structural proof
vga_probe_unit()(sys/dev/video/fb/vga.c:70-84):sw = vid_get_switch(VGA_DRIVER_NAME); if (sw == NULL) return 0;β returns success without writing*buf(vga.c:76-78).vid_get_switch()(sys/dev/video/fb/fb.c:203-220) walks thevideodriver_setlinker set (SET_FOREACH, fb.c:210). The entry is emitted byVIDEO_DRIVER(vga, vgavidsw, vga_configure)atvga.c:244(DATA_SET(videodriver_set, ...), fbreg.h:148-153).sys/platform/pc64/conf/files:72(dev/video/fb/vga.c optional vga) and:199(bus/isa/vga_isa.c optional vga) gate both files on the samevgaoption β one kernel module / one static image. There is no configuration in whichisavga_probe()executes withoutVIDEO_DRIVER(vga, ...)being linked (and thereforevid_get_switch("vga") != NULL).- Live confirmation (audit rig, Bochs VGA, GENERIC):
dmesg: vga0:
at port 0x3c0-0x3df iomem 0xa0000-0xbffff on isa1 static: va_io_base=IO_VGA(0x3C0, sys/bus/isa/isareg.h:124), va_io_size=32, va_mem window 0xa0000/0x20000 (vgareg.h:40-41) β from the static biosadapterinitializer at vga.c:175-179. The observed ranges match the static initializer exactly; thebcopy(adp, buf, sizeof(*buf))at vga.c:82 (fed by*adpp = &biosadapter, vga.c:850) is what landed inadp, i.e. the initialized path ran. Uninitialized stack would not coincidentally equal 0x3c0/32/0xa0000/0x20000 (note the initializer'sIO_VGA, 32pair β devinfo shows 0x3c0-0x3df = base+32-1).
What the impact would be if the path ever activated
- Four
ints (sys/sys/fbio.h:298-301) of stale kernel stack programmed as device resource ranges (bus_set_resource, vga_isa.c:118-121). - Leaked to userspace:
devinfo -rvprints per-device I/O port / I/O memory ranges and works for unprivileged users (verified live β see run.log,run_usersection). - Garbage rman claims activated by
isavga_attach()(RF_ACTIVE, vga_isa.c:144-148) β sign-extendedintgarbage can produce absurd windows; rman would refuse (start>end) or claim junk ranges. - ALSO in the same sw==NULL branch:
isavga_probe()would report success ("Generic ISA VGA", device_set_desc at vga_isa.c:115) on a machine with no registered VGA switch β probe-fails-open.
Verdict
not_reproduced β and it cannot be reproduced without editing kernel
build files to split vga.c from vga_isa.c (the audit treats sys/ as
read-only). The finding is filed as hardening (Info): the API contract of
vga_probe_unit() (return-0-without-initializing) is a booby trap, and
the caller pattern (stack struct, use-on-success) is exactly the shape
that turns into CVE-class uninit-read bugs the moment the two files are
ever decoupled. One-line fixes on both sides remove the trap permanently
(see fix.diff): bzero(&adp, ...) in isavga_probe() and return ENXIO
in vga_probe_unit() when sw == NULL (single caller verified:
vga_isa.c:116 is the only call site in the tree).
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 (bzero in isavga_probe + vga_probe_unit ENXIO; patch -p1 dry-run OK on both files). Kernel-rebuild validation not performed: Info hardening finding, nothing reproduced to flip (contract mandates rebuild validation only for reproduced memory corruption).
findings/poc/DF-5591/fix.diff (two-file unified diff, dry-run applied clean)
Confirmed kernel references
- sys/bus/isa/vga_isa.c:108
- sys/bus/isa/vga_isa.c:116-121
- sys/dev/video/fb/vga.c:70-84
- sys/dev/video/fb/fb.c:203-220
- sys/dev/video/fb/vga.c:244
- sys/platform/pc64/conf/files:72
- sys/platform/pc64/conf/files:199
- sys/sys/fbio.h:298-301
Detail
Evidence (decisive lines)
findings/poc/DF-5591/run.log (dmesg 'port 0x3c0-0x3df iomem 0xa0000-0xbffff' == static init; devinfo -rv works via run_user; conf/files:72+199 single gate); VERDICT.md (full structural proof: vga.c:76-78 -> fb.c:203-220 linker set -> vga.c:244 -> conf/files gate; single-caller check for the ENXIO fix)
PoC changes
No seeded PoC; authored introspection harness (trigger.sh/run.sh) whose deliverable is the NEGATIVE result: live window values matching the static initializer plus the host-side conf/files gate proving structural unreachability.
Verified recommended fix
bzero(&adp, sizeof(adp)) in isavga_probe() and make vga_probe_unit() return ENXIO instead of 0 when the video switch is absent (fix.diff, patch dry-run OK).
Verdict
Hardening finding, negative runtime result by construction: isavga_probe() (sys/bus/isa/vga_isa.c:108) declares video_adapter_t adp uninitialized and vga_probe_unit() (sys/dev/video/fb/vga.c:76-78) returns 0 WITHOUT writing *buf when vid_get_switch() is NULL, whereupon vga_isa.c:118-121 programs adp.va_io_base/va_io_size/va_mem_base/va_mem_size β four ints of stale kernel stack (sys/sys/fbio.h:298-301) β into the device resource list, which devinfo -rv exposes to unprivileged users (verified live). The branch is structurally unreachable in every shipped configuration because vga_isa.c and vga.c are gated on the SAME 'optional vga' option (sys/platform/pc64/conf/files:72,199), so the VIDEO_DRIVER(vga,...) linker-set entry (vga.c:244, walked by vid_get_switch at fb.c:210) is always present when isavga_probe runs; live proof: guest vga0 windows are exactly the static biosadapter values 0x3c0-0x3df/0xa0000-0xbffff (IO_VGA/32, EGA_BUF_BASE/EGA_BUF_SIZE, vga.c:175-179), not garbage. Filed Info: the return-0-without-initializing API contract is a booby trap one Makefile split away from a kernel-stack info leak plus garbage rman claims and probe-fails-open ('Generic ISA VGA' advertised with no switch registered).
No comments yet.