# DF-4194 — DRM tile-group ERR_PTR/NULL contract mismatch → wild-pointer deref (kernel panic)

`sys/dev/drm/drm_connector.c` — pass 2 (GLM 5.3 re-audit), source-level certain, hardware-gated.

## The bug (one paragraph)

`drm_mode_create_tile_group()` (sys/dev/drm/drm_connector.c:1909) returns an
**`ERR_PTR`** on both failure paths — `ERR_PTR(-ENOMEM)` at line 1917 (kzalloc
failure) and `ERR_PTR(ret)` at line 1929 (idr_alloc failure) — but its only
caller, `drm_parse_tiled_block()` (sys/dev/drm/drm_edid.c:5137-5142), checks
**only `if (!tg)`**, the NULL contract that upstream Linux v5.2 actually
implements (verified: upstream returns `NULL` on both paths). An `ERR_PTR` is
non-NULL, so the error check passes and line 5150 stores the poison:
`connector->tile_group = ERR_PTR(-ENOMEM)` (== `(struct drm_tile_group *)-12`).
Every subsequent consumer of `connector->tile_group` then dereferences or
refcounts a wild pointer and the kernel takes a page fault at a fixed unmapped
address. Note `connector->has_tile = true` was already set at drm_edid.c:5120
before the failure, so nothing else guards the dereferences.

## Crash sites (all reached after the poison is stored)

| site | code | effective address |
|---|---|---|
| sys/dev/drm/drm_edid.c:5148 | re-probe of the same sink: `drm_mode_put_tile_group(dev, connector->tile_group)` → `kref_put` | 0xfffffffffffffff4 (write) |
| sys/dev/drm/drm_edid.c:5218-5220 | probe of any sink without the tile block (out_drop_ref) → same `kref_put` | 0xfffffffffffffff4 (write) |
| sys/dev/drm/drm_connector.c:387-389 | `drm_connector_cleanup()` at driver unload → `drm_mode_put_tile_group` | 0xfffffffffffffff4 (write) |
| sys/dev/drm/drm_connector.c:1489-1493 | `drm_connector_set_tile_property()` (DP MST path, drm_dp_mst_topology.c:1219/2566): `snprintf("%d", connector->tile_group->id)` | 0x4 (read: -12+offsetof(id)=+16) |
| sys/dev/drm/drm_fb_helper.c:2388 | fb-helper debug print `tile_group->id` | 0x4 (read) |

Once the EDID has been parsed once with a failing allocation, a panic is
essentially unavoidable: the next probe of the same monitor, the next probe of
a different monitor, driver unload, or (MST) the immediate property update all
hit a site.

## Trigger conditions

1. A sink whose EDID carries a DisplayID extension with a TILED_DISPLAY block
   (`drm_find_displayid_extension` → `drm_parse_display_id` → tag
   `DATA_BLOCK_TILED_DISPLAY`, drm_edid.c:5183-5186). Threat actor: malicious
   monitor / DP-MST dock / USB-C dock / VM virtual display.
2. The tile-group creation must fail at that instant: `kzalloc(GFP_KERNEL)` or
   `idr_alloc(GFP_KERNEL)` failure — i.e. kernel memory pressure, which an
   unprivileged local user can aggravate. (`drm_mode_get_tile_group` returning
   NULL for a first-seen topology is the normal case.)

No user account or ioctls needed beyond the DRM probe itself; the probe runs
from `drm_mode_getconnector` with count_modes==0 (fill_modes →
drm_helper_probe_single_connector_modes → drm_get_edid → drm_get_displayid).

## Why not reproduced on the QEMU guest

The standard audit guest has **no DRM device** (`/dev/dri` absent, no drm/i915
kmods; only QEMU's virtio-vga `vgapci0@pci0:0:2.0` — see env.txt). There is no
connector, no EDID path, and no way to inject an EDID from inside the guest.
The demonstration binary (`trigger.c`) therefore reproduces the *pointer
arithmetic* of the mismatch on the guest — the exact fault addresses the four
sites will compute — rather than the kernel fault. The chain itself is proven
statically with line-level citations and a port-divergence check against
upstream Linux v5.2.

## Fix

Return `NULL` on failure, restoring the upstream contract the caller expects
(fix.diff):

```diff
 	tg = kzalloc(sizeof(*tg), GFP_KERNEL);
 	if (!tg)
-		return ERR_PTR(-ENOMEM);
+		return NULL;
@@
 	} else {
 		kfree(tg);
-		tg = ERR_PTR(ret);
+		tg = NULL;
 	}
```

With NULL, the caller's `if (!tg) return -ENOMEM;` fires, the parse error
propagates to `drm_get_displayid`'s `out_drop_ref` which clears `has_tile` and
`tile_group` — the exact upstream behavior.

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
