amdgpu_fbdev_init() publishes adev->mode_info.rfbdev before the fallible drm_fb_helper_init() and kfrees it without clearing on error β dangling pointer β deferred double-free/UAF in production teardown (DF-4914 radeon_fb twin)
Summary
Exact DF-4914 shape: store at :365 before three kcallocs, kfree at :373 without clearing, caller ignores the return - the driver carries the dangling pointer until fbdev_fini double-kfrees it and robj_is_fb walks freed memory. VERIFIED against STOCK amdgpu.ko: deterministic 'already free!' double-free panic at the fini site + fresh-boot slab reclaim with fully 0x41-controlled obj[0] chased by production code into a GPF (complete UAF control demonstrated). Clear-the-pointer fix (compile+load validated; live failure entry needs an AMD GPU, same limitation as DF-4914).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4925 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| amdfbuaf.c | β | 6.6 KB | view raw | |
| Makefile | β | 1.2 KB | β download | |
| build.sh | β | 178 B | view raw | |
| run.sh | β | 247 B | view raw | |
| README.md | β | 2.4 KB | β raw | |
| VERDICT.md | β | 5.8 KB | β raw | |
| verdict.json | β | 4.2 KB | view raw | |
| build.log | β | 7.4 KB | view raw | |
| run.log | β | 191 B | view raw | |
| panic-stage2.txt | β | 731 B | view raw | |
| panic-stage3.txt | β | 831 B | view raw | |
| fix.diff | β | 270 B | view raw | |
| fix_build.log | β | 1.2 MB | β download | |
| env.txt | β | 407 B | view raw |
DF-4925 β amdgpu_fbdev_init dangling adev->mode_info.rfbdev (DF-4914 twin)
amdgpu_fbdev_init() (sys/dev/drm/amd/amdgpu/amdgpu_fb.c:342-385) stores the
freshly kzalloc'd rfbdev into adev->mode_info.rfbdev at :365 before
the fallible drm_fb_helper_init() at :370; on its failure (:372-374) it
runs kfree(rfbdev); return ret; without clearing
adev->mode_info.rfbdev. The caller ignores the failure
(amdgpu_device.c:2662), so every later consumer β amdgpu_fbdev_fini()
(amdgpu_device.c:2744, driver unload / unplug / reboot), and
amdgpu_fbdev_robj_is_fb() (amdgpu_device.c:2847, suspend/eviction) β
dereferences freed memory, and amdgpu_fbdev_fini() kfrees it a second
time (amdgpu_fb.c:393).
Build (on the DragonFly guest, in this directory)
./build.sh # -> amdfbuaf.ko (production drm.ko + amdgpu.ko used as-is)
Run (as root)
./run.sh # stage 1 only: dangling-pointer print, no crash
Stage 2 (double free; panics β run on a fresh boot):
kldload drm && kldload amdgpu && kldload ./amdfbuaf.ko sysctl hw.amdfbuaf=2 # panic: memory chunk ... is already free! @ amdgpu_fbdev_fini
Stage 3 (slab reclaim + controlled UAF walk; panics β fresh boot):
kldload drm && kldload amdgpu && kldload ./amdfbuaf.ko sysctl hw.amdfbuaf=3 # reclaim of the freed struct amdgpu_fbdev block, 0x41-filled; # teardown chases rfb.base.obj[0]=0x4141414141414141 into # amdgpufb_destroy_pinned_object -> wild pointer deref
Expected output
- stage 1:
adev->mode_info.rfbdev = 0x... β kfree()d block still recorded (dangling) - stage 2:
panic: memory chunk 0x... is already free!withamdgpu_fbdev_finiin the backtrace - stage 3:
RECLAIMED+Fatal trap ... general protection faultinsideamdgpufb_destroy_pinned_object/amdgpu_bo_reservewalking0x4141414141414141
Notes
- No AMD GPU is required:
amdgpu_fbdev_init/_finiare plain module functions; the PoC injects the exact post-failure state (:360-:374) and drives the REAL production teardown from the stock/boot/kernel/amdgpu.ko. - On real hardware the failure entry is
drm_fb_helper_init()/-ENOMEM at driver load (three fallible kcallocs: drm_fb_helper.c:871/876/884) β the detonation is deferred to unload/reboot/suspend, a long-lived window in which the freed block can be reclaimed by unrelated same-size kernel allocations.
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.rfbdevbefore anything that can fail, - on
drm_fb_helper_init()failure (:370-375, three fallible kcallocs at drm_fb_helper.c:871/876/884 β -ENOMEM) runskfree(rfbdev); return ret;(:373-374) without clearingadev->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_initcontinues (: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.
Fix verification
not_testablefix.diff (one line: adev->mode_info.rfbdev = NULL before kfree at amdgpu_fb.c:373) compiles and loads: patched guest /usr/src amdgpu_fb.c -> make BUILD_RC=0 -> patched amdgpu.ko loaded and ran alongside the PoC (fix_build.log, run.log). Dynamic baseline-vs-patched behavior cannot be shown on this guest by construction: the fix lives in amdgpu_fbdev_init()'s error path, the entry to which (a graceful drm_fb_helper_init failure) cannot be induced live without an AMD GPU (allocators only fail catastrophically here), and the PoC injects the post-failure state directly β identical limitation as the verified DF-4914 twin. Behavior follows by inspection from the trace the PoC verified (store at :365, un-cleared free at :373, second free at :393).
fix.diff (git-apply clean); fix_build.log (full build, BUILD_RC=0, patched amdgpu.ko linked); run.log stage-1 re-run against the patched module; VERDICT.md 'Fix validation' section
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_fb.c:365
- sys/dev/drm/amd/amdgpu/amdgpu_fb.c:372
- sys/dev/drm/amd/amdgpu/amdgpu_fb.c:373
- sys/dev/drm/amd/amdgpu/amdgpu_fb.c:387
- sys/dev/drm/amd/amdgpu/amdgpu_fb.c:393
- sys/dev/drm/amd/amdgpu/amdgpu_device.c:2662
- sys/dev/drm/amd/amdgpu/amdgpu_device.c:2744
- sys/dev/drm/amd/amdgpu/amdgpu_device.c:2847
- sys/dev/drm/drm_fb_helper.c:871
- sys/dev/drm/drm_fb_helper.c:876
- sys/dev/drm/drm_fb_helper.c:884
Detail
Evidence (decisive lines)
panic-stage2.txt (double kfree: 'memory chunk 0xfffff800899945c0 is already free!' with amdgpu_fbdev_fini+0x78 in the trace); panic-stage3.txt ('RECLAIMED', 0x41-filled block, 'Fatal trap 9 ... Stopped at amdgpufb_destroy_pinned_object+0x1e: movl -0x2d0(%rdi),%eax'); run.log (stage-1 dangling print, sizeof(struct amdgpu_fbdev)=592); build.log (clean module build); fix_build.log (patched amdgpu.ko builds and loads)
PoC changes
No executable seed (fresh harness mirroring DF-4914's rfbuaf.c). Include flags taken verbatim from the in-tree sys/dev/drm/amd/amdgpu/Makefile; compiled and loaded first attempt.
Verified recommended fix
Clear adev->mode_info.rfbdev before kfree(rfbdev) in the drm_fb_helper_init error path of amdgpu_fbdev_init() (one line, see fix.diff).
Verdict
REPRODUCED against the stock /boot/kernel/amdgpu.ko: amdgpu_fbdev_init() stores adev->mode_info.rfbdev at amdgpu_fb.c:365 BEFORE the fallible drm_fb_helper_init() and, on its failure (:372-374), kfrees rfbdev without clearing the pointer; amdgpu_device_init() ignores the failure (amdgpu_device.c:2662), so the deferred teardown amdgpu_fbdev_fini() (amdgpu_device.c:2744) walked the dangling pointer in production code and double-kfreed it (deterministic slab panic 'memory chunk ... is already free!' at amdgpu_fbdev_fini+0x78), and after a 0x41-filled same-size slab reclaim the teardown chased rfb.base.obj[0]=0x4141414141414141 into amdgpufb_destroy_pinned_object (GPF at movl -0x2d0(%rdi)) β a fully-controlled UAF walk. Exact amdgpu twin of the verified DF-4914 (radeon_fb).
No comments yet.