---
name: "io_uring_register_bpf_filter"
section: "3"
platform: "linux"
package: "liburing"
version: "2.15-1"
release: "Arch Linux 2026-11-01"
platforms: ["linux"]
url: "https://new.carta.tech/io_uring_register_bpf_filter"
---

# io\_uring\_register\_bpf\_filter(3)

register classic BPF filters for io\_uring operations

## NAME

io\_uring\_register\_bpf\_filter, io\_uring\_register\_bpf\_filter\_task - register classic BPF filters for io\_uring operations

## SYNOPSIS

```
#include <liburing.h>
#include <liburing/io_uring/bpf_filter.h>
#include <linux/filter.h>
```

**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

These functions register classic BPF (cBPF) filters to restrict io\_uring operations. Filters can be used to implement security policies by allowing or denying specific operations based on their parameters.

[**io\_uring\_register\_bpf\_filter**(3)](/io_uring_register_bpf_filter/3.md) registers a filter on a specific *ring*. The filter only applies to operations submitted through that ring.

[**io\_uring\_register\_bpf\_filter\_task**(3)](/io_uring_register_bpf_filter_task/3.md) registers a filter on the calling task. The filter applies to all io\_uring rings created by the task after the filter is registered, and is inherited by child processes created via [**fork**(2)](/fork/2.md). Rings that were created before the filter was registered are not affected. Task-level filters cannot be removed and child processes cannot loosen restrictions set by their parent.

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 to register:

```
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];
};
```

*opcode* specifies which io\_uring operation the filter applies to (e.g., **IORING\_OP\_SOCKET**, **IORING\_OP\_NOP**, **IORING\_OP\_READ**).

*filter\_ptr* points to an array of *filter\_len* BPF instructions (**struct sock\_filter**). The filter is executed for each matching operation and must return non-zero to allow the operation or zero to deny it (resulting in **-EACCES** being returned to the application).

*pdu\_size* specifies the expected size in bytes of the operation-specific payload data for the given opcode (e.g., the socket or open structs inside **struct io\_uring\_bpf\_ctx**). For opcodes that have 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).

If the application's *pdu\_size* matches the kernel's expected size for the opcode, registration succeeds. If the sizes differ, the behavior depends on whether **IO\_URING\_BPF\_FILTER\_SZ\_STRICT** is set in *flags*:

- •

  If **IO\_URING\_BPF\_FILTER\_SZ\_STRICT** is set, registration fails with **-EMSGSIZE** if the sizes differ.

- •

  If **IO\_URING\_BPF\_FILTER\_SZ\_STRICT** is not set, registration is allowed if the application's *pdu\_size* is smaller than the kernel's. This permits older applications that were compiled against a smaller payload to still load filters, as the kernel can safely evaluate the filter on the subset of data the application expects.

- •

  Regardless of **IO\_URING\_BPF\_FILTER\_SZ\_STRICT**, registration always fails with **-EMSGSIZE** if the application's *pdu\_size* is larger than the kernel's, since the kernel cannot provide data that it does not support.

On an **-EMSGSIZE** failure, the kernel writes back the kernel's expected *pdu\_size* into the **struct io\_uring\_bpf\_filter** passed by the application. This allows the application to discover the kernel's expected payload size and adjust or retry accordingly.

*flags* can be zero or a bitwise OR of the following:

- **IO\_URING\_BPF\_FILTER\_DENY\_REST**

  When set, any opcode that does not have a filter registered will be denied. This allows creating an allowlist of permitted operations.

- **IO\_URING\_BPF\_FILTER\_SZ\_STRICT**

  When set, registration of a filter will fail with **-EMSGSIZE** if the application's *pdu\_size* does not exactly match the kernel's expected payload size for the opcode. Without this flag, the kernel permits filters where the application's *pdu\_size* is smaller than or equal to the kernel's.

### Filter Context

The BPF filter receives a context structure that can be inspected using **BPF\_LD** instructions with absolute addressing. The context layout is:

```
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 indicates the size in bytes of the operation-specific data passed in the union. A filter can check this value to verify it is receiving the expected payload. This is useful for forward compatibility: if a future kernel adds new members to an operation's context, the filter can inspect *pdu\_size* to determine whether those fields are present.

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 flags field contains the open flags (e.g., **O\_RDONLY**, **O\_CREAT**). The resolve field is only meaningful for **IORING\_OP\_OPENAT2** and 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 must return non-zero for the operation to be allowed. 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.

## ERRORS

- **-EINVAL**

  Invalid filter, opcode, or flags specified.

- **-EMSGSIZE**

  The application's *pdu\_size* does not match the kernel's expected payload size for the opcode. This occurs when **IO\_URING\_BPF\_FILTER\_SZ\_STRICT** is set and the sizes differ, or when the application's *pdu\_size* is larger than the kernel's regardless of flags.

- **-ENOMEM**

  Insufficient memory to register the filter.

- **-EFAULT**

  The filter pointer is invalid.

- **-EACCES**

  The caller does not have the **CAP\_SYS\_ADMIN** capability and the **no\_new\_privs** attribute is not set on the calling task. See [**prctl**(2)](/prctl/2.md) with **PR\_SET\_NO\_NEW\_PRIVS**.

## EXAMPLES

### Deny all NOP operations

```
#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);
```

### Allow only AF\_INET sockets

```
#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);
```

### Allow only NOP, deny everything else

```
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);
```

### Discover kernel pdu\_size for an opcode

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);
}
```

## NOTES

### Privilege Requirements

Similar to [**seccomp**(2)](/seccomp/2.md), registering BPF filters requires either the **CAP\_SYS\_ADMIN** capability or the **no\_new\_privs** attribute to be set on the calling task. This prevents an unprivileged process from installing a filter and then executing a setuid binary, which would run with elevated privileges but under the attacker-controlled filter.

To set the **no\_new\_privs** attribute, call:

```
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
```

Once set, **no\_new\_privs** cannot be unset and is inherited by child processes across [**fork**(2)](/fork/2.md) and preserved across [**execve**(2)](/execve/2.md).

### Inheritance

Task-level filters registered with [**io\_uring\_register\_bpf\_filter\_task**(3)](/io_uring_register_bpf_filter_task/3.md) are inherited by child processes. This allows a parent process to establish security restrictions that apply to all descendants. Children can add additional restrictions but cannot remove or weaken filters set by their ancestors.

Ring-level filters registered with [**io\_uring\_register\_bpf\_filter**(3)](/io_uring_register_bpf_filter/3.md) only apply to that specific ring and are not inherited.

## SEE ALSO

[**io\_uring\_register**(2)](/io_uring_register/2.md), [**io\_uring\_setup**(2)](/io_uring_setup/2.md), [**bpf**(2)](/bpf/2.md), [**seccomp**(2)](/seccomp/2.md)
