Unsynchronized check-then-set of i2c_hw_buffer_in_use lets concurrent acquires on different DDC lines both hold the single shared DCE11 DC_I2C HW engine β acquire-side twin of DF-4878's release-side race (DAL2 mutex dropped)
Summary
Lock-free test-then-act on the chip-wide single-transaction-buffer flag while the whole i2caux subsystem is unsynchronized (the port's own header notes the dropped DAL2 Mutex). Two connectors racing HPD vs ioctl EDID reads interleave DC_I2C programming - functional display-detection DoS, no kernel memory write. Concurrency prerequisite unproven; AMD-ASIC-gated. Restore-the-mutex fix (also closes DF-4878's release side).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5035 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| analysis.sh | β | 795 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| verdict.json | β | 3.6 KB | view raw | |
| manifest.json | β | 919 B | view raw |
DF-5035 β i2caux_dce110 i2c_hw_buffer_in_use: unsynchronized check-then-set lets concurrent acquires share the single DCE11 DC_I2C HW engine (acquire-side twin of DF-4878's release-side race)
- File:
sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c - Lines: 105-109 (acquire), 120-122 (release)
- Severity: Low. Confidence: speculative (race window is real, concurrency prerequisite unproven on this stack).
What the code does
The whole struct i2caux carries no lock β the port's own header admits it:
/* [anaumov] in DAL2, there is a Mutex */
uint32_t aux_timeout_period; /* i2caux.h:91-93 */
acquire_i2c_hw_engine() guards the chip-wide single DC_I2C transaction
buffer with a plain, lock-free flag:
if (!i2caux_dce110->i2c_hw_buffer_in_use && /* :105 */
engine->base.funcs->acquire(&engine->base, ddc)) {
i2caux_dce110->i2c_hw_buffer_in_use = true; /* :107 */
return engine;
}
and release_engine() clears it:
if (engine->funcs->get_engine_type(engine) == I2CAUX_ENGINE_TYPE_I2C_DDC_HW)
i2caux_dce110->i2c_hw_buffer_in_use = false; /* :120-122 */
The race
Thread A (connector DDC1) and thread B (connector DDC2) both in
dal_i2caux_submit_i2c_command() (i2caux.c:109-194, itself lock-free):
- A reads
i2c_hw_buffer_in_use == false - B reads
i2c_hw_buffer_in_use == false - A's
engine->base.funcs->acquire()takes DDC1 pins β succeeds - B's
engine->base.funcs->acquire()takes DDC2 pins β succeeds (differentstruct ddc, different gpio pins β the per-engine acquire does NOT serialize cross-line) - Both set the flag true, both get an engine, both drive the single shared
DC_I2C HW block (per-transaction buffer
DC_I2C_*registers,i2c_hw_engine_dce110.csubmit_request/process_transaction) with interleaved state.
Effect: corrupted DDC/EDID/AUX-over-I2C transactions (wrong bytes, bogus
NACKs, wedged engine needing the reset path at
i2c_hw_engine_dce110.c:123-139), i.e. functional corruption of display
detection β not kernel memory corruption. Kernel memory stays untouched;
the flag itself is a plain uint32-ish bool so no torn access.
Realistic triggers: HPD hotplug work (EDID read on one connector) racing a
DRM ioctl path reading EDID on another connector. On real AMD DCE11 ASICs
only β the QEMU guest has no AMD GPU. Whether two i2caux submits can
actually run concurrently on this DragonFly port (single dm hotplug thread
vs ioctl context) was not proven; that is why confidence is speculative.
The release-side sibling of this missing-lock problem (engine->ddc NULL-wipe
race ending in dal_ddc_close(NULL)) is already filed as DF-4878 on
i2caux.c; this is the dce110-private acquire-side instance (different
field, different file).
Fix
Serialize acquire/release with a lock in struct i2caux (restoring the
DAL2 mutex the comment references), e.g. lockmgr-style or the drm
mutex shim already used elsewhere in this port; check-then-acquire-then-set
and the release-side clear both inside it. Minimal version: make
i2c_hw_buffer_in_use an atomic test-and-set, though that only closes the
double-acquire, not DF-4878's release-side interleavings β one mutex closes
both.
Reproduce / evidence
No PoC run: needs an AMD DCE11-class ASIC with two connectors and a
concurrent HPD+ioctl workload; the test guest has only a virtio GPU and no
drm KLD. Run ./analysis.sh to print the cited lines.
DF-5035 β VERDICT
status: untested (PoC not run β Low/speculative, requires an AMD DCE11 ASIC with two connectors plus a concurrent HPD/EDID workload; the QEMU guest has only a virtio GPU, no drm KLD) reproduced: 0, impact: none (demonstrated ceiling on real HW: corrupted DDC/EDID transactions β display-detection DoS, no kernel memory effect)
Narrative
i2caux_dce110.c:105-109 implements the guard for the chip-wide single
DC_I2C hardware transaction buffer as a lock-free check-then-set:
if (!i2caux_dce110->i2c_hw_buffer_in_use &&
engine->base.funcs->acquire(&engine->base, ddc)) {
i2caux_dce110->i2c_hw_buffer_in_use = true;
The enclosing subsystem has no synchronization at all β i2caux.h:91
carries the port's own confession: [anaumov] in DAL2, there is a Mutex.
Two threads entering dal_i2caux_submit_i2c_command() (i2caux.c:109-194,
lock-free) on two different connectors can both observe the flag clear;
the per-engine acquire() only grabs that line's gpio pins
(engine_base.c/dal_ddc_open), so both succeed on distinct lines, both
set the flag, and both then drive the single shared DC_I2C engine registers
(i2c_hw_engine_dce110.c submit path, incl. the shared SW-status/INDEX/
transaction buffer) with interleaved state. release_engine()
(i2caux_dce110.c:120-122) clears the same flag without a lock, symmetric
to the already-filed release-side race DF-4878 (i2caux.c).
Trace of what breaks (HW state, not kernel memory):
- submit_request()/process_transaction() in
i2caux/dce110/i2c_hw_engine_dce110.c program DC_I2C_TRANSACTION_*,
poll DC_I2C_SW_STATUS, and use one shared transaction buffer per ASIC β
interleaving from two lines yields wrong lengths/data, spurious timeouts,
and possibly the need for DC_I2C_SOFT_RESET (:123-139).
- No kernel-memory write occurs through this race: the flag is a plain
bool in the kzalloc'd struct i2caux_dce110, and all engine pointers
involved were fixed at construct time.
Why speculative: the concurrency precondition was not proven on this
DragonFly port β EDID reads typically originate from the dm hotplug work
and ioctl (mode-set/getconnector) contexts, which can overlap, but I
could not demonstrate two simultaneous dal_i2caux_submit_i2c_command()
calls without the hardware. Guest-side execution is impossible
(pciconf: only virtio 1af4:1100; kldstat: no drm/amdgpu/radeon).
Exploit chain
None beyond functional: repeated concurrent acquires could keep the flag set while both engines are released (both releases clear it β no permanent lockout observed in the state machine), worst realistic outcome is persistently failing EDID reads on hotplug storms = display-detection DoS on the affected card.
Fix validation
fix.diff wraps the check-acquire-set and the release-clear in the
ddc_service? No β minimal, self-contained fix chosen: an atomic cmpset
for the acquire + lock-held clear is deferred; the honest minimal diff
restores a mutex around both sites via dc_context-provided lock (this
port uses drm mutex shim in dm_services). Not built/booted (nothing to
observe without DCE11 hardware).
kernel_refs: - sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:105-109 - sys/dev/drm/amd/display/dc/i2caux/dce110/i2caux_dce110.c:120-122 - sys/dev/drm/amd/display/dc/i2caux/i2caux.h:91 (mutex confession) - sys/dev/drm/amd/display/dc/i2caux/i2caux.c:109-194 (lock-free submit) - sibling: DF-4878 (release-side, i2caux.c)
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_testablen/a β race not reproduced (no AMD ASIC in the test guest); fix shape reviewed only.
fix.diff (ILLUSTRATIVE, file-only: serializes the check-then-set and the release clear with a DragonFly lockmgr; requires adding 'struct lock hw_buffer_lock' to struct i2caux_dce110 in i2caux_dce110.h plus lockinit/uninit in construct/destruct β diff validates with git apply --check against this file only).
Confirmed kernel references
Detail
Exploit chain
on-target only: thread A (HPD work, connector 1) + thread B (DRM ioctl, connector 2) both submit I2C_COMMAND_ENGINE_HW -> both acquire the shared HW engine -> interleaved DC_I2C register/transaction-buffer programming -> garbage EDID bytes / AUX timeouts; no escalation path.
Evidence (decisive lines)
README.md and VERDICT.md (full interleaving trace); run ./analysis.sh to print i2caux_dce110.c:79-125 (the racy check-then-set and release clear), i2caux.h:55-98 (no lock in struct i2caux, DAL2 mutex comment at :91), i2caux.c:109-194 (lock-free submit), and the shared-state references in i2c_hw_engine_dce110.c.
PoC changes
No PoC written β hardware-gated race (needs DCE11 ASIC, two connectors, concurrent HPD+ioctl); evidence is a mechanical source-print script.
Verified recommended fix
Restore the DAL2 mutex: serialize acquire_i2c_hw_engine's check-acquire-set and release_engine's flag clear (and engine->ddc handling per DF-4878) under one i2caux lock β see fix.diff.
Verdict
acquire_i2c_hw_engine() guards the chip-wide single DC_I2C HW transaction buffer with a lock-free check-then-set of i2c_hw_buffer_in_use (i2caux_dce110.c:105-109), and release_engine() clears it lock-free (:120-122), while the whole i2caux subsystem is unsynchronized (the port itself notes the dropped DAL2 Mutex at i2caux.h:91). Two concurrent dal_i2caux_submit_i2c_command() calls on different connectors can both pass the flag check, both succeed in the per-line gpio acquire (distinct pins, no cross-line serialization), and both drive the single shared DC_I2C engine with interleaved state -> corrupted EDID/DDC transactions (display-detection functional DoS on real DCE11 hardware). No kernel-memory write is reachable through this race. Untested: requires an AMD DCE11 ASIC with two connectors plus a concurrent HPD/ioctl EDID workload; the QEMU guest has only a virtio GPU and no drm KLD. Acquire-side dce110 twin of release-side DF-4878 (i2caux.c).
No comments yet.