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

radeon_uvd_send_msg NULL-deref panic: radeon_fence_ref(ib.fence) executed when radeon_ib_schedule() failed and left ib.fence NULL

Summary

send_msg guards the fence handout on the caller's out-param, not ib.fence; every ib_schedule error path returns before fence_emit assigns it, so *fence = radeon_fence_ref(NULL) executes - and &NULL->base is a small NON-NULL constant that defeats dma_fence_get's NULL check, so kref_get increments a near-NULL kernel address: panic. Reachable via handle-persist + fd-close -> free_handles -> get_destroy_msg when the UVD ring cannot schedule (reset windows), and at suspend. Panic-only. Guard-on-!r fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4501 Β· 10 files
FileTypeDescriptionSize
uvd_sendmsg_nullfence.c β€” 6.3 KB view raw
build.sh β€” 116 B view raw
run.sh β€” 212 B view raw
build.log β€” 285 B view raw
run.log β€” 275 B view raw
env.txt β€” 473 B view raw
VERDICT.md β€” 3.5 KB ↓ raw
README.md β€” 2.9 KB ↓ raw
fix.diff β€” 582 B view raw
fault-injection.diff β€” 639 B view raw

DF-4501 β€” radeon_uvd_send_msg() NULL-deref panic when radeon_ib_schedule() fails

Root cause

sys/dev/drm/radeon/radeon_uvd.c:763-766:

r = radeon_ib_schedule(rdev, &ib, NULL, false);

if (fence)
        *fence = radeon_fence_ref(ib.fence);

radeon_ib_get() sets ib->fence = NULL (sys/dev/drm/radeon/radeon_ib.c, "ib->fence = NULL;"). Every error return of radeon_ib_schedule() (sys/dev/drm/radeon/radeon_ib.c:122-181: !ring->ready, radeon_ring_lock failure, radeon_sync_rings failure, radeon_fence_emit failure) happens BEFORE radeon_fence_emit() assigns ib->fence β€” on failure ib.fence is still NULL.

radeon_fence_ref(NULL) (sys/dev/drm/radeon/radeon_fence.c:699) executes dma_fence_get(&fence->base). With fence == NULL the expression &fence->base evaluates to the small constant offsetof(struct radeon_fence, base) β€” a NON-NULL address β€” so dma_fence_get()'s if (fence) guard (sys/dev/drm/include/linux/dma-fence.h:90-95) PASSES and kref_get() performs an atomic increment at a near-NULL kernel address β†’ kernel panic (write to low canonical kernel memory).

Trigger chain (no-rollback precondition)

  1. radeon_uvd_cs_msg() allocates a UVD handle at CS parse time (radeon_uvd.c:522-526: atomic_cmpxchg(&handles[i], 0, handle) + filp[i] = p->filp). Nothing rolls this back when the ioctl's later radeon_ib_schedule() fails.
  2. Preconditions where the UVD ring cannot schedule while the UVD object/handles exist: UVD ring failed to come up (ring->ready false, e.g. firmware/startup failure after radeon_uvd_init() allocated the vcpu_bo), GPU reset/teardown windows, or ring lock pressure.
  3. User submits one create-msg CS β†’ error returned, handle stays.
  4. User closes the DRM fd β†’ postclose β†’ radeon_uvd_free_handles() (radeon_uvd.c:328-352) sees handle != 0 β†’ radeon_uvd_get_destroy_msg() (radeon_uvd.c:813) β†’ radeon_uvd_send_msg() β†’ radeon_ib_schedule() fails again β†’ radeon_fence_ref(NULL) β†’ panic.

The same crash is reachable via radeon_uvd_suspend() (radeon_uvd.c:249) and the uvd_v1_0.c call sites, all of which pass &fence unconditionally.

Build / run

cc -O2 -Wall -o uvd_sendmsg_nullfence uvd_sendmsg_nullfence.c
./uvd_sendmsg_nullfence /dev/dri/card0

The userspace half (create handle via raw CS ioctls, ignore the error, close fd) is in uvd_sendmsg_nullfence.c; the kernel-side precondition (dead UVD ring) can be forced deterministically with fault-injection.diff (force radeon_ib_schedule to return -EINVAL in send_msg's path) on any radeon+UVD box: with injection β†’ immediate panic at fd close; without β†’ normal operation. This guest has no radeon hardware (env.txt), so the fault-injection run could not be executed here.

VERDICT.md
↓ download raw

VERDICT β€” DF-4501

finding_id : DF-4501 status : untested (hardware-gated: needs a radeon ASIC with UVD; guest has no /dev/dri at all β€” env.txt) reproduced : 0 impact : none demonstrated (expected on hardware: panic β€” near-NULL kernel write from kref_get under dma_fence_get) confidence : likely

Mechanism, verified line-by-line

  • radeon_uvd.c:740-770 radeon_uvd_send_msg(): after r = radeon_ib_schedule(rdev, &ib, NULL, false); the code does if (fence) *fence = radeon_fence_ref(ib.fence); β€” the guard tests the caller's out-param, not ib.fence.
  • radeon_ib.c radeon_ib_get(): ib->fence = NULL; (initial state).
  • radeon_ib.c radeon_ib_schedule(): all four error returns (!ib->length_dw || !ring->ready, radeon_ring_lock fail, radeon_sync_rings fail, radeon_fence_emit fail) precede the single success-path assignment of ib->fence by radeon_fence_emit. On any of them ib.fence is NULL when send_msg dereferences it.
  • radeon_fence.c:699 radeon_fence_ref(): dma_fence_get(&fence->base); β€” with fence==NULL, &fence->base is the constant offsetof(struct radeon_fence, base) (small, NON-NULL), so dma-fence.h:90-95 if (fence) kref_get(&fence->refcount); takes the true branch and atomically increments a near-NULL address β†’ kernel panic (low-address write fault in kernel mode).
  • Callers that always pass &fence: radeon_uvd.c:263-264 (radeon_uvd_suspend), radeon_uvd.c:338-339 (radeon_uvd_free_handles, invoked from drm postclose), radeon_uvd.c:808/837 via get_create/destroy_msg, plus uvd_v1_0.c call sites.
  • Why a handle can exist while the ring is dead: radeon_uvd.c:522-526 allocates handles at CS parse time; radeon's CS ioctl has no UVD-handle rollback on the later ib_schedule failure.

Why untested on this guest

Guest is QEMU std-VGA DragonFly 6.5-DEVELOPMENT with no /dev/dri (env.txt); the radeon ioctl surface and the UVD ring do not exist. Compile check of the userspace trigger only (build.log). Deterministic on-hw validation path is documented in fault-injection.diff (NOT applied to sys/): force radeon_ib_schedule failure inside send_msg, run ./uvd_sendmsg_nullfence, expect immediate near-NULL panic at fd close; baseline without injection runs clean.

Fix (fix.diff, git-apply-verified)

-   if (fence)
-       *fence = radeon_fence_ref(ib.fence);
+   if (!r && fence)
+       *fence = radeon_fence_ref(ib.fence);

Only hand out a fence when the IB actually scheduled. (Belt-and-braces: make radeon_fence_ref() NULL-safe like its dma_fence counterpart.)

fix_status: not_testable (no radeon hardware in the audit guest).

kernel_refs

sys/dev/drm/radeon/radeon_uvd.c:740-770 (unguarded ib.fence use) sys/dev/drm/radeon/radeon_uvd.c:263,338,808,837 (callers passing &fence) sys/dev/drm/radeon/radeon_uvd.c:522-526 (handle alloc at parse time) sys/dev/drm/radeon/radeon_ib.c (ib_get: fence=NULL; schedule error paths) sys/dev/drm/radeon/radeon_fence.c:699 (ref without NULL check) sys/dev/drm/include/linux/dma-fence.h:89-95 (guard defeated by &NULL->base)

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 no→ patch + rebuild →patched clean

fix.diff authored and git apply --check verified; could not be kernel-built/behavior-tested on the audit guest (no radeon hardware). The one-line !r guard is provably sufficient by inspection: radeon_ib_schedule() assigns ib->fence only on the success path that returns 0.

findings/poc/DF-4501/fix.diff (git apply --check OK); radeon_ib.c radeon_ib_get/radeon_ib_schedule trace in VERDICT.md.
↓ fix.diffper-fix-DF-4501

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (mechanism trace radeon_uvd.c:740-770 + radeon_ib.c + radeon_fence.c:699 + dma-fence.h:89-95, and the no-rollback handle precondition); uvd_sendmsg_nullfence.c (create-msg CS + fd close); build.log/run.log; fault-injection.diff (deterministic kernel-side emulation, not applied to sys/); fix.diff (one-line guard, git-apply-verified).

PoC changes

N/A (authored fresh; userspace half only β€” the decisive faulting write happens in-kernel)

Verified recommended fix

In radeon_uvd_send_msg() only hand out the fence when the IB actually scheduled: if (!r && fence) *fence = radeon_fence_ref(ib.fence); (optionally also make radeon_fence_ref NULL-safe).

Verdict

radeon_uvd_send_msg() (radeon_uvd.c:765-766) calls radeon_fence_ref(ib.fence) guarded only by 'if (fence)' β€” the caller's out-param β€” while ib.fence is NULL on every radeon_ib_schedule() error path (radeon_ib.c: ib_get initializes fence=NULL; all error returns precede radeon_fence_emit). radeon_fence_ref(NULL) evaluates &((struct radeon_fence*)NULL)->base, a small NON-NULL constant, which defeats dma_fence_get()'s NULL guard (dma-fence.h:90-95) and makes kref_get() atomically increment a near-NULL kernel address -> panic. Reachable because UVD handles are allocated at CS parse time (radeon_uvd.c:522-526) with no rollback on ib_schedule failure, so on a board whose UVD ring cannot schedule, a user submitting one create-msg CS and closing the fd drives radeon_uvd_free_handles() (postclose) into the crash; suspend path radeon_uvd.c:263 equally affected. Mechanism verified line-by-line; NOT executed here β€” the audit guest has no DRM device (env.txt). Trigger compile-verified on the guest (build.log); deterministic on-hw procedure documented in fault-injection.diff.