The userspace graphics path and libdrm

Map userspace graphics buffers and DRM interfaces to display presentation.

15 min readChapter 121 of 168Intermediate → advanced
In this chapter

Before you start#

Read DRM/KMS and dma-buf and the userspace map and project baselines. This chapter studies libdrm 2.4.123, an independently pinned historical userspace release, alongside Linux v6.12 as the course's historical kernel baseline. They are not a tested compatibility matrix or current security recommendation. The lab reads sysfs metadata and does not open a DRM node or change a display.

Learning goals#

Distinguish rendering from display control, trace libdrm's resource query and cleanup, and explain why a GEM handle, framebuffer ID and DMA-BUF fd have different ownership and scope.

Source walkthrough#

From a display client to DRM resources
  1. Application or compositor

    Chooses a DRM device and requests resources.

  2. libdrm

    Marshals DRM ioctls and interprets errors.

  3. DRM file and objects

    Handles, framebuffers and DMA-BUF fds have distinct scopes.

  4. Driver and display hardware

    Permissions and hardware determine whether a modeset can succeed.

Conceptual model. Conditional userspace-to-kernel relationships, not a captured display operation.

A graphical application asks a rendering API to produce an image. A compositor can combine application buffers, apply a scene and submit frames to a display pipeline. The kernel DRM subsystem mediates device access, memory objects, synchronization and, where available, kernel mode setting (KMS). A driver and hardware actually implement the operations. Not every program uses the same renderer, compositor or display path; a headless renderer can have no active display. libdrm 2.4.123 provides userspace helpers around the DRM ABI, not a graphics driver or compositor by itself.

A DRM primary node such as cardN can expose display-control operations subject to permissions, DRM master or lease rules and the specific ioctl. A render node such as renderD128, where provided, supports render-oriented access without granting general modesetting control; its existence or accessibility depends on driver and policy. A process may be able to render but lack authority to change a connector or CRTC. A compositor or session manager coordinates display ownership under the actual desktop configuration. A pathname is not a privilege grant: opening a node can fail and individual ioctls can be denied.

In libdrm 2.4.123, xf86drm.c, drmIoctl wraps the system call. Its file uses an MIT-style permissive license; the following short excerpt preserves the source:

c
    do {
        ret = ioctl(fd, request, arg);
    } while (ret == -1 && (errno == EINTR || errno == EAGAIN));

The wrapper calls ioctl with the supplied DRM fd, request number and argument pointer. It retries on the specified interrupted or temporarily unavailable errors and otherwise returns the result. It does not validate every higher-level lifetime or turn an error into success. The fd refers to a kernel DRM file; an ioctl's input/output structure and permitted operation are defined by the relevant UAPI and driver. Repeated retry semantics are a property of this wrapper and must not be generalized to every unrelated ioctl.

xf86drmMode.c, drmModeGetResources, also MIT-style, starts a two-query resource discovery path:

c
	memclear(res);
	if (drmIoctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res))
		return 0;

The first call clears the request structure, asks the kernel for counts and returns a null pointer on ioctl failure. The function then allocates arrays based on those counts and issues another query to obtain IDs. If connectors or other resource counts grow between calls, its source frees the temporary arrays and retries. Allocation or second-query failure follows cleanup. A successful result is a userspace snapshot that the caller must release with the matching drmModeFreeResources; hotplug can change the real topology immediately afterward.

KMS objects and buffer ownership#

A connector represents an output connection and its properties; an encoder and CRTC participate in routing and scanout; planes describe layers of scanout data where supported. The exact graph and atomic properties come from the driver. A framebuffer object describes layout and references backing storage for scanout; it is not necessarily a whole display or a CPU mapping. A mode describes timing parameters, but the existence of a mode in a query does not grant permission to select it. Atomic KMS lets a client submit a set of property changes under the driver's validation and commit rules. It is not a promise that an arbitrary multi-device change or an entire desktop session is one indivisible transaction.

GEM (Graphics Execution Manager) provides kernel graphics-memory object infrastructure used by many drivers. A GEM handle is an integer in a DRM file's handle namespace that refers to a buffer object. Another DRM file cannot assume that the same integer names the same object. A framebuffer ID identifies a KMS framebuffer object, not the GEM handle itself. A DMA-BUF fd is a transferable file descriptor for a shared buffer object under the export/import and synchronization contracts. Exporting or importing can establish new references and handles; closing one fd or deleting one handle does not necessarily free backing storage while other references remain. Format, plane offsets, strides, modifiers and fences matter when sharing an image.

Consider a hypothetical compositor that has a primary DRM fd P and a client that has render fd R. The client allocates a buffer with local handle 7 in R and exports a DMA-BUF fd. The compositor imports that fd into P, receiving a different local handle, say 12, and creates a framebuffer ID 40 referencing the imported object with an agreed format and stride. The integers 7, 12 and 40 are distinct identifiers with different scopes; none is a physical address. The compositor must retain required references until scanout and synchronization permit release. If import or framebuffer creation fails, it closes/releases only the resources already acquired. This is an illustrative sequence, not a captured DRM trace.

Queries, hotplug, and failure#

The resource-query function's two-call pattern handles a race: the count of connectors can change between the count query and the ID query. For example, count query returns two connectors, a hotplug event makes three visible, and the second ioctl reports a count exceeding the allocated array. The library frees the temporary arrays and retries rather than treating a truncated list as complete. A topology can also change after a successful query; clients need the relevant event and re-query logic. A query's returned IDs are not permanent across every unplug, driver reload or session.

In xf86drmMode.c, drmModeGetResources, libdrm 2.4.123, MIT-style permission notice; this is the retry branch after the second query:

c
	/* The number of available connectors and etc may have changed with a
	 * hotplug event in between the ioctls, in which case the field is
	 * silently ignored by the kernel.
	 */
	if (counts.count_fbs < res.count_fbs ||
	    counts.count_crtcs < res.count_crtcs ||
	    counts.count_connectors < res.count_connectors ||
	    counts.count_encoders < res.count_encoders)
	{
		drmFree(U642VOID(res.fb_id_ptr));
		drmFree(U642VOID(res.crtc_id_ptr));
		drmFree(U642VOID(res.connector_id_ptr));
		drmFree(U642VOID(res.encoder_id_ptr));

		goto retry;
	}

The comparison is between the first query's counts and the second query's counts. If any count grew, the temporary ID arrays are freed and the function retries from the beginning. The excerpt does not show the allocation failures or the final copy into the public result, and a retry does not guarantee a stable topology.

Two-query resource snapshot
Two-query resource snapshot1. Application to libdrm: drmModeGetResources(fd). 2. libdrm to Kernel DRM (fd): GETRESOURCES, counts only. 3. Kernel DRM (fd) to libdrm (return): counts: 2 connectors. 4. libdrm to Kernel DRM (fd): allocate arrays, GETRESOURCES again. 5. Kernel DRM (fd) to libdrm (return): hotplug raised count to 3; only 2 slots: free arrays, retry. 6. libdrm to Kernel DRM (fd): GETRESOURCES (retry). 7. Kernel DRM (fd) to libdrm (return): IDs for 3 connectors. 8. libdrm to Application (return): snapshot owned by caller.ApplicationlibdrmKernel DRM (fd)drmModeGetResources(fd)GETRESOURCES, countsonlycounts: 2 connectorsallocate arrays,GETRESOURCES againhotplug raised count to3; only 2 slots: freearrays, retryGETRESOURCES (retry)IDs for 3 connectorssnapshot owned by caller
  1. Applicationlibdrm

    drmModeGetResources(fd)

  2. libdrmKernel DRM (fd)

    GETRESOURCES, counts only

  3. Kernel DRM (fd)libdrm

    Return. counts: 2 connectors

  4. libdrmKernel DRM (fd)

    allocate arrays, GETRESOURCES again

  5. Kernel DRM (fd)libdrm

    Return. hotplug raised count to 3; only 2 slots: free arrays, retry

  6. libdrmKernel DRM (fd)

    GETRESOURCES (retry)

  7. Kernel DRM (fd)libdrm

    Return. IDs for 3 connectors

  8. libdrmApplication

    Return. snapshot owned by caller

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Application → libdrm: drmModeGetResources(fd)
  2. libdrm → Kernel DRM (fd): GETRESOURCES, counts only
  3. Kernel DRM (fd) → libdrm (return): counts: 2 connectors
  4. libdrm → Kernel DRM (fd): allocate arrays, GETRESOURCES again
  5. Kernel DRM (fd) → libdrm (return): hotplug raised count to 3; only 2 slots: free arrays, retry
  6. libdrm → Kernel DRM (fd): GETRESOURCES (retry)
  7. Kernel DRM (fd) → libdrm (return): IDs for 3 connectors
  8. libdrm → Application (return): snapshot owned by caller
Conceptual model. Teaching model of drmModeGetResources in libdrm 2.4.123 against the Linux v6.12 DRM UAPI, with a hypothetical hotplug between the queries. Not a captured DRM trace.

A mode-setting request can fail because an fd lacks authority, a connector disappeared, a combination is unsupported, a framebuffer is incompatible or memory is unavailable. On failure, a client must check the return and preserve or clean up its objects according to the API. Display ownership can be revoked or a session can become inactive while work is in flight. A DRM fd reference preserves the kernel file under its rules but does not guarantee that a physical monitor remains attached. Driver and kernel locking protect individual object operations; userspace still needs to coordinate its threads, event callbacks and object lifetimes.

Sharing a DMA-BUF is not automatically zero-copy presentation. The receiver can import the storage, but incompatible modifiers, color formats, GPU devices or compositor policy can require a copy or conversion. Synchronization is also distinct: a buffer reference keeps storage alive, while implicit or explicit synchronization determines when producers and consumers may access it. A compositor that reuses a buffer before the applicable release/fence contract is satisfied can display stale or corrupted data. A screenshot of a display is not needed to understand these source-level boundaries.

From ioctl to kernel and back#

On the kernel side, opening a DRM node creates file-specific state; DRM ioctl dispatch validates the command and permissions before reaching core or driver operations. KMS resource requests return data to userspace through UAPI structures. The exact v6.12 bridge is documented in DRM UAPI and implemented in Linux drm_ioctl.c. A newer userspace library can run with different kernels only to the extent that the needed ABI and features are available; it must handle unsupported operations. Conversely, a source archive's tag does not establish what library a host process actually loaded.

A rendering command can use a render node and a KMS compositor can use a primary node; a direct-display application may combine responsibilities when authorized. A GPU command submission and a page-flip/atomic commit are different operations. The former asks a driver to execute work; the latter changes display state under the relevant KMS contract. A buffer can be ready for rendering yet unsuitable for scanout, or suitable for scanout but still being written. Device nodes, handles, framebuffer IDs and synchronization objects each answer a different question.

The resource query is a snapshot with ownership#

The two ioctl calls in drmModeGetResources have different purposes. Initially the request structure has no userspace ID arrays, so the kernel reports counts and limits. libdrm records those counts, allocates arrays sized for framebuffer, CRTC, connector and encoder identifiers, and asks again. It then copies the returned arrays into the public drmModeRes result and frees the temporary ioctl arrays. The caller owns the returned libdrm object and must use drmModeFreeResources; it does not own the kernel's actual connectors merely because it has their IDs. A zero count is not necessarily an error: a device can have no connectors in the observed configuration.

If an allocation fails at any intermediate point, err_allocs releases the temporary arrays that were allocated and the function returns null. If allocation of one of the public arrays fails after the result structure exists, the function frees the other public arrays and result before the shared temporary cleanup. These are two ownership layers. Forgetting which array is temporary can produce a leak or double free in an alternative implementation. If the topology grows between ioctl calls, the count comparison frees the temporary arrays and retries. A topology that keeps changing can complicate progress; a returned snapshot is still not a lock against a later hotplug.

A connector ID is a handle to an object in the DRM device's object namespace, and the ID's presence says nothing by itself about a monitor's current preferred mode or whether an application has authority to commit one. A display client normally queries properties and their possible values, validates a proposed combination and handles a commit result or event. The fact that libdrm can enumerate a CRTC does not mean it can be assigned to any connector with any format. Connector routing, plane capabilities, dimensions, format modifiers and driver state constrain the combination. Some systems use legacy KMS interfaces, others atomic interfaces; code must follow the capabilities actually reported.

Why three buffer identifiers are needed#

A GEM handle provides a process-facing reference within one DRM file. Its small integer can be reused after the handle closes and should not be cached as a global physical address. An exported DMA-BUF fd is instead a normal Unix file descriptor referring to a shareable buffer object; it can be transferred to another process and that process receives its own fd number. Importing the DMA-BUF into another DRM file can create a local GEM handle there. A KMS framebuffer object then describes how the scanout subsystem should interpret one or more backing objects. The same storage can therefore have several handles or descriptors with independently managed references.

Identifiers and their scopes
Identifiers and their scopesGEM handle 7: integer in render file R only → Shared buffer object: backing storage with its own reference count (names the object only within file R). DMA-BUF fd: transferable descriptor for one shared object → Shared buffer object: backing storage with its own reference count (same storage, via export and import). DMA-BUF fd: transferable descriptor for one shared object → GEM handle 12: integer in compositor file P, after import (import yields a new handle in P). Framebuffer ID 40: KMS object describing layout over a buffer → Scanout framebuffer: created in P from the imported handle (framebuffer references the imported object)names the object onlywithin file Rsame storage, viaexport and importimport yields a newhandle in Pframebuffer referencesthe imported objectGEM handle 7integer in render file RonlyDMA-BUF fdtransferable descriptor forone shared objectFramebuffer ID 40KMS object describing layoutover a bufferShared buffer objectbacking storage with its ownreference countGEM handle 12integer in compositor fileP, after importScanout framebuffercreated in P from theimported handle
GEM handle 7integer in render file R only
Shared buffer objectbacking storage with its own reference count
DMA-BUF fdtransferable descriptor for one shared object
Shared buffer objectbacking storage with its own reference count
DMA-BUF fdtransferable descriptor for one shared object
GEM handle 12integer in compositor file P, after import
Framebuffer ID 40KMS object describing layout over a buffer
Scanout framebuffercreated in P from the imported handle
Read the text version
  • GEM handle 7: integer in render file R only → Shared buffer object: backing storage with its own reference count (names the object only within file R)
  • DMA-BUF fd: transferable descriptor for one shared object → Shared buffer object: backing storage with its own reference count (same storage, via export and import)
  • DMA-BUF fd: transferable descriptor for one shared object → GEM handle 12: integer in compositor file P, after import (import yields a new handle in P)
  • Framebuffer ID 40: KMS object describing layout over a buffer → Scanout framebuffer: created in P from the imported handle (framebuffer references the imported object)
Conceptual model. Illustrative scopes for hypothetical integers 7, 12 and 40 from the chapter example. Conceptual model of DRM file-local handles, a shareable DMA-BUF fd and a KMS framebuffer ID; not a captured trace.

For the worked example, assume a buffer is 1920 pixels wide, 1080 high and uses a four-byte-per-pixel linear format with a stride of 7680 bytes. The nominal pixel region is 8,294,400 bytes, but a real allocation can have padding, a different modifier, multiple planes or additional alignment. Handle 7 in render file R does not tell the compositor that the format is compatible with a plane. After import into file P as handle 12, framebuffer creation supplies the required layout. If framebuffer creation fails, the imported handle and DMA-BUF fd still need their own release. If it succeeds, the framebuffer reference and any in-flight display commit also have to be released at the correct time.

Synchronization determines when the producer can write and the consumer can scan out. A renderer's submission success is not necessarily rendering completion; a display commit's acceptance is not necessarily the moment that image became visible. An explicit fence or applicable implicit synchronization scheme can communicate dependencies, but its exact guarantees depend on drivers and APIs. A shared fd by itself carries no promise that both sides agree on color space, format, modifier or completed work. A robust compositor checks capabilities and can use a copy/composition fallback rather than assuming every imported buffer is directly scannable.

Read-only evidence and what it omits#

The sysfs lab counts published names without opening /dev/dri nodes. It is safe for a machine with an active graphical session because it performs no modeset, plane update or buffer submission. Its limitation is substantial: a card entry can exist without a connected display, and a connector entry does not reveal which process owns display authority. A render node may not appear in /sys/class/drm in the same shape as a connector name, and container mounts can hide host devices. A count of zero means no matching entries were visible to the process, not that Linux lacks DRM support.

For deeper investigation in a prepared disposable guest, a version-matched DRM query tool could open a permitted node and inspect capabilities without committing a mode. Even then, reading driver metadata is not proof that a GPU rendered correctly or that the same path exists on another architecture. This chapter does not require a guest or a device ioctl: the pinned source provides the resource-count and failure examples, and the lab provides only a local metadata observation.

A successful ioctl still requires interpreting returned counts and object properties under the relevant UAPI. In the resource function, a second call can return fewer objects than the first count if topology changes; the returned result reflects the second call's data. Userspace should not retain a pointer into the temporary ioctl arrays after the function frees them. This is why the function constructs a separate public result and why the caller has a matching free operation.

Practice#

Source-only route: extract the pinned libdrm archive in a private directory and inspect xf86drm.c around drmIoctl and xf86drmMode.c around drmModeGetResources and drmModeFreeResources. In the two-connector-to-three example above, identify the first count, temporary allocations, changed count, free-and-retry branch and final owner. If the first ioctl fails, the function returns null before allocating the result. This supplied timeline is source evidence, not a test of a monitor.

Read-only local observation:

sh
python3 - <<'PY'
from pathlib import Path
p = Path('/sys/class/drm')
try:
    entries = sorted(p.iterdir()) if p.is_dir() else []
    print('DRM sysfs entries:', len(entries))
    print('card entries:', sum(x.name.startswith('card') and '-' not in x.name for x in entries))
    print('connector entries:', sum(x.name.startswith('card') and '-' in x.name for x in entries))
except OSError as e: print('DRM metadata unavailable:', e.strerror)
PY

Counts can be zero in a container, virtual machine or headless setup. A connector entry does not prove an active mode, display permission or a particular GPU path. An inaccessible sysfs directory is a valid result. No device node is opened and no screen, input or clipboard is captured. If drm_info is already available, consult its help before using it; this lesson does not require issuing device ioctls. Deliver the count or limitation and the handle/framebuffer/DMA-BUF ownership diagram.

Knowledge check#

  1. Why can drmModeGetResources repeat its query?
  2. What does drmIoctl retry in the quoted implementation?
  3. Why are GEM handle 7, imported handle 12 and framebuffer ID 40 not interchangeable?
  4. Does access to a render node imply permission to modeset?
  5. Why is a DMA-BUF fd insufficient evidence of zero-copy presentation?

Answers#

  1. Hotplug can increase resource counts between the count and data queries; the function frees temporary arrays and retries.
  2. It retries when the system call returns -1 with EINTR or EAGAIN; other errors are returned.
  3. Handles are scoped to DRM files while a framebuffer ID identifies a KMS object; each has its own reference and release rules.
  4. No. Render and primary-node/display-control permissions differ and individual operations are checked.
  5. Format, modifier, device compatibility and compositor policy can require a copy; sharing and synchronization are separate contracts.

Checkpoint#

Save the resource-query timeline, ownership diagram and metadata observation. Pass when you can explain the hotplug retry, both cleanup paths and the distinction between render access and display authority. Re-read the two pinned libdrm functions and retry if an identifier's scope is unclear. Progress is not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.