# DF-4555 — dce_aux: NULL-deref panic via ignored `acquire()` failure + unconditional `release_engine()` close of `engine->ddc`

## What

`release_engine()` (sys/dev/drm/amd/display/dc/dce/dce_aux.c:53-63) unconditionally
executes `dal_ddc_close(engine->ddc)`. `engine->ddc` is only ever assigned at the
*end* of a successful `acquire()` (dce_aux.c:894); after every completed
transaction `release_engine()` sets it back to NULL (dce_aux.c:60), and the
constructor initializes it to NULL (dce_aux.c:928). Therefore, whenever
`acquire()` fails — `is_engine_available()` reports DMCU ownership
(dce_aux.c:877-881), `dal_ddc_open()` fails (dce_aux.c:883-887), or
`acquire_engine()` loses arbitration (dce_aux.c:889-892) — `engine->ddc == NULL`.

The in-tree caller `dc_link_aux_transfer()` (sys/dev/drm/amd/display/dc/core/dc_link_ddc.c:655-684)
**ignores the return value of `acquire()`**, proceeds to submit the transaction on
an engine the thread does not own, and then always calls `release_engine()`
(dc_link_ddc.c:684) → `dal_ddc_close(NULL)`.

`dal_ddc_close()` (sys/dev/drm/amd/display/dc/gpio/gpio_service.c:587-592)
immediately evaluates `ddc->pin_clock` **without a NULL check** — a read from
near-NULL (offset of `pin_clock` in `struct ddc`) → kernel pagefault → panic.

(The NULL guard exists one function down — `dal_gpio_close()` at
sys/dev/drm/amd/display/dc/gpio/gpio_base.c:220-224 checks `if (!gpio) return;` —
but it is never reached because the deref happens while evaluating its argument.)

## Threat model

* Requires an AMD GPU driven by the DC/amdgpu path (dce80/100/110/112/120/dcn10
  resource pools all instantiate these engines — see dce80_resource.c:484,
  dce110_resource.c:620, dce112_resource.c:625, dce120_resource.c:393,
  dcn10_resource.c:625).
* Trigger surface: any DP AUX activity racing another user of the AUX engine or
  the DDC GPIO pins — HPD hotplug work vs. link training vs. MST sideband
  (`amdgpu_dm_mst_types.c:97,109,125,140` all funnel into
  `dc_link_aux_transfer`). The failure window is the arbitration/GPIO-open step.
* No unprivileged-local vector on DragonFly: the drm_dp_aux chardev is compiled
  out (`sys/dev/drm/drm_dp_aux_dev.c` body is `#if 0`), and modesetting needs
  DRM master. The realistic attacker is (a) malicious/marginal DisplayPort
  hardware timing HPD, or (b) unlucky contention → **local DoS (panic)**.

## Upstream divergence (proof this is a defect, not a design choice)

Current Linux performs the equivalent of:

```c
	if (!acquire(aux_engine, ddc_pin)) {      /* torvalds/linux dce_aux.c dce_aux_transfer_raw_with_ddc_pin() */
		*operation_result = AUX_CHANNEL_OPERATION_FAILED_ENGINE_ACQUIRE;
		return -1;
	}
	...
	release_engine(aux_engine);
```

i.e. upstream checks `acquire()` before submit/release; the DragonFly DC copy
(dated DC 3.1.x) predates that fix.

## Reproduction

Hardware-gated (needs an AMD DC GPU with DisplayPort). The audit guest runs
Bochs VGA (vendor 0x1234 device 0x1111) — see env.txt — so the path cannot be
exercised there. `trigger.c` documents the minimal concurrent-transfer trigger
for a machine that has the hardware.

## Fix

Guard in `release_engine()` (defense-in-depth) *and* honor `acquire()` failure
in `dc_link_aux_transfer()` (primary). See fix.diff.

## Impact ceiling

Kernel NULL-pointer read → panic → local/physical DoS on AMD-GPU systems.
No memory disclosure, no control over the faulting address (fixed near-NULL
offset), no write primitive.

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