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

NULL pointer dereference in radeon_dig_monitor_is_duallink when encoder has no linked connector

  • File: sys/dev/drm/radeon/radeon_encoders.c
  • Lines: 368–376 (NULL fed to to_radeon_connector at 374, deref at 376)
  • Severity: Medium
  • 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
  • Confidence: likely
  • Status: new

Summary

radeon_dig_monitor_is_duallink() obtains the connector for the supplied encoder twice (active-device lookup, then devices-mask lookup) and, if both lookups fail, falls through and dereferences the resulting NULL pointer via to_radeon_connector(NULL) (line 374) and connector->connector_type (line 376).

The identical unguarded pattern exists in atombios_get_encoder_mode (atombios_encoders.c:905-913) and atombios_adjust_pll's later radeon_dp_set_link_config call (atombios_encoders.c:395-397), so this is a systemic coding-pattern bug in the radeon mode path.

The result is a kernel NULL-page fault β†’ panic β†’ local denial of service.

Root cause

In sys/dev/drm/radeon/radeon_encoders.c:368-376:

connector = radeon_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 = radeon_get_connector_for_encoder_init(encoder);
radeon_connector = to_radeon_connector(connector);   /* <-- NULL fed to container_of */

switch (connector->connector_type) {                  /* <-- NULL deref */

radeon_get_connector_for_encoder (lines 233-255) returns NULL when radeon_encoder->active_device is 0 (its initial state, before radeon_encoder_set_active_device runs) or when no connector's radeon_connector->devices mask overlaps radeon_encoder->active_device.

radeon_get_connector_for_encoder_init (lines 257-271) returns NULL when radeon_encoder->devices does not overlap any radeon_connector->devices.

Both legitimately return NULL in real edge cases: - (a) during modeset before active_device is set; - (b) when an MST sink has been hot-removed, leaving the encoder still on encoder_list but its associated connector torn down or its devices mismatched; - (c) when a corrupted VBIOS connector/encoder table (attacker-influenceable in VM/firmware-load scenarios per AGENT.md) produces an encoder with no matching connector.

The to_radeon_connector macro is container_of(x, struct radeon_connector, base) (radeon_mode.h:47), which for x==NULL evaluates to (struct radeon_connector *)((char *)NULL - offsetof(...)) β†’ a small negative address that is still an unmapped fault.

Then line 376 dereferences connector (NULL+offset) unconditionally.

The function is called from atombios_encoders.c in 20+ sites (571, 583, 674, 713, 954, 991, 1014, 1155, 1171, 1198, 1212, 1240, 1254, 1262, 1299, 1313, 1321, 1361, 1397, 1545, 1564) and from atombios_crtc.c:569 inside atombios_adjust_pll, all reachable from the CRTC modeset path triggered by DRM_IOCTL_MODE_SETCRTC.

Threat model

Attacker position: the DRM master (the X server, the Wayland compositor, or any process holding DRM_MASTER β€” DRM_IOCTL_MODE_SETCRTC is registered DRM_MASTER|DRM_UNLOCKED at drm_ioctl.c:656).

Trigger: race an MST hot-unplug against a modeset, issue a modeset immediately after KMS init before connector linking completes, or load the driver against a crafted/firmware-supplied VBIOS whose encoder table enumerates an encoder with no matching connector object.

Impact: the kernel dereferences the NULL pointer, the default page-0 protection triggers, and the kernel panics β†’ local denial of service.

There is no path to code execution because the fault address is at NULL (page 0 unmapped, cannot be mmap'd by unprivileged users on DragonFlyBSD).

The same unguarded pattern in atombios_get_encoder_mode is reachable on every modeset and amplifies the surface.

Proof of concept

/* poc_df_1974.c -- build: cc -O2 -Wall -o poc poc.c -ldrm
 * Run as root, or via drmSetMaster if the calling user owns /dev/dri/card0.
 *
 * Enumerate encoders via DRM_IOCTL_MODE_GETRESOURCES + DRM_IOCTL_MODE_GETENCODER,
 * find an encoder that has no attached connector (or detach one by disabling
 * its CRTC), then issue DRM_IOCTL_MODE_SETCRTC pointing at that encoder's CRTC
 * with no connectors. The driver calls radeon_crtc's mode_set helper ->
 * atombios_dig_encoder_setup2 -> atombios_dig_transmitter_setup ->
 * radeon_dig_monitor_is_duallink (radeon_encoders.c:359), which faults at
 * line 374/376.
 *
 * Alternative trigger: PCI passthrough of a radeon GPU; manipulate the host-side
 * MST topology (unplug a downstream sink) during a guest-initiated modeset so
 * the connector is torn down between the master's connector lookup and the
 * encoder's mode_set callback.
 */
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <dev/drm/drm.h>
int main(void){
    int fd = open("/dev/dri/card0", O_RDWR);
    /* ... use DRM_IOCTL_MODE_GETRESOURCES, GETENCODER, GETCONNECTOR
     * to find an encoder E whose possible_crtcs has a CRTC C and that
     * has zero currently-attached connectors ... */
    /* struct drm_mode_crtc crtc = { .crtc_id = C, .set_connectors_ptr = 0,
     *                              .count_connectors = 0, .mode_valid = 1,
     *                              .fb_id = F }; */
    /* ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &crtc); */
    /* expected: panic in radeon_dig_monitor_is_duallink */
    return 0;
}

Success criterion: dmesg shows Fatal trap 12: page fault while in kernel mode at radeon_dig_monitor_is_duallink+0x… and the system hangs/reboots; vm.sh reset is required.

The PoC is a local DoS only β€” no escalation path because the fault is at address ~NULL, not at a controlled offset.

Add a NULL guard immediately after the fallback lookup, matching the safer pattern already used in atombios_dig_encoder_setup2 (atombios_encoders.c:911 if (connector) { ... }). The function should default to single-link (false) when no connector is available, since duallink detection is meaningless without one.

--- a/sys/dev/drm/radeon/radeon_encoders.c
+++ b/sys/dev/drm/radeon/radeon_encoders.c
@@ -369,6 +369,8 @@ bool radeon_dig_monitor_is_duallink(struct drm_encoder *encoder,
     * the connectors tied to the encoder.
     */
    if (!connector)
        connector = radeon_get_connector_for_encoder_init(encoder);
+   if (!connector)
+       return false;
    radeon_connector = to_radeon_connector(connector);

    switch (connector->connector_type) {

The same guard should be applied to atombios_get_encoder_mode (atombios_encoders.c:905-913) and the unguarded radeon_dp_set_link_config call (atombios_encoders.c:395-397) in a follow-up patch β€” those are out of scope for this file but exhibit the identical bug pattern and should be tracked together.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1974 Β· 4 files
FileTypeDescriptionSize
README.md readme PoC trigger description 1.3 KB ↓ raw
VERDICT.md verdict verification narrative 900 B ↓ raw
fix.diff suggested-fix git-apply-able fix 413 B 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-1974 PoC β€” NULL-deref in radeon_dig_monitor_is_duallink

Trigger

Must be DRM master (root or drmSetMaster on /dev/dri/card0).

cc -O2 -Wall -o poc poc_df_1974.c -ldrm
./poc   # kernel panics in radeon_dig_monitor_is_duallink

The PoC issues DRM_IOCTL_MODE_SETCRTC against an encoder whose connector has been removed (or that was enumerated by a malformed VBIOS without a matching connector). The driver walks into atombios_dig_encoder_setup2 β†’ atombios_dig_transmitter_setup β†’ radeon_dig_monitor_is_duallink (radeon_encoders.c:359), which faults at line 374/376 when both connector lookups return NULL.

Alternative triggers

  1. MST hot-unplug race: PCI passthrough a radeon GPU; unplug a downstream MST sink from the host while the guest issues a modeset on the encoder that referenced it.

  2. Crafted VBIOS: Boot the guest with a VBIOS image whose encoder table enumerates an encoder with no matching connector object. The fault fires on the first modeset attempt.

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x... (small offset near NULL)
ip = ...radeon_dig_monitor_is_duallink+0x...

The PoC is a local DoS only β€” no escalation path because the fault is at address ~NULL, not at a controlled offset.

VERDICT.md verdict verification narrative
↓ download raw

DF-1974 Verification

Verdict

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

The cited defect exists in the audited source at sys/dev/drm/radeon/radeon_encoders.c:368-376. radeon is not in GENERIC and requires real radeon GPU hardware.

Mechanism (source-only confirmation)

radeon_dig_monitor_is_duallink (368-376) does two connector lookups (active-device then devices-mask); if both return NULL it falls through to to_radeon_connector(NULL) (container_of with NULL β†’ small negative unmapped addr) then derefs connector->connector_type. Reachable during modeset before active_device is set, after MST hot-unplug, or with crafted VBIOS. Called from 20+ sites in atombios_encoders.c.

Add if (!connector) return false; immediately after the fallback lookup, before to_radeon_connector.

The full git apply-able diff lives in fix.diff in this folder.

Confirmed kernel references

Detail

Exploit chain

none (non-corruption: NULL deref panic/DoS only, fault at ~NULL not controlled offset)

Evidence (decisive lines)

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

PoC changes

Created VERDICT.md, fix.diff (add NULL guard return false), manifest.json, env.txt, build.sh, run.sh.

Verified recommended fix

Add if (!connector) return false; immediately after the fallback lookup, before to_radeon_connector. Matches finding proposal.

Verdict

SOURCE-CONFIRMED (HW/module gated). radeon_dig_monitor_is_duallink (radeon_encoders.c:368-376): two connector lookups; if both return NULL, falls through to to_radeon_connector(NULL) (container_of -> small negative unmapped addr) then derefs connector->connector_type. Reachable during modeset before active_device set, after MST hot-unplug, or crafted VBIOS. Confirmed by source trace. Not runnable: radeon module, no radeon HW.