Heap OOB read in drm_parse_tiled_block via undersized DisplayID tiled block
Summary
drm_parse_tiled_block at drm_edid.c:5103-5184: casts displayid_block* to displayid_tiled_block* (24B packed) and reads fields at offsets up to 23 without checking block->num_bytes>=21 (sizeof(displayid_tiled_block)-sizeof(displayid_block)). Caller while-loop at :5174 only checks idx+3+num_bytes<=128, not that num_bytes covers the full struct. Malicious DisplayID with DATA_BLOCK_TILED_DISPLAY(0x12) at byte 120 num_bytes=4 passes loop but reads 11 bytes past 128-byte EDID extension -> kernel heap OOB read. Leaked data flows to connector->tile_h/v_size (DRM ioctls) and topology_id (tile group IDR). Physical: malicious monitor/adapter/VM virtual display. Fix: check num_bytes>=21 before drm_parse_tiled_block.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1165 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Add `if (block->num_bytes < sizeof(struct displayid_tiled_block) - sizeof(struct displayid_block)) return -EINVAL;` at entry of drm_parse_tiled_block | 818 B | view raw |
| VERDICT.md | verdict | full mechanism trace + reachability note | 4.5 KB | β raw |
| README.md | readme | summary + reproduce steps | 1.5 KB | β raw |
| build.sh | build-script | apply fix + build drm.ko | 390 B | view raw |
| run.sh | run-script | placeholder runtime trigger (needs GPU) | 1.0 KB | view raw |
| build.log | build-log | tail of successful full drm.ko build (rc=0) | 1.7 KB | view raw |
| env.txt | environment | uname, cc, hw.model, module status | 454 B | 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-1165 PoC verification β source-level trace of drm_parse_tiled_block OOB read past EDID extension.
drm is not in X86_64_GENERIC; it exists only as a kld module. Loading drm on the QEMU guest succeeds but attaches to no device (no GPU), so no connector β no EDID read β drm_parse_tiled_block is never invoked. Verification is by source-level trace + build-validation of the fix.
Bug location
sys/dev/drm/drm_edid.c:5103β drm_parse_tiled_block entrysys/dev/drm/drm_edid.c:5112-5135β reads up to topology_id[7] (offset 23)sys/dev/drm/drm_edid.c:5174-5177β caller loop only checks num_bytes vs length, not vs struct sizesys/dev/drm/include/drm/drm_displayid.h:67-74β displayid_tiled_block layout (24B)
Mechanism
Caller guarantees idx + 3 + num_bytes <= length but not num_bytes >= 21
(the tiled-block body size). A DATA_BLOCK_TILED_DISPLAY block at idx near the
end of a 128-byte EDID extension with small num_bytes (e.g. 4) causes
drm_parse_tiled_block to read offsets up to 23 from the block start, walking
past the extension into adjacent kernel heap. Leaked bytes flow to
connector->tile_h/v_size and tile->topology_id (IDR-persisted).
Reproduce
- Apply
fix.diffto /usr/src. cd /usr/src/sys/dev/drm/drm && make KMOD=drmβ drm.ko builds clean.- Runtime test requires DRM-attached GPU + malicious monitor/adapter/VM display with crafted DisplayID (not present on this guest).
See VERDICT.md for the full analysis.
DF-1165 β drm_parse_tiled_block: heap OOB read of EDID extension (source-only verification)
Verdict: REPRODUCED at source level (drm is a kld module, not in GENERIC; not runtime-triggerable here without a DRM-attached GPU + crafted EDID)
Mechanism
drm is not in X86_64_GENERIC (no device drm in
sys/config/X86_64_GENERIC). It exists only as the loadable module
/boot/kernel/drm.ko and is parent to amdgpu.ko/radeon.ko/i915.ko.
drm_parse_display_id walks the DisplayID blocks of an EDID extension
(sys/dev/drm/drm_edid.c:5174-5177):
while (block = (struct displayid_block *)&displayid[idx],
idx + sizeof(struct displayid_block) <= length &&
idx + sizeof(struct displayid_block) + block->num_bytes <= length &&
block->num_bytes > 0) {
This guarantees idx + 3 + num_bytes <= length (length is typically 128 β
one EDID extension block). It does not guarantee that num_bytes is large
enough to cover the specific block type being parsed.
When the block tag is DATA_BLOCK_TILED_DISPLAY (0x12), control enters
drm_parse_tiled_block (sys/dev/drm/drm_edid.c:5103-5155), which casts the
block to struct displayid_tiled_block and reads fields out to offset 23 from
the block start (sys/dev/drm/include/drm/drm_displayid.h:67-74):
/* drm_edid.c:5106 */
struct displayid_tiled_block *tile = (struct displayid_tiled_block *)block;
/* 5112 */ w = tile->tile_size[0] | tile->tile_size[1] << 8; /* offsets 7..10 */
/* 5113 */ h = tile->tile_size[2] | tile->tile_size[3] << 8;
/* 5115 */ num_v_tile = (tile->topo[0] & 0xf) | (tile->topo[2] & 0x30); /* offsets 4..6 */
/* 5117 */ tile_v_loc = (tile->topo[1] & 0xf) | ...;
/* 5128 */ connector->tile_h_size = w + 1;
/* 5135 */ DRM_DEBUG_KMS("vend %c%c%c\n", tile->topology_id[0..2]); /* offsets 16..18 */
/* 5137 */ tg = drm_mode_get_tile_group(connector->dev, tile->topology_id); /* offsets 16..23 */
struct displayid_tiled_block is 24 bytes (3-byte base + 21-byte body). The
function reads up to topology_id[7] at offset 23 from the block start. The
caller only guaranteed idx + 3 + num_bytes <= length β it never checked
num_bytes >= 21.
Worked example. EDID extension length = 128. A DATA_BLOCK_TILED_DISPLAY
block placed at idx = 120 with num_bytes = 4 passes the caller's loop
(120 + 3 + 4 = 127 <= 128) but drm_parse_tiled_block then reads bytes at
absolute offsets 120 + {4..23} = {124..143}. Bytes 128..143 (up to 16
bytes) lie past the EDID extension in kernel heap. The finding's claim of
"11 bytes past 128-byte EDID extension" corresponds to a slightly different
placement but is the same class β read past the declared extension into
adjacent heap.
The leaked bytes flow into:
- connector->tile_h_size / tile_v_size (settable via DRM ioctls after the
fact, but originally populated from this OOB read),
- tile->topology_id (8-byte buffer) used to allocate/create a
drm_tile_group IDR entry β so the OOB heap bytes get persisted in a
kernel IDR and indirectly influence connector state.
Trigger reachability on this guest
- The QEMU guest has no GPU.
drm.kodoes load, but with no DRM device attached there is no connector β no EDID read βdrm_parse_tiled_blockis never invoked. - A malicious monitor / DP-HDMI adapter / VM virtual display is the realistic threat model. None is present here.
Fix
fix.diff adds the missing struct-size check at the entry of
drm_parse_tiled_block:
if (block->num_bytes < sizeof(struct displayid_tiled_block) -
sizeof(struct displayid_block))
return -EINVAL;
(= num_bytes < 21). On a malformed block the function now returns early
instead of reading off the end of the EDID extension.
Build verification of the fix
drm.ko built successfully from the patched source on the guest
(cc 8.3 [DragonFly], full module build with -j6):
=== DRM_BUILD_DONE rc=0 ===
Full output captured to /root/drm_build.log on the guest; key tail in
build.log. The fix is build-clean (placed after all declarations to avoid
the -Wold-style-declaration / C90 declaration-after-statement warning).
Fix-validation status
not_testable β runtime trigger requires a DRM-attached GPU whose connector
EDID (or a hot-plugged malicious monitor / DP-HDMI adapter / VM virtual
display) carries a crafted DisplayID tiled block with num_bytes < 21. The
guest has no GPU. We confirmed the fix applies cleanly and the patched
full drm.ko module compiles; the change adds a single comparison.
Fix verification
not_testablecompile validated
module build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source+harness. drm_parse_tiled_block no num_bytes size check -> 16B OOB heap read. drm kld-only, no GPU.
No comments yet.