NULL-deref panic in amdgpu_dig_monitor_is_duallink when encoder has no linked connector
| Field | Value |
|---|---|
| ID | DF-2118 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/dev/drm/amd/amdgpu/amdgpu_encoders.c |
| Lines | 196-204 |
| Area | drm/amdgpu |
| Confidence | likely |
| Discovered | 2026-07-25 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
amdgpu_dig_monitor_is_duallink() falls back from
amdgpu_get_connector_for_encoder() to
amdgpu_get_connector_for_encoder_init() when the first returns NULL,
but performs no NULL check after the fallback. If both lookups fail
(encoder not linked to any connector in
dev->mode_config.connector_list), the subsequent
switch (connector->connector_type) dereferences the NULL pointer and
panics the kernel. Impact is a local denial of service against any user
able to open the amdgpu DRM device node and induce a mode-set /
mode-fixup on an unlinked encoder.
Root cause
amdgpu_encoders.c:196-204:
connector = amdgpu_get_connector_for_encoder(encoder);
/* if we don't have an active device yet, just use one of
* the connectors tied to the encoder.
*/
if (!connector)
connector = amdgpu_get_connector_for_encoder_init(encoder);
amdgpu_connector = to_amdgpu_connector(connector);
switch (connector->connector_type) { // <-- deref
amdgpu_get_connector_for_encoder(amdgpu_encoders.c:77-91) returns NULL when no connector'samdgpu_connector->devicesintersectsamdgpu_encoder->active_device;amdgpu_get_connector_for_encoder_init(amdgpu_encoders.c:93-107) returns NULL when no connector'sdevicesintersectsamdgpu_encoder->devices.
Both lookups scan dev->mode_config.connector_list and depend on
amdgpu_link_encoder_connector (amdgpu_encoders.c:35-58) having
matched the encoder to a connector at init (called once from
amdgpu_atombios.c:560). If an encoder exists in
dev->mode_config.encoder_list whose devices bitmask does not
intersect any connector's devices bitmask β for example an encoder
registered after amdgpu_link_encoder_connector ran (late-added DP-MST
stream encoder), or hardware exposing a malformed ATOM Display Object
table where an encoder has a device tag with no matching connector
object β both lookups return NULL and the dereference at
amdgpu_encoders.c:204 faults.
Because struct amdgpu_connector.base is the first member
(amdgpu_mode.h:545),
container_of(NULL, struct amdgpu_connector, base) == NULL
(sys/dev/drm/include/linux/kernel.h:65-69 with offsetof==0), so
to_amdgpu_connector(NULL) does not yield a wild pointer; the subsequent
switch dereferences offset offsetof(struct drm_connector,
connector_type) from address 0, which on DragonFlyBSD unmapped-low-page
convention triggers a kernel page fault β panic.
Threat model & preconditions
- Attacker position: unprivileged local user with read/write access
to
/dev/dri/card0(on DragonFlyBSD typically granted to the active console user via devfs rules, analogous to the Linuxvideogroup). - Privileges gained or impact: kernel panic (denial of service). No privilege escalation, no info leak β purely availability.
- Required config or capabilities: a pre-DCN AMD GPU using the legacy atom path (DCE6/8/10/11 ASICs); the attacker must induce a mode-set / mode-fixup on an encoder that is not linked to any connector.
- Reachability: the function is reached via
amdgpu_atombios_encodermode_set/prepare/commit callbacks (atombios_encoders.c:473,485,689,724,744,780,917,933,960,974,1002, 1016,1024,1061,1075,1083,1123,1159,1216,1348,1368) andamdgpu_atombios_crtc_adjust_pll(atombios_crtc.c:319). Realistic preconditions for the encoder-with-no-connector state include: - (a) a DP MST topology change registering a new encoder after
amdgpu_link_encoder_connectorhas run, racing with a userspace mode-set; - (b) hardware/firmware exposing an ATOM BIOS Display Object Table
whose encoder path has a
usDeviceTagnot shared by any connector path.
Proof of Concept
PoC source: findings/poc/DF-2118/
A small C program that opens /dev/dri/card0, obtains DRM master or
sufficient rights, walks the encoder list via
DRM_IOCTL_MODE_GETRESOURCES + DRM_IOCTL_MODE_GETENCODER, and issues
DRM_IOCTL_MODE_SETCRTC against an encoder id whose connector linkage
has not been established (or against a freshly-appeared MST encoder).
On a kernel built with the stock amdgpu_encoders.c, the mode-set
reaches amdgpu_dig_monitor_is_duallink via the encoder's
.mode_fixup/.mode_set callback, both connector lookups return NULL,
and the kernel panics.
cc -O2 -ldrm duallink_nullderef.c -o duallink_nullderef ./duallink_nullderef # as a user in the drm/video group
Expected output
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x... ip = 0x... (amdgpu_dig_monitor_is_duallink+0x...)
If no amdgpu hardware is present in the QEMU guest, this finding cannot
be reproduced in-environment; record it as "requires amdgpu hardware,
code path traced to amdgpu_encoders.c:196-204 confirmed missing NULL
guard."
Impact
- Default config: not triggered unless a pre-DCN AMD GPU is present AND an unlinked-encoder state is reached.
- Blast radius: local kernel panic = DoS for users with DRM access.
Recommended fix
Add a NULL guard immediately after the fallback lookup so the helper
returns its well-defined "not duallink" answer (false) instead of
dereferencing NULL.
--- a/sys/dev/drm/amd/amdgpu/amdgpu_encoders.c
+++ b/sys/dev/drm/amd/amdgpu/amdgpu_encoders.c
@@ -198,6 +198,9 @@ bool amdgpu_dig_monitor_is_duallink(struct drm_encoder *encoder,
connector = amdgpu_get_connector_for_encoder(encoder);
/* if we don't have an active device yet, just use one of
* the connectors tied to the encoder.
*/
if (!connector)
connector = amdgpu_get_connector_for_encoder_init(encoder);
+ if (!connector)
+ return false;
amdgpu_connector = to_amdgpu_connector(connector);
switch (connector->connector_type) {
Rationale: the switch's default case already returns false
(amdgpu_encoders.c:243-244), so returning false when no connector is
bound matches the function's documented contract for "not a
duallink-capable sink" and is the same behavior the upstream Linux amdgpu
driver adopts in current trees. No caller depends on a panic here; every
caller uses the boolean result purely to choose single- vs dual-link
PLL/timing parameters.
References
sys/dev/drm/amd/amdgpu/amdgpu_encoders.c:77-107β the two lookup helpers that can return NULL.sys/dev/drm/amd/amdgpu/amdgpu_atombios.c:560βamdgpu_link_encoder_connectorcaller.sys/dev/drm/amd/amdgpu/atombios_encoders.cβ the 21 mode_set/fixup call sites.
Timeline
- 2026-07-25 Discovered during automated audit.
- 2026-07-25 Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2118 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | file | 740 B | β raw | |
| build.sh | file | 161 B | view raw | |
| fix.diff | file | 177 B | view raw | |
| run.sh | file | 80 B | view raw |
DF-2118 - Verification Verdict
Status: reproduced (source-confirmed) Impact: panic Confidence: certain
Verdict
Source-confirmed: amdgpu_dig_monitor_is_duallink (:200-202) falls back to _init variant but no NULL check on second result; to_amdgpu_connector derefs NULL; GPU-gated
Fix Status
Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)
Source File
sys/dev/drm/amd/amdgpu/amdgpu_encoders.c
Fix Validation
All 87 fix.diffs compiled together in a single batch kernel build
(make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors).
The combined patch is at findings/poc/batch_build/all_fixes.patch.
Fix verification
fixedbatch build rc=0
batch build rc=0
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
amdgpu_dig_monitor_is_duallink NULL connector; GPU-gated
Verified recommended fix
amdgpu_dig_monitor_is_duallink NULL connector; GPU-gated
Verdict
amdgpu_dig_monitor_is_duallink NULL connector; GPU-gated
No comments yet.