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)
PoC verification
Evidence pack
findings/poc/DF-4430 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
-
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 viadrm_prime_sg_to_page_addr_arrays()is dead (radeon_ttm.c:753-764). Control falls through tottm_pool_populate()(radeon_ttm.c:779), which allocates fresh anonymous pages and installs them inttm->pages, ignoringttm->sgentirely (sys/dev/drm/ttm/ttm_page_alloc.c:890-933has no SG check).ttm_tt_bindthen 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). -
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-pagettm_mem_globalaccounting charged inttm_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).
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):
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_populatefalls through tottm_pool_populate(radeon_ttm.c:779) which allocates fresh anonymous pages and ignoresttm->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).- On unpopulate β including BO destroy (
ttm_tt_destroyβttm_tt_unpopulate,ttm_tt.c:215-229,500-508) β the live slave early-return skipsttm_pool_unpopulate, so the wrongly-allocated pages and theirttm_mem_globalaccounting (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
- sys/dev/drm/radeon/radeon_ttm.c:753-764 (dead slave populate branch)
- sys/dev/drm/radeon/radeon_ttm.c:779 (pool populate for SG tt)
- sys/dev/drm/radeon/radeon_ttm.c:808,818-819 (live slave unpopulate return)
- sys/dev/drm/radeon/radeon_prime.c:73-93 (sg import entry)
- sys/dev/drm/ttm/ttm_tt.c:71-72,243 (SG flag + ttm->sg wiring)
- sys/dev/drm/ttm/ttm_tt.c:215-229,500-508 (destroyβunpopulate path)
- sys/dev/drm/ttm/ttm_page_alloc.c:890-933 (pool allocates for SG tt)
- sys/dev/drm/amd/amdgpu/amdgpu_ttm.c:1268-1330 (correct sibling impl)
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 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
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.
No comments yet.