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)
PoC verification
Evidence pack
findings/poc/DF-4914 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.rfbdevbefore 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) runsfini: drm_fb_helper_fini(...)βfree: kfree(rfbdev);(:405) without clearingrdev->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_createfailure β 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_initcontinues (: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_testablefix.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.
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.
No comments yet.