# 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 up
  `i2c->bit.*` callbacks and calls ONLY `i2c_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 default `lock_ops` (`if (!adapter->lock_ops)
  adapter->lock_ops = &i2c_default_lock_ops;`).
* `sys/dev/drm/linux_i2c.c:128-134` — DF's `i2c_bit_add_bus()` sets
  `adapter->algo` and `adapter->retries` only. (Upstream Linux's
  `i2c_bit_add_bus` internally calls `i2c_add_adapter`; the DF port does
  not, so the CALLER must — radeon does.)
* `sys/dev/drm/linux_i2c.c:92-104` — `i2c_transfer()` dereferences
  `adapter->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 own `lock_ops` (`drm_dp_i2c_lock_ops`) and calls
    `i2c_add_adapter()`.
  Only amdgpu's bit branch omits it.

## 2. Reachability / threat model

* Which buses: every `!hw_capable` GPIO 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 is `rec->hw_capable && amdgpu_hw_i2c`; `amdgpu_hw_i2c`
  defaults to 1 (amdgpu_drv.c:88) and is irrelevant for `!hw_capable`
  records — 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:1552 `i2c_transfer(adapter, ...)`).
  * This file's own exported router helpers:
    `amdgpu_i2c_router_select_ddc_port` (amdgpu_display.c:463,
    amdgpu_connectors.c:292) and `amdgpu_i2c_router_select_cd_port`
    (dce_v8_0.c:3229, dce_v10_0.c:3341, dce_v11_0.c:3467 — HPD handlers),
    both via `amdgpu_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_bus` is
  called before `master_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's `i2c_bit_algo.master_xfer` is the `bit_xfer`
  stub, 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.diff` applied to guest `/usr/src/sys/dev/drm/amd/amdgpu/amdgpu_i2c.c`
  (grep confirms the new `i2c_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_xfer` is 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 failed` lines also
  live-demonstrate the uninitialized `*val` read/print at
  amdgpu_i2c.c:313-314 (DF-4962): the second `%02x` is the never-assigned
  `val`.

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