io_uring_register_bpf_filter(3)
๐ NAME ๐
io_uring_register_bpf_filter, io_uring_register_bpf_filter_task โ okay so these are the besties that register classic BPF filters for io_uring operations. This is from the liburing Manual (you know, the io_uring_register_bpf_filter(3) corner of the universe) ๐ฆ
โจ SYNOPSIS โจ
No cap, first you gotta pull in the headers:
#include <liburing.h>
#include <liburing/io_uring/bpf_filter.h>
#include <linux/filter.h>
And then the two functions themselves, serving looks:
int io_uring_register_bpf_filter(struct io_uring *ring,
struct io_uring_bpf *bpf);
int io_uring_register_bpf_filter_task(struct io_uring_bpf *bpf);
๐ธ DESCRIPTION ๐ธ
So literally what these functions do is register classic BPF (cBPF) filters to restrict what io_uring operations are allowed to go down. Think of it as a bouncer at the club door ๐ชโจ โ you can build security policies by allowing or denying specific operations based on their parameters. Security policy era, honestly.
io_uring_register_bpf_filter(3) registers a filter on one specific ring. That filter only applies to operations submitted through that ring. It's like a filter that only cares about its own little circle.
io_uring_register_bpf_filter_task(3) registers a filter on the calling task. And bestie, the scope here is bigger:
- It applies to all
io_uringrings created by the task after the filter is registered. - It is inherited by child processes created via
fork(2). - Rings that were created before the filter was registered are not affected. (Sorry, no retroactive glow-up ๐ )
- Task-level filters cannot be removed โ periodt. Once they're on, they're on.
- Child processes cannot loosen restrictions set by their parent. No negotiating with the ancestors ๐ โโ๏ธ
The bpf argument is a pointer to a struct io_uring_bpf with cmd_type set to IO_URING_BPF_CMD_FILTER. The embedded struct io_uring_bpf_filter describes the filter you're registering:
struct io_uring_bpf_filter {
__u32 opcode; /* io_uring opcode to filter */
__u32 flags; /* IO_URING_BPF_FILTER_* */
__u32 filter_len; /* number of BPF instructions */
__u8 pdu_size; /* expected pdu size for opcode */
__u8 resv[3];
__u64 filter_ptr; /* pointer to BPF filter */
__u64 resv2[5];
};
Let's break down the vibe check on each field, bestie:
opcode says which io_uring operation the filter is for (like IORING_OP_SOCKET, IORING_OP_NOP, IORING_OP_READ โ you get it โจ).
filter_ptr points to an array of filter_len BPF instructions (struct sock_filter). The filter runs for each matching operation, and here's the rule: it must return non-zero to allow the operation, or zero to deny it โ and denying means the app gets -EACCES back. Binary. Iconic. ๐
pdu_size is the expected size in bytes of the operation-specific payload data for that opcode (like the socket or open structs inside struct io_uring_bpf_ctx). For opcodes with no extra payload, this should be zero. For IORING_OP_SOCKET this would be 12 (three 4-byte members), and for IORING_OP_OPENAT and IORING_OP_OPENAT2 this would be 24 (three 8-byte members).
Now, the size-matching drama, because it matters:
If the application's pdu_size matches the kernel's expected size for the opcode โ registration succeeds. Slay. ๐ซถ
If the sizes differ, it depends on whether IO_URING_BPF_FILTER_SZ_STRICT is set in flags:
- If
IO_URING_BPF_FILTER_SZ_STRICTis set, registration fails with-EMSGSIZEif the sizes differ. - If
IO_URING_BPF_FILTER_SZ_STRICTis not set, registration is allowed if the application'spdu_sizeis smaller than the kernel's. This lets older applications that were compiled against a smaller payload still load filters, since the kernel can safely evaluate the filter on the subset of data the application expects. (Nostalgic apps deserve love too ๐) - Regardless of
IO_URING_BPF_FILTER_SZ_STRICT, registration always fails with-EMSGSIZEif the application'spdu_sizeis larger than the kernel's โ because the kernel cannot provide data that it does not support. It's not being shady, it literally doesn't have it. ๐
On an -EMSGSIZE failure, the kernel writes back the kernel's expected pdu_size into the struct io_uring_bpf_filter that the application passed in. So your app can discover the kernel's expected payload size and adjust or retry accordingly. Very "here's the answer, bestie, take notes" of the kernel ๐โจ
flags can be zero, or a bitwise OR of these:
IO_URING_BPF_FILTER_DENY_RESTโ when set, any opcode that does not have a filter registered will be denied. This lets you create an allowlist of permitted operations. Allowlist only, nobody else gets in ๐ฅIO_URING_BPF_FILTER_SZ_STRICTโ when set, registration of a filter will fail with-EMSGSIZEif the application'spdu_sizedoes not exactly match the kernel's expected payload size for the opcode. Without this flag, the kernel permits filters where the application'spdu_sizeis smaller than or equal to the kernel's.
๐ง Filter Context ๐ง
The BPF filter gets handed a context structure it can inspect using BPF_LD instructions with absolute addressing. The layout is literally this:
struct io_uring_bpf_ctx {
__u64 user_data; /* offset 0: user_data from SQE */
__u8 opcode; /* offset 8: io_uring opcode */
__u8 sqe_flags; /* offset 9: SQE flags */
__u8 pdu_size; /* offset 10: aux data size for filter */
__u8 pad[5]; /* offset 11-15: padding */
union {
struct {
__u32 family; /* offset 16: socket family */
__u32 type; /* offset 20: socket type */
__u32 protocol; /* offset 24: socket protocol */
} socket;
struct {
__u64 flags; /* offset 16: open flags */
__u64 mode; /* offset 24: file mode */
__u64 resolve; /* offset 32: resolve flags */
} open;
};
};
The pdu_size field tells you the size in bytes of the operation-specific data passed in the union. A filter can check this value to verify it's receiving the expected payload. And bestie, this is genuinely useful for forward compatibility: if a future kernel adds new members to an operation's context, the filter can inspect pdu_size to figure out whether those fields are even present. Future-proof girlie behavior ๐ฎ
For IORING_OP_SOCKET operations, the socket family, type, and protocol fields are populated and can be used to filter based on socket parameters. pdu_size is set to 12 (three 4-byte members).
For IORING_OP_OPENAT and IORING_OP_OPENAT2 operations, the open flags, mode, and resolve fields are populated:
- The
flagsfield contains the open flags (e.g.,O_RDONLY,O_CREAT). - The
resolvefield is only meaningful forIORING_OP_OPENAT2and contains resolve flags (e.g.,RESOLVE_IN_ROOT).
pdu_size is set to 24 (three 8-byte members).
๐ Filter Stacking ๐
Multiple filters can be registered for the same opcode. When multiple filters exist, they are evaluated in order, and all of them must return non-zero for the operation to be allowed. Every single one. It's a group project and everyone has to pass ๐ค
For task-level filters, the child's filters are evaluated before the parent's filters.
โญ RETURN VALUE โญ
On success, these functions return 0. On failure, they return a negative error code. Short and sweet.
๐จ ERRORS ๐จ
-EINVALโ Invalid filter, opcode, or flags specified. Something in there was giving "wrong" energy.-EMSGSIZEโ The application'spdu_sizedoes not match the kernel's expected payload size for the opcode. This happens whenIO_URING_BPF_FILTER_SZ_STRICTis set and the sizes differ, or when the application'spdu_sizeis larger than the kernel's regardless of flags. Size mismatch moment ๐ญ-ENOMEMโ Insufficient memory to register the filter. No room, bestie.-EFAULTโ The filter pointer is invalid. Bad pointer, babe.-EACCESโ The caller does not have theCAP_SYS_ADMINcapability and theno_new_privsattribute is not set on the calling task. Seeprctl(2)withPR_SET_NO_NEW_PRIVS. Basically: permissions said no ๐
๐งช EXAMPLES (yes bestie, real code) โจ
๐ Deny all NOP operations
Okay so this is the "no NOPs allowed, ever, not on my watch" setup:
#include <sys/prctl.h>
#include <linux/filter.h>
#include <liburing.h>
#include <liburing/io_uring/bpf_filter.h>
struct sock_filter deny_filter[] = {
BPF_STMT(BPF_RET | BPF_K, 0), /* return 0 (deny) */
};
struct io_uring_bpf bpf = {
.cmd_type = IO_URING_BPF_CMD_FILTER,
.filter = {
.opcode = IORING_OP_NOP,
.filter_len = 1,
.filter_ptr = (unsigned long) deny_filter,
},
};
/* Must set no_new_privs before registering task filters */
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
/* Register on a specific ring */
io_uring_register_bpf_filter(&ring, &bpf);
/* Or register on the task */
io_uring_register_bpf_filter_task(&bpf);
Literally one instruction and the whole opcode is cancelled. The filter just returns 0 (deny) and that's the entire personality. ๐
๐ Allow only AF_INET sockets
This one is giving main character security energy โ it peeks at the socket family inside the context and says "IPv4 or gtfo":
#include <sys/prctl.h>
#include <linux/filter.h>
#include <sys/socket.h>
#include <liburing.h>
#include <liburing/io_uring/bpf_filter.h>
#define CTX_OFF_SOCKET_FAMILY 16
struct sock_filter inet_only_filter[] = {
/* Load socket family from context */
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_SOCKET_FAMILY),
/* If family == AF_INET, jump to allow */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1),
/* Allow: return 1 */
BPF_STMT(BPF_RET | BPF_K, 1),
/* Deny: return 0 */
BPF_STMT(BPF_RET | BPF_K, 0),
};
struct io_uring_bpf bpf = {
.cmd_type = IO_URING_BPF_CMD_FILTER,
.filter = {
.opcode = IORING_OP_SOCKET,
.filter_len = 4,
.filter_ptr = (unsigned long) inet_only_filter,
.pdu_size = 12, /* 3x __u32: family, type, protocol */
},
};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
io_uring_register_bpf_filter_task(&bpf);
Notice the pdu_size = 12 there? That's 3x __u32: family, type, protocol. The filter is 4 instructions long (filter_len = 4), and it's wired to IORING_OP_SOCKET. Slay. ๐ง
๐ Allow only NOP, deny everything else
The inverse vibe โ one tiny allow, and then IO_URING_BPF_FILTER_DENY_REST handles all the haters for you:
struct sock_filter allow_filter[] = {
BPF_STMT(BPF_RET | BPF_K, 1), /* return 1 (allow) */
};
struct io_uring_bpf bpf = {
.cmd_type = IO_URING_BPF_CMD_FILTER,
.filter = {
.opcode = IORING_OP_NOP,
.flags = IO_URING_BPF_FILTER_DENY_REST,
.filter_len = 1,
.filter_ptr = (unsigned long) allow_filter,
},
};
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
io_uring_register_bpf_filter_task(&bpf);
Allowlist supremacy, honestly. ๐
๐ Discover kernel pdu_size for an opcode
This next one's a lil detective arc. It shows off the -EMSGSIZE write-back so you can find out what payload size the kernel actually expects:
This example demonstrates how to use the -EMSGSIZE write-back to discover the kernel's expected payload size.
struct sock_filter allow[] = {
BPF_STMT(BPF_RET | BPF_K, 1),
};
struct io_uring_bpf bpf = {
.cmd_type = IO_URING_BPF_CMD_FILTER,
.filter = {
.opcode = IORING_OP_SOCKET,
.flags = IO_URING_BPF_FILTER_SZ_STRICT,
.filter_len = 1,
.filter_ptr = (unsigned long) allow,
.pdu_size = 0, /* intentionally wrong */
},
};
int ret;
ret = io_uring_register_bpf_filter(&ring, &bpf);
if (ret == -EMSGSIZE) {
/* kernel wrote back expected size */
printf("kernel pdu_size for SOCKET: %u\n",
bpf.filter.pdu_size);
/* retry with correct size */
ret = io_uring_register_bpf_filter(&ring, &bpf);
}
Yes the pdu_size = 0 is on purpose. Intentionally wrong, bestie โ that's how you get the answer back. ๐ญ
๐ NOTES (the serious bits, but make it cute) ๐ธ
๐ Privilege Requirements
Okay so this is important. Similar to seccomp(2), registering BPF filters requires either the CAP_SYS_ADMIN capability or the no_new_privs attribute to be set on the calling task.
Why tho? Because otherwise an unprivileged process could install a filter and then execute a setuid binary โ which would run with elevated privileges but under the attacker-controlled filter. Nope. Not today. Not on this kernel. ๐
To set the no_new_privs attribute, call:
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
Once it's set,no_new_privscannot be unset, and it's inherited by child processes acrossfork(2)and preserved acrossexecve(2). One-way door, bestie. ๐ช๐
๐ซถ Inheritance
Task-level filters registered with io_uring_register_bpf_filter_task(3) are inherited by child processes. So a parent process can establish security restrictions that apply to all of its descendants. Family rules, literally.
Children can add additional restrictions, but they cannot remove or weaken filters set by their ancestors. Nice try though. ๐
Ring-level filters registered with io_uring_register_bpf_filter(3) only apply to that specific ring, and they are not inherited. Ring-exclusive. Main character behaviour. ๐
๐ SEE ALSO
And that's the whole page, bestie โ you ate it. ๐
This one comes from the liburing-2.14 era, dated January 18, 2026. Go forth and filter responsibly. ๐โจ๐ฆ
๐ฆ where this page comes from
- arch rolling: liburing 2.15-1 ยท GPL-2.0-only WITH Linux-syscall-note OR MIT AND LGPL-2.0-or-later AND MIT
- ubuntu stonking: liburing-dev 2.15-1
- ๐ the page's license: LGPL-2.0-or-later
- Copyright (C) 2025 Jens Axboe <axboe@kernel.dk>
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms