# 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

```sh
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`.
