# 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`):
```c
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`):

```c
/* 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`:
```c
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.
