NULL pointer dereference in dm_dp_mst_get_modes via unchecked dc_link_add_remote_sink return
- File:
sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c - Lines: 237β246 (deref at 243, too-late guard at 246)
- Severity: Medium
- CVSS 3.1:
CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - CWE: CWE-476 NULL Pointer Dereference
- Confidence: certain
- Status: new
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.0x04or0xFF) - Byte
0x7fadjusted so the 1-byte checksum over the 128-byte block is0 - Other 128*(extensions) bytes need not be valid blocks β
dc_link_add_remote_sinkrejects 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.
Recommended fix
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
sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm_mst_types.c:237-246β the buggy derefsys/dev/drm/amd/display/dc/core/dc.c:1765-1821βdc_link_add_remote_sinkNULL-return pathssys/dev/drm/drm_edid.c:1649-1653β EDIDextensionsfield accepted verbatim with no capsys/dev/drm/amd/display/dc/dc_types.h:98βDC_MAX_EDID_BUFFER_SIZE == 512
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1966 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
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.
Recommended fix
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 --checkon 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
- 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
- _
- m
- s
- t
- _
- t
- y
- p
- e
- s
- .
- c
- :
- 2
- 3
- 7
- -
- 2
- 4
- 6
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.
No comments yet.