Linux DMA-BUF CPU Access: mmap Coherency and Fence Handoffs
Bracket CPU mappings of shared DMA-BUFs with matching sync ioctls, wait on the correct fences, and keep cache maintenance distinct from ownership.
DMA-BUF lets Linux subsystems and processes share a buffer through a file descriptor without requiring every consumer to know where the backing storage lives. A userspace importer may map that object and read or write it on the CPU, but an mmap() pointer is not by itself a synchronization protocol. Correct access has two separate parts: wait until conflicting device work has completed, then bracket each CPU access interval so the exporter can perform any cache-coherency work it requires.
This article covers the userspace CPU-mapping contract. It does not allocate a DMA-BUF, define an image format, or replace the producer’s GPU, media, or display synchronization API. Obtain the FD and its size from the subsystem that created or transferred it; do not infer a portable allocation method from the mapping example.
Map the FD only when its exporter supports CPU access
An importer can call mmap() on a DMA-BUF FD when the exporter supplies a compatible mapping operation. Mapping support, valid protections, layout, and access lifetime are still exporter and subsystem contracts. The FD identifies a shared object, not a promise that its bytes form a linear image with a particular stride or pixel format. For image data, obtain plane offsets, strides, modifiers, and format from the API that negotiated the buffer.
The DMA_BUF_IOCTL_SYNC argument contains access flags, not an offset and length. Treat the CPU access interval as applying to the DMA-BUF object and obey the producer’s rules even when the application touches only a subrange of the mapping. DMA_BUF_SYNC_READ means the CPU reads; DMA_BUF_SYNC_WRITE means the CPU writes; use DMA_BUF_SYNC_RW when both happen in that interval. Do not declare a read-only pass as a write merely out of habit, since the exporter can use direction to choose its coherency work.
Pair every START with an END
Before dereferencing the mapped address, issue DMA_BUF_IOCTL_SYNC with DMA_BUF_SYNC_START and the intended read/write flags. After the CPU work is complete, issue the same ioctl with DMA_BUF_SYNC_END and the same access-direction flags. Repeat the pair for each distinct CPU access interval. A mapping can remain established across intervals, but the map itself does not replace the bracket.
The ioctl provides cache-coherency hooks; it does not lock the object against another CPU process or device. An application must arrange that the buffer is ready before CPU access and prevent conflicting work from racing with it. After SYNC_END, submit follow-up device work only according to the producer and consumer APIs, and do not hand the buffer to another stage as complete if the end operation failed.
The kernel documentation notes that DMA_BUF_IOCTL_SYNC can return EINTR or EAGAIN; those cases require restarting the synchronization operation. A blocking helper may retry. An event-driven program should schedule the same START or END phase for retry without touching the mapping in between. If START never succeeds, do not read or write the mapped bytes. If END has not succeeded, do not report the CPU transaction as complete or submit dependent device work.
Fence completion is not cache maintenance
For implicit synchronization, the DMA-BUF FD is pollable. A poll for read readiness (POLLIN, or EPOLLIN) checks completion of the most recent write or exclusive fence; poll for write readiness (POLLOUT, or EPOLLOUT) checks all attached fences. This lets an importer wait for earlier device access before beginning a conflicting CPU phase. Readiness reports fence completion, not that CPU cache preparation has already happened: the START ioctl is still required.
Some APIs use explicit synchronization instead of implicit reservation-object fences. In that case, wait on the explicit fence or synchronization object required by that API before accessing the mapping. Polling the DMA-BUF FD cannot stand in for an explicit fence that was never attached to the buffer. Likewise, START/END do not create a producer-to-consumer fence or serialize two independent CPU processes; use the relevant API’s locks, fence handoff, and ownership rules.
A blocking CPU read sequence
The example assumes dmabuf_fd was received from an API that permits mmap(), length is within the exported object’s size, and the application uses implicit DMA-BUF fences for this handoff. It copies bytes into ordinary application memory, so it does not interpret an image format. A write path uses PROT_WRITE, waits for write readiness, and uses DMA_BUF_SYNC_WRITE instead of the read flags.
#define _GNU_SOURCE
#include <errno.h>
#include <linux/dma-buf.h>
#include <poll.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
static int wait_for_implicit_access(int fd, short event)
{
struct pollfd pfd = { .fd = fd, .events = event };
for (;;) {
int rc = poll(&pfd, 1, -1);
if (rc < 0 && errno == EINTR)
continue;
if (rc < 0)
return -errno;
if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))
return -EIO;
if (pfd.revents & event)
return 0;
}
}
/* This blocking example restarts transient ioctl failures as documented. */
static int dma_buf_sync_retry(int fd, uint64_t flags)
{
struct dma_buf_sync sync = { .flags = flags };
for (;;) {
if (ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync) == 0)
return 0;
if (errno == EINTR || errno == EAGAIN)
continue;
return -errno;
}
}
static int copy_dmabuf_to_cpu(int fd, size_t length, void *destination,
size_t destination_capacity)
{
void *mapping;
int rc;
if (length == 0 || destination == NULL || destination_capacity < length)
return -EINVAL;
mapping = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
if (mapping == MAP_FAILED)
return -errno;
rc = wait_for_implicit_access(fd, POLLIN);
if (rc < 0)
goto unmap;
rc = dma_buf_sync_retry(fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ);
if (rc < 0)
goto unmap; /* Do not touch mapping unless START succeeded. */
memmove(destination, mapping, length);
rc = dma_buf_sync_retry(fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ);
/* Do not publish the copy as a completed handoff if END failed. */
unmap:
if (munmap(mapping, length) < 0 && rc == 0)
rc = -errno;
return rc;
}
This synchronous example assumes retrying EAGAIN can make progress. A nonblocking pipeline should put that retry on its event loop instead of spinning. If END fails after the copy, the destination bytes exist locally, but the application must treat the DMA-BUF transaction as incomplete and follow its recovery policy before returning the shared object to a producer. For explicit-fence users, replace the implicit poll with the API’s fence wait; keep the START/END bracket in either case.
Review the whole ownership transition
For each CPU access path, record who exported the FD, whether the exporter supports this mapping, which format/layout contract applies, which fence model is in use, which access direction the CPU needs, and what event permits the next device operation. Test both read and write paths on supported non-coherent platforms where possible; a coherent development machine can hide missing cache-maintenance calls. Exercise interrupted synchronization calls, exporter errors, fence delays, and teardown while an access interval is active.
The central distinction is simple but important: a fence answers when another participant’s work is finished; DMA_BUF_IOCTL_SYNC brackets CPU access and gives the exporter a chance to maintain coherency. Neither mechanism alone grants an application exclusive ownership. Treat them as separate gates in one explicit handoff protocol.
Related:
- Linux DMA API: Mapping, Coherency, and Device Ownership
- Linux DRM Atomic KMS: Plane State, Test Commits, and Page Flips
Sources: