Linux DRM - Code Flow of open() and ioctl()
The code path of open() and ioctl() in Linux DRM v6.19: drm_stub_open, drm_file_alloc, drm_ioctl dispatch and permission checks, plus a libdrm probe example.
October 28, 2026
Linux DRM - Code Flow of open() and ioctl()
Reqruies
- Kernel: Based on Linux v6.19 source
- Library: libdrm 2.4.129
- Prerequisites: Linux DRM Part 2: Core Initialization and Driver Registration
Introduction
| Part | Topic |
|---|---|
| Part 1 | Architecture and the Big Picture |
| Part 2 | Core Initialization and Driver Registration |
In Part 2, we looked at the process of creating nodes such as
/dev/dri/card0. This post traces the kernel functions executed the moment a user uses that node—specifically through open() and ioctl(). Finally, we will look at an example performing the same tasks in user space using libdrm.Code Flow 3: open()
When a user calls
open("/dev/dri/card0"), the kernel looks up the stub fops previously registered with major number 226. The stub does not perform real work itself; instead, it swaps out the file operations table with the driver's own.drivers/gpu/drm/drm_drv.c - drm_stub_open() (L1188-1216)c
Source drivers/gpu/drm/drm_drv.c L1188-L1216 (Linux v6.19)
open("/dev/dri/card0") Processing FlowMermaidsequenceDiagram participant U as User Process participant V as VFS (chrdev 226) participant S as drm_stub_open participant O as drm_open / drm_open_helper U->>V: open("/dev/dri/card0") V->>S: drm_stub_fops.open S->>S: drm_minor_acquire(iminor(inode))<br/>→ struct drm_minor S->>S: replace_fops(filp, driver->fops) S->>O: filp->f_op->open() (= drm_open) O->>O: drm_file_alloc() → struct drm_file O->>O: If primary, drm_master_open() O-->>U: fd
The
.open hook held in the driver's fops is drm_open(). This comes from a single macro line.include/drm/drm_gem.h - #define DRM_GEM_FOPS (L464-473)c
Source include/drm/drm_gem.h L464-L473 (Linux v6.19)
Inside this macro lie all the entry points we will see throughout this series:
.unlocked_ioctl = drm_ioctl (ioctl), .read = drm_read (reading events), .mmap = drm_gem_mmap (buffer mapping), and .poll = drm_poll. Here are drm_open() and drm_open_helper():drivers/gpu/drm/drm_file.c - drm_open() (L369-403)c
Source drivers/gpu/drm/drm_file.c L369-L403 (Linux v6.19)
drm_open() resolves the minor and forwards it to drm_open_helper().drivers/gpu/drm/drm_file.c - drm_open_helper() (L316-355)c
Source drivers/gpu/drm/drm_file.c L316-L355 (Linux v6.19)
drm_file_alloc() allocates the state corresponding to a single open(). The object_idr mentioned earlier, the event queue, and other fields are initialized here.drivers/gpu/drm/drm_file.c - drm_file_alloc() (L132-197)c
Source drivers/gpu/drm/drm_file.c L132-L197 (Linux v6.19)
drm_gem_open() initializes the handle table, which internally calls
idr_init_base(&file_private->object_idr, 1). This is the reason why handle numbers start from 1 (leaving 0 to represent "none"). Thanks to filp->private_data = priv, subsequent ioctl, read, and mmap calls can directly obtain drm_file from file->private_data.Code Flow 4: ioctl()
ioctl(fd, DRM_IOCTL_xxx, arg) enters into drm_ioctl(), which serves as fops->unlocked_ioctl.Dispatch Flow in drm_ioctl()Mermaidflowchart TD A["ioctl(fd, DRM_IOCTL_xxx, arg)"] --> B["drm_ioctl()"] B --> C{"nr ≥ DRM_COMMAND_BASE<br/>(0x40) ?"} C -->|"Yes : Driver ioctl"| D["dev->driver->ioctls[nr - 0x40]"] C -->|"No : Core ioctl"| E["drm_ioctls[nr]"] D --> F["copy_from_user → kdata"] E --> F F --> G["drm_ioctl_kernel()"] G --> H{"drm_ioctl_permit()<br/>AUTH · MASTER · ROOT_ONLY · RENDER_ALLOW"} H -->|"Denied"| I["-EACCES"] H -->|"Permitted"| J["func(dev, kdata, file_priv)"] J --> K["copy_to_user → result"]
drivers/gpu/drm/drm_ioctl.c - drm_ioctl() (L821-919)c
Source drivers/gpu/drm/drm_ioctl.c L821-L919 (Linux v6.19)
Summarizing the core steps:
- Checks whether the
typeof the ioctl number is'd'(DRM_IOCTL_BASE). - If the number is 0x40 or greater, it looks up the entry in the driver table (
dev->driver->ioctls); otherwise, it looks in the core table (drm_ioctls[]). - Rather than trusting the size provided by the user, it calculates the buffer size using the kernel definition (
ioctl->cmd) (Do not trust userspace). - After
copy_from_user(),drm_ioctl_kernel()checks permissions and invokes the actual function.
drivers/gpu/drm/drm_ioctl.c - drm_ioctl_kernel() (L787-805)c
Source drivers/gpu/drm/drm_ioctl.c L787-L805 (Linux v6.19)
The core ioctl table is an array constructed using macros. Selecting only the entries we will encounter in this series yields the following list. The flags on the far right correspond to the permission rules mentioned earlier.
drivers/gpu/drm/drm_ioctl.c - drm_ioctls (L632-735)c
Source drivers/gpu/drm/drm_ioctl.c L632-L735 (Linux v6.19)
libdrm: The View from User Space
Now let's trace the flow above directly from user space. The following program opens a DRM device, prints driver details, and lists KMS objects.
drm_probe.c - List driver info and KMS objectsc
Each libdrm function maps directly to a single kernel ioctl. You can match the exact names from the
drm_ioctls[] table in the code above:| libdrm Function | ioctl | Kernel Handler | Flags |
|---|---|---|---|
drmGetVersion() | DRM_IOCTL_VERSION | drm_version() | RENDER_ALLOW |
drmGetCap() | DRM_IOCTL_GET_CAP | drm_getcap() | RENDER_ALLOW |
drmSetClientCap() | DRM_IOCTL_SET_CLIENT_CAP | drm_setclientcap() | None |
drmModeGetResources() | DRM_IOCTL_MODE_GETRESOURCES | drm_mode_getresources() | None |
drmModeGetConnector() | DRM_IOCTL_MODE_GETCONNECTOR | drm_mode_getconnector() | None |
drmModeGetPlaneResources() | DRM_IOCTL_MODE_GETPLANERESOURCES | drm_mode_getplane_res() | None |
drmModeObjectGetProperties() | DRM_IOCTL_MODE_OBJ_GETPROPERTIES | drm_mode_obj_get_properties_ioctl() | None |
This program has been verified to compile with
libdrm 2.4.129. Running it requires access permissions to /dev/dri/card0 (usually available in a desktop login session, or requires membership in the video/render group). Running strace -e trace=openat,ioctl ./drm_probe shows the sequence of ioctls invoked by the user (depending on the strace version, it will also display decoded names such as DRM_IOCTL_VERSION).Code Locations
| Step | Function | Location (Linux v6.19) |
|---|---|---|
| open: Create file state | drm_open_helper() | drivers/gpu/drm/drm_file.c |
| open: Allocate drm_file | drm_file_alloc() | drivers/gpu/drm/drm_file.c |
| open: Set master | drm_master_open() | drivers/gpu/drm/drm_auth.c |
| ioctl: Entry and dispatch | drm_ioctl() | drivers/gpu/drm/drm_ioctl.c |
| ioctl: Check permissions | drm_ioctl_permit() | drivers/gpu/drm/drm_ioctl.c |
| ioctl: Core ioctl table | drm_ioctls | drivers/gpu/drm/drm_ioctl.c |
Summary
Here is what we covered in this post:
- open: The stub replaces the fops with the driver's own, and
drm_file_alloc()creates the state (handle table, event queue) for a singleopen(). - ioctl:
drm_ioctl()locates the entry in the core or driver table, anddrm_ioctl_permit()validates permissions before calling the handler. - Most libdrm functions are thin wrappers around these
ioctlcalls.
Next up is rendering. First, we will examine how GEM handles stored in
drm_file's object_idr are created, and how their buffers are read and written by the CPU.