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

dce110_apply_ctx_for_surface programs a front end for a head pipe whose plane was detached (plane_state NULL) -> kernel NULL-deref panic

Summary

The walk loops every pipe whose stream matches and unconditionally calls program_front_end_for_pipe -> set_default_colors which dereferences pipe_ctx->plane_state->color_space. The core deliberately produces head pipes with stream!=NULL && plane_state==NULL (dc_remove_plane_from_context: 'For head pipe detach surfaces from pipe'). Sibling code guards exactly this state (core/dc.c, dcn10_hw_sequencer.c, dc_stream.c) - the dce110 callee (inherited unchanged by DCE80/100/110/112/120, none override) does not. Trigger: atomic commit disabling the PRIMARY plane while an OVERLAY plane and the CRTC remain, then any commit reaching dc_commit_state. Any local user with /dev/dri/cardN access (root:video 0660) on DCE80..DCE120 hardware. Compile-tested on guest BUILD_RC=0; runtime hardware-gated; chain proven hop-by-hop at source. Read-only NULL deref -> panic. Guard-the-loop fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3946 Β· 8 files
FileTypeDescriptionSize
README.md doc build/run/expected + source chain 3.2 KB ↓ raw
VERDICT.md verdict verification narrative 3.1 KB ↓ raw
trigger_dce110_headplane.c trigger raw DRM_IOCTL_MODE_ATOMIC trigger (3-commit sequence) 5.1 KB view raw
build.sh script build command 116 B view raw
run.sh script run command 168 B view raw
build.log log guest compile output 355 B view raw
run.log log guest run output (no DRI adapter β†’ hardware-gated exit) 355 B view raw
fix.diff suggested-fix plane_state NULL guard in apply_ctx_for_surface 748 B view raw
README.md doc build/run/expected + source chain
↓ download raw

DF-3946 β€” dce110_apply_ctx_for_surface NULL plane_state dereference (kernel panic)

Severity: Medium (local DoS, unprivileged DRI user) Β· CWE-476 File: sys/dev/drm/amd/display/dc/dce110/dce110_hw_sequencer.c

Build / Run / Expected

cc -O2 -o trigger_dce110_headplane trigger_dce110_headplane.c   # builds anywhere (raw ioctls)
./trigger_dce110_headplane /dev/dri/card0

On hardware with an AMD pre-DCN10 ASIC (DCE80/100/110/112/120 β€” all share dce110_hw_sequencer_construct, see dce80_hw_sequencer.c, dce100_…, dce112_…, dce120_hw_sequencer.c): executing the 3-commit atomic sequence (primary+overlay on β†’ primary off/overlay on β†’ any commit that reaches dc_commit_state) causes an immediate kernel page fault in set_default_colors().

On the audit QEMU guest there is no AMD GPU, so runtime verification is inconclusive (hardware-gated); the reachability chain is proven at source level below.

Why it is real (source chain, every hop cited)

  1. dc_remove_plane_from_context() β€” sys/dev/drm/amd/display/dc/core/dc_resource.c:1462-1467: removing a plane from the head pipe does pipe_ctx->plane_state = NULL; pipe_ctx->bottom_pipe = NULL; while pipe_ctx->stream stays set ("For head pipe detach surfaces from pipe"). β†’ a pipe with stream != NULL && plane_state == NULL is a normal, intentionally produced state.
  2. Userland reaches that from any KMS client: amdgpu_dm_atomic_check β†’ dm_update_planes_state β†’ dc_remove_plane_from_context (amdgpu_dm.c:5426) when an atomic commit disables a plane. With β‰₯1 remaining plane (the overlay/underlay bottom pipe) stream_status->plane_count stays β‰₯ 1.
  3. amdgpu_dm_atomic_commit_tail β†’ dc_commit_state(dm->dc, dc_state) (amdgpu_dm.c:4837, dc_state = dm_state->context, set for every commit that touched planes, amdgpu_dm.c:4752-4755).
  4. dc_commit_state β†’ for each non-mode-changed stream: dc->hwss.apply_ctx_for_surface(dc, streams[i], plane_count, context) (core/dc.c:955-959; second identical call at :992-997).
  5. dce110_apply_ctx_for_surface (dce110_hw_sequencer.c:2743-2763) walks all pipes with pipe_ctx->stream == stream β€” including the head pipe from step 1 β€” and calls dce110_program_front_end_for_pipe() on it (line 2757) without any plane_state NULL check.
  6. First dereference: set_default_colors() at dce110_hw_sequencer.c:2095-2114, line 2100: default_adjust.in_color_space = pipe_ctx->plane_state->color_space; β†’ read from NULL+offset β†’ kernel page fault β†’ panic.

Sibling code explicitly guards this exact state, proving the state is anticipated elsewhere: - core/dc.c:1521-1522: if (!pipe_ctx->plane_state) continue; - dcn10_hw_sequencer.c:2386-2391: handles !pipe_ctx->plane_state. - dc_stream.c cursor path guards !pipe_ctx->plane_state (core/dc_stream.c:251-256).

The caller-side guard (dc.c:1521) protects only the dc_commit_updates_for_stream fast path; the dc_commit_state path (dc.c:956/:992) has no such guard, and the dce80/dce100/dce110/dce112/ dce120 hwss tables do not override apply_ctx_for_surface.

Fix (also in fix.diff)

Skip pipes with no plane in the program loop, mirroring core/dc.c:1521.

VERDICT.md verdict verification narrative
↓ download raw

DF-3946 β€” Verification Verdict

Status: inconclusive (hardware-gated; source-level chain certain) Impact demonstrated: none (panic expected on hardware) Confidence: certain (source), runtime not testable on this guest

Reproduction environment

  • Audit guest: DragonFly x86-64 QEMU/KVM VM β€” no AMD GPU / no DRI adapter, so the DRM/amdgpu path cannot execute at all. trigger_dce110_headplane.c compile-verifies only (see build.log).
  • Target hardware class: any amdgpu pre-DCN10 ASIC (DCE80/100/110/112/120): Oland/Tonga/Fiji/Carrizo/Stoney/Polaris/Vega10 β€” all five hwss variants inherit dce110_apply_ctx_for_surface (verified: no override in dce80/dce100/dce112/dce120 funcs tables).

Why the bug is certain at source level (path:line)

# Fact Reference
1 Removing a head-pipe plane sets plane_state = NULL, keeps stream sys/dev/drm/amd/display/dc/core/dc_resource.c:1462-1467
2 Unprivileged KMS user can drive step 1 via atomic plane-disable sys/dev/drm/amd/display/amdgpu_dm/amdgpu_dm.c:5426 (dc_remove_plane_from_context)
3 Commit tail always calls dc_commit_state for plane-updating commits amdgpu_dm.c:4837 with dc_state from dm_state->context (amdgpu_dm.c:4752-4755)
4 dc_commit_state calls apply_ctx_for_surface per remaining stream (plane_count β‰₯ 1 while overlay survives) core/dc.c:955-959 and :992-997
5 dce110_apply_ctx_for_surface walks pipes by stream match only β€” no plane_state check dce110_hw_sequencer.c:2743-2763
6 First NULL deref set_default_colors β†’ dce110_hw_sequencer.c:2100 (pipe_ctx->plane_state->color_space), reached from line 2619 via 2757
7 The state is guarded in sibling code (state is real & anticipated) core/dc.c:1521-1522; dcn10_hw_sequencer.c:2386-2391; core/dc_stream.c:251-256

Conclusion: NULL-pointer read at a small constant offset β†’ kernel page fault β†’ panic. Read-only primitive (color_space/format/update_flags reads); no corruption-controlled write β†’ local DoS (Medium), not an escalation path.

Why not reproduced / not false-positive

  • not-reproducible-on-guest: QEMU has no AMD display adapter; amdgpu module has nothing to bind. This is a hardware prerequisite, not a code-path liveness issue: every hop of the chain above exists in this tree.
  • not-false-positive: the producer (dc_remove_plane_from_context head-pipe detach) and the unguarded consumer are both live code on the same commit path; DCN10's own handler demonstrates the intended guard.

Fix validation

fix.diff authored (guard added in dce110_apply_ctx_for_surface program loop, mirroring core/dc.c:1521). Kernel rebuild + on-hardware validation not possible on this guest β†’ 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

not_testable: no AMD GPU in the guest; fix.diff is a minimal guard identical in shape to the existing dc.c:1521 guard

fix.diff in pack; guest cannot rebuild-test the amdgpu DC path without the hardware
↓ fix.diffper-fix-DF-3946

Confirmed kernel references

Detail

Exploit chain

DRI user (root:video 0660 node, drm_drv.c:1178+drmP.h:108-110) -> atomic commit: primary+overlay on; then primary off/overlay on (dc_remove_plane_from_context detaches head pipe plane); commit tail dc_commit_state -> apply_ctx_for_surface(stream, plane_count=1) -> program loop processes head pipe with plane_state==NULL -> set_default_colors reads pipe_ctx->plane_state->color_space -> NULL+0xd0 read -> kernel panic.

Evidence (decisive lines)

VERDICT.md table of 7 cited hops; trigger_dce110_headplane.c (3-commit atomic sequence); build.log/run.log (guest compile OK, no DRI adapter); fix.diff (guard mirroring dc.c:1521).

PoC changes

no seed; trigger authored fresh with raw DRM_IOCTL_MODE_ATOMIC structs (no libdrm dependency) so it compiles on the stock guest

Verified recommended fix

Skip pipes with !pipe_ctx->plane_state in dce110_apply_ctx_for_surface's program loop (mirror core/dc.c:1521).

Verdict

Source-certain NULL-deref: dce110_apply_ctx_for_surface (dce110_hw_sequencer.c:2743-2763) walks pipes by stream match without a plane_state NULL check and calls dce110_program_front_end_for_pipe -> set_default_colors (:2100) which dereferences plane_state; the core deliberately produces head pipes with stream!=NULL && plane_state==NULL (dc_resource.c:1462-1467, driven by unprivileged KMS atomic plane-disable via amdgpu_dm.c:5426) and the commit tail unconditionally runs dc_commit_state (amdgpu_dm.c:4837) -> apply_ctx_for_surface (dc.c:955-959). Sibling code guards the exact state (dc.c:1521-1522, dcn10_hw_sequencer.c:2386). Runtime inconclusive: the audit QEMU guest has no AMD GPU, so the amdgpu/DC path cannot execute; trigger compile-tested on the guest (BUILD_RC=0). Expected impact on DCE80/100/110/112/120 hardware: kernel page-fault panic from an unprivileged DRI user (local DoS).