# 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):

```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()`:

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