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
| Part | Topic |
|---|---|
| Part 1 | Architecture and the Big Picture |
| Part 2 | Core Initialization and Driver Registration |
| Part 3 | open() 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.| Question | Code 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_resvthat 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 objectMermaidflowchart 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
Source drivers/gpu/drm/drm_gem.c L1027-L1032 (Linux v6.19)
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
Source drivers/gpu/drm/drm_gem.c L437-L495 (Linux v6.19)
idr_alloc(&file_priv->object_idr, NULL, 1, 0, ...): Allocates an available number >= 1 from this file's IDR table. Initially,NULLis placed instead of the object pointer.drm_vma_node_allow(): Grants this file permission tommapthis object (checked later viadrm_vma_node_is_allowed).obj->funcs->open(): Backend callback.- Finally,
idr_replace()swapsNULLwith 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)MermaidsequenceDiagram 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
Source drivers/gpu/drm/drm_dumb_buffers.c L191-L228 (Linux v6.19)
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
Source include/drm/drm_gem_shmem_helper.h L302-L304 (Linux v6.19)
drivers/gpu/drm/drm_gem_shmem_helper.c - drm_gem_shmem_dumb_create() (L558-568)c
Source drivers/gpu/drm/drm_gem_shmem_helper.c L558-L568 (Linux v6.19)
drivers/gpu/drm/drm_gem_shmem_helper.c - drm_gem_shmem_create_with_handle() (L470-491)c
Source drivers/gpu/drm/drm_gem_shmem_helper.c L470-L491 (Linux v6.19)
The operational sequence unfolds as follows:
drm_gem_shmem_create()creates the object with an initial reference count of 1.drm_gem_handle_create()creates the handle. Internally,drm_gem_object_handle_get()incrementshandle_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.- 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'sobject_idr, freeing the object.
Cleanup upon file closure is just as straightforward:
drivers/gpu/drm/drm_gem.c - drm_gem_release() (L1043-1049)c
Source drivers/gpu/drm/drm_gem.c L1043-L1049 (Linux v6.19)
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
Source drivers/gpu/drm/drm_gem_shmem_helper.c L52-L94 (Linux v6.19)
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
Source drivers/gpu/drm/drm_gem_shmem_helper.c L230-L262 (Linux v6.19)
drivers/gpu/drm/drm_gem.c - drm_gem_get_pages() (L623-688)c
Source drivers/gpu/drm/drm_gem.c L623-L688 (Linux v6.19)
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 → mmapMermaidsequenceDiagram 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
Source drivers/gpu/drm/drm_gem.c L395-L420 (Linux v6.19)
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
Source drivers/gpu/drm/drm_gem.c L557-L564 (Linux v6.19)
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
Source drivers/gpu/drm/drm_gem.c L1205-L1251 (Linux v6.19)
- It finds the vma_node using
vma->vm_pgoff(= offset converted to page count) and retrievesdrm_gem_objectviacontainer_of. drm_vma_node_is_allowed(node, priv): Only files previously authorized bydrm_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
Source drivers/gpu/drm/drm_gem.c L1146-L1187 (Linux v6.19)
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
Source drivers/gpu/drm/drm_gem_shmem_helper.c L653-L691 (Linux v6.19)
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
Source drivers/gpu/drm/drm_gem_shmem_helper.c L571-L599 (Linux v6.19)
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
| Stage | Function | Location (Linux v6.19) |
|---|---|---|
| GEM: handle table initialization | drm_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 lookup | drm_gem_object_lookup() | drivers/gpu/drm/drm_gem.c |
| dumb: size validation and dumb_create invocation | drm_mode_create_dumb() | drivers/gpu/drm/drm_dumb_buffers.c |
| dumb: shmem backend creation | drm_gem_shmem_dumb_create() | drivers/gpu/drm/drm_gem_shmem_helper.c |
| dumb: creation and handle association | drm_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 acquisition | drm_gem_get_pages() | drivers/gpu/drm/drm_gem.c |
| mmap: returning offset | drm_gem_dumb_map_offset() | drivers/gpu/drm/drm_gem.c |
| mmap: entry | drm_gem_mmap() | drivers/gpu/drm/drm_gem.c |
| mmap: linking vm_ops | drm_gem_mmap_obj() | drivers/gpu/drm/drm_gem.c |
| mmap: page fault | drm_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.
mmapresolves 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.