Negative PCX width/height passes mode-match and drives a multi-exabyte bcopy -> kernel panic / OOB write past the video window
| Field | Value |
|---|---|
| ID | DF-2110 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/dev/video/fb/pcx/splash_pcx.c |
| Lines | 176-254 |
| Area | video/fb |
| Confidence | certain |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
pcx_init computes pcx_info.width = xmax - xmin + 1 (and likewise
height) from u_short header fields without checking xmin <= xmax or
that the result is positive, so a crafted PCX with xmin > xmax yields a
negative width. pcx_start's adapter-mode selection uses a signed
vi_width >= pcx_info.width comparison that is trivially true for any
negative width, so the malformed image is accepted. pcx_draw then hands
that negative int as the size argument to
bcopy(line, vidmem+pos, pcx_info.width), which implicit-converts to a
~2^64 size_t, copying out of a 1024-byte stack buffer and past the
mapped video-memory window. The result is a guaranteed kernel page-fault
(persistent boot-time panic) and potential corruption of kernel memory
adjacent to the framebuffer mapping.
Root cause
splash_pcx.c:176-177:
pcx_info.width = hdr->xmax - hdr->xmin + 1;
pcx_info.height = hdr->ymax - hdr->ymin + 1;
hdr->xmin/xmax/ymin/ymax are u_short (pcxheader, splash_pcx.c:146),
promoted to int for the subtraction, so when xmin > xmax the result is
a small negative int stored into pcx_info.width (a signed int,
splash_pcx.c:62).
The validation block at splash_pcx.c:165-175 checks
manufactor/version/encoding/nplanes/bpp/bpsl and the 0x0C
palette marker, but never checks xmin <= xmax, ymin <= ymax,
width > 0, or height > 0.
splash_pcx.c:103 then does
if (info.vi_width >= pcx_info.width && info.vi_height >= pcx_info.height && ...)
β a signed comparison where any positive vi_width (e.g. 320 for
M_VGA_CG320) is >= a negative width, so the mode is selected and
pcx_draw runs.
In pcx_draw, splash_pcx.c:214 x = (swidth - pcx_info.width)/2
becomes a large positive value (swidth - negative), and
splash_pcx.c:245 if (pos + pcx_info.width > banksize) evaluates
pos + (negative) which is less than banksize, so the else branch at
splash_pcx.c:254 executes
bcopy(line, vidmem + pos, pcx_info.width);
with pcx_info.width negative; bcopy's third parameter is size_t, so
(size_t)(-49) on LP64 = 0xFFFFFFFFFFFFFFCF, an enormous copy that
reads the line[1024] stack array far past its end and writes
vidmem+pos far past the va_window mapping.
Threat model & preconditions
- Attacker position: anyone able to write the splash file used at boot β a root user on the running system, an attacker with physical / evil-maid access to the boot partition, or a maliciously modified install/boot image.
- Privileges gained or impact: guaranteed kernel page-fault during early-boot splash drawing β persistent denial of service (the machine cannot finish booting). The OOB write lands just past the VGA framebuffer kernel mapping and may corrupt adjacent kernel memory; control is difficult but not impossible in an early-boot context where many protections are not yet active.
- Required config or capabilities: splash-screen support compiled in
and a matching video mode to exist (default
M_VGA_CG320on any VGA console). No privilege check, no signature, no sandbox sits between the loader andpcx_init. - Reachability: the PCX blob is supplied by
loader(8)as thesplash_image_datapreload module (splash.c:54-75, populated from/bootby the loader's splash directives). On the next boot (and every VT switch that re-invokespcx_splash->pcx_draw) the kernel faults.
Proof of Concept
PoC source: findings/poc/DF-2110/
Build a 947-byte malformed PCX whose header inverts xmin/xmax so
width is negative while height stays positive (so the draw loop
actually executes), with bpsl set small but valid, then point the loader
at it and reboot.
// gen_pcx.c β produces /boot/splash.pcx
unsigned char hdr[128]; memset(hdr,0,sizeof(hdr));
hdr[0]=10; /* manufactor = 10 (ZSoft PCX) */
hdr[1]=5; /* version = 5 */
hdr[2]=1; /* encoding = 1 (RLE) */
hdr[3]=8; /* bpp = 8 */
uint16_t xmin=100, ymin=0, xmax=50, ymax=10; /* width=-49, height=11 */
memcpy(hdr+4,&xmin,2); memcpy(hdr+6,&ymin,2);
memcpy(hdr+8,&xmax,2); memcpy(hdr+10,&ymax,2);
hdr[65]=1; /* nplanes = 1 */
uint16_t bpsl=8; memcpy(hdr+66,&bpsl,2); /* bpsl=8 <= 1024 */
// 128-byte header + 50 RLE literal bytes + 0x0C marker + 768-byte palette = 947 bytes
Loader wiring (/boot/loader.conf):
splash_pcx_load="YES" splash_image_data_load="YES" splash_image_data_name="/boot/splash.pcx" splash_image_data_type="splash_image_data" venbdrk.vbe_max="YES"
Build on the DragonFly guest: cc -o gen_pcx gen_pcx.c && ./gen_pcx then
reboot.
Expected output
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x... ... bcopy+0x... pcx_draw+0x...
The kernel faults during console initialization and either drops to the ddb debugger or reboots/panics every boot β a persistent boot-time DoS.
Impact
- Default config: only triggered when the operator (or attacker) has placed a crafted splash image.
- Blast radius: persistent boot-time DoS; potential corruption of kernel memory adjacent to the framebuffer mapping.
Recommended fix
Validate the geometry in pcx_init before trusting width/height/
bpsl. Reject non-positive or inverted dimensions, require bpsl >= 1,
and require width <= bpsl so bcopy(line, ..., width) never reads past
the decoded portion of the stack buffer (this also closes the info-leak
sibling DF-2111). As defense-in-depth, also clamp the bcopy size in
pcx_draw.
--- a/sys/dev/video/fb/pcx/splash_pcx.c
+++ b/sys/dev/video/fb/pcx/splash_pcx.c
@@ -165,9 +165,16 @@ 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) {
kprintf("splash_pcx: invalid PCX image\n");
return 1;
}
The || chain short-circuits left-to-right, so xmin > xmax is rejected
before the (u_int)(xmax - xmin + 1) subtraction is ever evaluated,
keeping the arithmetic safe.
References
- DF-2111 β sibling info-leak from the same missing-geometry-validation root cause.
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-2110 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 731 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 170 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2110 - Verification Verdict
Status: reproduced (source-confirmed) Impact: panic Confidence: certain
Verdict
Source-confirmed: pcx_init (:176-177) computes width=xmax-xmin+1 from u_short header without checking xmin<=xmax; malformed PCX causes huge width; splash/boot-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_init width without xmin<=xmax; splash-gated
Verified recommended fix
pcx_init width without xmin<=xmax; splash-gated
Verdict
pcx_init width without xmin<=xmax; splash-gated
No comments yet.