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

dc_link_is_dp_sink_present frees a BORROWED struct ddc on DDC-open failure: use-after-free on every later DDC/AUX transaction plus stale-pointer double free at connector teardown

Summary

On the dal_ddc_open() failure path, dc_link_is_dp_sink_present calls dal_gpio_destroy_ddc(&ddc) on a pointer borrowed from the link's ddc_service β€” destroying pin_data, pin_clock and kfrees the struct ddc while ddc_service->ddc_pin (the sole owner) still points at freed memory. Every subsequent DDC/AUX operation dereferences the dangling pointer, and the owner destroy at teardown dereferences and kfrees the stale object again β€” uncontrolled double free. Trigger: HPD-driven detect racing AUX/I2C transactions holding the same DDC pins in other GPIO modes (ALREADY_OPENED), a stalling sink widens the window. Verified with a verbatim-code harness on the guest (borrowed free -> dangling owner pointer -> UAF read on next AUX -> owner-destroy deref of freed memory, deterministic across 3 runs); fixed harness balanced. Live trigger hardware-gated (no AMD GPU).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3995 Β· 12 files
FileTypeDescriptionSize
ddc_uaf_harness.c β€” 8.0 KB view raw
ddc_uaf_harness_fixed.c β€” 7.7 KB view raw
build.sh β€” 109 B view raw
run.sh β€” 101 B view raw
build.log β€” 190 B view raw
run.log β€” 1.3 KB view raw
run.2.log β€” 10 B view raw
run.3.log β€” 10 B view raw
fix_run.log β€” 84 B view raw
env.txt β€” 204 B view raw
fix.diff β€” 817 B view raw
VERDICT.md β€” 4.6 KB ↓ raw
VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (drop the destroy of the borrowed ddc) applied to the harness: baseline UAF=YES/UAF_DEREF=YES RC=2; patched chain keeps ddc_pin live until owner destroy, 4 allocs / 4 frees balanced, RC=0 (fix_run.log). No kernel build performed (no AMD GPU to attach dc_link); the diff deletes a single erroneous call.

findings/poc/DF-3995/fix_run.log; findings/poc/DF-3995/ddc_uaf_harness_fixed.c
↓ fix.diffper-fix-DF-3995

Confirmed kernel references

Detail

Exploit chain

DP hotplug/detect races AUX engine pin ownership (malicious sink can stall AUX transactions to widen the window) -> single dal_ddc_open failure frees struct ddc + 2 gpio objects -> reclaimed by attacker-influenced drm allocations -> later AUX/DPCD traffic UAF-writes through freed gpio pointers; teardown double-frees stale pointers read from reclaimed memory (arbitrary-free shape) -> heap corruption -> uid=0 potential on no-hardening targets. Live chain hardware-gated (no AMD GPU in QEMU).

Evidence (decisive lines)

run.log (borrowed free, DANGLING ddc_service->ddc_pin, UAF read at next AUX transaction, owner-destroy UAF deref; summary UAF=YES UAF_DEREF=YES, RC=2), run.2.log/run.3.log (deterministic), fix_run.log (patched chain clean RC=0), VERDICT.md full path:line trace

PoC changes

Seed sketch replaced entirely: ownership-chain harness reproducing dal_gpio_create_ddc/dal_gpio_destroy_ddc/dal_ddc_service create+destroy and the verbatim dc_link_is_dp_sink_present failure path; freed-pointer registry (v1 used freed-memory poisoning which the allocator overwrote - fixed) proves dangling + double-free without dereferencing freed memory in the harness itself.

Verified recommended fix

Remove the dal_gpio_destroy_ddc(&ddc) call at dc_link.c:362 - the pointer is borrowed from ddc_service which destroys it at teardown - see fix.diff

Verdict

Reproduced via verbatim ownership-chain harness on the guest (no AMD GPU present on this VM, so a live kernel trigger is impossible): when dal_ddc_open fails with the pins concurrently held by an aux/i2c engine transaction, dc_link_is_dp_sink_present (dc_link.c:362) kfrees the BORROWED struct ddc and both gpio pins while ddc_service->ddc_pin still points at them - subsequent DDC/AUX traffic on the connector dereferences freed memory (dc_link_ddc.c:643-656) and the owner destroy at driver teardown (dc_link_ddc.c:307) dereferences the freed object and kfrees stale pin pointers (uncontrolled double free). Exit code 2, deterministic over 3 runs. Fix validated at harness level: dropping the borrowed destroy yields a balanced 4/4 alloc-free chain with no dangling pointer.