# DF-3995 — VERDICT

**Finding:** `dc_link_is_dp_sink_present()` destroys a DDC pin object it does
not own — use-after-free on every later DDC/AUX transaction plus double free
at connector/link teardown.

**Status: reproduced (verbatim-code ownership-chain harness; live-hardware
trigger not possible on the QEMU guest — no AMD GPU).**
Confidence: certain (code defect), likely (in-field triggerability).

## Root cause (path:line)

- `sys/dev/drm/amd/display/dc/core/dc_link.c:348` —
  `ddc = dal_ddc_service_get_ddc_pin(link->ddc);` returns a **borrowed**
  pointer. The object is owned by `link->ddc` (`struct ddc_service`), created
  once in `core/dc_link_ddc.c:272` (`ddc_service->ddc_pin =
  dal_gpio_create_ddc(...)`) and is supposed to be destroyed only by the
  owner in `dal_ddc_service_destroy` → `destruct` (dc_link_ddc.c:306-307).
- `sys/dev/drm/amd/display/dc/core/dc_link.c:360-365` — on
  `dal_ddc_open(ddc, GPIO_MODE_INPUT, GPIO_DDC_CONFIG_TYPE_MODE_I2C)`
  failure the code calls `dal_gpio_destroy_ddc(&ddc)`, which
  (gpio/gpio_service.c:459-471) destroys `pin_data`, `pin_clock`, **kfrees
  the `struct ddc`**, and nulls only the local variable — while
  `ddc_service->ddc_pin` still points at the freed memory.

## Consequences

1. **Use-after-free:** every later use of the connector's DDC service dereferences
   `ddc_service->ddc_pin`:
   - `dc_link_is_dp_sink_present` itself on the next hotplug/detect
     (dc_link.c:360 → `dal_ddc_open(freed->pin_data)`);
   - `get_ddc_line` (dc_link.c:969 → `dal_ddc_get_line`);
   - every DDC/AUX submission: `core/dc_link_ddc.c:643-656` reads
     `ddc->ddc_pin->pin_data->en` to select the AUX engine
     (freed-memory read; in-kernel: garbage engine index /
     `res_pool->engines[...]` OOB or fault).
2. **Double free:** at link destruction `destruct(ddc_service)`
   (dc_link_ddc.c:306-307) calls `dal_gpio_destroy_ddc(&ddc->ddc_pin)` on
   the already-freed object: it reads `pin_data`/`pin_clock` **out of freed
   heap** and kfrees those stale pointers — an uncontrolled double free /
   arbitrary-free-shaped primitive, plus `kfree` of the already-freed ddc.

## Trigger / threat model

`dal_ddc_open` fails when either DDC gpio is already open — e.g. while an
AUX/I2C engine transaction holds the pins (`dal_gpio_open_ex` returns
`GPIO_RESULT_ALREADY_OPENED` when `gpio->pin != NULL`, gpio_base.c:59-67).
The engines open the same `struct ddc` in different modes
(i2caux/aux_engine.c:82, i2caux/i2c_hw_engine.c:190,
i2caux/i2c_sw_engine.c:468, dce/dce_aux.c:883, dce/dce_i2c_hw.c:376,
dce/dce_i2c_sw.c:386). HPD-driven detect (userspace DRM ioctls:
DRM_IOCTL_MODE_GETCONNECTOR / hotplug events) races MST/link-loss AUX
traffic on the same connector; a malicious or flaky DP sink (the threat
model already established by DF-1319/DF-3987/DF-3988) can stall AUX
transactions (defer/NACK timeouts hold the pins open longer), widening the
window. A single occurrence permanently corrupts the connector's DDC
service.

## Reproduction

- Guest: DragonFly 6.5-DEVELOPMENT (see env.txt) — **no AMD GPU present**
  (`pciconf` shows no amdgpu device), so a live kernel trigger cannot be run
  on this guest.
- `ddc_uaf_harness.c` reproduces the exact ownership chain with the verbatim
  destroy logic; with an injected `dal_ddc_open` failure it shows (run.log):
  the borrowed free, the DANGLING `ddc_service->ddc_pin`, the UAF read on
  the next AUX transaction, and the owner-destroy dereference of freed
  memory. Exit code 2. Deterministic across runs (run.2/run.3 logs).

## Exploit chain (post-trigger, on real hardware)

free(struct ddc + 2×struct gpio) → subsequent AUX/DPCD traffic UAF-writes
gpio open state through freed pointers → object placement (the ~24-byte ddc
and the kmalloc'd `struct gpio` sit in small-object slabs contended by
attacker-influenced drm allocations) allows reclamation; teardown then
kfrees stale pointers read from reclaimed memory (arbitrary-free shape).
On the standard no-hardening target this is a privesc-grade primitive;
full chain is hardware-gated and not demonstrated here.

## Fix

`fix.diff` — remove the `dal_gpio_destroy_ddc(&ddc)` call at dc_link.c:362
(the caller does not own `ddc`). Fix-validated with
`ddc_uaf_harness_fixed.c`: patched chain exits 0, ddc_pin stays live until
owner destroy, 4 allocs / 4 frees balanced (fix_run.log).

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