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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4925 Β· 14 files
FileTypeDescriptionSize
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! with amdgpu_fbdev_fini in the backtrace
  • stage 3: RECLAIMED + Fatal trap ... general protection fault inside amdgpufb_destroy_pinned_object / amdgpu_bo_reserve walking 0x4141414141414141

Notes

  • No AMD GPU is required: amdgpu_fbdev_init/_fini are 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.
VERDICT.md
↓ download raw

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.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-4925

Confirmed kernel references

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