β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4555

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4555 Β· 10 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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:

    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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-4555

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.