# DF-4925 VERDICT — REPRODUCED (UAF / double-free; impact: kernel memory corruption, DoS)

## Finding

`amdgpu_fbdev_init()` (sys/dev/drm/amd/amdgpu/amdgpu_fb.c:342-385) — the
amdgpu twin of DF-4914 (radeon_fb), verified against the stock module:

* :360 kzalloc's `struct amdgpu_fbdev`,
* :365 stores it into `adev->mode_info.rfbdev` **before** anything that can
  fail,
* on `drm_fb_helper_init()` failure (:370-375, three fallible kcallocs at
  drm_fb_helper.c:871/876/884 → -ENOMEM) runs `kfree(rfbdev); return ret;`
  (:373-374) **without clearing `adev->mode_info.rfbdev`**.

The only place the pointer is cleared is `amdgpu_fbdev_fini()` (:394) — i.e.
AFTER `amdgpu_fbdev_destroy()` already used it.  Meanwhile
`amdgpu_device_init()` ignores the init failure entirely
(amdgpu_device.c:2662 `amdgpu_fbdev_init(adev);` — no return check), so the
driver stays loaded with a dangling `mode_info.rfbdev` and every later
consumer — `amdgpu_fbdev_fini()` (amdgpu_device.c:2744, driver unload /
device unplug / reboot) and `amdgpu_fbdev_robj_is_fb()` (amdgpu_device.c:2847,
called from BO unpin during suspend) — dereferences freed memory.

The double free: `amdgpu_fbdev_fini()` :393 `kfree(adev->mode_info.rfbdev)`
on the block already freed at :373.

## Reproduction (2026-09-25, guest DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC)

Production modules: stock `/boot/kernel/drm.ko` + `/boot/kernel/amdgpu.ko`
(no AMD GPU present; module load needs no hardware).  PoC module
`amdfbuaf.ko` drives the REAL `amdgpu_fbdev_fini()` from the loaded module.

Stage 1 (state injection of the exact failed-init state — amdgpu_fb.c:360-374):

    adev->mode_info.rfbdev = 0xfffff8008f7dc7c0 — kfree()d block still recorded (dangling)
    sizeof(struct amdgpu_fbdev) = 592

Stage 2 (production teardown on the dangling pointer) — deterministic panic:

    panic: memory chunk 0xfffff800899945c0 is already free!
    chunk_mark_free() at chunk_mark_free+0xae
    _kfree() at _kfree+0x262
    amdgpu_fbdev_fini() at amdgpu_fbdev_fini+0x78        ← amdgpu_fb.c:393

Stage 3 (freed block reclaimed by an ordinary same-size allocation, filled
with 0x41; production teardown) — UAF with fully controlled data:

    DF-4925: reclaim attempt: 0xfffff8008f7fcf40 (dangling 0xfffff8008f7fcf40, RECLAIMED)
    DF-4925: reclaimed block 0x41-filled (rfb.base.obj[0]=0x4141414141414141)
    Fatal trap 9: general protection fault while in kernel mode
    Stopped at amdgpufb_destroy_pinned_object+0x1e: movl -0x2d0(%rdi),%eax

`rdi` is `gem_to_amdgpu_bo(0x4141414141414141)` — the teardown consumed the
attacker-reclaimed block's fields inside production amdgpu code.  Whoever
can reclaim the 592-byte slab block between driver-load failure and teardown
(any same-size kernel allocation under attacker influence) controls the
object the kernel walks and double-frees.

## Why the failure is realistic on hardware

* `drm_fb_helper_init`/-ENOMEM under memory pressure at driver load;
* after the failure, `amdgpu_device_init` continues (:2662 ignores the
  error) — the panic/corruption is DEFERRED to unload/reboot/suspend, a
  long-lived window in which the freed block is reused by unrelated kernel
  activity;
* the deferred suspend-path consumer (amdgpu_device.c:2847) walks the same
  dangling pointer on every suspend/eviction cycle.

## Impact ceiling

Kernel heap corruption: double free (deterministic slab panic or freelist
corruption on builds without the debug detector) plus use-after-free with
attacker-influenced contents (stage-3 wild chase starts from a fully
controlled `rfb.base.obj[0]`).  Turning this into code execution requires
the load-time failure to be induced (OOM/VRAM pressure on a system with an
AMD GPU) and slab reuse engineering — plausible for a local attacker with
long-lived access, not weaponized here.  Severity: Medium (memcorrupt);
CVSS context: local, requires GPU + load-time resource exhaustion.
Identical class/shape to the verified DF-4914 (radeon_fb).

## Fix validation

`fix.diff`: one line — `adev->mode_info.rfbdev = NULL;` at the head of the
:372-374 error path.  Validated: guest /usr/src (clean-source snapshot,
md5-identical to the audited tree) patched, `make` in
/usr/src/sys/dev/drm/amd/amdgpu → BUILD_RC=0, patched module loads and runs
(fix_build.log; stage-1 re-run against the patched module in run.log).
**Dynamic baseline-vs-patched re-run of the PoC is not possible on this
guest** (same construction limitation as DF-4914): the failure ENTRY (a
graceful `amdgpu_fbdev_init` error) cannot be produced live without an AMD
GPU — the in-guest allocators only fail catastrophically (oversized kcalloc
wedges in M_WAITOK paging), and the PoC's stage 1 injects the post-failure
state directly, so patched-module stages 2/3 would be unchanged by
construction (the fix is in the init error path, which the injection
bypasses).  Classification: fix_status=not_testable (compile + load
validated; behavior follows by inspection from the trace the PoC verified:
store at :365, un-cleared free at :373, second free at :393).

## PoC changes vs any seed

No executable seed existed (fresh harness authored for this file, mirroring
the DF-4914 `rfbuaf.c` structure).  Include flags were taken verbatim from
the in-tree amdgpu Makefile (sys/dev/drm/amd/amdgpu/Makefile:359-381); the
module compiled and loaded on the first attempt.

## Guest hygiene

Guest state at end of this finding's verification: `/root/DF-4925` PoC
pack, fix applied to /usr/src amdgpu_fb.c + rebuilt amdgpu.ko there,
`testnv` user added (nvmm phase).  `vm.sh reset with-src` performed at the
end of the run.

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