# DF-4748 — VERDICT

**Status: reproduced** (verbatim-code rig on the DragonFly guest; the
in-kernel manifestation is hardware-gated — the audit guest has no AMD
ASIC / DP MST hub, see `env.txt`). **Impact: kernel memory corruption**
(UAF read + attacker-steered writes). **Confidence: certain** for the rig
reproduction (byte-identical production code, deterministic, 3/3 runs),
`likely` for on-ASIC manifestation (identical code, only the slab timing
is unexercised).

## What was run (DF-4165/4247/4631 verbatim-rig protocol)

`rig/` machine-extracts the production code **byte-identical** from
`sys/` (66 verbatim blocks; `rig/check_verbatim.sh` re-verified them on
the guest against `/usr/src` — the tree the running kernel was compiled
from — and proves the fixed slice is exactly pristine + `fix.diff`):

- `amdgpu_dm_helpers.c:137-172/182-266/281-305/307-338` — `get_payload_table`
  and the three dm MST payload helpers (deref sites :197/:289/:317)
- `amdgpu_dm_mst_types.c:182-197/210-258/283-308/310-373/375-398` —
  connector destroy (kfree), get_modes (`dc_sink->priv = aconnector` :243),
  fake-encoder create, add connector, **`dm_dp_destroy_mst_connector`**
- `dc_sink.c:34-104`, `dc.c:1750-1847`, `dc_stream.c:38-157` — the whole
  dc_sink/dc remote-sink/dc_stream refcount machinery
- `dc_link.c:2322-2372/2374-2452/2454-2530/2630-2642` —
  `update_mst_stream_alloc_table`, `allocate_mst_payload`,
  `deallocate_mst_payload`, `core_link_disable_stream`
- `drm_dp_mst_topology.c` payload machinery (assign/put payload id,
  init_vcpi, allocate/reset/deallocate_vcpi, find_vcpi_slots,
  calc_pbn_mode, update_payload_part1/2, payload step1/2, validated-port
  walk, put_port)
- drm connector refcount plumbing (`drm_connector.c:152-160`
  `drm_connector_free`, `drm_mode_object.c` get/put, `drm_connector.h`
  `drm_connector_put`, tree kref/refcount bodies, `fixpt31_32.c` math)

The harness (`rig/mst_rig.c`) drives the exact production sequences:
plug (`dm_dp_add_mst_connector` → `dm_dp_mst_get_modes` →
`dc_link_add_remote_sink`, sink refcount 2) → enable
(`dc_create_stream_for_sink`, refcount 3) → unplug
(`dm_dp_destroy_mst_connector` verbatim: refcount 3→**1, held solely by
the stream**, connector kfree'd via `drm_connector_put` →
`drm_connector_free` → `dm_dp_mst_connector_destroy`) → use (the display
session reacting: `core_link_disable_stream` → `deallocate_mst_payload`
→ the helpers, with the OLD stream that still holds the sink alive).

## Results (deterministic, 3/3 identical runs — run.log, run.2.log, run.3.log)

| scenario | baseline (verbatim) | fixed (verbatim + fix.diff) |
|---|---|---|
| control / accounting | CLEAN 0 (refcount discipline exact: after unplug the sink's life is owned solely by the stream) | CLEAN 0 |
| disable-guard (freed page PROT_NONE) | **exit 77: fault at 0x…e8048 INSIDE the freed `amdgpu_dm_connector` [0x…e8000+208) — the `mst_port` member read at `amdgpu_dm_helpers.c:199`** | FIX_CLEAN 0 |
| disable-poison (0xAA rewrite) | **exit 77: wild-pointer deref (non-canonical → #GP)** | FIX_CLEAN 0 |
| disable-stale (old values intact) | completes: LATENT read (dock mgr still valid, `port` was pre-nulled) — matches real-world latency of this bug class | FIX_CLEAN 0 |
| disable-zero (scrubbed) | completes: `!aconnector->mst_port` early-return | FIX_CLEAN 0 |
| disable-groom (attacker wins the slot) | **exit 77: CONTROLLED WRITE — verbatim `drm_dp_mst_reset_vcpi_slots` + `drm_dp_mst_deallocate_vcpi` zeroed `vcpi{5,0x1111,0x3333,0x22}` at the ATTACKER-CHOSEN address (victim object), via the freed connector's authored `->port`** | FIX_BLOCKED_WRITE 0 (victim byte-identical) |
| enable-groom (re-plug path) | **exit 77: CONTROLLED WRITE — verbatim `drm_dp_init_vcpi` wrote `vcpi{1,551,65536,1}` at the attacker-chosen address + `assign_payload_id` wrote `mgr->payload_mask/vcpi_mask` and `proposed_vcpis[0] = &victim->vcpi`** | FIX_BLOCKED_WRITE 0 |

The stable pre-use state is printed by every scenario:
`sink alive (refcount 1, held by stream) with sink->priv = <freed connector>`
— i.e. the sink demonstrably OUTLIVES the connector with the
never-cleared back-pointer dangling.

## The bug (all path:line refs verified in-tree)

1. **Set, never cleared.** Sole assignment in the amd display tree:
   `amdgpu_dm_mst_types.c:243 — dc_sink->priv = aconnector;`
2. **Deref sites.** `amdgpu_dm_helpers.c:197/289/317` —
   `aconnector = stream->sink->priv;`; the guard at :199
   (`!aconnector || !aconnector->mst_port`) is value-only.
3. **The free.** `dm_dp_destroy_mst_connector` (mst_types.c:375-398)
   drops the list ref (`dc_link_remove_remote_sink`, dc.c:1823) and the
   connector's ref (`dc_sink_release`), then `drm_connector_put` →
   `drm_connector_free` → `dm_dp_mst_connector_destroy` (mst_types.c:182,
   `kfree(amdgpu_dm_connector)`). `sink->priv` is never nulled.
   *(Ref correction vs the original filing: the MST-child free site is
   `dm_dp_mst_connector_destroy` at mst_types.c:193, not
   `amdgpu_dm.c:3187` — that one is the non-MST connector destroy; both
   are raw kfrees, conclusion unchanged.)*
4. **The sink outlives the connector.** The enabled `dc_stream_state`
   holds its own sink ref (`dc_stream.c:66` construct →
   `dc_sink_retain`; released in `destruct`, dc_stream.c:112). After the
   unplug destroy the refcount is exactly 1 = the stream's (rig-asserted).
5. **The use.** Next commit touching the old stream:
   `core_link_disable_stream` (dc_link.c:2630) →
   `deallocate_mst_payload` (:2454) → helper :197/:199 — UAF read of the
   freed connector; with slab reuse (groom), the authored
   `mst_port`/`port` steer the verbatim payload machinery into
   attacker-chosen memory (controlled writes, proven both disable- and
   enable-path). Re-plug/enable path: `allocate_mst_payload` (:2374,
   caller core_link_enable_stream :2621).

## Threat model

Malicious/flapping MST dock (DF-1837) or physical unplug racing the
display session (DF-4631 — the radeon twin, triaged High/memcorrupt).
No privilege boundary beyond owning the display session; the disable-path
trigger (userspace reacting to an HPD by turning the CRTC off) is the
NORMAL reaction to an unplug.

## Fix validation (fix.diff, A/B + real module build)

- Rig A/B: the fixed build compiles the byte-identical pristine +
  fix.diff slice (checker-proven bidirectionally) and is CLEAN on every
  scenario, including groom (victim untouched). The fix NULLs
  `dc_sink->priv` in `dm_dp_destroy_mst_connector` before the refs drop;
  the helpers already early-return on NULL priv (:199).
- Real kernel module build: on the guest, pristine `amdgpu.ko` built
  (bsd.kmod.mk), then fix.diff applied (fuzz=0, hunk at 385) and the
  module rebuilt cleanly — amdgpu_dm_mst_types.o recompiled, amdgpu.ko
  relinked (fix_build.log). Tree reverted byte-exact afterwards
  (sha256 match with the read-only repo tree).

## Honesty notes / limitations

- No AMD ASIC on the guest: the in-kernel manifestation (which slab the
  connector lands in, HPD interrupt timing) is unexercised. What IS
  exercised is the full production code path itself, byte-identical,
  with slab state modeled at the boundaries (guard/stale/zero/poison/
  groom). The latent-mode results (stale/zero complete without faulting)
  honestly reflect that the bug often does NOT crash on first use — the
  severity comes from reuse, exactly as modeled by groom.
- Shims (documented in `rig_shims.h`/`rig_types.h`): allocator with free
  modes, locks, logging, atomics/bitops, DPCD/sideband transport,
  `dm_helpers_parse_edid_caps` (audited separately, killed candidates),
  the dc_link PBN math helpers (`get_pbn_per_slot`/`get_pbn_from_timing`,
  pure bandwidth arithmetic upstream of the defect), and minimized
  production-named carrier structs. No defect logic lives in a shim; the
  defect-relevant functions are verbatim.

## Killed candidates (pass 2, from the original filing — unchanged)

get_payload_table OOB (bounded by max_payloads=4 < 6);
proposed_vcpis NULL deref (parallel invariant); remote-sink EDID size
(rejected >512 — the uncapped local-EDID path is DF-1837, folded there);
parse_edid_caps copies (bounded); aux defer loop (off-file);
read_local_edid !aconnector (unreachable on local sinks); dtn log
truncation (bounded); display_color_depth default (latent-robustness
only).

## 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.
