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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1165 Β· 9 files
FileTypeDescriptionSize
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
README.md readme summary + reproduce steps
↓ download 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

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

  1. Apply fix.diff to /usr/src.
  2. cd /usr/src/sys/dev/drm/drm && make KMOD=drm β†’ drm.ko builds clean.
  3. 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.

VERDICT.md verdict full mechanism trace + reachability note
↓ download raw

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.ko does load, but with no DRM device attached there is no connector β†’ no EDID read β†’ drm_parse_tiled_block is 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_testable

compile 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.