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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5591 Β· 10 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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

  1. 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).
  2. vid_get_switch() (sys/dev/video/fb/fb.c:203-220) walks the videodriver_set linker set (SET_FOREACH, fb.c:210). The entry is emitted by VIDEO_DRIVER(vga, vgavidsw, vga_configure) at vga.c:244 (DATA_SET(videodriver_set, ...), fbreg.h:148-153).
  3. 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 same vga option β€” one kernel module / one static image. There is no configuration in which isavga_probe() executes without VIDEO_DRIVER(vga, ...) being linked (and therefore vid_get_switch("vga") != NULL).
  4. 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 biosadapter initializer at vga.c:175-179. The observed ranges match the static initializer exactly; the bcopy(adp, buf, sizeof(*buf)) at vga.c:82 (fed by *adpp = &biosadapter, vga.c:850) is what landed in adp, i.e. the initialized path ran. Uninitialized stack would not coincidentally equal 0x3c0/32/0xa0000/0x20000 (note the initializer's IO_VGA, 32 pair β€” 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 -rv prints per-device I/O port / I/O memory ranges and works for unprivileged users (verified live β€” see run.log, run_user section).
  • Garbage rman claims activated by isavga_attach() (RF_ACTIVE, vga_isa.c:144-148) β€” sign-extended int garbage 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_testable
baseline no→ patch + rebuild →patched clean

fix.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)
↓ fix.diffper-fix-DF-5591

Confirmed kernel references

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).