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)
PoC verification
Evidence pack
findings/poc/DF-3653 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
- Disable CRTC (active=0, MODE_ID=0, connector CRTC_ID=0).
- 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_modesetis true this round, butdm_update_crtcs_state()skips stream creation becauseactive == 0(amdgpu_dm.c:5293-5294modereset_requiredβgoto next_crtc). After commit:connector->state->crtc == crtc,crtc->state->{enable=1,active=0},dm_crtc_state->stream == NULL. - 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 sodrm_atomic_crtc_needs_modeset()is false;is_scaling_state_different()is true β commit tail enters the scaling block withstream == 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
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_modesetforces 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_changedtrue this round; indm_update_crtcs_state(enable=true)modereset_required()is true becauseactive == 0(amdgpu_dm.c:5293-5294) βgoto next_crtcβ no stream is created; theBUG_ON(dm_new_crtc_state->stream == NULL)at 5344 is skipped by theactiveterm of the condition at 5333-5335. After swap:connector->state->crtc == crtc,crtc->state->{enable=1, active=0, mode},stream == NULL. (mode_validin 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_checkonly setsoverall_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 withstream == 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_testableFix 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
Confirmed kernel references
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:4882
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:4885
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:2479
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:5293
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:5333
- sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:5344
- sys/dev/drm/drm_atomic.c:819
- sys/dev/drm/drm_ioctl.c:681
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).
No comments yet.