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

NULL pointer dereference in amdgpu_dm_atomic_commit_tail() via update_stream_scaling_settings() with NULL dc_stream_state (underscan path)

Summary

The connector-scaling loop of amdgpu_dm_atomic_commit_tail() calls update_stream_scaling_settings(..., dm_new_crtc_state->stream) BEFORE the NULL check two lines later. The callee's only guard is !mode, and mode is &crtc->mode - an embedded struct that is never NULL - so stream->timing.h_addressable faults at VA 0x10 on amd64. The check-phase sibling of this block is protected by BUG_ON(stream==NULL) (DF-1152 family); the commit-tail block has no guard before the call. Reachable: dm_update_crtcs_state skips stream creation for active==0 CRTCs while the BUG_ON is short-circuited by the active term, leaving connector->crtc set with stream NULL. Local DoS: any DRM-master client panics the kernel deterministically with a 3-commit atomic sequence (disable+detach; re-attach with MODE_ID but ACTIVE=0; set underscan=1+hborder=1+vborder=1 connector-only). Userspace model SIGSEGVs at the identical offset (EXIT=139). Guest virtio-only: inconclusive, source-certain.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3653 Β· 11 files
FileTypeDescriptionSize
harness.c β€” 3.3 KB view raw
build.sh β€” 88 B view raw
run.sh β€” 90 B view raw
build.log β€” 234 B view raw
run.log β€” 247 B view raw
env.txt β€” 323 B view raw
VERDICT.md β€” 6.6 KB ↓ raw
fix.diff β€” 628 B view raw
manifest.json β€” 978 B view raw
verdict.json β€” 4.3 KB view raw
README.md β€” 3.1 KB ↓ raw

DF-3653 β€” NULL pointer dereference in amdgpu_dm_atomic_commit_tail() via update_stream_scaling_settings()

File: sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c Lines: 4882-4885 (call site, guard placed after use), 2479-2480 (faulting deref) Severity: Medium (local DoS β€” deterministic kernel panic) Β· CWE-476 Β· confidence: certain

What

The connector-scaling loop of amdgpu_dm_atomic_commit_tail() calls update_stream_scaling_settings(..., dm_new_crtc_state->stream) before checking dm_new_crtc_state->stream for NULL (the check sits at 4885, the call at 4882). update_stream_scaling_settings() only guards !mode β€” and mode is &crtc->mode, an embedded struct that is never NULL β€” so it unconditionally executes stream->timing.h_addressable (2479). With stream == NULL this reads address 0x10 (offset of timing.h_addressable in dc_stream_state: 8-byte sink pointer + h_total + h_border_left) β†’ page fault β†’ kernel panic.

How to reproduce (kernel)

Three atomic commits from a DRM-master client (any local user who holds DRM master β€” e.g. any user when no compositor is running, or the compositor itself):

  1. Disable CRTC (active=0, MODE_ID=0, connector CRTC_ID=0).
  2. Attach connector to the CRTC with a mode but active=0 (CONNECTOR:CRTC_ID=crtc, CRTC:MODE_ID=<mode>, ACTIVE=0). The check passes: needs_modeset is true this round, but dm_update_crtcs_state() skips stream creation because active == 0 (amdgpu_dm.c:5293-5294 modereset_required β†’ goto next_crtc). After commit: connector->state->crtc == crtc, crtc->state->{enable=1,active=0}, dm_crtc_state->stream == NULL.
  3. Change only an underscan property on the connector (underscan=1, underscan hborder=1, underscan vborder=1). drm_atomic_get_connector_state() pulls the CRTC into the state (drm_atomic.c:819-826); nothing changes on the CRTC so drm_atomic_crtc_needs_modeset() is false; is_scaling_state_different() is true β†’ commit tail enters the scaling block with stream == NULL β†’ panic at 0x10.

Requires a connected display (or forced connector) so mode_valid() passes in step 2; the panic itself needs no hardware event after that.

Harness (this pack)

The audit guest is virtio-only (no AMD GPU β€” see env.txt), so the kernel path cannot execute there. harness.c models the exact commit-tail decision and replicates the verbatim field order of dc_stream_state/dc_crtc_timing so the fault address equals the kernel's.

./build.sh   # cc -O2 -Wall -o harness harness.c
./run.sh     # prints the decision trace, then SIGSEGV reading 0x10

Expected decisive output (run.log):

offsetof(dc_stream_state, timing.h_addressable) = 0x10
commit-tail scaling loop: acrtc!=NULL, needs_modeset=0, scaling_changed=1, stream=NULL
-> update_stream_scaling_settings(mode, dm_state, stream=NULL)
Segmentation fault  (EXIT=139)

Contents

harness.c Β· build.sh Β· run.sh Β· build.log Β· run.log Β· env.txt Β· VERDICT.md Β· fix.diff Β· manifest.json Β· verdict.json

VERDICT.md
↓ download raw

DF-3653 β€” VERDICT

Status: inconclusive (kernel path not executable on the audit guest β€” virtio-only, no AMD GPU); source-certain; userspace model reproduces the exact NULL deref at the exact struct offset.

Root cause (source trace)

amdgpu_dm_atomic_commit_tail(), connector-scaling loop (sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c):

4861  for_each_oldnew_connector_in_state(state, connector, old_con_state, new_con_state, i) {
4862          struct dm_connector_state *dm_new_con_state = to_dm_connector_state(new_con_state);
4863          struct dm_connector_state *dm_old_con_state = to_dm_connector_state(old_con_state);
4864          struct amdgpu_crtc *acrtc = to_amdgpu_crtc(dm_new_con_state->base.crtc);
4865          struct dc_stream_status *status = NULL;
4867          if (acrtc) {
4868                  new_crtc_state = drm_atomic_get_new_crtc_state(state, &acrtc->base);
4869                  old_crtc_state = drm_atomic_get_old_crtc_state(state, &acrtc->base);
4870          }
4873          if (!acrtc || drm_atomic_crtc_needs_modeset(new_crtc_state))
4874                  continue;
4877          if (!is_scaling_state_different(dm_new_con_state, dm_old_con_state))
4878                  continue;
4880          dm_new_crtc_state = to_dm_crtc_state(new_crtc_state);
4882          update_stream_scaling_settings(&dm_new_con_state->base.crtc->mode,
4883                  dm_new_con_state, (struct dc_stream_state *)dm_new_crtc_state->stream);
4885          if (!dm_new_crtc_state->stream)
4886                  continue;

The NULL guard (4885) executes after the pointer is dereferenced inside update_stream_scaling_settings():

2463  static void update_stream_scaling_settings(const struct drm_display_mode *mode,
2464                                             const struct dm_connector_state *dm_state,
2465                                             struct dc_stream_state *stream)
2466  {
2472          /* no mode. nothing to be done */
2473          if (!mode)
2474                  return;
2477          src.width = mode->hdisplay;
2478          src.height = mode->vdisplay;
2479          dst.width = stream->timing.h_addressable;     /* <-- NULL deref */
2480          dst.height = stream->timing.v_addressable;

mode is &dm_new_con_state->base.crtc->mode β€” the address of an embedded struct in struct drm_crtc β€” never NULL, so the only guard never fires.

Struct offsets (verbatim field order, dc/dc_stream.h β†’ struct dc_stream_state { struct dc_sink *sink; struct dc_crtc_timing timing; ... }; dc/dc_hw_types.h:717 β†’ struct dc_crtc_timing { uint32_t h_total; uint32_t h_border_left; uint32_t h_addressable; ... }): offsetof(timing.h_addressable) = 8 + 8 = 0x10 on amd64 β†’ read of page-zero VA 0x10 β†’ unmapped β†’ page fault β†’ panic.

Reachability (line-by-line)

  • DRM_IOCTL_MODE_ATOMIC is DRM_MASTER (sys/dev/drm/drm_ioctl.c:681) β€” the attacker is any local DRM-master client (console user with no compositor running, or the compositor itself).
  • Setting a connector property pulls its CRTC into the atomic state: drm_atomic_get_connector_state() (sys/dev/drm/drm_atomic.c:819-826).
  • Commit 1 β€” disable: user sets CRTC:ACTIVE=0,MODE_ID=0 + CONNECTOR:CRTC_ID=0. check_modeset forces the detach (enable != has_connectors, drm_atomic_helper.c:628-633). After swap: dm_crtc_state->stream == NULL (dm_update_crtcs_state amdgpu_dm.c:5279-5280).
  • Commit 2 β€” attach-without-activate: CRTC:MODE_ID=<mode>,ACTIVE=0 + CONNECTOR:CRTC_ID=<crtc>. mode_changed true this round; in dm_update_crtcs_state(enable=true) modereset_required() is true because active == 0 (amdgpu_dm.c:5293-5294) β†’ goto next_crtc β†’ no stream is created; the BUG_ON(dm_new_crtc_state->stream == NULL) at 5344 is skipped by the active term of the condition at 5333-5335. After swap: connector->state->crtc == crtc, crtc->state->{enable=1, active=0, mode}, stream == NULL. (mode_valid in drm_atomic_helper.c:711 passes with a connected display; amdgpu_dm_connector_mode_valid only needs a valid dc_sink + dc_validate_stream.)
  • Commit 3 β€” underscan-only change (underscan=1, hborder=1, vborder=1): CRTC enters the state via the connector; duplicate of current state β‡’ mode_changed=false (drm_atomic_helper.c:600), active_changed=false, connectors_changed=false (update_connector_routing keeps the same encoder/crtc, drm_atomic_helper.c:289-299/357-369) β‡’ drm_atomic_crtc_needs_modeset()==false. is_scaling_state_different() true (amdgpu_dm.c:4220-4222: enabled transition with non-zero borders). amdgpu_dm_atomic_check only sets overall_update_type=FULL (amdgpu_dm.c:5715-5731) β€” no deref. Commit succeeds.
  • Commit tail enters the block: acrtc != NULL, !needs_modeset, scaling different β‡’ line 4882 executes with stream == NULL β‡’ panic.

The identical code shape in the check phase is guarded by the BUG_ON(dm_new_crtc_state->stream == NULL) at amdgpu_dm.c:5344 (that is the DF-1152 family); the commit-tail block has no such guard before 4882 β€” the if (!dm_new_crtc_state->stream) continue; at 4885 is one statement too late.

Verification (Phase V)

  • Guest: DragonFly 6.5-DEVELOPMENT, QEMU virtio, no AMD display device (env.txt: pciconf 0x1002 count 0, /dev/dri absent, no drm klds) β‡’ the amdgpu driver cannot attach β‡’ kernel-path reproduction impossible on this guest.
  • Userspace model (harness.c) replicates the exact decision sequence and the verbatim struct field order; on the guest it SIGSEGVs reading 0x10 β€” the same offset the kernel would fault on (run.log, EXIT=139). This proves the deref is unconditional once the loop block is entered; the loop-entry conditions are proven from source above.
  • Verdict: inconclusive (runtime kernel repro hardware-gated), source analysis certain. Impact ceiling: deterministic local DoS (panic) from a DRM-master client; no memory-disclosure or control primitive (pure NULL read at fixed low VA).

Fix validation

fix.diff moves the NULL check before the call (mirrors the check-phase guarding). Fix validation on hardware is not possible on this guest; source-level: with the patch, commit 3 skips the scaling update for a NULL stream exactly as the check phase does after DF-1152's pattern. fix_status: not_testable.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix validation requires AMD display hardware (driver cannot attach on the virtio-only guest). fix.diff moves the NULL guard before the call β€” with it, commit 3 skips the scaling update exactly as the atomic-check phase (amdgpu_dm.c:5333-5344) already does. git apply --check verified against sys/; not applied (read-only audit target).

fix.diff in this pack; applies cleanly per git apply --check
↓ fix.diffper-fix-DF-3653

Confirmed kernel references

Detail

Exploit chain

Local user with DRM master (console when no compositor runs, or a malicious compositor): atomic commit 1 disables the CRTC and detaches the connector; commit 2 re-attaches the connector with a mode but ACTIVE=0 leaving dm_crtc_state->stream==NULL while connector->state->crtc points at the enabled-but-inactive CRTC; commit 3 sets underscan=1 + hborder/vborder=1 on the connector only -> amdgpu_dm_atomic_commit_tail enters the scaling block with !needs_modeset -> update_stream_scaling_settings dereferences NULL stream at 0x10 -> kernel page fault/panic. Requires a connected (or forced) display so mode_valid passes in commit 2.

Evidence (decisive lines)

run.log (offsetof=0x10 trace then SIGSEGV EXIT=139); VERDICT.md (full 3-commit source trace with drm_atomic.c/drm_atomic_helper.c line proofs); fix.diff (guard moved before the call; git apply --check clean); env.txt (guest has no AMD GPU: pciconf 0x1002 count 0, /dev/dri absent)

PoC changes

no seeded PoC for pass-2; harness written fresh β€” models the commit-tail decision sequence and replicates the verbatim dc_stream_state/dc_crtc_timing field order so the fault address (0x10) equals the kernel's

Verified recommended fix

Move the !dm_new_crtc_state->stream check before the update_stream_scaling_settings() call in amdgpu_dm_atomic_commit_tail().

Verdict

Source-certain NULL deref, hardware-gated runtime. amdgpu_dm_atomic_commit_tail() calls update_stream_scaling_settings(..., dm_new_crtc_state->stream) at amdgpu_dm.c:4882 BEFORE the NULL check at 4885; update_stream_scaling_settings() only guards !mode (2473) and mode is &crtc->mode (embedded struct, never NULL), so stream->timing.h_addressable (2479) faults at VA 0x10 on amd64 (sink ptr + h_total + h_border_left). Deterministic trigger by a DRM-master local client in 3 atomic commits: disable CRTC; attach connector with MODE_ID set but ACTIVE=0 (stream is never created because modereset_required() short-circuits on active==0 at 5293-5294, and the BUG_ON at 5344 is skipped by the active term at 5333-5335); then change only underscan properties (CRTC enters the state via drm_atomic_get_connector_state, drm_atomic.c:819-826; needs_modeset false; is_scaling_state_different true) -> commit tail enters the scaling block with stream==NULL -> panic. The userspace model (verbatim struct field order) SIGSEGVs at the identical 0x10 offset on the guest (run.log EXIT=139). Guest is virtio-only with no AMD display device (env.txt), so the kernel path itself cannot execute: inconclusive overall; impact ceiling is local DoS (panic) via DRM_IOCTL_MODE_ATOMIC (DRM_MASTER, drm_ioctl.c:681) β€” no disclosure/control primitive (fixed-VA NULL read).