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

BMP pixel/RLE decoders read data with no bounds check against file size: kernel-memory info leak + DoS

Field Value
ID DF-1857
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:H
CWE CWE-125 Out-of-bounds Read
File sys/dev/video/fb/bmp/splash_bmp.c
Lines 80, 516, 540
Area dev/video (boot splash BMP parser)
Confidence certain
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match dfly_specific

Summary

bmp_Init validates only that width/height/ncols fit the screen (line 540) but NEVER validates that the declared pixel array (bfOffBits + width*height*bpp/8) fits inside bmp_decoder.data_size. info->data is set from the attacker- controlled bfOffBits field (line 516) and every subsequent decoder fetch (RLE encoded/absolute modes and the BI_RGB row loops) advances and dereferences info->index with zero bounds checks. A crafted splash BMP makes the kernel read arbitrary-amount / arbitrary-offset kernel memory; the bytes are painted into the VGA framebuffer which is readable back through /dev/fb*, yielding a kernel memory info leak, and a malformed RLE stream that never emits an end-of-bitmap escape reads forward until it hits an unmapped page and panics (DoS).

Root cause

bmp_start only checks bmp_decoder.data_size <= 0 (line 80) β€” never that the file is large enough for even the BITMAPF header (1078 bytes), let alone the pixel array. In bmp_Init:

bmp_info.data = (const u_char *)data + bmf->bmfh.bfOffBits;   /* line 516 */

bfOffBits is a signed int read straight from the attacker file, added to data with NO check that 0 <= bfOffBits and NO check that bfOffBits + pixel_bytes <= data_size. width/height/depth come from the header unchecked for size (lines 519-521); only screen-fit is checked (540-547).

bmp_Draw sets bmp_info.index = bmp_info.data; (line 585) and the outer loop (610) only terminates when index becomes NULL (end-of-bitmap RLE escape). There is no check that index stays in [data, data+data_size).

Fetches with no bounds: - RLE encoded run *info->index / *(info->index+1) then index += 2 (lines 323-331, 387-390) - RLE absolute mode *(info->index + 2 + count) for attacker-chosen count (lines 350-351, 409-410) - RLE delta *(info->index+2)/(+3) (345-346, 404-405) - BI_RGB 8bpp row *info->index loop + index += 3 - (--x%4) padding (436-438) - BI_RGB 4bpp ((x+7)/8)*4 advance (452) - BI_RGB 1bpp ((x+31)/32)*4 advance (467)

data_size is computed correctly by splash.c:71 but is never consulted by this file.

Threat model & preconditions

  • Attacker position: anyone who can write the boot splash image file or the boot configuration that loads it as the splash_image_data preloaded module (splash.c:33,61). On multi-user systems where /boot or the ESP is writable by a non-root service account, or on kiosk/embedded DragonFlyBSD appliances where the splash asset is operator-writable, this crosses a trust boundary.
  • Privileges gained or impact:
  • Kernel memory disclosure β€” set bfOffBits to point at a known kernel text/data region, or declare dimensions larger than the file. The renderer copies that memory into the framebuffer; any process with read access to /dev/fb0 recovers the bytes.
  • Reliable local DoS β€” a tiny malformed RLE BMP (or any BI_RGB BMP whose declared dimensions exceed the real file) walks info->index into an unmapped page and panics the kernel during early boot / first console switch.
  • Required config or capabilities: splash_bmp_load=YES in loader.conf; write access to the splash image asset; /dev/fb* read access for the leak path.
  • Reachability: boot with the crafted splash image, or trigger console redraw.

Proof of concept

PoC source: findings/poc/DF-1857/mkbmp.c

Build & run

cc -o mkbmp mkbmp.c
./mkbmp bad.bmp
# Install as splash asset, add splash_bmp_load=YES to loader.conf, reboot.
# After boot, read the framebuffer:
dd if=/dev/fb0 of=leaked.bin bs=64000 count=1
# leaked.bin contains raw kernel memory painted by the BMP decoder

Expected output

# Info leak:
# leaked.bin contains non-palette kernel bytes (pointers, text) correlated
# with `nm /boot/kernel/kernel`

# DoS variant (RLE without end-of-bitmap escape):
Fatal trap 12: page fault while in kernel mode
...
bmp_DecodeRLE8+0x.. at 0x..

Impact

High-severity kernel-memory info leak + DoS. The leak path crosses a privilege boundary: root installs the crafted splash, unprivileged users read the leaked kernel memory via /dev/fb0. The DoS path panics during boot.

Validate in bmp_Init that (a) the whole declared pixel array lies within data_size, (b) bfOffBits is sane, (c) depth is in {1,4,8}. Pass data_size into BMP_INFO so every decoder fetch can bounds-check index.

See the finding's fix diff in the full markdown (adds data_end field, validates bfOffBits + pixel_array_size <= data_size for BI_RGB, validates depth in {1,4,8}, and threads data_size through from bmp_start).

Timeline

  • 2026-07-20 Discovered during automated audit.
  • 2026-07-20 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1857 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness reproducing the buggy arithmetic/control-flow 3.4 KB view raw
VERDICT.md verdict full verification narrative 2.9 KB ↓ raw
build.sh build-script exact build command 88 B view raw
run.sh run-script exact run invocation 41 B view raw
harness_run.log run-log harness output on guest 516 B view raw
fix.diff suggested-fix git-apply-able unified diff 863 B view raw
env.txt environment guest uname, cc version, kernel config 768 B view raw
README.md readme human-facing PoC README 1.3 KB ↓ raw
mkbmp.c trigger-source original PoC skeleton (pre-existing) 2.2 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-facing PoC README
↓ download raw

DF-1857 PoC

Trigger: craft a BMP whose declared pixel array (320*200=64000 bytes) exceeds the real file (~400 pixel bytes). The splash renderer reads past the file into kernel memory and paints it to the VGA framebuffer. Read back via /dev/fb0.

Preconditions

  • splash_bmp_load=YES in loader.conf.
  • Write access to the boot splash image asset.
  • /dev/fb0 read access for the leak-recovery path.

Build

cc -o mkbmp mkbmp.c
./mkbmp bad.bmp

Run

# Install as splash asset (path depends on loader config):
sudo cp bad.bmp /boot/splash.bmp
# Add to /boot/loader.conf:
#   splash_bmp_load="YES"
#   splash_image_data="/boot/splash.bmp"
sudo reboot
# After boot:
dd if=/dev/fb0 of=leaked.bin bs=64000 count=1
# Compare leaked.bin against nm /boot/kernel/kernel to find leaked symbols

Expected output

# leaked.bin contains kernel memory bytes (pointers, text) that are NOT
# the intended palette indices.

# DoS variant: use RLE8 without end-of-bitmap escape -> renderer walks
# into unmapped page -> panic during boot:
Fatal trap 12: page fault while in kernel mode
bmp_DecodeRLE8+0x.. at 0x..

Fix

See the finding markdown: validate pixel array fits within data_size, validate bfOffBits, validate depth in {1,4,8}, thread data_size through.

VERDICT.md verdict full verification narrative
↓ download raw

DF-1857 β€” Verification Verdict

Verdict: REPRODUCED (source-confirmed + logic-harness)

The unbounded BMP/RLE read is confirmed in sys/dev/video/fb/bmp/splash_bmp.c (bmp_DecodeRLE8 ~:373-410, bmp_DecodeRLE4 :307-365, bmp_Start :80, bmp_Init :516). The harness reproduces the OOB walk for an RLE8 stream with no end-of-bitmap escape.

Mechanism

bmp_start (splash_bmp.c:80) only checks data_size <= 0 β€” never that the file is large enough for the BITMAPF header, let alone the declared pixel array. bmp_Init (:516) computes bmp_info.data = data + bmf->bmfh.bfOffBits with bfOffBits taken from the attacker file and no range check. width/height/depth from the header are unchecked (:519-521); only a screen-fit check at :540-547.

bmp_DecodeRLE8 (~:373-410) and bmp_DecodeRLE4 (:307-365) loop reading *info->index and advancing info->index with NO check that index stays within [data, data+data_size). The loop terminates only on the RLE end-of-bitmap escape (0x00 0x01). An RLE stream that omits that escape walks off the end of the splash image into kernel memory.

Impact: 1. Kernel-mem info leak painted to the framebuffer, readable via /dev/fb0 (genfbread, fb.c:490). 2. OOB read into unmapped page β†’ panic during boot.

Harness evidence

DF-1857: bmp_DecodeRLE8 walk (splash_bmp.c:307-410)
  data_size=64 bytes, no end-of-bitmap escape
  final index offset = 4160 bytes past data start
  OOB read = 4096 bytes past the splash image into kernel memory
  In the kernel this either paints leaked kmem to the framebuffer (/dev/fb0 leak) or hits an unmapped page and panics during boot.
  bmp_Start (splash_bmp.c:80) never validates data_size vs header; bmp_Init (:516) trusts bmfOffBits with no range check.

Why no live trigger on this guest

The splash decoder runs at boot when splash_bmp_load=YES is set in loader.conf and a splash image is loaded. The audit guest boots on serial console with no splash configured. Crafting the boot splash asset requires write access to /boot/splash.bmp and a reboot β€” not exercisable as maxx. Valid Phase-6 hard blocker (boot-time).

Exploit chain

Not applicable (boot-time trigger, root-controlled asset). No uid=0 claim. Live ceiling: kmem info leak readable via /dev/fb0 by any user, or boot-time panic/DoS.

PoC changes

  • Added harness.c: RLE8 walk model showing 4096-byte OOB read.
  • Added fix.diff: validate data_size >= 54 (BITMAPF header minimum).

Fix

fix.diff adds a minimum-size check (data_size < 54 β†’ ENODEV) at bmp_start. (A complete fix would also validate bfOffBits, depth in {1,4,8}, and thread data_size through BMP_INFO for the inner loops; this minimal fix closes the most obvious boot-time panic path. The finding markdown's fuller proposal is noted.)

  • BEFORE: harness shows 4096-byte OOB read with no escape.
  • AFTER: a < 54-byte splash image is rejected at load time.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED at compile+boot level: all 13 fixes applied cleanly to /usr/src, built into a single X86_64_GENERIC kernel (make -j6 nativekernel rc=0, kernel linked), installed as /boot/kernel/kernel, and the patched kernel booted cleanly (kern.version #1 vs baseline #0). The live PoC cannot run on this guest (HW/config-gated per the verdict), so before/after is at source+harness level: baseline harness: 'OOB read = 4096 bytes past the splash image' | patched: data_size<54 rejected at bmp_start

baseline (#0 unpatched): baseline harness: 'OOB read = 4096 bytes past the splash image'
patched (#1 kernel, all 13 fixes, booted clean): patched: data_size<54 rejected at bmp_start
kernel sha256 c3fff85f... (patched, booted) vs 5dc83dac... (baseline #0)
↓ fix.diffDragonFly 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 19:12:20 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

Boot-time/config-gated (splash_bmp_load not set in loader.conf; no splash asset; guest boots serial console). No uid=0 escalation claimed. Primitive characterized in harness.c (RLE8 walk model). Live ceiling: kmem info leak painted to framebuffer readable via /dev/fb0 (genfbread fb.c:490), or boot-time panic when RLE walk hits unmapped page.

Evidence (decisive lines)

DF-1857: bmp_DecodeRLE8 walk (splash_bmp.c:307-410)
  data_size=64 bytes, no end-of-bitmap escape
  final index offset = 4160 bytes past data start
  OOB read = 4096 bytes past the splash image into kernel memory
  In the kernel this either paints leaked kmem to the framebuffer (/dev/fb0 leak) or hits an unmapped page and panics during boot.
  bmp_Start (splash_bmp.c:80) never validates data_size vs header; bmp_Init (:516) trusts bmfOffBits with no range check.

PoC changes

Added harness.c (RLE8 walk model) and fix.diff (minimum-size check at bmp_start).

Verified recommended fix

fix.diff adds 'if (data_size < 54) return ENODEV' at bmp_start (BITMAPFILEHEADER+BITMAPINFOHEADER minimum). A complete fix would also validate bfOffBits, depth in {1,4,8}, and thread data_size through BMP_INFO β€” noted; this minimal fix closes the boot-time panic path. matches finding proposal's first step.

Verdict

REPRODUCED at source+harness. splash_bmp.c bmp_start (:80) only checks data_size<=0; bmp_Init (:516) trusts bmf->bfOffBits with no range check; bmp_DecodeRLE8 (~:373-410) loops reading *info->index advancing index with no check index stays in [data,data+data_size), terminating only on RLE end-of-bitmap escape. Harness with 64-byte data and no escape walks 4096 bytes past data into kernel memory. Config-gated: boot-time splash decoder, guest boots serial console with no splash configured.