amdgpu atom-i2c consumes the shared ATOM fb scratch after amdgpu_atom_execute_table released ctx->mutex β radeon's scratch_mutex fix was never ported; concurrent hw-DDC/atom-fb users deliver cross-contaminated DDC data
Summary
The copy-out of the shared scratch runs after ctx->mutex is released - a concurrent hw-i2c on another DDC bus (amdgpu_hw_i2c defaults 1) overwrites the scratch in the window, delivering another bus's/torn EDID bytes. The radeon twin fixed exactly this upstream with scratch_mutex (never ported to amdgpu). Memory-safe (num<=255 vs 20KiB scratch); same-class hole at atombios_dp.c:114. Port-the-scratch_mutex fix (4 files).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5163 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ddc_race_sketch.c | β | 2.3 KB | view raw | |
| build.sh | β | 104 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 122 B | view raw | |
| run.log | β | 212 B | view raw | |
| env.txt | β | 531 B | view raw | |
| fix.diff | β | 2.4 KB | view raw | |
| README.md | β | 876 B | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-5163 β amdgpu atom-i2c scratch race (missing port of radeon scratch_mutex)
PoC verdict: not_reproduced on audit guest (virtio VGA, amdgpu never
attaches). Structural race proven by inspection; see VERDICT.md.
Reproduce (on amdgpu hardware)
Two hw_capable DDC buses under concurrent probe (amdgpu_hw_i2c=1 default in
this tree), e.g. repeated display hotplug polling on two connectors while
reading EDIDs; look for EDID bytes from one connector appearing on the other
(torn/interleaved reads). ddc_race_sketch.c documents the window; there is
no userland-only driver for this path on DragonFly (the amdgpu i2c adapters
are internal to drm DDC probing).
Artifacts
ddc_race_sketch.cβ documented trigger sketch (compile-clean)fix.diffβ 4-file port of radeon's scratch_mutex fix (git-apply-able)VERDICT.mdβ full line-cited analysis
DF-5163 β amdgpu atom-i2c reads the shared ATOM scratch outside any scratch lock (radeon's scratch_mutex fix never ported)
Verdict (short)
Not reproduced on the audit guest β the guest VGA is virtio (chip 0x11111234,
Red Hat), amdgpu never attaches, and the race requires real ASICs with two
hw-capable DDC buses under concurrent probe load. The structural defect is
proven by inspection (likely confidence: the window is unambiguous in the
source; manifestation needs concurrent atom-fb users, which is plausible
during multi-connector hotplug storms but was not observable here).
Memory-safe: the read is bounded (num <= 255, atombios_i2c.c:79) and the
scratch is >= 20 KiB (amdgpu_atombios.c:1813-1821) β impact ceiling is
cross-contaminated DDC data, not corruption.
The defect, line by line
-
amdgpu_atombios_i2c_process_i2c_ch()takes only the per-bus lock:mutex_lock(&chan->mutex)βsys/dev/drm/amd/amdgpu/atombios_i2c.c:52.chanis the single i2c channel; it does NOT serialize anything across different buses. -
The atom table is executed synchronously β
amdgpu_atom_execute_table(adev->mode_info.atom_context, index, ...)βsys/dev/drm/amd/amdgpu/atombios_i2c.c:94. Inside,ctx->mutexserializes concurrent executions (sys/dev/drm/amd/amdgpu/atom.c:1263) and is released when execute_table returns (atom.c:1278). -
The firmware deposits the received i2c bytes in the shared fb scratch
atom_context->scratch(allocated once per GPU:amdgpu_atombios.c:1814-1821, default 20 KiB, may be enlarged per VBIOS), and the driver then copies them out:amdgpu_atombios_copy_swap(buf, base, num, false)βsys/dev/drm/amd/amdgpu/atombios_i2c.c:103-104β with no lock held that another atom-fb user respects. -
Window: bus X executes (scratch := Xdata); bus Y executes (scratch := Ydata); bus X's copy_swap returns Ydata (or a torn mix) as X's i2c read. Any concurrent atom command that touches the fb window (a second ProcessI2cChannelTransaction on another DDC bus is the natural one β the algorithm is selected per-bus at
sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:187-190wheneverrec->hw_capable && amdgpu_hw_i2c, andamdgpu_hw_i2cdefaults to 1 in this tree,sys/dev/drm/amd/amdgpu/amdgpu_drv.c:88) can interleave. -
The radeon twin already fixed this class:
sys/dev/drm/radeon/atom.h:128declaresstruct lock scratch_mutex, it is initialized atsys/dev/drm/radeon/radeon_device.c:994(LK_CANRECURSE), taken around the whole table execution insys/dev/drm/radeon/atom.c:1247-1252, and held across execute+read bysys/dev/drm/radeon/atombios_i2c.c:51/99(andatombios_dp.c:101/149). The amdgpustruct atom_context(sys/dev/drm/amd/amdgpu/atom.h:128-147) has no such member β the fix was never ported to the amdgpu copy of this driver pair.
Impact
- Data confusion only: a DDC/EDID read attributed to the wrong bus, or torn.
EDID parsing then consumes bytes from the other connector's transaction.
No OOB (
numbounded; scratch oversized), no uninit leak (scratch is kzalloc'd), no write to shared state (copy direction is scratchβbuf). - Severity Info; realistic ceiling is a misdected monitor β worth fixing
because the identical code path in radeon was deemed race-worthy upstream,
and because the same unprotected post-execute scratch read also exists in
sys/dev/drm/amd/amdgpu/atombios_dp.c:114(DP aux path), which the same one-lock fix umbrella would cover.
Fix
fix.diff (4 files, mirrors the radeon fix exactly):
- atom.h: add struct lock scratch_mutex to struct atom_context;
- atom.c: rename the body to amdgpu_atom_execute_table_scratch_unlocked()
and make the public amdgpu_atom_execute_table() hold scratch_mutex
around it (lock order scratch β ctx->mutex, same as radeon);
- atombios_i2c.c: hold scratch_mutex across execute+copy_swap
(LK_CANRECURSE makes the nesting with execute_table legal);
- amdgpu_atombios.c: lockinit at the context-creation site next to the
existing agmiacm lockinit.
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_testableFix validation not testable: no AMD GPU on the audit guest, so neither baseline nor patched behavior can be exercised. fix.diff mirrors the upstream-accepted radeon fix line-for-line in structure and applies cleanly (git apply --check).
fix.diff (atom.h/atom.c/atombios_i2c.c/amdgpu_atombios.c); radeon reference implementation cited in VERDICT.md
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/atombios_i2c.c:52
- sys/dev/drm/amd/amdgpu/atombios_i2c.c:94
- sys/dev/drm/amd/amdgpu/atombios_i2c.c:103
- sys/dev/drm/amd/amdgpu/atom.c:1263
- sys/dev/drm/amd/amdgpu/atom.c:1278
- sys/dev/drm/amd/amdgpu/atom.h:128
- sys/dev/drm/amd/amdgpu/amdgpu_atombios.c:1814
- sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:187
- sys/dev/drm/amd/amdgpu/amdgpu_drv.c:88
- sys/dev/drm/radeon/atom.h:128
- sys/dev/drm/radeon/atom.c:1247
- sys/dev/drm/radeon/atombios_i2c.c:51
- sys/dev/drm/amd/amdgpu/atombios_dp.c:114
Detail
Exploit chain
thread A (DDC bus X) executes ProcessI2cChannelTransaction filling scratch with X's reply; thread B (DDC bus Y) executes the same table overwriting scratch; thread A's amdgpu_atombios_copy_swap (atombios_i2c.c:104) then returns Y's bytes as X's i2c read -> misattributed/torn EDID on connector probing. No memory-corruption or disclosure step exists (bounded copy out of zero-initialized oversize scratch).
Evidence (decisive lines)
VERDICT.md (line-cited window + radeon fix references); run.log (guest: BUILD_OK, VGA is virtio, AMDGPU_NOT_LOADED); fix.diff (4-file scratch_mutex port, git apply --check clean)
PoC changes
no orchestrator seed existed; conceptual trigger sketch authored fresh (no userland driver for amdgpu internal DDC buses on DragonFly β race is kernel-internal)
Verified recommended fix
Port radeon's scratch_mutex: add struct lock scratch_mutex to amdgpu atom_context, hold it around the whole amdgpu_atom_execute_table and across the i2c copy_swap window (see fix.diff, 4 files).
Verdict
Structural data race, proven by inspection, not observable on this guest (virtio VGA; amdgpu never attaches β env.txt/run.log). amdgpu_atombios_i2c_process_i2c_ch() copies the received i2c bytes out of the shared atom fb scratch (atombios_i2c.c:103-104) after amdgpu_atom_execute_table() has already released ctx->mutex (atom.c:1278); amdgpu's struct atom_context lacks the scratch_mutex that radeon added in 2015 to close exactly this window (radeon/atom.h:128, radeon/atom.c:1247-1252, radeon/atombios_i2c.c:51/99). Two concurrent atom-fb users on different hw_capable DDC buses (selected by default since amdgpu_hw_i2c==1, amdgpu_drv.c:88) interleave and deliver cross-contaminated/torn DDC data. Memory-safe (num<=255 vs >=20KiB kzalloc'd scratch) β impact ceiling is wrong EDID/DDC data, severity Info.
No comments yet.