drm_mode_create_tile_group() ERR_PTR vs NULL contract mismatch poisons connector->tile_group β wild-pointer kref_put/deref at four sites (kernel panic)
Summary
drm_mode_create_tile_group returns ERR_PTR(-ENOMEM)/ERR_PTR(ret) on its two failure paths, but its only caller drm_parse_tiled_block checks only 'if (!tg)' - the NULL contract upstream v5.2 actually implements. The ERR_PTR passes the check, is stored in connector->tile_group with has_tile already true, and is then kref_put or dereferenced by connector_cleanup, set_tile_property, and two drm_edid sites - deterministic page fault at 0xfffffffffffffff4 (write) or 0x4 (read). Trigger: a monitor/DP-MST dock/USB-C dock/VM virtual display presenting an EDID with a DisplayID TILED_DISPLAY block, probed while the tile-group alloc fails (memory pressure, aggravatable by an unprivileged local user); next probe/property update/unload panics. Demo shows exact fault addresses on the guest; kernel-level hardware-gated. Restore-NULL-contract fix (two tokens).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4194 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | β | 4.0 KB | view raw | |
| build.sh | β | 118 B | view raw | |
| run.sh | β | 104 B | view raw | |
| build.log | β | 197 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| env.txt | β | 289 B | view raw | |
| fix.diff | β | 479 B | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| README.md | β | 1.6 KB | β raw | |
| manifest.json | β | 1.5 KB | view raw | |
| verdict.json | β | 4.2 KB | view raw |
DF-4194 evidence pack β DRM tile-group ERR_PTR/NULL contract mismatch
Finding: drm_mode_create_tile_group() (sys/dev/drm/drm_connector.c:1917,1929) returns ERR_PTR on failure while its only caller drm_parse_tiled_block() (sys/dev/drm/drm_edid.c:5141) checks only "!tg" β the upstream-Linux-v5.2 NULL contract. The ERR_PTR is stored in connector->tile_group (drm_edid.c:5150) and later dereferenced/kref_put by four sites, panicking the kernel at fixed unmapped addresses (0xfffffffffffffff4 / 0x4).
Contents: trigger.c userspace demonstration of the mismatch + exact fault addresses (layout of struct drm_tile_group, ERR_PTR(-ENOMEM) arithmetic) build.sh build command (runs on the DFBSD guest; any unix works) run.sh run command build.log full untrimmed compiler output (guest: DragonFly cc/gcc) run.log full untrimmed output of the decisive run (guest) env.txt guest uname + DRM-absence proof (no /dev/dri, no drm kmod, only QEMU virtio-vga) fix.diff git-apply-able one-line-per-path fix vs sys/dev/drm/drm_connector.c (authored post-verification; never applied to sys/) VERDICT.md full narrative with line citations and crash-site table manifest.json / verdict.json machine-readable records
Expected output of ./run.sh: the ERR_PTR value 0xfffffffffffffff4 shown to pass the caller's NULL check, and the two computed fault addresses (0xfffffffffffffff4 for the kref_put sites, 0x4 for the ->id sites).
Status: untested at the kernel level on this guest (no DRM hardware β hardware-gated), source-level certain.
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
- A sink whose EDID carries a DisplayID extension with a TILED_DISPLAY block
(
drm_find_displayid_extensionβdrm_parse_display_idβ tagDATA_BLOCK_TILED_DISPLAY, drm_edid.c:5183-5186). Threat actor: malicious monitor / DP-MST dock / USB-C dock / VM virtual display. - The tile-group creation must fail at that instant:
kzalloc(GFP_KERNEL)oridr_alloc(GFP_KERNEL)failure β i.e. kernel memory pressure, which an unprivileged local user can aggravate. (drm_mode_get_tile_groupreturning 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):
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.
Fix verification
not_testablefix.diff restores the upstream v5.2 NULL contract (both failure paths return NULL); the caller's existing 'if (!tg) return -ENOMEM;' then fires and drm_get_displayid's out_drop_ref clears has_tile/tile_group exactly as upstream. git apply --check passes vs the read-only sys/ tree. Kernel-build validation was not performed: the guest has no DRM hardware, so neither baseline nor patched kernel can exercise the path; the change is the literal upstream fix.
findings/poc/DF-4194/fix.diff; upstream v5.2 source excerpt in VERDICT.md
Confirmed kernel references
Detail
Exploit chain
malicious monitor/dock/VM-display EDID with a DisplayID TILED_DISPLAY block + GFP_KERNEL alloc failure under (locally aggravatable) kernel memory pressure -> drm_get_edid -> drm_get_displayid -> drm_parse_tiled_block stores ERR_PTR in connector->tile_group -> next probe / sink change / unload / MST set_tile_property dereferences or kref_puts (struct drm_tile_group*)-12 -> page fault at 0xfffffffffffffff4 or 0x4 -> kernel panic (local DoS; fixed-address fault, no corruption primitive).
Evidence (decisive lines)
VERDICT.md crash-site table; run.log (guest-computed fault addresses 0xfffffffffffffff4 and 0x4 from the real struct layout); env.txt (no DRM device on guest); fix.diff (git apply --check clean vs sys/); upstream v5.2 comparison quoted in VERDICT.md.
PoC changes
No seed PoC existed; authored trigger.c from scratch. First guest build hit a missing
Verified recommended fix
Make drm_mode_create_tile_group() return NULL (not ERR_PTR) on both failure paths, matching upstream Linux v5.2 and the caller's existing NULL check.
Verdict
Source-certain ERR_PTR/NULL contract mismatch. drm_mode_create_tile_group() (sys/dev/drm/drm_connector.c:1917,1929) returns ERR_PTR on allocation failure, but its only caller drm_parse_tiled_block() (sys/dev/drm/drm_edid.c:5141) checks only 'if (!tg)' β the NULL contract upstream Linux v5.2 actually implements (verified against git.kernel.org v5.2: both failure paths return NULL). The ERR_PTR passes the check and is stored as connector->tile_group (drm_edid.c:5150) with connector->has_tile already true (drm_edid.c:5120). Four later sites dereference or kref_put the poison pointer at fixed unmapped addresses (0xfffffffffffffff4 write via kref_put at drm_edid.c:5148/5218 and drm_connector.c:388; 0x4 read via ->id at drm_connector.c:1490 and drm_fb_helper.c:2388) -> deterministic kernel page fault/panic on the next EDID probe, sink change, MST property update, or driver unload. Kernel-level reproduction was not possible on the audit guest: it has no DRM device at all (/dev/dri absent, no drm/i915 kmods, only QEMU virtio-vga β env.txt), so the EDID/DisplayID path cannot be exercised; trigger.c instead demonstrates the exact pointer arithmetic on the guest (run.log).
No comments yet.