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_datapreloaded module (splash.c:33,61). On multi-user systems where/bootor 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
bfOffBitsto 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/fb0recovers the bytes. - Reliable local DoS β a tiny malformed RLE BMP (or any BI_RGB BMP whose
declared dimensions exceed the real file) walks
info->indexinto an unmapped page and panics the kernel during early boot / first console switch. - Required config or capabilities:
splash_bmp_load=YESin 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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-1857 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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=YESin loader.conf.- Write access to the boot splash image asset.
/dev/fb0read 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.
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: validatedata_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
fixedVALIDATED 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)
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- f
- b
- /
- b
- m
- p
- /
- s
- p
- l
- a
- s
- h
- _
- b
- m
- p
- .
- c
- :
- 8
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- f
- b
- /
- b
- m
- p
- /
- s
- p
- l
- a
- s
- h
- _
- b
- m
- p
- .
- c
- :
- 3
- 7
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- f
- b
- /
- b
- m
- p
- /
- s
- p
- l
- a
- s
- h
- _
- b
- m
- p
- .
- c
- :
- 5
- 1
- 6
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.
No comments yet.