Linux DRM - Sharing Buffers with PRIME and dma-buf

How Linux DRM shares buffers between processes and devices with PRIME and dma-buf: handle to fd export, fd to handle import, the dma_buf_ops table, and a dumb buffer plus PRIME example.
October 30, 2026

Linux DRM - Sharing Buffers with PRIME and dma-buf

Reqruies
  • Kernel: Based on Linux v6.19 source
  • Library: libdrm 2.4.129
  • Prerequisites: Linux DRM Part 4 GEM

Introduction

PartTopic
Part 1Architecture and the Big Picture
Part 2Core Initialization and Driver Registration
Part 3open() and ioctl() Code Flow
Part 4GEM: handle, dumb buffer, mmap
The GEM handles we saw in Part 4 are only valid within a drm_file. For a display server (compositor) to receive a buffer rendered by an application, or for a camera or video decoder to read a buffer created by a GPU, a sharing mechanism passable via an fd is required. This article walks through that mechanism, dma-buf, and PRIME, the DRM feature that bridges GEM and dma-buf.

PRIME: Sharing Buffers Between Processes and Devices

GEM handles are meaningful only within a drm_file and cannot be passed directly to other processes. For a display server (compositor) to receive buffers drawn by applications, or for a camera/video decoder to read buffers produced by the GPU, a sharing mechanism that can be passed as an fd is necessary. Linux's answer to this is dma-buf, and the DRM feature bridging GEM and dma-buf is called PRIME.
dma-buf: exporter · importer · attachment
Mermaid
flowchart TB subgraph EXP["Exporter (e.g., GPU Driver A)"] OBJ["drm_gem_object"] --- DB["struct dma_buf<br/>ops = drm_gem_prime_dmabuf_ops"] end DB --> FD["dma-buf fd<br/>Can be passed to another process via SCM_RIGHTS"] FD -->|"dma_buf_attach<br/>dma_buf_map_attachment"| ATT subgraph IMP["Importer (different driver or same driver)"] ATT["dma_buf_attachment"] --> SGT["sg_table<br/>(list of physical pages)"] --> OBJ2["drm_gem_object (import)"] end
  • The exporter wraps its buffer into a struct dma_buf and exposes it as an fd.
  • The importer calls dma_buf_attach() with that fd, then dma_buf_map_attachment() to obtain an sg_table (list of physical pages) and creates its own buffer object.
  • Data is not copied. Both sides view the same physical pages.

export: handle → dma-buf fd

The user space calls DRM_IOCTL_PRIME_HANDLE_TO_FD. Because this ioctl sets DRM_RENDER_ALLOW in its flags, it can also be called on render nodes (see the ioctl table in Part 3).
drivers/gpu/drm/drm_prime.c - drm_prime_handle_to_fd_ioctl() (L533-549)
c
drivers/gpu/drm/drm_prime.c - drm_gem_prime_handle_to_fd() (L510-530)
c
It reserves an fd, acquires the dma-buf, and associates it with that fd using fd_install(). The key function responsible for acquiring the dma-buf is drm_gem_prime_handle_to_dmabuf().
drivers/gpu/drm/drm_prime.c - drm_gem_prime_handle_to_dmabuf() (L431-494)
c
The branching logic inside this function forms the core of this section:
  1. drm_gem_object_lookup(): Looks up the object by handle.
  2. If already exported in this file (drm_prime_lookup_buf_by_handle), it returns that dma-buf as-is.
  3. If it is an imported object, it returns the original dma-buf (obj->import_attach->dmabuf).
  4. If obj->dma_buf already exists, it is reused.
  5. A new one is created only when none of the above exist (export_and_register_object).
Thus, exporting the same handle multiple times still yields the exact same dma-buf. This explains why export #1 and export #2 point to the identical inode in the example below.
drivers/gpu/drm/drm_prime.c - export_and_register_object() (L375-407)
c
If obj->funcs->export exists, the backend implementation is used; otherwise, it falls back to the core default drm_gem_prime_export().
drivers/gpu/drm/drm_prime.c - drm_gem_prime_export() (L916-931)
c
dma_buf_export_info holds ops (the dma-buf operation table) and priv = obj (the GEM object). This serves as the link connecting dma-buf back to the GEM object. The operation table is defined as follows:
drivers/gpu/drm/drm_prime.c - drm_gem_prime_dmabuf_ops (L827-836)
c
map_dma_buf is invoked when the importer calls dma_buf_map_attachment(). It retrieves the page list via the GEM object's funcs->get_sg_table() and performs DMA mapping for the importer device.
drivers/gpu/drm/drm_prime.c - drm_gem_map_dma_buf() (L666-692)
c
drm_gem_prime_export() calls drm_gem_dmabuf_export(), which calls dma_buf_export() from the dma-buf core and increments reference counts on both the device and the GEM object (ensuring the device and object are not freed while the dma-buf remains alive).
drivers/gpu/drm/drm_prime.c - drm_gem_dmabuf_export() (L239-254)
c
The dma-buf core implementation is as follows:
drivers/dma-buf/dma-buf.c - dma_buf_export() (L683-759)
c
drivers/dma-buf/dma-buf.c - dma_buf_getfile() (L602-632)
c
Here, the true nature of dma-buf is revealed: dma-buf = a pseudo file backed by an anonymous inode + operation table + private data. As a result, it can be handled via an fd, transmitted to another process via SCM_RIGHTS, and its reference count drops when calling close(). Because the file name is designated as "dmabuf", it shows up in /proc/<pid>/fd/ as follows:
drivers/dma-buf/dma-buf.c - dmabuffs_dname() (L126-140)
c
Before being named, /proc/self/fd/N for a dma-buf resolves to /dmabuf:.

import: dma-buf fd → handle

The receiving end calls DRM_IOCTL_PRIME_FD_TO_HANDLE.
drivers/gpu/drm/drm_prime.c - drm_gem_prime_fd_to_handle() (L292-359)
c
It acquires the dma-buf from the fd via dma_buf_get(fd). If it is an already-known dma-buf, the existing handle is returned. If it is encountered for the first time, a GEM object is created via drm_gem_prime_import(), and it is registered into this file's handle table with drm_gem_handle_create_tail(). During object creation, two distinct cases exist:
drivers/gpu/drm/drm_prime.c - drm_gem_prime_import_dev() (L968-1020)
c
  • If the dma-buf was exported by the same DRM device (drm_gem_is_prime_exported_dma_buf), no new object is created; it simply increments the reference count on the original object (dma_buf->priv). Sharing within the same device incurs virtually zero overhead.
  • If it was exported by a different device, an sg_table is obtained via dma_buf_attach() → dma_buf_map_attachment_unlocked(), and the driver callback gem_prime_import_sg_table() instantiates its own GEM object backed by those pages. Because obj->import_attach is populated, the object is subsequently treated as an "imported object" (e.g., drm_gem_dumb_map_offset() shown earlier returns -EINVAL for imported objects).

Hands-on: Dumb Buffers and PRIME

The entire workflow above is demonstrated in a single program. A dumb buffer is created, rendered into, exported via PRIME, and imported by another open() call (a different drm_file) to read the exact same pixels.
drm_dumb_prime.c - dumb buffer and PRIME
c
Here is how each line maps to the kernel code examined earlier:
User CodeWhat happens in the kernel
DRM_IOCTL_MODE_CREATE_DUMBdrm_mode_create_dumb() → dumb_create → drm_gem_handle_create(). The first handle of a new drm_file is typically 1 (the table starts from 1)
DRM_IOCTL_MODE_MAP_DUMBdrm_gem_dumb_map_offset() → vma_node address becomes the offset
mmap(..., fd, offset)drm_gem_mmap() → drm_gem_mmap_obj() → connects pages upon page fault
drmPrimeHandleToFD ×2On the second call, drm_gem_prime_handle_to_dmabuf() returns the same cached dma-buf (hence the same inode)
readlink("/proc/self/fd/N")Name of dma-buf file → dmabuffs_dname() → /dmabuf:
drmPrimeFDToHandle(fd2, ...)Takes the same-device branch of drm_gem_prime_import_dev() → new handle is created in fd2's table
This example has been verified to compile against libdrm 2.4.129. Because the environment where this post was written lacked /dev/dri access permissions, runtime output is not included. Running it yourself will demonstrate that both export operations point to the same inode, and the importer reads identical pixel values. (Note: drivers such as i915 use their own GEM implementation rather than the shmem helper, but the core parts covered in this article—the handle table and PRIME branching—proceed in the exact same way.)

Code Locations

StepFunctionLocation (Linux v6.19)
PRIME: handle → dma-bufdrm_gem_prime_handle_to_dmabuf()drivers/gpu/drm/drm_prime.c
PRIME: create new dma-bufexport_and_register_object()drivers/gpu/drm/drm_prime.c
PRIME: dma-buf operation tabledrm_gem_prime_dmabuf_opsdrivers/gpu/drm/drm_prime.c
PRIME: export dma-buf (core)dma_buf_export()drivers/dma-buf/dma-buf.c
PRIME: fd → handledrm_gem_prime_fd_to_handle()drivers/gpu/drm/drm_prime.c
PRIME: import objectdrm_gem_prime_import_dev()drivers/gpu/drm/drm_prime.c

Summary

Key takeaways from this post:
  • dma-buf = a pseudo file backed by an anonymous inode + operation table + private data. Being an fd, it can be passed to other processes via SCM_RIGHTS.
  • export (drm_gem_prime_handle_to_dmabuf) searches cache first, creating a new instance only when none exists. Exporting the same handle multiple times yields the exact same dma-buf.
  • import (drm_gem_prime_import_dev) reuses the original object directly if exported by the same device; if from a different device, it invokes attach → map_attachment to obtain an sg_table and creates an object via the driver callback. Data is never copied.
Once buffers are shared, coordination over "who is writing when" is essential. Next, we will cover dma_fence, which handles synchronization, and drm_sched, which queues and schedules GPU workloads.
Jooojub
System S/W engineer
Explore Tags
Series
    Recent Post
    © 2026. jooojub. All right reserved.