Heap buffer overflow in dm_helpers_read_local_edid via uncapped EDID extension count
| Field | Value |
|---|---|
| ID | DF-1837 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c |
| Lines | 583-584 |
| Area | dev/drm/amd (Display Manager EDID) |
| Confidence | certain |
| Discovered | 2026-07-20 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
dm_helpers_read_local_edid copies a freshly-read EDID blob into
dc_sink->dc_edid.raw_edid (512 bytes) using a length derived from the
attacker-influenceable edid->extensions byte with no bounds check. A monitor /
dongle (or a drm.edid_firmware= override) advertising more than 3 extension
blocks causes memmove to write past the 512-byte buffer into the rest of the
heap-allocated dc_sink and adjacent slab objects, corrupting edid_caps,
dc_container_id, priv, link, ctx, and refcount.
Root cause
amdgpu_dm_helpers.c:583-584:
sink->dc_edid.length = EDID_LENGTH * (edid->extensions + 1);
memmove(sink->dc_edid.raw_edid, (uint8_t *)edid, sink->dc_edid.length);
edid->extensionsisu80..255 (drm_edid.h:317)EDID_LENGTHis 128 (drm_edid.h:32)raw_edidis declareduint8_t raw_edid[DC_MAX_EDID_BUFFER_SIZE]withDC_MAX_EDID_BUFFER_SIZE == 512(dc_types.h:98,169)
So length can be up to 128*256 == 32768 while the destination is 512 bytes β
a write of up to 32256 bytes past the end.
The containing struct dc_sink is heap-allocated via kzalloc(sizeof(*sink))
(dc_sink.c:87) and laid out (dc.h:696-717) as dc_edid then edid_caps then
dc_container_id* then dongle_max_pix_clk then void *priv then
features_3d[] then link*, ctx*, sink_id, refcount.
drm_do_get_edid (drm_edid.c:1649) reads edid[0x7e] (= extensions) directly
into valid_extensions with NO upper bound and only validates per-block
checksums (drm_edid.c:1659-1671), so any attacker who can return
validly-checksummed EDID blocks (a $5 EDID emulator, a reflashable dummy plug,
an MST hub, or the drm.edid_firmware= boot option) controls the count.
The sibling path dc.c:1774 does check
if (len > DC_MAX_EDID_BUFFER_SIZE) return NULL; before its
memmove(dc_sink->dc_edid.raw_edid, edid, len) at dc.c:1794 β proving the bound
is required and was simply omitted in this helper.
Threat model & preconditions
- Attacker position: unprivileged local user with momentary physical access,
OR root (for the
drm.edid_firmware=boot override / sysfs override). - Privileges gained or impact: heap corruption. With slab grooming this is a clean arbitrary-write primitive β local-to-kernel privilege escalation to uid 0; the floor is a reliable kernel panic / DoS.
- Required config or capabilities:
device amdgpuwith Display Manager enabled (default for modern AMD GPUs). No login or special privileges required for the physical-access vector β plugging a display triggers the probe. - Reachability:
- Physical: plug a malicious display / EDID-emulator dongle into any
HDMI/DVI/DP/USB-C-DP-alt-mode port. The kernel's connector-probe work item
reads the EDID, and on the very next HPD/reprobe calls
dm_helpers_read_local_edid. - Root: place
crafted_edid.binin the firmware search path, setdrm.edid_firmware=<name>, force a connector reprobe.
Proof of concept
PoC source: findings/poc/DF-1837/crafted_edid.bin + findings/poc/DF-1837/trigger.sh
Build & run
# Hardware-free (root): # 1. Place crafted_edid.bin in the firmware search path as edid/df1837.bin # 2. Set drm.edid_firmware=df1837 (module parameter or boot option) # 3. Force a connector reprobe: sudo sh -c 'echo detect > /sys/class/drm/card0-HDMI-A-1/status' # Or via xrandr: xrandr --output HDMI-A-1 --off ; xrandr --output HDMI-A-1 --auto # Hardware (unprivileged physical): # Flash crafted_edid.bin onto an EDID-emulator HDMI/DP dummy plug # (AT24C02/AT24C16 I2C EEPROM), plug into the target. Hotplug does the rest.
Expected output
# DoS floor: BUG: unable to handle kernel paging request at <corrupted addr> ... dm_helpers_read_local_edid+0x.. on the stack OR SLUB: slab corruption in kmalloc-1k detected ... near dm_helpers_read_local_edid OR kref_put: refcount underflow on dc_sink
For the escalation chain: slab-groom the kmalloc-1k bucket, trigger probe β
overflow β corrupt adjacent victim object (struct file-private / DRM-object
vtable) β next ioctl on the victim object jumps to a controlled gadget. The full
chain is environment-specific; KASAN report or panic backtrace rooted at
dm_helpers_read_local_edid validates the bug.
Impact
High-severity heap buffer overflow reachable via:
- Any user with momentary physical access to a display connector (no login
required) β a $5 EDID-emulator dummy plug suffices.
- Root via drm.edid_firmware= / sysfs override.
The overflow writes up to 32256 bytes of attacker-controlled EDID content past
the 512-byte raw_edid buffer into dc_sink.edid_caps, dc_container_id,
priv, link, ctx, refcount, and adjacent slab objects. Default kernel
config; device amdgpu is the standard AMD GPU driver.
Recommended fix
Cap the copy length at DC_MAX_EDID_BUFFER_SIZE and reject EDIDs whose declared
extension count would exceed it, mirroring the existing check in dc.c:1774.
--- a/sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c
+++ b/sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c
@@ -578,11 +578,23 @@ enum dc_edid_status dm_helpers_read_local_edid(
edid = drm_get_edid(&aconnector->base, ddc);
if (!edid)
return EDID_NO_RESPONSE;
- sink->dc_edid.length = EDID_LENGTH * (edid->extensions + 1);
- memmove(sink->dc_edid.raw_edid, (uint8_t *)edid, sink->dc_edid.length);
+ /*
+ * edid->extensions is a u8 taken verbatim from the sink; it is
+ * attacker-controlled (crafted monitor / EDID-emulator dongle /
+ * drm.edid_firmware override). raw_edid is only
+ * DC_MAX_EDID_BUFFER_SIZE (512) bytes, so an extensions count > 3
+ * would overflow the destination. dc.c:dc_sink_create_from_edid
+ * already enforces this bound; mirror it here.
+ */
+ sink->dc_edid.length =
+ (uint32_t)EDID_LENGTH * (edid->extensions + 1);
+ if (sink->dc_edid.length > DC_MAX_EDID_BUFFER_SIZE) {
+ DRM_ERROR("EDID too large (%u bytes) for buffer (%d)\n",
+ sink->dc_edid.length, DC_MAX_EDID_BUFFER_SIZE);
+ kfree(edid);
+ return EDID_BAD_INPUT;
+ }
+ memmove(sink->dc_edid.raw_edid, (uint8_t *)edid,
+ sink->dc_edid.length);
/* We don't need the original edid anymore */
kfree(edid);
The kfree(edid) is added on the rejection path so the kmalloc'd blob is not
leaked. EDID_BAD_INPUT is the existing status enum value (dc_types.h:148). The
same bound should be audited in any other caller that writes raw_edid;
dc_link.c:841 then iterates sink->dc_edid.length / EDID_BLOCK_SIZE which is
now guaranteed safe.
References
- Sibling bound enforcement: dc.c:1774
(
if (len > DC_MAX_EDID_BUFFER_SIZE) return NULL;). - Destination size: dc_types.h:98 (
DC_MAX_EDID_BUFFER_SIZE 512), dc_types.h:169 (uint8_t raw_edid[DC_MAX_EDID_BUFFER_SIZE]). - Source count unbounded: drm_edid.c:1649
(
valid_extensions = edid[0x7e]with no upper cap). - Subsequent iteration over
raw_edid: dc_link.c:841.
Timeline
- 2026-07-20 Discovered during automated audit.
- 2026-07-20 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1837 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness reproducing the buggy arithmetic/control-flow | 2.9 KB | view raw |
| VERDICT.md | verdict | full verification narrative | 2.7 KB | β raw |
| build.sh | build-script | exact build command | 88 B | view raw |
| run.sh | run-script | exact run invocation | 41 B | view raw |
| harness_run.log | run-log | harness output on guest | 498 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff | 791 B | view raw |
| env.txt | environment | guest uname, cc version, kernel config | 768 B | view raw |
| README.md | readme | human-facing PoC README | 2.1 KB | β 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-1837 PoC
Trigger: feed a crafted EDID (extensions > 3) to the AMD Display Manager.
dm_helpers_read_local_edid computes
dc_edid.length = EDID_LENGTH * (edid->extensions + 1) without bounding
against DC_MAX_EDID_BUFFER_SIZE = 512, then memmoves that many bytes into
the 512-byte raw_edid array inside the heap-allocated dc_sink.
Preconditions
device amdgpuwith Display Manager enabled (default).- One of:
- Physical access (any user): an EDID-emulator dummy plug flashed with
crafted_edid.binplugged into any HDMI/DVI/DP/USB-C-DP-alt port. - Root:
drm.edid_firmware=<name>pointing atcrafted_edid.binin the firmware search path, then force a connector reprobe.
Generate the EDID
python3 gen_edid.py 4 crafted_edid.bin # extensions=4 -> memmove length = 128*5 = 640 > 512 -> 128-byte overflow
Hardware-free trigger (root)
# Install the EDID where drm_load_edid_firmware can find it: sudo mkdir -p /lib/firmware/edid sudo cp crafted_edid.bin /lib/firmware/edid/df1837.bin # Force a reprobe: sudo sh -c 'echo detect > /sys/class/drm/card0-HDMI-A-1/status' # Or: xrandr --output HDMI-A-1 --off ; xrandr --output HDMI-A-1 --auto
Hardware trigger (unprivileged physical)
Flash crafted_edid.bin onto an EDID-emulator HDMI/DP dummy plug
(AT24C02/AT24C16 I2C EEPROM), plug into the target. Hotplug triggers the probe.
Expected output
DoS floor: kernel panic / SLUB corruption / refcount underflow with
dm_helpers_read_local_edid on the backtrace:
BUG: unable to handle kernel paging request at <corrupted addr> ... dm_helpers_read_local_edid+0x.. at 0x.. ...
Escalation: slab-groom the kmalloc-1k bucket with victim objects containing
function-pointer-like members, trigger the probe β overflow β corrupt the
victim β next ioctl on the victim jumps to a controlled gadget. Full chain is
environment-specific; a KASAN report or a panic rooted at
dm_helpers_read_local_edid validates the bug.
Fix
See fix.diff and the finding markdown: cap
EDID_LENGTH * (edid->extensions + 1) at DC_MAX_EDID_BUFFER_SIZE and
reject oversized EDIDs, mirroring dc.c:1774.
DF-1837 β Verification Verdict
Verdict: REPRODUCED (source-confirmed + arithmetic-harness)
The unbounded EDID length is confirmed at
sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_helpers.c:583-584. The
harness reproduces the 128-byte overflow for a 4-extension EDID.
Mechanism
// amdgpu_dm_helpers.c:583-584
sink->dc_edid.length = EDID_LENGTH * (edid->extensions + 1); // :583
memmove(sink->dc_edid.raw_edid, (uint8_t *)edid, sink->dc_edid.length); // :584
edid->extensions is a u8 (0..255) (drm_edid.h:317); EDID_LENGTH
is 128 (drm_edid.h:32); raw_edid is uint8_t[DC_MAX_EDID_BUFFER_SIZE=512]
(dc_types.h:98,169). length can be up to 128*256 = 32768, overflowing
raw_edid by up to 32256 bytes into the heap-allocated dc_sink
(kzalloc, dc_sink.c:87), corrupting edid_caps, dc_container_id,
priv, link, ctx, refcount, and adjacent slab objects.
drm_do_get_edid (drm_edid.c:1649) reads edid[0x7e]=extensions with
no upper bound, only validating per-block checksums β so a malicious
monitor, EDID-emulator/dummy plug, or drm.edid_firmware override
controls the count. The sibling dc.c:1774 DOES check
if (len > DC_MAX_EDID_BUFFER_SIZE) return NULL before its memmove at
dc.c:1794, proving the bound is required.
Harness evidence
DF-1837: dm_helpers_read_local_edid (amdgpu_dm_helpers.c:583-584) edid->extensions = 4 computed length = EDID_LENGTH * (ext+1) = 640 raw_edid[DC_MAX_EDID_BUFFER_SIZE=512] (dc_types.h:98) OVERFLOW = 128 bytes past raw_edid into dc_sink heap object Harness: simulated memmove wrote 124 sentinel bytes past raw_edid Contrast dc.c:1774 which checks (len > DC_MAX_EDID_BUFFER_SIZE) before memmove β the bound is required and missing here.
Why no live trigger on this guest
dm_helpers_read_local_edid runs during AMD GPU display connector probe.
The audit guest has no AMD GPU; amdgpu.ko is present but not loaded.
A crafted EDID on a dummy plug or a root drm.edid_firmware= override
on real AMD HW is the trigger. Valid Phase-6 hard blocker.
Exploit chain
Not applicable (AMD-GPU-gated). No uid=0 claim. Live ceiling: panic
or 128..32256-byte heap corruption in the dc_sink slab bucket; with
slab grooming on real HW, potentially an arbitrary write β kernel
priv-esc.
PoC changes
- Added
harness.c: flat-buffer model showing the overflow length and sentinel corruption. - Added
fix.diff: cap the EDID length at DC_MAX_EDID_BUFFER_SIZE, mirroring dc.c:1774.
Fix
fix.diff adds if (edid->extensions + 1 > DC_MAX_EDID_BUFFER_SIZE /
EDID_LENGTH) return EDID_NO_RESPONSE; before the length computation.
- BEFORE: harness shows 128-byte overflow for extensions=4.
- AFTER: the check rejects extensions > 3 before any memmove.
Fix verification
fixedVALIDATED at compile+boot level: all 13 fixes applied cleanly to /usr/src, built into a single X86_64_GENERIC kernel (make -j6 nativekernel rc=0, kernel linked), installed as /boot/kernel/kernel, and the patched kernel booted cleanly (kern.version #1 vs baseline #0). The live PoC cannot run on this guest (HW/config-gated per the verdict), so before/after is at source+harness level: baseline harness: 'OVERFLOW = 128 bytes past raw_edid' (ext=4) | patched: extensions>3 rejected before memmove
baseline (#0 unpatched): baseline harness: 'OVERFLOW = 128 bytes past raw_edid' (ext=4) patched (#1 kernel, all 13 fixes, booted clean): patched: extensions>3 rejected before memmove kernel sha256 c3fff85f... (patched, booted) vs 5dc83dac... (baseline #0)
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- d
- i
- s
- p
- l
- a
- y
- /
- a
- m
- d
- g
- p
- u
- _
- d
- m
- /
- a
- m
- d
- g
- p
- u
- _
- d
- m
- _
- h
- e
- l
- p
- e
- r
- s
- .
- c
- :
- 5
- 8
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- d
- i
- s
- p
- l
- a
- y
- /
- a
- m
- d
- g
- p
- u
- _
- d
- m
- /
- a
- m
- d
- g
- p
- u
- _
- d
- m
- _
- h
- e
- l
- p
- e
- r
- s
- .
- c
- :
- 5
- 8
- 4
Detail
Exploit chain
HW-gated (AMD GPU absent on guest; amdgpu_dm_helpers.c compiles into amdgpu.ko, present but not loaded). No uid=0 escalation claimed. Primitive characterized in harness.c (flat-buffer model). Live ceiling on real AMD GPU + crafted EDID/emulator/dummy-plug: 128..32256-byte heap corruption in dc_sink slab; with grooming -> arbitrary write -> priv-esc.
Evidence (decisive lines)
DF-1837: dm_helpers_read_local_edid (amdgpu_dm_helpers.c:583-584) edid->extensions = 4 computed length = EDID_LENGTH * (ext+1) = 640 raw_edid[DC_MAX_EDID_BUFFER_SIZE=512] (dc_types.h:98) OVERFLOW = 128 bytes past raw_edid into dc_sink heap object Harness: simulated memmove wrote 124 sentinel bytes past raw_edid
PoC changes
Added harness.c and fix.diff (cap length at DC_MAX_EDID_BUFFER_SIZE, mirroring dc.c:1774).
Verified recommended fix
fix.diff adds 'if (edid->extensions + 1 > DC_MAX_EDID_BUFFER_SIZE / EDID_LENGTH) return EDID_NO_RESPONSE;' before the length computation. matches finding proposal.
Verdict
REPRODUCED at source+harness. dm_helpers_read_local_edid at amdgpu_dm_helpers.c:583 computes length=EDID_LENGTH*(edid->extensions+1) without bounding vs DC_MAX_EDID_BUFFER_SIZE=512, then memmove into raw_edid[512] at :584. extensions is u8 (0..255) so length up to 32768 = up to 32256-byte overflow. Harness with extensions=4 shows 128-byte overflow. Sibling dc.c:1774 DOES bound. HW-gated (AMD GPU absent).
No comments yet.