DF-4961 / verdict.json
{ "finding_id": "DF-4961", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "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 modules -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 DFs 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 files 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 radeons 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.", "exploit_chain": "amdgpu driver bound (module load / boot config) -> VBIOS GPIO_I2C record with hw_capable=0 (every boards 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": "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)", "kernel_refs": [ "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" ], "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 builds 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).", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT #0 x86_64 (kernel DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC)", "runtime_sec": 1500, "guest_dirty": 0, "build_cmd": "guest: cd /tmp/df4961 && make (agi2c_probe.c + REAL /usr/src/sys/dev/drm/amd/amdgpu/amdgpu_i2c.c + /usr/src/sys/dev/drm/linux_i2c.c, amdgpu module CFLAGS, -Werror)", "run_cmd": "guest: kldload -v /tmp/df4961/agi2c.ko (baseline: panic at i2c_transfer+0x2b, guest down; patched: clean load+unload, guest up); full cycle: ./run.sh", "code_hash": "4f7b924e953313980b4200cb318014dd55b91ec175e21f00217d75fcac523d23", "notes": "Guest reset (with-src) performed after the runs - clean for the next spawn. radeon_i2c.c:970-972 (#ifdef __DragonFly__ i2c_add_adapter setup lock_ops) and drm_dp_helper.c:1124 (aux->ddc.lock_ops) prove the DF compat layer requires every adapter to get lock_ops and that only amdgpus bit branch forgot. Side observation (functional, not memory-safety, out of scope): DFs compat bit_xfer is an explicit stub (linux_i2c.c:106-111) so GPIO bit-bus DDC is functionally inert even after the fix - all get/put bytes take the failed path. The fixed runs i2c 0x03 0x00 read failed prints demonstrate the uninitialized *val read (DF-4962) live. Hardware-gated production trigger: needs an AMD GPU; panic is kernel-driven (display detect/HPD poll) once bound, no user action required.", "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 DFs i2c_bit_add_bus install default lock_ops like upstream); see fix.diff", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff (the radeon-style #ifdef __DragonFly__ i2c_add_adapter() call after i2c_bit_add_bus()) applies clean to the guests /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_evidence": "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" } |