# DF-5218 — VERDICT

**Status: `inconclusive` (runtime) / proven at source level (ledger model
reproduces both violations).**
**Impact class (if hardware present): kernel heap leak + kref UAF write /
double-free.  Reproduced on guest: no — the path requires an AMD DC GPU; the
guest has only the QEMU emulated VGA (vendor 0x1234), so `amdgpu` binds zero
devices and `dc_sink_create()` never executes (proven in env.txt).**

## 1. What was audited

`sys/dev/drm/amd/display/dc/core/dc_sink.c` (108 LOC) — the DC core sink
lifecycle: `construct`/`destruct`, `dc_sink_create`/`dc_sink_retain`/
`dc_sink_release`/`dc_sink_free`.  The file itself is a correct, thin
kzalloc/construct/kref wrapper:

* `dc_sink_create` (dc_sink.c:85-104): kzalloc -> construct -> `kref_init`
  (refcount = 1, creator's).  `construct` (dc_sink.c:42-62) only fails when
  `init_params->link == NULL` (dc_sink.c:47-48) and in that case nothing has
  been written, so the `kfree` on the failure path (dc_sink.c:100) needs no
  destruct.  **File-local verdict: clean.**
* `destruct` (dc_sink.c:34-40) frees `dc_container_id` — dead-but-safe: a
  tree-wide search shows `dc_container_id` is *only ever assigned NULL*
  (dc_sink.c:38, :55); no code path allocates it.  No double-free clone hazard
  either: the "logically cloneable up to refcount" comment (dc.h:713-716) is
  vestigial — no code memcpys a `struct dc_sink`.
* No locking, no usercopy, no ioctl entry — the entire attack surface is the
  retain/release API consumed by DC/DM on display events.

The pass-2 finding is therefore an **API-contract violation by its callers**
(the sink-lifecycle interplay this file audit was scoped to hunt):

## 2. Violation A — permanent sink leak in `emulated_link_detect`

`emulated_link_detect()` (amdgpu_dm.c:741-826) runs for boot-forced connectors
when nothing is physically attached (callers: amdgpu_dm.c:869 resume,
:1092 HPD, :1219 HPD-RX, :1886 init — all gated on
`aconnector->base.force && new_connection_type == dc_connection_none`).

Ledger (verbatim from source):

```
751: prev_sink = link->local_sink;            /* borrowed                */
753: if (prev_sink != NULL)
754:     dc_sink_retain(prev_sink);           /* +1 temp ref             */
...      /* default: ... return;  (:798)  — temp ref LEAKED             */
804: sink = dc_sink_create(&sink_init_data);  /* +1 = link's ref         */
805: if (!sink) { ... return; (:807) }        — temp ref LEAKED         */
810: link->local_sink = sink;                 /* old link ref STRANDED   */
812: dm_helpers_read_local_edid(...)          /* DF-1837's overflow path */
     /* function end — temp ref LEAKED (no dc_sink_release anywhere)    */
```

Consequences: on every call with a prior `local_sink`, **two** references to
the old `struct dc_sink` are stranded (the link's, overwritten at :810 without
release, and the temp from :754, never released on any exit).  The object
(≈1 KB: embedded `dc_edid.raw_edid[512]` + `edid_caps` + `features_3d[]`,
dc.h:696-717) can never reach refcount 0.  Forced headless connector +
repeated S3 resume ⇒ unbounded kernel heap growth.

Contrast with the correct sibling `dc_link_detect()` (dc_link.c:642-902),
which retains prev_sink, calls `link_disconnect_sink()` (dc_link.c:492-500,
releases the link ref) and releases the temp ref on **every** exit path
(:696, :710, :724, :735, :759, :791, :902).

## 3. Violation B — release of a borrowed ref ⇒ premature free / UAF

`amdgpu_dm_update_connector_after_detect()` branch A (amdgpu_dm.c:975-1005)
runs for the same forced connectors once `dc_em_sink` exists
(gate :974-975; `dc_em_sink` created in `create_eml_sink`, amdgpu_dm.c:3271,
on the first `mode_valid` call — amdgpu_dm.c:3320-3322).

DM's convention (everywhere except this branch): `aconnector->dc_sink` is a
**borrowed** pointer — assigned with no `dc_sink_retain` at amdgpu_dm.c:995,
:1001, :1044, :3278-3280.  Branch A nevertheless does:

```
992: if (aconnector->dc_sink != aconnector->dc_em_sink)
993:     dc_sink_release(aconnector->dc_sink);   /* ref DM never took   */
```

Two states make this a memory-safety bug:

* **Old sink still is `link->local_sink`** (refcount 1 = link's): :993 drops
  the link's reference ⇒ refcount 0 ⇒ sink freed **while
  `link->local_sink` still points at it** ⇒ use-after-free on every later
  touch (next `dc_link_detect` → `link_disconnect_sink` → `dc_sink_release`,
  stream commits, EDID ioctls).
* **EDID-change cycle** (the deterministic one): a later `dc_link_detect`
  with a differing EDID frees the old sink itself (retain :644 →
  `link_disconnect_sink` :647 → create new :755 → release prev :902 ⇒
  refcount 0 ⇒ freed).  DM still holds the stale borrowed pointer, and the
  following branch-A update executes `dc_sink_release()` on the **freed**
  object: `kref_put()` (atomic decrement — UAF write into a freed slab
  block) and, when the corrupted count reaches 0, `dc_sink_free()` →
  `destruct()` + `kfree()` on the freed block — a double free that hands the
  block back to the allocator while it may already be reused (arbitrary-free
  of a live slab victim; sink-sized kmalloc zone, groomable timing between
  the free at dc_link.c:902 and the release at amdgpu_dm.c:993).

Note the accidental interaction: Violation A's stranded references keep the
old *emulated* sinks over-refcounted, which masks Violation B on the
headless path — the undercount UAF surfaces on the **display-attached**
forced-connector path instead (where `emulated_link_detect` never runs).
Both ledgers are proven in `ledger_model.c` (`bug-leak`, `bug-uaf` modes),
whose output matches the by-hand accounting above (run.log).

## 4. Reachability chain (who can trigger this)

1. `connector->force` is set **only** from the kernel command line
   (`drm_connector_get_cmdline_mode`, sys/dev/drm/drm_connector.c:137-140 —
   `video=<conn>:...e[d]`), from `drm.edid_firmware=` (sys/dev/drm/drm_edid.c:1606-1625),
   or by the driver itself (`create_eml_sink` sets DRM_FORCE_OFF).  ⇒ the
   configuration gate is boot-time/root privilege.
2. The DC sink machinery itself requires an amdgpu-bound AMD ASIC with DC
   enabled (DCE_VERSION_10_0 = Tonga/Fiji, dc_resource.c:71-76; DCE 11.x/12.x
   likewise) ⇒ specific hardware.
3. Given (1)+(2): the **leak** repeats on every suspend/resume or HPD cycle
   of the forced headless connector; the **UAF** fires on any two detect
   cycles with differing EDIDs (monitor/KVM swap — a physical action that
   needs no privilege beyond the configured connector).

Severity assessment: Medium — genuine kernel heap corruption (memcorrupt
class), but gated behind privileged boot configuration and AMD hardware, and
the UAF trigger additionally needs a physical EDID change.  The leak variant
is a slow local DoS only.

## 5. Why not `reproduced` on the guest

* Guest: DragonFly 6.5-DEVELOPMENT x86_64 (uname in env.txt); the only VGA
  device is `vgapci0@pci0:0:2:0 chip=0x11111234` (QEMU virtual VGA, vendor
  0x1234).  `kldload amdgpu` succeeds but binds **zero** devices — dmesg
  shows only `[drm] amdgpu kernel modesetting enabled.` and no probe output;
  `dc_create`/`dc_sink_create` never run.  No KVM device-assignment of an
  AMD GPU is available in this environment, and QEMU has no AMD DC GPU model.
* The ledger model (`ledger_model.c`) was therefore built and executed on the
  guest (`cc 8.3 [DragonFly]`) and reproduces both violations exactly
  (run.log): 9 live sinks after 8 cycles (leak), and one release-on-freed +
  one double-free attempt in the EDID-change cycle (UAF), with the `fixed`
  mode returning to exactly 1 live sink and zero UAF hits.

Attempts: 2 (guest hardware-gate probe incl. amdgpu kldload; ledger-model
build+run).  Guest was reset clean after probing (`vm.sh reset with-src`);
no kernel changes were made.

## 6. Fix validation

`fix.diff` implements: (a) `emulated_link_detect` adopts the
`link_disconnect_sink`-style reference transfer with `goto out` +
release-on-all-exits; (b) branch A drops the unowned `dc_sink_release`
(amdgpu_dm.c:993) and its counterpart retain (:1003), leaving
`aconnector->dc_sink` uniformly borrowed as in the normal path.  A patched
kernel **cannot be exercised** on this guest (no AMD GPU ⇒ the changed code
never executes — the baseline itself does not run), so
`fix_status = not_testable`; the model's `fixed` mode (same pairing
semantics) demonstrates the corrected ledger returns to zero leaks / zero
UAF hits.  fix.diff was authored against the read-only sys/ tree and was
never applied there.

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