dce_aux: ignored acquire() failure + unconditional release_engine() closes NULL ddc β near-NULL deref panic under AUX/GPIO contention
Summary
release_engine unconditionally does dal_ddc_close(engine->ddc), but ddc is only assigned at the end of a successful acquire - and the live caller ignores acquire's return, submits on an unowned engine, then always releases: dal_ddc_close(NULL) evaluates ddc->pin_clock without a NULL check (the guard exists one level down, never reached). Upstream checks acquire. Contention window = AUX arbitration vs HPD hotplug work/MST sideband; malicious/marginal DP hardware timing. NULL-deref panic only. Honor-acquire + NULL-safe-release fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4555 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.2 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| trigger.c | β | 2.2 KB | view raw | |
| run.sh | β | 89 B | view raw | |
| run.log | β | 89 B | view raw | |
| env.txt | β | 472 B | view raw | |
| panic.txt | β | 43 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| manifest.json | β | 920 B | view raw | |
| verdict.json | β | 2.7 KB | view raw |
DF-4555 β dce_aux release_engine() NULL-deref (ignored acquire() failure)
Build
Hardware-gated conceptual trigger (trigger.c): no build needed; it documents
the concurrent-AUX contention that forces acquire() failure. On DragonFly
there is no dp_aux chardev, so the real trigger is HPD/retrain contention.
Run
./run.sh (records the guest attempt + hardware inventory).
Expected
On a machine with an AMD DC GPU, when two AUX consumers race (HPD hotplug
work vs link training vs MST sideband) and one thread's
aux_engine->funcs->acquire() fails (dce_aux.c:877-892 failure exits leave
engine->ddc == NULL), dc_link_aux_transfer() (dc_link_ddc.c:656 ignores
the result) still calls release_engine() (dc_link_ddc.c:684) β
dal_ddc_close(NULL) (dce_aux.c:58) β dal_gpio_close(ddc->pin_clock)
evaluated with ddc==NULL (gpio_service.c:590) β near-NULL read β panic:
fatal fault in dal_gpio_close+... <- dal_ddc_close <- release_engine <- dc_link_aux_transfer
On the audit guest: exits 1 with "hardware-gated" message; devinfo -v
shows zero 0x1002 devices (see env.txt).
Success criterion
Panic signature above on affected hardware; absence of the fault after applying fix.diff (acquire() honored + NULL-safe release_engine).
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,140all funnel intodc_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.cbody 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:
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.
Fix verification
not_testableFix not validated on guest: baseline cannot run (no AMD GPU). fix.diff is line-accurate against dce_aux.c:53-63 / dc_link_ddc.c:655-656 and mirrors upstream's acquire()-check; validated by inspection only.
findings/poc/DF-4555/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (full path:line proof + upstream divergence); trigger.c (contention trigger doc); env.txt + run.log (guest hardware absence proof); fix.diff
PoC changes
Seed replaced: no runnable userspace trigger exists on DragonFly (dp_aux chardev compiled out, modesetting needs DRM master); trigger.c rewritten as documented contention recipe; run.log records the honest hardware-gated attempt.
Verified recommended fix
Honor acquire() return in dc_link_aux_transfer() and make release_engine() NULL-safe (see fix.diff).
Verdict
Provable NULL-deref path: dc_link_aux_transfer() (dc_link_ddc.c:655-684) ignores aux_engine->funcs->acquire() failure; on failure engine->ddc is NULL (only set at dce_aux.c:894 on success, cleared at :60 on release), so its unconditional release_engine() (dce_aux.c:58) calls dal_ddc_close(NULL), which dereferences ddc->pin_clock without a NULL check (gpio_service.c:590) -> near-NULL read -> panic. Upstream Linux checks acquire() in dce_aux_transfer_raw_with_ddc_pin(), confirming the defect. Not reproduced on the audit guest because it has no AMD GPU (Bochs 0x1234:0x1111, zero 0x1002 devices; env.txt/run.log) and DragonFly compiles out the dp_aux chardev (drm_dp_aux_dev.c is #if 0), so the trigger requires an AMD-DC host with AUX/GPIO contention (hotplug vs retrain vs MST sideband). Defect certain from source; manifestation hardware-gated.
No comments yet.