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

radeon_verify_access returns 0 unconditionally: GEM ownership check dropped β€” any video-group user can mmap every other user's GPU buffer objects

Summary

The port's TTM .verify_access authorizes every mmap of every user BO to any open file of the card node: the per-file GEM-handle authorization (drm_vma_node_verify_access) present upstream and live in the sibling amdgpu_ttm.c was #if 0'd out, leaving a bare return 0. Full chain: mmap -> drm_mmap_single (no auth gate) -> ttm_bo_mmap -> best-match vma lookup over every user BO -> verify==0 -> cross-user read/write of other users' scanout buffers, textures, upload buffers. Kernel-internal BOs carry no vma node, so ceiling is user GEM objects (Medium). Restore-the-upstream-check fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4429 Β· 10 files
FileTypeDescriptionSize
README.md β€” 5.7 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
poc_mmap_bo.c β€” 2.4 KB view raw
victim_bo.c β€” 1.9 KB view raw
build.sh β€” 125 B view raw
run.sh β€” 129 B view raw
fix.diff β€” 840 B view raw
env.txt β€” 275 B view raw
verdict.json β€” 3.9 KB view raw
manifest.json β€” 968 B view raw

DF-4429 β€” radeon_verify_access drops the GEM ownership check (unconditional return 0)

What

sys/dev/drm/radeon/radeon_ttm.c:234-244 β€” radeon_verify_access(), the .verify_access callback of struct ttm_bo_driver radeon_bo_driver (radeon_ttm.c:882), is stubbed to return 0 for every caller:

static int radeon_verify_access(struct ttm_buffer_object *bo, struct file *filp)
{
#if 0
    struct radeon_bo *rbo = container_of(bo, struct radeon_bo, tbo);
#endif
    if (radeon_ttm_tt_has_userptr(bo->ttm))   /* always false: see 849-861 */
        return -EPERM;
    return 0;
}

radeon_ttm_tt_has_userptr() itself is hardwired to return false (radeon_ttm.c:849-861, the real body is #if 0), so the function is an unconditional return 0. The per-file GEM-handle authorization that upstream Linux radeon performs (drm_vma_node_verify_access(&rbo->tbo.vma_node, filp->private_data)) is gone, and the sibling driver in this very tree still has it: sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:311-325 calls drm_vma_node_verify_access(&abo->gem_base.vma_node, filp->private_data).

Attack chain (source-verified)

  1. /dev/dri/cardN is root:video 0660 (sys/dev/drm/include/drm/drmP.h:108-110, created at sys/dev/drm/drm_drv.c:1178). Any local user in group video can open it. (On a shared desktop every GPU user is in that group.)
  2. mmap(fd, addr, len, prot, MAP_SHARED, offset) on the card node hits drm_cdevsw .d_mmap_single β†’ drm_mmap_single() (sys/dev/drm/drm_drv.c:1254-1266) β†’ ttm_bo_mmap_single() (sys/dev/drm/ttm/ttm_bo_vm.c:766-807) β†’ ttm_bo_mmap() (sys/dev/drm/ttm/ttm_bo_vm.c:431-473). No authenticated check on this path (the priv->authenticated gate exists only on the legacy drm_mmap map-hash path, sys/dev/drm/drm_vm.c:53-55).
  3. ttm_bo_vm_lookup() (ttm_bo_vm.c:408-429) resolves the requested page range in bdev->vma_manager with best-match semantics: any node that spans the requested range is returned (drm_vma_offset_lookup_locked() doc, sys/dev/drm/drm_vma_manager.c). Every user-created GEM BO is registered there β€” ttm_bo_init() adds bo->vma_node for ttm_bo_type_device and ttm_bo_type_sg (sys/dev/drm/ttm/ttm_bo.c:1266-1271) β€” and radeon hands exactly those offsets to userspace (radeon_bo_mmap_offset() β†’ drm_vma_node_offset_addr(&bo->tbo.vma_node), sys/dev/drm/radeon/radeon_object.h:117-120).
  4. ttm_bo_mmap() then calls driver->verify_access(bo, filp) (ttm_bo_vm.c:447) β€” which for radeon always succeeds, regardless of whether the calling file holds a GEM handle for the BO.
  5. Result: a mapping over the victim BO (cdev_pager_allocate, OBJT_MGTDEVICE, ttm_bo_vm.c:794-800); faults resolve to the BO's pages (GTT system pages or VRAM via the fictitious range). Read and write.

Practical exploitation

Offsets are device-global, allocated sequentially from page DRM_FILE_PAGE_OFFSET (= 2^20 pages = 4 GiB, radeon_ttm.c:48/906). They are only returned through per-handle ioctls, but because lookup is best-match and verify_access is a no-op, an attacker does not need exact knowledge: probing one page at a time from offset 4 GiB upward maps every live BO (hit β†’ the node spanning that page; miss β†’ EINVAL). This gives a video-group user read/write access to every other user's radeon GPU buffers: Xorg's scanout/framebuffer contents (other users' windows, VT contents), other users' textures and upload buffers.

Not reachable: kernel-internal BOs (ring buffers, IB pool, page tables) are ttm_bo_type_kernel and never get a vma node (ttm_bo.c:1269-1271), so the ceiling is user-created GEM objects β€” cross-user disclosure + integrity, not direct kernel-memory access.

PoC

poc_mmap_bo.c β€” unprivileged prober: opens the card, sweeps single-page mmap()s from DRM_FILE_PAGE_OFFSET, classifies hits, and (in --read mode) dumps the first bytes of each mapped page. Run as victim: victim_bo.c allocates a GTT BO with a marker pattern via libdrm-free raw ioctls.

Build / run (on a machine with radeon KMS hardware)

cc -O2 -o poc_mmap_bo poc_mmap_bo.c
cc -O2 -o victim_bo victim_bo.c
# terminal A (victim user, in video group):
./victim_bo            # allocates marker BO, prints its mmap offset
# terminal B (attacker user, in video group):
./poc_mmap_bo          # sweeps offsets; marker appears in foreign BO page
./poc_mmap_bo --read   # dump foreign BO contents

Success criterion: attacker's sweep finds and reads a BO it never created (marker pattern from the victim's user visible in the dump), while the same PoC against the amdgpu node (or after the fix) returns EACCES.

Verification status

The audit QEMU guest has no AMD/ATI GPU (env.txt: no /dev/dri, vgapci0 = 0x1234:1111 bochs stub), so the trigger cannot run here. Verdict is source-verified only (status untested), following the DF-4018/DF-4019 sibling precedent.

Fix

Restore the upstream check, mirroring the in-tree amdgpu implementation:

--- a/sys/dev/drm/radeon/radeon_ttm.c
+++ b/sys/dev/drm/radeon/radeon_ttm.c
@@ -234,10 +234,11 @@ static void radeon_move_null(struct ttm_buffer_object *bo,
 static int radeon_verify_access(struct ttm_buffer_object *bo, struct file *filp)
 {
-#if 0
    struct radeon_bo *rbo = container_of(bo, struct radeon_bo, tbo);
-#endif

-   if (radeon_ttm_tt_has_userptr(bo->ttm))
-       return -EPERM;
+   if (radeon_ttm_tt_has_userptr(bo->ttm))
+       return -EPERM;

-   return 0;
+   return drm_vma_node_verify_access(&rbo->gem_base.vma_node,
+                     filp->private_data);
 }

drm_vma_node_allow() is already recorded per GEM handle creation (sys/dev/drm/drm_gem.c:471 in drm_gem_handle_create()), so no other change is required. See fix.diff.

VERDICT.md
↓ download raw

DF-4429 VERDICT β€” radeon_verify_access: GEM ownership check dropped

Bottom line

Source-verified, hardware-gated (not runnable on the audit guest): the DragonFlyBSD radeon port's .verify_access TTM callback authorizes every mmap of every user-created GPU buffer object to any open file descriptor of the card node. The upstream authorization call is #if 0'd out; the sibling amdgpu driver in the same tree still performs it.

Reproduction status

untested β€” the audit QEMU guest has no AMD/ATI GPU (env.txt: no /dev/dri, only the QEMU bochs VGA stub 0x1234:1111), so the radeon driver never attaches there and the trigger cannot execute. The finding is verified by complete source-path tracing (below), matching the DF-4018/DF-4019 sibling precedent for amdgpu_ttm hardware-gated verdicts.

Why the bug is real β€” complete path, every hop cited

  1. Sink: sys/dev/drm/radeon/radeon_ttm.c:234-244 β€” radeon_verify_access() reduces to return 0. The only conditional calls radeon_ttm_tt_has_userptr(), which is itself hardwired return false (radeon_ttm.c:849-861 β€” real body #if 0). Registered as .verify_access at radeon_ttm.c:882.
  2. Reachability of the sink: mmap on /dev/dri/cardN β†’ drm_mmap_single() (sys/dev/drm/drm_drv.c:1254-1266) β†’ ttm_bo_mmap_single() (sys/dev/drm/ttm/ttm_bo_vm.c:766-807) β†’ ttm_bo_mmap() (ttm_bo_vm.c:431-473, calls verify_access at :447). This path performs no priv->authenticated check β€” the legacy-auth gate exists only in the old map-hash drm_mmap (sys/dev/drm/drm_vm.c:53-55), not in drm_mmap_single.
  3. Object discovery: ttm_bo_vm_lookup() (ttm_bo_vm.c:408-429) uses drm_vma_offset_lookup_locked() β€” best-match: any node spanning the requested page range matches. All user BOs (ttm_bo_type_device, created by every RADEON_GEM_CREATE) are registered in bdev->vma_manager at sys/dev/drm/ttm/ttm_bo.c:1266-1271; offsets handed to userspace come from exactly that node (sys/dev/drm/radeon/radeon_object.h:117-120). Therefore single-page probes from DRM_FILE_PAGE_OFFSET (4 GiB, radeon_ttm.c:48,906) upward enumerate and map every live user BO.
  4. Contrast in-tree: sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:311-325 performs drm_vma_node_verify_access(&abo->gem_base.vma_node, filp->private_data); the allow-side is recorded per GEM handle at sys/dev/drm/drm_gem.c:471. Upstream Linux radeon performs the same check.
  5. Access gate: card node is root:video 0660 (sys/dev/drm/include/drm/drmP.h:108-110, sys/dev/drm/drm_drv.c:1178). Attackers must be in group video β€” i.e. exactly the class of users who share a GPU on multi-user desktops.

Impact

Cross-user confidentiality and integrity of GPU memory: any video-group user can read/write every other user's radeon GEM BOs β€” Xorg's scanout buffers (window/VT contents of other users), textures, upload buffers. Kernel-internal BOs (ttm_bo_type_kernel: rings, IB pool) carry no vma node (ttm_bo.c:1269) and are not exposed; the ceiling is user GEM objects. No direct kernel-memory disclosure β†’ not Critical; multi-user group-gated β†’ Medium.

Exploit chain (for a hardware owner)

victim (userA): ./victim_bo fills a 4 MiB GTT BO with DF4429MARK. attacker (userB): ./poc_mmap_bo --read sweeps 4 GiB..8 GiB mmap offsets on the same card node; hits whose dump contains the marker prove foreign-BO read; PROT_WRITE mappings give symmetric write.

Fix validation

fix.diff restores drm_vma_node_verify_access(&rbo->gem_base.vma_node, filp->private_data) (mirrors amdgpu_ttm.c). Cannot be kernel-built/tested on this guest (no radeon hardware; the change only compiles in the radeon module path) β†’ fix_status: not_testable.

Kernel refs

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 restores the upstream ownership check exactly as the in-tree amdgpu driver does it (amdgpu_ttm.c:311-325); allow-side already exists (drm_gem.c:471). Cannot be compiled/loaded on the audit guest (no radeon hardware; module only attaches to real devices), so fix validation is not_testable here.

fix.diff; VERDICT.md 'Fix validation' section
↓ fix.diffper-fix-DF-4429

Confirmed kernel references

Detail

Exploit chain

['victim (userA, video group): victim_bo.c creates 4MiB GTT BO, fills marker DF4429MARK', 'attacker (userB, video group): poc_mmap_bo.c sweeps single-page mmap(fd,off>=4GiB) on the same card node β€” best-match lookup + verify_access==0 maps every live BO', "read marker from userB's dump => cross-user GPU memory disclosure; PROT_WRITE mapping gives symmetric corruption of userA's buffers"]

Evidence (decisive lines)

README.md (chain with path:line at every hop), VERDICT.md (full narrative + why guest-untestable), poc_mmap_bo.c + victim_bo.c (runnable on any radeon-equipped DFly box), env.txt (guest hardware absence proof), fix.diff (restores drm_vma_node_verify_access, mirrors amdgpu_ttm.c:311-325)

PoC changes

authored fresh (no seed sketch). Compile-verified on the audit guest: needed -I/usr/src/sys/dev/drm/include/uapi -I/usr/src/sys/dev/drm/include -D__user= -D__kernel= for the uapi ioctls and in victim_bo.c; all binaries build clean (build.log). Runtime blocked by hardware (no /dev/dri on guest).

Verified recommended fix

radeon_verify_access: return drm_vma_node_verify_access(&rbo->gem_base.vma_node, filp->private_data);

Verdict

source-verified cross-user GPU memory access-control bypass; untested on the audit guest because it has no AMD GPU (no /dev/dri, bochs VGA only β€” env.txt). Full path traced: mmap(card) -> drm_mmap_single (drm_drv.c:1254, no auth check) -> ttm_bo_mmap_single (ttm_bo_vm.c:766) -> ttm_bo_mmap (ttm_bo_vm.c:447 verify_access==0) with best-match vma lookup (ttm_bo.c:1266 registers every user BO from page 2^20/4GiB up), node perms root:video 0660 (drmP.h:108).