Linux DRM - GEM: handle, dumb buffer, mmap

How Linux DRM manages GPU buffers with GEM: drm_gem_object, per-file handles, dumb buffer creation and the mmap fake offset path with page faults in Linux v6.19.
October 29, 2026

Linux DRM - GEM: handle, dumb buffer, mmap

Reqruies
  • Kernel: Based on Linux v6.19 source
  • Prerequisites: Linux DRM Part 2, Part 3 open() and ioctl()

Introduction

PartTopic
Part 1Architecture and the Big Picture
Part 2Core Initialization and Driver Registration
Part 3open() and ioctl() Code Flow
In Parts 1 through 3, we examined the DRM architecture and the open/ioctl flow, noting that object_idr inside struct drm_file serves as the GEM handle table. This post begins from there.
QuestionCode Examined in This Post
Who creates and references the buffers used by the GPU?GEM (drm_gem_object, handle)
How is the simplest buffer created?Dumb buffer creation (DRM_IOCTL_MODE_CREATE_DUMB)
How does the CPU read and write to that buffer?mmap, fake offset, page fault

Why Is Memory Management DRM's Job

Because the GPU is a separate processor from the CPU, rendering graphics requires both the CPU and GPU to manage the same memory together. Textures, framebuffers, and command buffers all fall into this category. This presents challenges that cannot be solved with standard malloc.
  • To allow GPU access, memory must be pinned to physical pages and mapped into a GPU-accessible address space (via an IOMMU or the GPU's own page table).
  • When a process terminates, the GPU memory it occupied must be automatically reclaimed.
  • Different processes must view the same buffer, such as a display server and a client application.
  • Synchronization is required so that the CPU does not modify a buffer while the GPU is actively accessing it.
DRM solves this problem through an interface called GEM (Graphics Execution Manager). It represents every GPU buffer as a single struct drm_gem_object, while a backend (helper) determines where the actual memory is sourced. Representative examples include drm_gem_shmem_helper.c, which uses main memory shmem; drm_gem_dma_helper.c, which uses contiguous memory; and TTM-based drm_gem_ttm_helper.c, which manages dedicated GPU memory (VRAM) as well. The code in this post follows the simplest shmem backend (used by drivers like vkms).

The Core of GEM: drm_gem_object and Handles

The Object: struct drm_gem_object

include/drm/drm_gem.h - struct drm_gem_object (L280-455)
c
Source include/drm/drm_gem.h L280-L455 (Linux v6.19)
  • funcs: The function table implemented by the backend (free, mmap, vmap, export, get_sg_table, ...). The core handles common tasks and delegates memory-type-specific logic here.
  • filp: In the shmem backend, this is the shmem file holding the actual backing memory of the object.
  • vma_node: Holds the "fake offset" used to locate the object during mmap (covered in the mmap section below).
  • dma_buf, import_attach: The dma-buf connection used when sharing buffers with other devices (covered in the PRIME section).
  • resv: The dma_resv that aggregates fences attached to this buffer (covered in the synchronization section).
include/drm/drm_gem.h - struct drm_gem_object_funcs (L72-235)
c
Source include/drm/drm_gem.h L72-L235 (Linux v6.19)

The Name Tag: Handles Are per fd

Userspace cannot directly know the object's kernel pointer. Instead, it receives an integer handle. This handle is an index into drm_file.object_idr introduced in Part 1. In other words, handles reside in an independent namespace for each open()ed file descriptor (fd); even for the exact same object, different processes can have different handles.
Handles are per drm_file — different numbers can point to the same object
Mermaid
flowchart LR subgraph PA["Process A's drm_file"] IA["object_idr<br/>1 → obj X<br/>2 → obj Y"] end subgraph PB["Process B's drm_file"] IB["object_idr<br/>1 → obj Y<br/>2 → obj Z"] end X["drm_gem_object X"] Y["drm_gem_object Y<br/>handle_count = 2"] Z["drm_gem_object Z"] IA -->|"handle 1"| X IA -->|"handle 2"| Y IB -->|"handle 1"| Y IB -->|"handle 2"| Z
This table is initialized during open() (in drm_file_alloc, covered in Part 3). This is also where handle numbering is configured to start from 1.
drivers/gpu/drm/drm_gem.c - drm_gem_open() (L1027-1032)
c
The function that creates handles is drm_gem_handle_create(), while the actual work is performed by its tail function.
drivers/gpu/drm/drm_gem.c - drm_gem_handle_create_tail() (L437-495)
c
  • idr_alloc(&file_priv->object_idr, NULL, 1, 0, ...): Allocates an available number >= 1 from this file's IDR table. Initially, NULL is placed instead of the object pointer.
  • drm_vma_node_allow(): Grants this file permission to mmap this object (checked later via drm_vma_node_is_allowed).
  • obj->funcs->open(): Backend callback.
  • Finally, idr_replace() swaps NULL with the actual object pointer. This sequence prevents concurrent threads from looking up a half-initialized handle.

Code Flow 1: Creating a Dumb Buffer

Let us trace GEM through the simplest buffer creation path: the dumb buffer. This is the most basic buffer used for software rendering on screen; it lacks GPU acceleration features and is essentially "flat memory that the CPU can paint into." Userspace creates one using a single ioctl: DRM_IOCTL_MODE_CREATE_DUMB.
DRM_IOCTL_MODE_CREATE_DUMB processing flow (shmem backend)
Mermaid
sequenceDiagram participant U as Userspace participant D as drm_mode_create_dumb participant S as shmem helper participant G as GEM core U->>D: CREATE_DUMB<br/>(width, height, bpp) D->>D: Validate size<br/>(overflow, PAGE_ALIGN) D->>S: driver->dumb_create() S->>S: Create shmem file<br/>Reserve mmap offset S->>G: drm_gem_handle_create() G->>G: idr_alloc → handle G-->>U: handle · pitch · size
Calculating and validating the buffer size using width, height, and bpp passed by userspace is the core's job.
drivers/gpu/drm/drm_dumb_buffers.c - drm_mode_create_dumb() (L191-228)
c
dev->driver->dumb_create is the driver callback. As seen in Part 2, the DRM_GEM_SHMEM_DRIVER_OPS macro in vkms's drm_driver populates this callback with drm_gem_shmem_dumb_create.
include/drm/drm_gem_shmem_helper.h - #define DRM_GEM_SHMEM_DRIVER_OPS (L302-304)
c
drivers/gpu/drm/drm_gem_shmem_helper.c - drm_gem_shmem_dumb_create() (L558-568)
c
drivers/gpu/drm/drm_gem_shmem_helper.c - drm_gem_shmem_create_with_handle() (L470-491)
c
The operational sequence unfolds as follows:
  1. drm_gem_shmem_create() creates the object with an initial reference count of 1.
  2. drm_gem_handle_create() creates the handle. Internally, drm_gem_object_handle_get() increments handle_count, taking an additional object reference only when it is the first handle (if (obj->handle_count++ == 0) drm_gem_object_get(obj)). Subsequent handles simply increment the counter.
  3. Finally, drm_gem_object_put() drops the initial reference (source comment: "handle holds it now"). The object's lifecycle is now tied to the handle. When userspace closes the handle, or when the file is closed, drm_gem_release() releases all remaining handles in that file's object_idr, freeing the object.
Cleanup upon file closure is just as straightforward:
drivers/gpu/drm/drm_gem.c - drm_gem_release() (L1043-1049)
c
Even if a process crashes unexpectedly, the kernel invokes this function while closing the fd, preventing GPU memory leaks. Now let us turn back to __drm_gem_shmem_init(), which creates the object and performs two critical tasks.
drivers/gpu/drm/drm_gem_shmem_helper.c - __drm_gem_shmem_init() (L52-94)
c
  • drm_gem_object_init_with_mnt() creates a shmem file using shmem_file_setup("drm mm object", size, ...). Each object is essentially an anonymous memory file with a fixed size.
  • drm_gem_create_mmap_offset() pre-allocates and reserves the fake mmap offset for this object (covered in the mmap section below).
At this stage, no physical pages have been allocated yet. Pages are allocated on demand later inside drm_gem_shmem_get_pages_locked().
drivers/gpu/drm/drm_gem_shmem_helper.c - drm_gem_shmem_get_pages_locked() (L230-262)
c
drivers/gpu/drm/drm_gem.c - drm_gem_get_pages() (L623-688)
c
shmem_read_folio_gfp() reads pages from the shmem file (allocating them if not already present) into an array, and mapping_set_unevictable() pins them to prevent swapping. This is the concrete code implementing the principle stated earlier: "to be used by a GPU, memory must be pinned to physical pages."

Code Flow 2: Painting from the CPU with mmap

Although the buffer has been created, the CPU cannot access it yet. Because multiple buffers reside behind the single /dev/dri/card0 file descriptor, calling mmap(fd, ...) alone does not identify which buffer to map. DRM solves this using a fake offset, repurposing the file offset (the final argument to mmap) as an object identifier.
Two stages of mmap: obtaining offset → mmap
Mermaid
sequenceDiagram participant U as Userspace participant K as DRM core participant V as VM (page fault) U->>K: ioctl(MAP_DUMB, {handle}) K->>K: handle → drm_gem_object (object_idr) K-->>U: offset (address of obj->vma_node) U->>K: mmap(NULL, size, ..., fd, offset) K->>K: drm_gem_mmap(): offset → vma_node → obj<br/>Check drm_vma_node_is_allowed() K->>K: drm_gem_mmap_obj(): setup vm_ops, funcs->mmap() U->>V: First memory access V->>K: Fault handler drm_gem_shmem_fault() K-->>V: Map shmem page into PTE

Step 1: Getting the Offset (MAP_DUMB)

drivers/gpu/drm/drm_gem.c - drm_gem_dumb_map_offset() (L395-420)
c
drm_gem_object_lookup() looks up the object using the handle (object_idr query) and returns the address of the reserved vma_node as the offset. This offset is not an actual file offset, but a fake address managed per device by drm_vma_offset_manager.
drivers/gpu/drm/drm_gem.c - drm_gem_create_mmap_offset_size() (L557-564)
c

Step 2: mmap()

The mmap() syscall enters via .mmap = drm_gem_mmap in DRM_GEM_FOPS, which we explored in Part 3.
drivers/gpu/drm/drm_gem.c - drm_gem_mmap() (L1205-1251)
c
  • It finds the vma_node using vma->vm_pgoff (= offset converted to page count) and retrieves drm_gem_object via container_of.
  • drm_vma_node_is_allowed(node, priv): Only files previously authorized by drm_vma_node_allow() are allowed through. This safeguard prevents other processes from mapping someone else's buffer simply by guessing the offset value.
drivers/gpu/drm/drm_gem.c - drm_gem_mmap_obj() (L1146-1187)
c
This links the object's funcs->mmap and vm_ops. The shmem backend uses this mmap call to actually acquire pages (get_pages) and configure the mapping with VM_PFNMAP.
drivers/gpu/drm/drm_gem_shmem_helper.c - drm_gem_shmem_mmap() (L653-691)
c
When userspace first reads or writes to the mapped address, a page fault occurs, and the object's fault handler maps the backing pages into the page table entries one by one.
drivers/gpu/drm/drm_gem_shmem_helper.c - drm_gem_shmem_fault() (L571-599)
c
shmem->pages[page_offset] is the array collected earlier by drm_gem_get_pages(), and vmf_insert_pfn() maps that page directly into the user's virtual address space. Through this mechanism, a single user line such as *(uint32_t *)(px + ...) = color writes directly into the shmem pages backing the GEM object.

Code Locations

StageFunctionLocation (Linux v6.19)
GEM: handle table initializationdrm_gem_open()drivers/gpu/drm/drm_gem.c
GEM: handle creation (idr_alloc)drm_gem_handle_create_tail()drivers/gpu/drm/drm_gem.c
GEM: object lookupdrm_gem_object_lookup()drivers/gpu/drm/drm_gem.c
dumb: size validation and dumb_create invocationdrm_mode_create_dumb()drivers/gpu/drm/drm_dumb_buffers.c
dumb: shmem backend creationdrm_gem_shmem_dumb_create()drivers/gpu/drm/drm_gem_shmem_helper.c
dumb: creation and handle associationdrm_gem_shmem_create_with_handle()drivers/gpu/drm/drm_gem_shmem_helper.c
dumb: shmem file creation, mmap offset reservation__drm_gem_shmem_init()drivers/gpu/drm/drm_gem_shmem_helper.c
dumb: page acquisitiondrm_gem_get_pages()drivers/gpu/drm/drm_gem.c
mmap: returning offsetdrm_gem_dumb_map_offset()drivers/gpu/drm/drm_gem.c
mmap: entrydrm_gem_mmap()drivers/gpu/drm/drm_gem.c
mmap: linking vm_opsdrm_gem_mmap_obj()drivers/gpu/drm/drm_gem.c
mmap: page faultdrm_gem_shmem_fault()drivers/gpu/drm/drm_gem_shmem_helper.c

Summary

In this post, we have explored the following:
  • GPU buffers are represented by a single GEM object, which userspace references via an independent handle per fd (drm_file). The handle's lifetime governs the object's lifetime.
  • Dumb buffers are implemented through a shmem file + mmap fake offset, with physical pages allocated on demand when first needed.
  • mmap resolves the object via the fake offset and maps backing pages one by one upon a page fault. drm_vma_node_is_allowed() prevents unauthorized processes from mapping arbitrary buffers by simply knowing their offset.
Handles are meaningful only within their originating drm_file. Next, we will explore how to share these buffers with other processes or hardware devices using PRIME and dma-buf.
Jooojub
System S/W engineer
Explore Tags
Series
    Recent Post
    © 2026. jooojub. All right reserved.