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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5035 Β· 6 files
FileTypeDescriptionSize
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)

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):

  1. A reads i2c_hw_buffer_in_use == false
  2. B reads i2c_hw_buffer_in_use == false
  3. A's engine->base.funcs->acquire() takes DDC1 pins β€” succeeds
  4. B's engine->base.funcs->acquire() takes DDC2 pins β€” succeeds (different struct ddc, different gpio pins β€” the per-engine acquire does NOT serialize cross-line)
  5. 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.c submit_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.

VERDICT.md
↓ download raw

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

n/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).
↓ fix.diffper-fix-DF-5035

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).