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)
PoC verification
Evidence pack
findings/poc/DF-3995 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 bylink->ddc(struct ddc_service), created once incore/dc_link_ddc.c:272(ddc_service->ddc_pin = dal_gpio_create_ddc(...)) and is supposed to be destroyed only by the owner indal_ddc_service_destroyβdestruct(dc_link_ddc.c:306-307).sys/dev/drm/amd/display/dc/core/dc_link.c:360-365β ondal_ddc_open(ddc, GPIO_MODE_INPUT, GPIO_DDC_CONFIG_TYPE_MODE_I2C)failure the code callsdal_gpio_destroy_ddc(&ddc), which (gpio/gpio_service.c:459-471) destroyspin_data,pin_clock, kfrees thestruct ddc, and nulls only the local variable β whileddc_service->ddc_pinstill points at the freed memory.
Consequences
- Use-after-free: every later use of the connector's DDC service dereferences
ddc_service->ddc_pin: -dc_link_is_dp_sink_presentitself 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-656readsddc->ddc_pin->pin_data->ento select the AUX engine (freed-memory read; in-kernel: garbage engine index /res_pool->engines[...]OOB or fault). - Double free: at link destruction
destruct(ddc_service)(dc_link_ddc.c:306-307) callsdal_gpio_destroy_ddc(&ddc->ddc_pin)on the already-freed object: it readspin_data/pin_clockout of freed heap and kfrees those stale pointers β an uncontrolled double free / arbitrary-free-shaped primitive, pluskfreeof 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
(
pciconfshows no amdgpu device), so a live kernel trigger cannot be run on this guest. ddc_uaf_harness.creproduces the exact ownership chain with the verbatim destroy logic; with an injecteddal_ddc_openfailure it shows (run.log): the borrowed free, the DANGLINGddc_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
fixedfix.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
Confirmed kernel references
- sys/dev/drm/amd/display/dc/core/dc_link.c:348
- sys/dev/drm/amd/display/dc/core/dc_link.c:362
- sys/dev/drm/amd/display/dc/core/dc_link.c:969
- sys/dev/drm/amd/display/dc/core/dc_link_ddc.c:272
- sys/dev/drm/amd/display/dc/core/dc_link_ddc.c:307
- sys/dev/drm/amd/display/dc/core/dc_link_ddc.c:643
- sys/dev/drm/amd/display/dc/gpio/gpio_service.c:459
- sys/dev/drm/amd/display/dc/gpio/gpio_base.c:59
- sys/dev/drm/amd/display/dc/i2caux/aux_engine.c:82
- sys/dev/drm/amd/display/dc/i2caux/i2c_hw_engine.c:190
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.
No comments yet.