The user ring buffer: a channel into BPF
Every ring buffer so far carried data kernel-to-user. The user ring buffer runs the other way: user space is the producer and the BPF program is the consumer, draining submitted samples with bpf_user_ringbuf_drain and a callback. Learn when to reach for it over a plain map, and meet the dynptr that each sample arrives as.
Ring buffers have appeared all through this book as the kernelβs way to stream
events up to user space β opensnoop, the L7 probes, the capstone all use one
in that direction. The user ring buffer (BPF_MAP_TYPE_USER_RINGBUF,
kernel 6.1) is the mirror image: user space is the producer, and a BPF
program is the consumer. Itβs the clean way to feed a stream of data into a
running program β and a good lens on a couple of newer primitives, the
drain-with-callback pattern and the dynptr each sample arrives as.
This is a genuine frontier chapter. Aya recognizes the map type, but the
ergonomic pieces β a user-space producer and the kernel-side drain helper β
are still emerging, and the sample callback uses dynptr, which this part
covers in its own chapter later. So the canonical C is in
examples/50-user-rb/reference/, with an Aya-flavored sketch alongside it; the
README.md is candid about what runs today.
Why not just write a map?
The fair question: user space can already write a regular BPF map that a program reads, so why a whole new map type? Because that pattern fits a few values, not a stream. If user space needs to hand the program an ordered sequence of variable-length messages β work items, a batch of records, commands β a shared map turns into a hand-rolled queue with its own locking and overrun problems. The user ring buffer gives you that queue for free, with the same reserve/submit discipline as the kernel-to-user ring buffer, and lets the program consume a whole batch atomically in its own context via one drain call. Rule of thumb: a setting β write a map; a stream β user ring buffer.
How it works
Two halves, mirror images of the ordinary ring buffer:
- User space (producer) reserves space, writes a sample, and submits it β
user_ring_buffer__reserve/__submitin libbpf terms. Submitting makes the sample visible to the kernel consumer. - BPF (consumer) calls
bpf_user_ringbuf_drain(&map, callback, ctx, flags)while it runs. The kernel walks the pending samples and invokes your callback once per sample; the callback receives the sample as abpf_dynptr(a bounds-checked handle to a variable-length region), from which you pull your record withbpf_dynptr_data.
The canonical C makes the consumer side concrete:
struct { __uint(type, BPF_MAP_TYPE_USER_RINGBUF); __uint(max_entries, 256 * 1024); } user_rb SEC(".maps");
static long on_sample(struct bpf_dynptr *dynptr, void *ctx) {
struct sample *s = bpf_dynptr_data(dynptr, 0, sizeof(*s));
if (!s) return 0;
__sync_fetch_and_add(&total_count, 1);
__sync_fetch_and_add(&total_sum, s->value);
return 0; /* return 1 to stop draining early */
}
SEC("tracepoint/syscalls/sys_enter_getpid")
int drain_it(void *ctx) {
bpf_user_ringbuf_drain(&user_rb, on_sample, NULL, 0); /* consume the batch */
return 0;
}
Reading it:
- The program is attached to some event β here
getpid, so the loader can trigger draining on demand by callinggetpid(). In production youβd drain on whatever event is relevant; the key idea is that draining happens when the program runs, not on a timer of its own. bpf_user_ringbuf_drainconsumes everything pending in one call, invokingon_sampleper sample. Returning0keeps going; returning1stops early (useful for backpressure).- Each sample is a
bpf_dynptrβ you donβt get a raw pointer, you get a handle the verifier can bounds-check, andbpf_dynptr_datahands you a typed, length-checked view. That indirection is what makes variable-length user input safe to touch in the kernel.
The Aya rendering follows the same shape β a UserRingBuf map, a drain call,
a callback β but treat it as a sketch: the producer and drain wrappers are
still settling, and the dynptr accessor is the subject of a later chapter.
Build, deploy, observe
cd examples/50-user-rb && ./demo.sh
The demo submits a stream of numbers from user space into the ring buffer,
triggers the program to drain them (by calling getpid in a loop), and reads
back the aggregate the callback built. In the terminal youβll see the count
and sum climb as samples are consumed. In Grafana (127.0.0.1:3000 β
Explore), graph rate(ebpf_userrb_messages_total[1m]) to watch the consume
rate β user-space-produced data, counted by a kernel program.
Cross-check
[vm]$ sudo bpftool map show | grep -i user_ringbuf # the map type
[vm]$ sudo bpftool map dump name AGG # the aggregate the callback built
bpftool listing a user_ringbuf map confirms the channel exists; the AGG
mapβs count matching the number of samples you submitted confirms every
user-space sample reached the kernel consumer.
What you learned
- The user ring buffer reverses the usual flow: user space produces, a
BPF program consumes, draining submitted samples with
bpf_user_ringbuf_drainand a per-sample callback. - Reach for it over a plain map when youβre feeding a stream of variable-length messages, not a few settings β you get an ordered queue and atomic batch consumption for free.
- Each sample arrives as a
bpf_dynptr, a bounds-checked handle β the safe way to touch variable-length user input, and a primitive this part returns to.
Next, Chapter 51 looks at running eBPF logic in user space β turning the model inside out once more.
Verification status: verified β Fedora 44, kernel 7.1.3. Built and run on the lab VM (Fedora 44, kernel 7.1.3-200.fc44): user space produces 1000 samples and the BPF program drains them (count=1000, sum=500500). Uses aya 0.14 + aya-ebpf 0.2.