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

PRIME import on radeon: populate() ignores the sg table (fresh pages bound into the GART instead of the exporter's) and unpopulate() leaks those pages + TTM accounting per import cycle

Summary

For PRIME-imported dma-bufs the populate slave branch is #ifdef DUMBBELL_WIP - dead - so ttm_pool_populate allocates fresh pages bound into the GART: the GPU view of an imported buffer diverges from the exporter's memory. The unpopulate slave early-return IS live, so those pages and ttm_mem_global accounting are never freed, including at BO destroy. amdgpu implements the slave path live. Multi-GPU reachability; loop export->import->GEM_CLOSE grows wired kernel memory (local DoS) plus silently-wrong PRIME data. Un-ifdef-the-slave-branch fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4430 Β· 9 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
poc_prime_import.c β€” 4.0 KB view raw
build.sh β€” 100 B view raw
run.sh β€” 151 B view raw
fix.diff β€” 1.2 KB view raw
env.txt β€” 275 B view raw
verdict.json β€” 3.6 KB view raw
manifest.json β€” 981 B view raw

DF-4430 β€” radeon PRIME import: populate ignores the sg table; unpopulate

leaks the pages (DUMBBELL_WIP divergence vs amdgpu)

What

On the DragonFlyBSD radeon port, a PRIME-imported buffer object (a dma-buf from another DRM driver, e.g. i915/amdgpu/second radeon) is created as ttm_bo_type_sg with ttm->sg set (sys/dev/drm/radeon/radeon_prime.c:73-93 β†’ radeon_bo_create(..., sg, ...); sys/dev/drm/radeon/radeon_object.c β†’ type ttm_bo_type_sg; sys/dev/drm/ttm/ttm_tt.c:71-72 sets TTM_PAGE_FLAG_SG, ttm_tt.c:243 copies bo->sg).

The SG ("slave") handling in the TTM glue is #ifdef DUMBBELL_WIP β€” not compiled β€” with two consequences:

  1. Wrong pages bound β€” radeon_ttm_tt_populate() (sys/dev/drm/radeon/radeon_ttm.c:730-801): the slave branch that would install the sg pages via drm_prime_sg_to_page_addr_arrays() is dead (radeon_ttm.c:753-764). Control falls through to ttm_pool_populate() (radeon_ttm.c:779), which allocates fresh anonymous pages and installs them in ttm->pages, ignoring ttm->sg entirely (sys/dev/drm/ttm/ttm_page_alloc.c:890-933 has no SG check). ttm_tt_bind then points the GART at those fresh pages (radeon_ttm.c:659), so the GPU view of an imported dma-buf is not the exporter's memory: imported data is invisible, and GPU writes land in orphan pages (silent data divergence between exporter and importer β€” data-corruption class).

  2. Permanent page + accounting leak β€” radeon_ttm_tt_unpopulate() (radeon_ttm.c:803-847): the slave early-return is live (radeon_ttm.c:808,818-819), so the pages that populate wrongly allocated are never returned to the pool and the per-page ttm_mem_global accounting charged in ttm_pool_populate() (ttm_page_alloc.c:913-921) is never freed. Teardown leaks too: ttm_tt_destroy() β†’ ttm_tt_unpopulate() β†’ same early-return (sys/dev/drm/ttm/ttm_tt.c:215-229,500-508). Repeated import/destroy cycles grow non-swappable kernel memory and drain the TTM global memory pool until GPU allocations fail (local DoS).

Contrast: sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:1268-1330 β€” the sibling driver's populate/unpopulate implement the full slave/SG path live (no ifdef), e.g. amdgpu_ttm_tt_populate allocates ttm->sg and fills ttm->pages/dma_address from the sg table.

Reachability

Requires radeon to import a foreign dma-buf: PRIME fd passing between two processes with GPUs under different drivers on one box (i915+radeon, amdgpu+radeon, radeon+radeon), via DRM_IOCTL_PRIME_HANDLE_TO_FD / DRM_IOCTL_PRIME_FD_TO_HANDLE on the exporting and importing cards (sys/dev/drm/drm_prime.c:599,709-745; radeon hooks registered at sys/dev/drm/radeon/radeon_drv.c:698-704). Same-driver self-import short-circuits to the original object and is not affected. Niche (multi-GPU) β†’ Low severity.

PoC

poc_prime_import.c β€” exports a dma-buf from card0 (exporter, e.g. i915), imports it on the radeon card1, mmaps both sides, writes a marker through the exporter mapping and shows the importer's GPU-visible copy does not contain it; loops import/destroy 10k times while watching vmstat -m/sysctl hw.drm... growth to demonstrate the page leak.

Build / run (multi-GPU DFly box with radeon as importer)

cc -O2 -o poc_prime_import poc_prime_import.c
DRM_EXPORT=/dev/dri/card0 DRM_IMPORT=/dev/dri/card1 ./poc_prime_import

Success criterion: (a) marker written via exporter's mmap absent from the imported BO read via the radeon mmap (wrong-pages), (b) steady kernel-memory growth across cycles (leak). On amdgpu-as-importer the same test shows the marker propagating (control).

Verification status

Audit QEMU guest has one bochs VGA stub and no radeon/amdgpu/i915 DRM device (env.txt) β€” not runnable; source-verified only (untested), DF-4018/4019 precedent.

Fix

Port the amdgpu approach into radeon's populate/unpopulate (un-ifdef the slave branches and use the sg table), or reject SG imports until that porting is done:

--- a/sys/dev/drm/radeon/radeon_ttm.c
+++ b/sys/dev/drm/radeon/radeon_ttm.c
@@ radeon_ttm_tt_populate
+   bool slave = !!(ttm->page_flags & TTM_PAGE_FLAG_SG);
    ...
+   if (slave && ttm->sg) {
+       drm_prime_sg_to_page_addr_arrays(ttm->sg, ttm->pages,
+                        gtt->ttm.dma_address,
+                        ttm->num_pages);
+       ttm->state = tt_unbound;
+       return 0;
+   }
@@ radeon_ttm_tt_unpopulate
    if (slave)
-       return;
+       return;     /* slave pages belong to the sg table; see fix.diff */

See fix.diff for the full patch (the populate side is the security-relevant half; the unpopulate early-return is correct once populate stops using the pool for slave tts).

VERDICT.md
↓ download raw

DF-4430 VERDICT β€” radeon PRIME import: populate ignores ttm->sg (wrong

pages bound) and unpopulate leaks the pool pages

Bottom line

Source-verified, hardware-gated (audit guest has no AMD GPU / second DRM device). The DragonFlyBSD radeon port compiles out the SG ("slave") handling in radeon_ttm_tt_populate (#ifdef DUMBBELL_WIP, sys/dev/drm/radeon/radeon_ttm.c:753-764) while leaving the slave early-return live in radeon_ttm_tt_unpopulate (radeon_ttm.c:808,818-819). Net effect for any PRIME-imported BO (ttm_bo_type_sg, created at sys/dev/drm/radeon/radeon_prime.c:73-93):

  1. ttm_bo_handle_move_mem β†’ ttm_tt_bind (sys/dev/drm/ttm/ttm_bo.c:331-347) β†’ ttm_tt_populate (ttm_tt.c:468-482) β†’ radeon_ttm_tt_populate falls through to ttm_pool_populate (radeon_ttm.c:779) which allocates fresh anonymous pages and ignores ttm->sg (sys/dev/drm/ttm/ttm_page_alloc.c:890-933, no SG check) β€” the GART then binds those pages (radeon_ttm.c:659). The GPU view of an imported dma-buf is not the exporter's memory: silent data divergence (importer reads zeros, GPU writes never reach the real buffer).
  2. On unpopulate β€” including BO destroy (ttm_tt_destroy β†’ ttm_tt_unpopulate, ttm_tt.c:215-229,500-508) β€” the live slave early-return skips ttm_pool_unpopulate, so the wrongly-allocated pages and their ttm_mem_global accounting (ttm_page_alloc.c:913-921) leak permanently. Repeat import/destroy cycles = unbounded wired-kernel-memory growth + TTM accounting exhaustion (GPU alloc failures) = local DoS.

In-tree contrast: sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:1268-1330 implements the slave path live (no ifdef), proving the port simply left radeon's half unfinished rather than a deliberate design choice.

Reproduction status

untested β€” guest has one bochs VGA stub, no /dev/dri (env.txt); the trigger additionally needs two DRM devices with radeon as importer. Sibling precedent: DF-4018/4019 (source-verified, untested).

Reachability & impact ceiling

Requires cross-driver dma-buf import onto radeon (i915+radeon, amdgpu+radeon, radeon+radeon) β€” a rare multi-GPU configuration, hence Low. Same-driver self-import short-circuits (sys/dev/drm/drm_prime.c:709-745 returns the original object) and is unaffected. Within that config any user with both card nodes open (video group) can drive the leak loop unprivileged.

Fix validation

fix.diff un-ifdefs the slave branch of radeon_ttm_tt_populate (fills ttm->pages/dma_address from ttm->sg via drm_prime_sg_to_page_addr_arrays, matching amdgpu) β€” once populate no longer uses the pool for slave tts, the existing slave early-return in unpopulate becomes correct (slave pages belong to the sg table, not the pool). Cannot be built/loaded on this guest (no radeon hardware) β†’ 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 un-ifdefs the slave populate branch; cannot compile/load on the audit guest (no radeon hardware, no multi-GPU), so fix validation is not_testable here.

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

Confirmed kernel references

Detail

Exploit chain

['process with both card nodes open exports a dma-buf from card0 (DRM_IOCTL_PRIME_HANDLE_TO_FD)', 'imports it on the radeon card (DRM_IOCTL_PRIME_FD_TO_HANDLE -> radeon_gem_prime_import_sg_table -> ttm_bo_type_sg)', 'first validate/move populates the tt: radeon_ttm.c:779 ttm_pool_populate allocates fresh pages ignoring ttm->sg -> GART binds wrong pages (importer reads zeros; GPU writes never reach exporter)', 'GEM_CLOSE destroys the BO: ttm_tt_destroy -> radeon_ttm_tt_unpopulate -> slave early-return -> pages+accounting leak; loop N times for unbounded wired-memory growth']

Evidence (decisive lines)

README.md (chain, path:line), VERDICT.md, poc_prime_import.c (runnable on multi-GPU DFly box), fix.diff (un-ifdefs the slave populate branch, amdgpu-style), env.txt (guest hardware absence)

PoC changes

authored fresh (no seed sketch). Compile-verified on the audit guest with -I uapi kernel include paths and -D__user= (build.log). Runtime blocked: guest has neither radeon hardware nor a second DRM device.

Verified recommended fix

Un-ifdef the slave branch of radeon_ttm_tt_populate (fill ttm->pages/dma_address from ttm->sg via drm_prime_sg_to_page_addr_arrays, as amdgpu_ttm.c:1268+ does); the existing slave early-return in unpopulate then becomes correct.

Verdict

source-verified divergence/leak; untested on the audit guest (no AMD GPU, no second DRM device β€” env.txt). DF-4018/4019 precedent. Low: rare multi-GPU config, unprivileged within it.