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

Uninitialized kernel-stack bytes leaked to the console splash screen when bpsl < width (or compressed stream is short)

Field Value
ID DF-2111
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:N/A:N
CWE CWE-908 Use of Uninitialized Resource
File sys/dev/video/fb/pcx/splash_pcx.c
Lines 176-254
Area video/fb
Confidence likely
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match novel

Summary

pcx_draw declares u_char line[MAXSCANLINE] on the stack without zeroing it and only fills the first bpsl bytes (inner loop bounded by j < pcx_info.bpsl), but then bcopys width bytes from line into video memory. pcx_init never enforces bpsl >= width (or bpsl >= 1), so a crafted PCX with bpsl smaller than width β€” or a compressed stream that ends before bpsl bytes are decoded β€” causes the unfilled tail of the stack buffer to be painted onto the splash screen, leaking whatever the kernel stack previously held (pointers, potentially defeating KASLR; credential/key fragments) to anyone with a view of the physical console.

Root cause

  • splash_pcx.c:194 u_char line[MAXSCANLINE]; is uninitialized automatic storage.
  • The decode loop at splash_pcx.c:225-237 fills line[j++] only while j < pcx_info.bpsl (and while i < pcx_info.zlen), so at most the first bpsl bytes are written.
  • The blit at splash_pcx.c:245-255 then does bcopy(line, vidmem + pos, pcx_info.width) / bcopy(line + j, vidmem, pcx_info.width - j) using pcx_info.width, not the number of bytes actually decoded and not min(width, bpsl).
  • pcx_init (splash_pcx.c:176, 178) sets width = xmax - xmin + 1 and bpsl = hdr->bpsl independently with no bpsl >= width and no bpsl >= 1 check.

Two reachable cases leak stack:

  • (a) bpsl < width (e.g. bpsl=8, width=320, both pass validation and match M_VGA_CG320) leaves line[8..319] uninitialized.
  • (b) bpsl >= width but the RLE stream terminates early (i >= zlen) before bpsl bytes are produced, leaving line[produced..width-1] uninitialized.

Either way width - bpsl (or width - produced) uninitialized stack bytes reach the framebuffer and the screen.

Threat model & preconditions

  • Attacker position: same ingest path as DF-2110 β€” the attacker crafts /boot/splash.pcx and reboots.
  • Privileges gained or impact: the bytes painted are stale kernel-stack contents from pcx_draw's own frame and prior call frames on the same stack. An attacker or observer who can read the physical console (or capture dmesg/screenshot during boot) obtains kernel stack residue. The leak is narrow (≀ ~320 bytes per scanline, bounded by vi_width ≀ 320 for the only mode in pcx_start's modes[] table at splash_pcx.c:70-73) and its content is non-deterministic, hence Low; but kernel pointers in those bytes would aid KASLR bypass and the bug is trivially reachable.
  • Required config or capabilities: splash-screen support compiled in and a matching video mode (default M_VGA_CG320 on any VGA console).
  • Reachability: same as DF-2110 β€” PCX blob is supplied by loader(8) as the splash_image_data preload module.

Proof of Concept

PoC source: findings/poc/DF-2111/

Build a PCX with a valid positive width (e.g. 320 to match M_VGA_CG320) but bpsl deliberately smaller (e.g. bpsl=16), supply enough RLE data to fill only 16 bytes per scanline, then boot with splash enabled and photograph/read the console: the right portion of each splash scanline (columns 16..319) shows uninitialized kernel stack instead of image data.

// gen_leak.c β€” produces /boot/splash.pcx
unsigned char hdr[128]; memset(hdr,0,sizeof(hdr));
hdr[0]=10; hdr[1]=5; hdr[2]=1; hdr[3]=8;
uint16_t xmin=0,ymin=0,xmax=319,ymax=199;  /* width=320, height=200 */
memcpy(hdr+4,&xmin,2); memcpy(hdr+6,&ymin,2);
memcpy(hdr+8,&xmax,2); memcpy(hdr+10,&ymax,2);
hdr[65]=1; uint16_t bpsl=16; memcpy(hdr+66,&bpsl,2);  /* bpsl << width */
// 128-byte header + 16 RLE bytes + 0x0C marker + 768-byte palette

Wire the loader as in DF-2110 and reboot.

Expected output

The splash image's right ~304 columns per row render as non-image
(stack-garbage) pixels. Sampling those pixels (e.g. via the genfb mmap
path or a screen capture) yields kernel-stack-derived bytes; repeating
across reboots with different prior stack activity demonstrates the leak
varies, confirming it is uninitialized memory rather than image data.

Impact

  • Default config: only triggered when the operator (or attacker) has placed a crafted splash image.
  • Blast radius: ≀ ~320 bytes/scanline of stale kernel stack leaked to the physical console. KASLR-bypass value if the bytes hold kernel pointers.

Fixed by the same pcx_init validation block proposed for DF-2110 β€” requiring bpsl >= 1 and (u_int)(xmax-xmin+1) <= bpsl guarantees line is fully populated across [0, width) before the blit. Additionally, for early-stream-termination robustness, zero line at declaration or bound the bcopy size to the count of bytes actually decoded that scanline. Minimal hardened diff (subsumes both findings):

--- a/sys/dev/video/fb/pcx/splash_pcx.c
+++ b/sys/dev/video/fb/pcx/splash_pcx.c
@@ -165,6 +165,11 @@ pcx_init(const char *data, int size)
     if (size < 128 + 1 + 1 + 768
    || hdr->manufactor != 10
    || hdr->version != 5
    || hdr->encoding != 1
    || hdr->nplanes != 1
    || hdr->bpp != 8
+   || hdr->bpsl < 1
    || hdr->bpsl > MAXSCANLINE
+   || hdr->xmin > hdr->xmax
+   || hdr->ymin > hdr->ymax
+   || (u_int)(hdr->xmax - hdr->xmin + 1) > (u_int)hdr->bpsl
+   || (u_int)(hdr->ymax - hdr->ymin + 1) > MAXSCANLINE
    || data[size-769] != 12) {
@@ -193,1 +193,1 @@
-    u_char line[MAXSCANLINE];
+    u_char line[MAXSCANLINE] = {0};

References

  • DF-2110 β€” sibling OOB-write/panic from the same missing-geometry validation.
  • sys/dev/video/fb/splash.c:54-75 β€” splash_image_data preload module ingestion from /boot.

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2111 Β· 4 files
FileTypeDescriptionSize
VERDICT.md file 742 B ↓ raw
build.sh file 161 B view raw
fix.diff file 170 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2111 - Verification Verdict

Status: reproduced (source-confirmed) Impact: none Confidence: likely

Verdict

Source-confirmed: pcx_draw (:194) declares u_char line[MAXSCANLINE] without zeroing; bcopy width bytes but only bpsl filled; uninitialized stack read if width>bpsl; splash-gated

Fix Status

Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)

Source File

sys/dev/video/fb/pcx/splash_pcx.c

Fix Validation

All 87 fix.diffs compiled together in a single batch kernel build (make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors). The combined patch is at findings/poc/batch_build/all_fixes.patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

pcx_draw uninit stack; splash-gated

Verified recommended fix

pcx_draw uninit stack; splash-gated

Verdict

pcx_draw uninit stack; splash-gated