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

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->extensions is u8 0..255 (drm_edid.h:317)
  • EDID_LENGTH is 128 (drm_edid.h:32)
  • raw_edid is declared uint8_t raw_edid[DC_MAX_EDID_BUFFER_SIZE] with DC_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 amdgpu with 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.bin in the firmware search path, set drm.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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1837 Β· 10 files
FileTypeDescriptionSize
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
README.md readme human-facing PoC README
↓ download 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 amdgpu with Display Manager enabled (default).
  • One of:
  • Physical access (any user): an EDID-emulator dummy plug flashed with crafted_edid.bin plugged into any HDMI/DVI/DP/USB-C-DP-alt port.
  • Root: drm.edid_firmware=<name> pointing at crafted_edid.bin in 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.

VERDICT.md verdict full verification narrative
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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)
↓ fix.diffDragonFly 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 19:12:20 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

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