amdgpu bit-bang i2c adapters registered without lock_ops β NULL function-pointer call in i2c_transfer() β kernel panic on first DDC/EDID access
Summary
The bit branch registers via i2c_bit_add_bus only, but DF's compat core installs lock_ops ONLY in i2c_add_adapter - every i2c_transfer on a !hw_capable GPIO DDC bus calls lock_ops->lock_bus() through NULL (radeon's identical branch carries the explicit DFly fix; only amdgpu forgot it). REPRODUCED on guest with the REAL unmodified amdgpu_i2c.c+linux_i2c.c: Fatal trap 12 at i2c_transfer+0x2b (VA 0x0), guest down; the radeon-style i2c_add_adapter fix behaviorally validated (clean chain, load/unload). Boot/hotplug-time crash on any AMD-GPU system; fixed-NULL ceiling = DoS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4961 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| agi2c_probe.c | β | 8.0 KB | view raw | |
| Makefile | β | 1.4 KB | β download | |
| build.sh | β | 244 B | view raw | |
| run.sh | β | 189 B | view raw | |
| build.log | β | 11.8 KB | view raw | |
| build.2.log | β | 11.8 KB | view raw | |
| run.log | β | 1.2 KB | view raw | |
| run.2.log | β | 2.1 KB | view raw | |
| panic.txt | β | 732 B | view raw | |
| env.txt | β | 203 B | view raw | |
| fix.diff | β | 694 B | view raw | |
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 6.9 KB | β raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 7.7 KB | view raw |
DF-4961 β amdgpu_i2c.c bit-bang adapters have NULL adapter->lock_ops β NULL function-pointer call in i2c_transfer() β kernel panic
What
amdgpu_i2c_create()'s bit-bang branch (sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:193-212)
registers the adapter with i2c_bit_add_bus() only. In DragonFly's linux-compat
i2c core, ONLY i2c_add_adapter() installs adapter->lock_ops
(sys/dev/drm/linux_i2c.c:51-52); i2c_bit_add_bus() does not (it only sets
algo and retries, linux_i2c.c:128-134). Every subsequent
i2c_transfer() on such an adapter executes
adapter->lock_ops->lock_bus(adapter, I2C_LOCK_SEGMENT) at linux_i2c.c:99
β a call through a NULL pointer β kernel panic.
radeon's identical bit branch carries an explicit
#ifdef __DragonFly__ i2c_add_adapter(&i2c->adapter); #endif with the comment
"setup lock_ops" (radeon_i2c.c:970-972), and the DP aux core assigns its own
lock_ops (drm_dp_helper.c:1124) β only amdgpu_i2c.c's bit branch omits it.
Reachability (production)
- Every
!rec->hw_capableGPIO DDC bus (legacy/VGA-class connectors; thehw_capable && amdgpu_hw_i2cbranch at amdgpu_i2c.c:184 is not taken for them βamdgpu_hw_i2cdefaults to 1,amdgpu_drv.c:88, irrelevant here). - First
i2c_transfer()on the adapter: EDID probe viadrm_get_edid(..., &ddc_bus->adapter)(amdgpu_connectors.c:308-313) βdrm_do_probe_ddc_edid(drm_edid.c:1552), or this file's ownamdgpu_i2c_router_select_ddc_port()(amdgpu_display.c:463,amdgpu_connectors.c:292) /..._cd_port()(dce_v8_0.c:3229,dce_v10_0.c:3341,dce_v11_0.c:3467). - Trigger is kernel-driven (display detect / HPD poll) once the amdgpu driver is bound β no user interaction needed; panic on driver load/probe.
PoC (this directory)
The probe KLD compiles the REAL, UNMODIFIED audited files from the guest's
own /usr/src/sys (sha256-verified identical to the audit tree) β
sys/dev/drm/amd/amdgpu/amdgpu_i2c.c(the audited file)sys/dev/drm/linux_i2c.c(the compat core with the deref)
β plus agi2c_probe.c (fake drm_device/pci_dev/bus_rec/connector + linkage
stubs for the never-reached atombios hw algo / mmio accessors / logging).
The NULL deref happens in i2c_transfer() BEFORE any hardware access, so no
AMD GPU is required.
Build
./build.sh # or manually: transfer agi2c_probe.c + Makefile, then
# cd /tmp/df4961 && make (-Werror, bsd.kmod.mk)
Run
./run.sh # phase 1: baseline -> PANIC
# phase 2: fix.diff applied to /usr/src copy, rebuild
# -> lock_ops set, no panic, clean kldunload
Expected output
Baseline (run.log):
agi2c: chan=0xfffff8008de6f580 adapter.name='AMDGPU i2c bit bus DF4961-probe' agi2c: adapter.algo=0xffffffff826025a0 (== &i2c_bit_algo: YES - bit bus) master_xfer=0xffffffff82600d10 agi2c: adapter.lock_ops=0 <- i2c_transfer() needs this non-NULL agi2c: FIRING - REAL amdgpu_i2c_router_select_ddc_port(&fconn)... Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 Stopped at i2c_transfer+0x2b: movq (%rax),%rax <- loading lock_bus from NULL
Guest dies (vm.sh status β down).
Patched (run.2.log):
agi2c: adapter.lock_ops=0xffffffff826025d0 <- i2c_transfer() needs this non-NULL agi2c: lock_ops set - fix applied, firing anyway (no panic expected) agi2c: FIRING - REAL amdgpu_i2c_router_select_ddc_port(&fconn)... agi2c: [drm_dbg stub amdgpu_i2c_get_byte] i2c 0x03 0x00 read failed ... (4 get/put ops complete via the compat bit_xfer stub) agi2c: survived amdgpu_i2c_router_select_ddc_port (unfixed build panics ...) agi2c: unloaded
KLDLOAD2_OK + KLDUNLOAD_OK, guest stays up.
Fix
fix.diff β mirror radeon: call i2c_add_adapter() after
i2c_bit_add_bus() in the bit branch (git-apply-able against the repo root).
DF-4961 β VERDICT
Bottom line
REPRODUCED (in-kernel, real audited code): NULL function-pointer
dereference β kernel panic. The probe KLD compiles the REAL, UNMODIFIED
sys/dev/drm/amd/amdgpu/amdgpu_i2c.c (the audited file) and the REAL
sys/dev/drm/linux_i2c.c (the DragonFly linux-compat i2c core), both
sha256-verified byte-identical to the audit tree, and drives the production
chain with the audited file's own exported API:
amdgpu_i2c_create(dev, rec, "DF4961-probe") amdgpu_i2c.c:160-219
rec->hw_capable == false β bit branch amdgpu_i2c.c:193-212
i2c_bit_add_bus(&i2c->adapter) amdgpu_i2c.c:207
β sets adapter->algo only, NOT lock_ops linux_i2c.c:128-134
β returns chan with adapter.lock_ops == NULL (printed: 0)
amdgpu_i2c_router_select_ddc_port(&fconn) amdgpu_i2c.c:341-366
β amdgpu_i2c_get_byte() amdgpu_i2c.c:284-316
β i2c_transfer(&i2c_bus->adapter, msgs, 2) amdgpu_i2c.c:309
β adapter->lock_ops->lock_bus(...) linux_i2c.c:99
== (*(struct i2c_lock_operations *)NULL)->lock_bus()
Observed (run.log): adapter.lock_ops=0, then
Fatal trap 12: page fault ... fault virtual address = 0x0 ... Stopped at
i2c_transfer+0x2b: movq (%rax),%rax β the very instruction that loads the
lock_bus function pointer from the NULL lock_ops. The guest died
(vm.sh status β down).
Fix validated behaviorally: with fix.diff (the same
#ifdef __DragonFly__ i2c_add_adapter(&i2c->adapter); that radeon's bit
branch already carries, radeon_i2c.c:970-972) applied to the guest's
/usr/src/sys copy, the rebuilt module shows
adapter.lock_ops=0xffffffff826025d0 (set), the identical call chain runs to
completion β the compat bit_xfer stub returns 0, all four router get/put
byte ops take their "failed" debug path β NO panic, module loads AND
unloads cleanly, guest stays up (run.2.log).
1. Root cause, line by line
sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:193-212β the bit branch sets upi2c->bit.*callbacks and calls ONLYi2c_bit_add_bus(&i2c->adapter)(line 207).i2c_add_adapter()is never called on this path (the hw branch at lines 184-192 does call it, line 190).sys/dev/drm/linux_i2c.c:45-55βi2c_add_adapter()is the ONLY code that installs a defaultlock_ops(if (!adapter->lock_ops) adapter->lock_ops = &i2c_default_lock_ops;).sys/dev/drm/linux_i2c.c:128-134β DF'si2c_bit_add_bus()setsadapter->algoandadapter->retriesonly. (Upstream Linux'si2c_bit_add_businternally callsi2c_add_adapter; the DF port does not, so the CALLER must β radeon does.)sys/dev/drm/linux_i2c.c:92-104βi2c_transfer()dereferencesadapter->lock_ops->lock_bus(...)(line 99) with no NULL check.- Corroboration that this is a known DF-specific requirement:
sys/dev/drm/radeon/radeon_i2c.c:969-973β the identical radeon bit branch carries#ifdef __DragonFly__ /* setup lock_ops */ i2c_add_adapter(&i2c->adapter); #endif.sys/dev/drm/drm_dp_helper.c:1124,1156β the DP aux i2c adapter assigns its ownlock_ops(drm_dp_i2c_lock_ops) and callsi2c_add_adapter(). Only amdgpu's bit branch omits it.
2. Reachability / threat model
- Which buses: every
!hw_capableGPIO DDC line parsed from the VBIOS GPIO_I2C table (legacy/VGA-class connectors; boards commonly have both hw-capable DP aux lines and GPIO-only lines). The check at amdgpu_i2c.c:184 isrec->hw_capable && amdgpu_hw_i2c;amdgpu_hw_i2cdefaults to 1 (amdgpu_drv.c:88) and is irrelevant for!hw_capablerecords β they ALWAYS take the bit branch. - Production callers of
i2c_transfer()on those adapters: drm_get_edid(connector, &amdgpu_connector->ddc_bus->adapter)(amdgpu_connectors.c:308-313) βdrm_do_probe_ddc_edid(drm_edid.c:1552i2c_transfer(adapter, ...)).- This file's own exported router helpers:
amdgpu_i2c_router_select_ddc_port(amdgpu_display.c:463, amdgpu_connectors.c:292) andamdgpu_i2c_router_select_cd_port(dce_v8_0.c:3229, dce_v10_0.c:3341, dce_v11_0.c:3467 β HPD handlers), both viaamdgpu_i2c_get_byte/put_byte(amdgpu_i2c.c:309, :334). - Trigger: kernel-driven display detect / HPD polling / EDID probe β no user action required once the amdgpu driver is bound. On real hardware this is a boot-time or hotplug-time panic (local DoS, unauthenticated from the "console user" perspective).
- The full hardware entry is gated on AMD GPU hardware absent from this
guest; the panic itself is INDEPENDENT of hardware because
lock_busis called beforemaster_xfer(no mmio access happens first). The probe reproduces the exact code path with the exact audited objects; only the atombios hw algo (unreachable on the bit path), the mmio accessors (never dispatched β DF'si2c_bit_algo.master_xferis thebit_xferstub, linux_i2c.c:106-111) and logging are stubbed.
3. Observations mapped
| Observation | Value |
|---|---|
| Baseline kldload | panic Fatal trap 12, VA=0x0, i2c_transfer+0x2b: movq (%rax),%rax, guest down |
| Patched kldload | lock_ops=0xffffffff826025d0, no panic, KLDLOAD2_OK, KLDUNLOAD_OK, guest up |
| Status | reproduced |
| Impact | panic (local DoS on AMD-GPU systems; hardware-gated) |
| uid0 chain | none β NULL fp call at fixed address 0, no control over the call target (lock_ops is NULL, not attacker-written); DoS is the ceiling |
4. Fix validation
fix.diffapplied to guest/usr/src/sys/dev/drm/amd/amdgpu/amdgpu_i2c.c(grep confirms the newi2c_add_adapter(&i2c->adapter);at line 216), rebuilt with-Werror(build.2.log), identical probe re-run: previously-fatal chain completes, module unloads cleanly, guest stays up.- Side note observed in the fixed run: DF's compat
bit_xferis an explicit stub (linux_i2c.c:106-111"XXX Linux really does try to transfer some data here") β so even with the panic fixed, GPIO bit-bus DDC transfers functionally no-op on DragonFly (every get/put byte logs "read/write failed"). That is a functional gap of the compat layer, not a memory-safety issue, and is outside this finding's scope. - The fixed run's
[drm_dbg stub] i2c 0x03 0x00 read failedlines also live-demonstrate the uninitialized*valread/print at amdgpu_i2c.c:313-314 (DF-4962): the second%02xis the never-assignedval.
5. Artifacts
run.log (baseline panic) Β· run.2.log (fixed, clean) Β· panic.txt Β·
build.log/build.2.log (real-file -Werror builds, baseline + patched) Β·
agi2c_probe.c + Makefile (probe KLD) Β· build.sh/run.sh Β·
env.txt Β· fix.diff
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 (the radeon-style #ifdef DragonFly i2c_add_adapter() call after i2c_bit_add_bus()) applies clean to the guest's /usr/src copy (grep shows the new call at line 216), rebuilds with -Werror (build.2.log compiles the patched real amdgpu_i2c.c), and BEHAVIORALLY fixes the panic: the identical probe call that faulted at i2c_transfer+0x2b on baseline now runs the full amdgpu_i2c_router_select_ddc_port() chain with lock_ops=0xffffffff826025d0 installed, takes the compat stub bit_xfer 'read/write failed' path for all four router ops, module loads AND unloads cleanly, guest stays up (run.2.log, vm.sh status: up).
fix.diff; run.2.log ('adapter.lock_ops=0xffffffff826025d0', 'survived amdgpu_i2c_router_select_ddc_port', KLDLOAD2_OK, KLDUNLOAD_OK); build.2.log (patched real amdgpu_i2c.c compiled -Werror, link of agi2c.ko); grep of patched /usr/src file showing i2c_add_adapter at line 216
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:193-212
- sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:207
- sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:284-316
- sys/dev/drm/amd/amdgpu/amdgpu_i2c.c:341-366
- sys/dev/drm/linux_i2c.c:45-55
- sys/dev/drm/linux_i2c.c:92-104
- sys/dev/drm/linux_i2c.c:128-134
- sys/dev/drm/radeon/radeon_i2c.c:969-973
- sys/dev/drm/drm_dp_helper.c:1124
- sys/dev/drm/amd/amdgpu/amdgpu_connectors.c:308-313
- sys/dev/drm/drm_edid.c:1552
- sys/dev/drm/amd/amdgpu/amdgpu_drv.c:88
Detail
Exploit chain
amdgpu driver bound (module load / boot config) -> VBIOS GPIO_I2C record with hw_capable=0 (every board's legacy/VGA DDC lines) -> amdgpu_i2c_create() bit branch (amdgpu_i2c.c:193-212) leaves adapter.lock_ops NULL -> kernel-driven display detect / HPD poll / EDID probe (amdgpu_connectors.c:308-313 -> drm_edid.c:1552, or amdgpu_display.c:463 / dce_v8_0.c:3229 / dce_v10_0.c:3341 / dce_v11_0.c:3467 router selects) -> i2c_transfer() (amdgpu_i2c.c:309) -> adapter->lock_ops->lock_bus() at linux_i2c.c:99 = call through NULL -> page fault at VA 0x0 -> panic. No uid0: the faulting call loads its target from address 0 - not attacker-controllable content - so the primitive is a fixed NULL fp call (DoS only).
Evidence (decisive lines)
run.log (baseline: 'agi2c: adapter.lock_ops=0', 'agi2c: FIRING - REAL amdgpu_i2c_router_select_ddc_port', 'Fatal trap 12: page fault ... fault virtual address = 0x0', 'Stopped at i2c_transfer+0x2b: movq (%rax),%rax', guest down); run.2.log (patched: 'adapter.lock_ops=0xffffffff826025d0', four drm_dbg 'read/write failed' lines over the compat stub, 'survived amdgpu_i2c_router_select_ddc_port', KLDLOAD2_OK + KLDUNLOAD_OK, guest up); panic.txt; build.log/build.2.log (-Werror builds of the real audited files, baseline + patched); env.txt; fix.diff (git-apply-able, mirrors radeon_i2c.c:970-972)
PoC changes
wrote the KLD from scratch (agi2c_probe.c + Makefile): no seed PoC existed. Build iterations: (1) direct sys/systm.h include clashes with the kconfig.h force-include (same issue DF-4827 hit) - dropped it, linux/printk.h pulls sys/systm.h in a working order; (2) amdgpu_atombios_i2c_init needed its real prototype - include amdgpu_atombios.h; (3) nested '/' and '/' inside explanatory comments tripped -Werror=comment - reworded; (4) kldload failed with 'amdgpu_mm_rreg undefined' (RREG32/WREG32 expand to real functions) - added never-executed linkage stubs for amdgpu_mm_rreg/amdgpu_mm_wreg; also stubbed amdgpu_atombios_i2c_xfer/_func (unreachable on the bit path) and drm_dbg/drm_err (logging; stubs always print so the fixed build's 'read failed' path is visible - the prints also live-demonstrate the uninitialized *val at amdgpu_i2c.c:313-314). amdgpu_hw_i2c defined to its production default 1 (amdgpu_drv.c:88).
Verified recommended fix
In amdgpu_i2c_create()'s bit branch, call i2c_add_adapter(&i2c->adapter) after i2c_bit_add_bus(&i2c->adapter), mirroring radeon_i2c.c:970-972 (or make DF's i2c_bit_add_bus install default lock_ops like upstream); see fix.diff
Verdict
Deterministic NULL function-pointer dereference reproduced in-kernel: the probe KLD compiles the REAL unmodified audited amdgpu_i2c.c plus the REAL linux_i2c.c compat core (guest /usr/src/sys sha256 06714f0b.../73ef89d2... == audit tree) with the amdgpu module's -Werror flags, and calls the real amdgpu_i2c_create() with a !hw_capable bus record (the GPIO-only DDC lines every VBIOS table declares for legacy connectors). amdgpu_i2c_create() takes the bit branch (amdgpu_i2c.c:193-212) which registers the adapter via i2c_bit_add_bus() ONLY - in DF's compat layer only i2c_add_adapter() installs adapter->lock_ops (linux_i2c.c:51-52) - so the adapter comes back with lock_ops == NULL (printed live). The file's own exported amdgpu_i2c_router_select_ddc_port() -> amdgpu_i2c_get_byte() (amdgpu_i2c.c:309) -> i2c_transfer() then executes adapter->lock_ops->lock_bus() at linux_i2c.c:99 through NULL: 'Fatal trap 12 ... fault virtual address = 0x0 ... Stopped at i2c_transfer+0x2b: movq (%rax),%rax' - the exact instruction loading lock_bus from the NULL ops table. Guest died (vm.sh status: down). The deref precedes any hardware access, so no AMD GPU is required for the reproduction; the production entry (real amdgpu probe -> drm_get_edid(&ddc_bus->adapter) at amdgpu_connectors.c:308-313 -> drm_edid.c:1552, or the router_select callers amdgpu_display.c:463 / dce_v*_0.c hpd handlers) is gated on AMD hardware absent from this guest but reaches the identical i2c_transfer() call, kernel-driven on display detect/hotplug with no user action. Fix validated behaviorally: fix.diff (the same #ifdef DragonFly i2c_add_adapter(&i2c->adapter) call radeon's bit branch already carries, radeon_i2c.c:970-972) applied to the guest /usr/src copy, rebuilt -Werror: lock_ops=0xffffffff826025d0, the identical chain completes (compat bit_xfer stub -> four 'read/write failed' debug lines), no panic, module loads AND unloads, guest stays up. uid0 chain: none - the call target is loaded from NULL (fixed address 0), no attacker control over target or arguments; impact ceiling is reliable local DoS (panic) on amdgpu-bound systems.
No comments yet.