β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4914

radeon_fbdev_init() failure leaves rdev->mode_info.rfbdev dangling (published before any fallible step, kfree'd on the error path without clearing) β€” production teardown radeon_fbdev_fini() then use-after-frees and double-kfrees it

Summary

rfbdev is stored BEFORE the fallible helpers, every error exit kfrees without clearing, and radeon_modeset_init ignores the failure - the driver stays live with a dangling pointer that the standard unload/reboot teardown walks and kfrees AGAIN, while interim consumers deref it during operation. VERIFIED against the stock production radeon.ko: deterministic double-free panic ('already free!' at the :415 site) and, on a fresh boot, an ordinary same-size kzalloc reclaims the block and the production teardown chases fb->obj[0]=0x4141... into a GPF - full field control demonstrated. Clear-the-pointer fix (compile+load validated).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4914 Β· 14 files
FileTypeDescriptionSize
rfbuaf.c β€” 6.3 KB view raw
Makefile β€” 442 B ↓ download
build.sh β€” 75 B view raw
run.sh β€” 181 B view raw
run.log β€” 3.1 KB view raw
console.log β€” 104 B view raw
panic-stage2.txt β€” 798 B view raw
panic-stage3.txt β€” 607 B view raw
env.txt β€” 369 B view raw
fix_build.log β€” 146.5 KB view raw
VERDICT.md β€” 6.1 KB ↓ raw
fix.diff β€” 266 B view raw
verdict.json β€” 5.6 KB view raw
manifest.json β€” 953 B view raw
VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff (rdev->mode_info.rfbdev = NULL at the free: label of radeon_fbdev_init) compiles cleanly into radeon.ko on the guest's md5-identical /usr/src tree and the patched module loads (fix_build.log). A dynamic baseline-vs-patched PoC re-run is not possible on this guest: the graceful failure entry cannot be produced without a radeon GPU (allocators wedge or panic rather than return ENOMEM; num_crtc=0 succeeds via deferred-setup), and the PoC injects the post-failure state directly, bypassing the fixed statement by construction.

fix.diff; fix_build.log (BUILD=0 for patched radeon.ko); run.log tail (FIXED-RADEON-LOADED + stage 1 executes); VERDICT.md fix-validation section.
↓ fix.diffper-fix-DF-4914

Confirmed kernel references

Detail

Exploit chain

local attacker on a system with a radeon GPU: (1) induce radeon_fbdev_init failure at driver load (memory pressure for drm_fb_helper_init/-ENOMEM, or VRAM exhaustion during fb_probe/radeonfb_create on small-VRAM cards) -> mode_info.rfbdev dangling for the whole driver lifetime (failure ignored at radeon_display.c:1640); (2) reclaim the freed ~1.2KB slab block with controlled kernel allocations; (3) at driver unload/reboot/hotplug, radeon_fbdev_fini walks attacker-controlled fields (fb->obj[0] -> radeonfb_destroy_pinned_object -> radeon_bo_reserve/kunmap/unpin on forged radeon_bo) and double-kfrees the block. Demonstrated through step 3's wild-pointer chase (stage 3); weaponization to uid=0 (forging radeon_bo/ttm objects) not pursued on the no-GPU guest.

Evidence (decisive lines)

panic-stage2.txt (double free: chunk_mark_free <- _kfree <- radeon_fbdev_fini+0x78 = radeon_fb.c:415), panic-stage3.txt (GPF at radeonfb_destroy_pinned_object+0x1c after slab reclaim, fb.obj[0]=0x4141414141414141), run.log (all stages + fixed-module check), console.log, fix_build.log (patched radeon.ko BUILD=0)

PoC changes

Initial draft forced drm_fb_helper_init ENOMEM via mode_config.num_crtc=0x20000000; on this kernel the GFP_KERNEL kcalloc wedges the guest in M_WAITOK paging instead of failing (pagedaemon 'Continuous paging'), and num_crtc=0 / disconnected-connector variants SUCCEED (-EAGAIN becomes deferred-setup success). Restructured to documented state injection of the exact failed-init state (radeon_fb.c:374-405) with real drm_fb_helper_prepare, driving the REAL production radeon_fbdev_fini from stock /boot/kernel/radeon.ko. Also fixed DFly DECLARE_MODULE usage (moduledata_t struct required; passing the handler function GPFs in module_register/strcmp at module registration).

Verified recommended fix

Clear rdev->mode_info.rfbdev (NULL) before kfree(rfbdev) on the radeon_fbdev_init error paths (single line at the free: label covers fini+free).

Verdict

Use-after-free + double-free: radeon_fbdev_init() publishes rfbdev into rdev->mode_info.rfbdev at radeon_fb.c:379 before any fallible step, and its error exits (fini:/free:) kfree(rfbdev) at :405 WITHOUT clearing the pointer. radeon_modeset_init() ignores the failure (radeon_display.c:1640), so the driver carries the dangling pointer until teardown; the production teardown radeon_modeset_fini -> radeon_fbdev_fini (radeon_display.c:1655) then walks the freed rfbdev in radeon_fbdev_destroy() and kfree()s it AGAIN at radeon_fb.c:415. Reproduced on the guest against the stock production radeon.ko: stage-2 run panics deterministically 'memory chunk ... is already free!' with stack chunk_mark_free <- kfree <- radeon_fbdev_fini+0x78; stage-3 run first reclaims the freed slab block with an ordinary same-size allocation (RECLAIMED, 0x41-filled) and the production teardown then chases the fully controlled fb->obj[0]=0x4141414141414141 into radeonfb_destroy_pinned_object, GPF at radeonfb_destroy_pinned_object+0x1c (movl -0x2d0(%rdi)). Also en route: radeon_fbdev_robj_is_fb() and radeon_fb_connector() deref the same dangling pointer during normal driver operation. Because no radeon GPU exists in the QEMU guest, the graceful init-failure entry could not be driven live (oversized kcalloc wedges in M_WAITOK paging; num_crtc=0 succeeds via -EAGAIN->deferred), so stage 1 injects the byte-exact post-failure state (the statements of radeon_fb.c:374-405) and stages 2/3 drive the real teardown code from the loaded production module.