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

## Finding

`radeon_fbdev_init()` (sys/dev/drm/radeon/radeon_fb.c:357-407):

* :374 kzalloc's `struct radeon_fbdev`,
* :379 stores it into `rdev->mode_info.rfbdev` **before** anything that can
  fail (its own comment-free sequencing),
* on every failure (`drm_fb_helper_init` :384-387, `single_add_all_connectors`
  :389-391, `initial_config` :396-398) runs `fini: drm_fb_helper_fini(...)` →
  `free: kfree(rfbdev);` (:405) **without clearing `rdev->mode_info.rfbdev`**.

The only place the pointer is cleared is `radeon_fbdev_fini()` (:417) — i.e.
AFTER `radeon_fbdev_destroy()` already used it.  Meanwhile
`radeon_modeset_init()` ignores the init failure entirely
(radeon_display.c:1640 `radeon_fbdev_init(rdev);` — no return check), so the
driver stays loaded with a dangling `mode_info.rfbdev` and every later
consumer — `radeon_fbdev_fini()` (radeon_display.c:1655, driver unload /
device unplug / reboot), `radeon_fbdev_robj_is_fb()` (radeon_fb.c:427-435,
called from BO validation/eviction), `radeon_fb_{add,remove}_connector()`
(hotplug) — dereferences freed memory.

The double free: `radeon_fbdev_fini()` :415 `kfree(rdev->mode_info.rfbdev)`
on the block already freed at :405.

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

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

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

    rdev->mode_info.rfbdev = 0xfffff8011788c900 — kfree()d block still recorded (dangling)

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

    panic: memory chunk 0xfffff8011788c900 is already free!
    chunk_mark_free() at chunk_mark_free+0xae
    _kfree() at _kfree+0x262
    radeon_fbdev_fini() at radeon_fbdev_fini+0x78        ← radeon_fb.c:415
    sysctl_run() at sysctl_run+0x1c4

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

    DF-4914: reclaim attempt: 0xfffff801179c4440 (dangling 0xfffff801179c4440, RECLAIMED)
    DF-4914: reclaimed block 0x41-filled (fb.obj[0]=0x4141414141414141)
    Fatal trap 9: general protection fault while in kernel mode
    Stopped at radeonfb_destroy_pinned_object+0x1c: movl -0x2d0(%rdi),%eax

`rdi` is `gem_to_radeon_bo(0x4141414141414141)` — the teardown consumed the
attacker-reclaimed block's fields inside production radeon code.  Whoever can
reclaim the ~1.2KB 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;
* `initial_config` → `radeonfb_create` failure — classically VRAM exhaustion
  during fbcon object creation on small-VRAM cards (the code even special
  cases ≤8MB/≤32MB cards at :367-372) — feeding DF-1969's leak family the
  same entry;
* after the failure, `radeon_modeset_init` continues (:1640 ignores the
  error) — the panic/corruption is DEFERRED to unload/reboot/hotplug, a
  long-lived window in which the freed block is reused by unrelated kernel
  activity.

## 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 `fb->obj[0]`).  Turning this into code execution requires the
load-time failure to be induced (OOM/VRAM pressure on a system with a radeon
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.

## Fix validation

`fix.diff`: one line — `rdev->mode_info.rfbdev = NULL;` at the `free:` label
(covers both `fini:` and `free:` falls-through).  Validated: guest /usr/src
(md5-identical to the audited tree) patched, `make -j6` in
/usr/src/sys/dev/drm/radeon → BUILD=0, fixed module loads and runs
(fix_build.log, run.log tail).  **Dynamic baseline-vs-patched re-run of the
PoC is not possible on this guest**: the failure ENTRY (a graceful
`radeon_fbdev_init` error) cannot be produced live without a radeon GPU — the
in-guest allocators only fail catastrophically (oversized kcalloc wedges in
M_WAITOK paging; observed before the PoC was restructured to state
injection), and the PoC's stage 1 injects the post-failure state directly, so
patched-module stages 2/3 would be unchanged by construction.  Classification:
fix_status=not_testable (compile + load validated; behavior follows by
inspection from the trace the PoC verified: store at :379, un-cleared free at
:405, second free at :415).

## PoC changes vs the initial seed

Initial draft called `radeon_fbdev_init()` directly on the fake rdev; the
forced-ENOMEM via `num_crtc=0x20000000` kcalloc wedged the guest in M_WAITOK
paging (pagedaemon "Continuous paging") instead of failing, and the
`num_crtc=0`/disconnected-connector routes turned out to succeed (-EAGAIN is
converted to deferred-setup success in this tree's drm_fb_helper).  Restructured
to documented state injection of the exact :374-405 sequence.  Also fixed the
DFly `DECLARE_MODULE` usage (moduledata_t required — first load GPF'd in
module_register/strcmp because the handler function was passed instead).

## Guest hygiene

Guest left state: /root/rfb (PoC), /root/fix.diff applied to
/usr/src/sys/dev/drm/radeon/radeon_fb.c + rebuilt radeon.ko there;
`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.
