Arbitrary code execution via Rockchip U-Boot's BMP RLE8 decoder
Summary
Rockchip’s fork of U-Boot 2017.09 contains a BMP RLE8 decoder in drivers/video/drm/bmp_helper.c. The decode_rle8_bitmap function handles the RLE8 DELTA escape (opcodes 0x00 0x02 dx dy) without any bounds checking on the destination pointer. An attacker who controls the BMP file decoded at boot can move the write pointer arbitrarily far backwards from the decode buffer and overwrite U-Boot code or data in memory. Combined with attacker-controlled colour table entries, this gives full 16-bit write-what-where capability - enough for arbitrary code execution during the boot process.
No public CVE corresponds to this bug. It was confirmed working on-device against U-Boot 2017.09-gaf487a79ad.
The vulnerable code
The DELTA case in decode_rle8_bitmap looks like this:
case BMP_RLE8_DELTA:
x += bmap[2];
if (flip) {
y -= bmap[3];
dst -= bmap[3] * linesize;
dst += bmap[2] * 2;
} else {
y += bmap[3];
dst += bmap[3] * linesize;
dst += bmap[2] * 2;
}
bmap += 4;
break;
In flip mode (the common case - any BMP with a positive height field), y starts at height - 1 and decrements. A single DELTA subtracts bmap[3] from y, which can drive it negative with a single byte. After the negative step, the existing bounds checks on subsequent encoded and unencoded runs still pass: y < height is true for any negative y (signed comparison), and y >= height || x >= width likewise fails to reject. The decoder continues writing through the now-underflowed dst pointer.
Write primitive
The decoder writes 16-bit RGB565 values looked up from the BMP colour table. All 256 colour table entries are attacker-controlled, giving full 16-bit value control per write position.
With width = 4096 (producing linesize = 8192), a single DELTA moves the pointer backwards by up to 255 * 8192 bytes - roughly 2 MiB. Multiple DELTAs chain freely, extending the reach to anywhere below the decode buffer in the address space.
Each ARM64 instruction is 32 bits, so two adjacent 16-bit encoded runs are needed per instruction. Alignment must be tracked carefully across runs, but the primitive is otherwise straightforward to use for patching arbitrary code in U-Boot’s relocated text segment, which sits below the video memory region where the decode buffer is allocated.
Exploitability
U-Boot on Rockchip SoCs decodes a boot logo BMP during early board initialisation (board_late_init -> rockchip_show_logo). The display memory pool is allocated at the top of RAM, with U-Boot’s relocated code and data sitting immediately below it. The BMP decode buffer is allocated from the bottom of the display pool, so a backwards DELTA naturally crosses into U-Boot’s own code.
An attacker with the ability to supply a crafted BMP through any path that reaches this decoder - whether via the boot image’s resource partition, an unverified secondary slot, or any other mechanism that feeds rockchip_show_logo - can overwrite U-Boot instructions in memory during the boot process and redirect execution.
Affected code
This bug exists in the Rockchip-maintained fork of U-Boot at the next-dev branch, in drivers/video/drm/bmp_helper.c. The same decoder may be present in other Rockchip U-Boot branches that share this DRM-based display path. Any Rockchip SoC that uses this code path to render a boot logo is potentially affected.
Fix
Four changes are needed in decode_rle8_bitmap. A fix has been submitted as rockchip-linux/u-boot#100.
DELTA: add bounds check
The DELTA handler is the primary vulnerability. It adjusts x, y, and dst with no validation. The fix adds a bounds check after the existing adjustment. If y has gone negative or either coordinate is out of range, decoding stops. The adjusted dst is never dereferenced because the loop exits immediately:
bmap += 4;
if (x >= width || y < 0 || y >= height)
decode = 0;
break;
EOL: add bounds check
The EOL handler has the same class of bug: in flip mode, y-- can underflow past zero. The fix adds a bounds check after the existing y adjustment:
if (y < 0 || y >= height)
decode = 0;
break;
Encoded runs: reject negative y
The encoded run bounds check uses if (y < height), which passes for any negative y because both operands are signed. The fix adds a lower-bound check:
if (y >= 0 && y < height) {
Unencoded runs: reject negative y
The unencoded run bounds check uses if (y >= height || x >= width), which likewise misses negative y. The fix adds the missing check:
if (y < 0 || y >= height || x >= width) {