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:194u_char line[MAXSCANLINE];is uninitialized automatic storage.- The decode loop at
splash_pcx.c:225-237fillsline[j++]only whilej < pcx_info.bpsl(and whilei < pcx_info.zlen), so at most the firstbpslbytes are written. - The blit at
splash_pcx.c:245-255then doesbcopy(line, vidmem + pos, pcx_info.width)/bcopy(line + j, vidmem, pcx_info.width - j)usingpcx_info.width, not the number of bytes actually decoded and notmin(width, bpsl). pcx_init(splash_pcx.c:176, 178) setswidth = xmax - xmin + 1andbpsl = hdr->bpslindependently with nobpsl >= widthand nobpsl >= 1check.
Two reachable cases leak stack:
- (a)
bpsl < width(e.g.bpsl=8,width=320, both pass validation and matchM_VGA_CG320) leavesline[8..319]uninitialized. - (b)
bpsl >= widthbut the RLE stream terminates early (i >= zlen) beforebpslbytes are produced, leavingline[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.pcxand 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 byvi_width β€ 320for the only mode inpcx_start'smodes[]table atsplash_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_CG320on any VGA console). - Reachability: same as DF-2110 β PCX blob is supplied by
loader(8)as thesplash_image_datapreload 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.
Recommended fix
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_datapreload module ingestion from/boot.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2111 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedbatch build rc=0
batch 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
No comments yet.