* [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring
@ 2026-02-11 14:32 Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 1/5] io_uring: introduce callback driven main loop Pavel Begunkov
` (5 more replies)
0 siblings, 6 replies; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 14:32 UTC (permalink / raw)
To: io-uring; +Cc: asml.silence, bpf, axboe, Alexei Starovoitov
This series introduces a way to override the standard io_uring_enter
syscall execution with an extendible event loop, which can be controlled
by BPF via new io_uring struct_ops or from within the kernel.
There are multiple use cases I want to cover with this:
- Syscall avoidance. Instead of returning to the userspace for
CQE processing, a part of the logic can be moved into BPF to
avoid excessive number of syscalls.
- Access to in-kernel io_uring resources. For example, there are
registered buffers that can't be directly accessed by the userspace,
however we can give BPF the ability to peek at them. It can be used
to take a look at in-buffer app level headers to decide what to do
with data next and issuing IO using it.
- Smarter request ordering and linking. Request links are pretty
limited and inflexible as they can't pass information from one
request to another. With BPF we can peek at CQEs and memory and
compile a subsequent request.
- Feature semi-deprecation. It can be used to simplify handling
of deprecated features by moving it into the callback out core
io_uring. For example, it should be trivial to simulate
IOSQE_IO_DRAIN. Another target could be request linking logic.
- It can serve as a base for custom algorithms and fine tuning.
Often, it'd be impractical to introduce a generic feature because
it's either niche or requires a lot of configuration. For example,
there is support min-wait, however BPF can help to further fine tune
it by doing it in multiple steps with different number of CQEs /
timeouts. Another feature people were asking about is allowing
to over queue SQEs but make the kernel to maintain a given QD.
- Smarter polling. Napi polling is performed only once per syscall
and then it switches to waiting. We can do smarter and intermix
polling with waiting using the hook.
It might need more specialised kfuncs in the future, but the core
functionality is implemented with just two simple functions. One
returns region memory, which gives BPF access to CQ/SQ/etc. And
the second is for submitting requests. It's also given a structure
as an argument, which is used to pass waiting parameters.
It showed good numbers in a test that sequentially executes N nop
requests, where BPF was more than twice as fast than a 2-nop
request link implementation.
I've got ideas on how the user space part while writing toy programs,
mostly about simplifying life to BPF writers, but I want to turn it
into something more cohesive before posting.
v5: - Selftests are now using vmlinux.h
- Checking for unexpected loop return codes
- Remove KF_TRUSTED_ARGS (default)
- Squashed one of the patches, it's more sensible this way
v4: - Separated the event loop from the normal waiting path.
- Improved the selftest.
v3: - Removed most of utility kfuncs and replaced it with a single
helper returning the ring memory.
- Added KF_TRUSTED_ARGS to kfuncs
- Fix ifdef guarding
- Added a selftest
- Adjusted the waiting loop
- Reused the bpf lock section for task_work execution
Pavel Begunkov (5):
io_uring: introduce callback driven main loop
io_uring/bpf-ops: implement loop_step with BPF struct_ops
io_uring/bpf-ops: add kfunc helpers
io_uring/bpf-ops: implement bpf ops registration
selftests/io_uring: add a bpf io_uring selftest
include/linux/io_uring_types.h | 10 +
io_uring/Kconfig | 5 +
io_uring/Makefile | 3 +-
io_uring/bpf-ops.c | 271 +++++++++++++++++++
io_uring/bpf-ops.h | 28 ++
io_uring/io_uring.c | 8 +
io_uring/loop.c | 96 +++++++
io_uring/loop.h | 27 ++
tools/testing/selftests/Makefile | 3 +-
tools/testing/selftests/io_uring/Makefile | 162 +++++++++++
tools/testing/selftests/io_uring/basic.bpf.c | 131 +++++++++
tools/testing/selftests/io_uring/common.h | 6 +
tools/testing/selftests/io_uring/runner.c | 107 ++++++++
13 files changed, 855 insertions(+), 2 deletions(-)
create mode 100644 io_uring/bpf-ops.c
create mode 100644 io_uring/bpf-ops.h
create mode 100644 io_uring/loop.c
create mode 100644 io_uring/loop.h
create mode 100644 tools/testing/selftests/io_uring/Makefile
create mode 100644 tools/testing/selftests/io_uring/basic.bpf.c
create mode 100644 tools/testing/selftests/io_uring/common.h
create mode 100644 tools/testing/selftests/io_uring/runner.c
--
2.52.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v5 1/5] io_uring: introduce callback driven main loop
2026-02-11 14:32 [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Pavel Begunkov
@ 2026-02-11 14:32 ` Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 2/5] io_uring/bpf-ops: implement loop_step with BPF struct_ops Pavel Begunkov
` (4 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 14:32 UTC (permalink / raw)
To: io-uring; +Cc: asml.silence, bpf, axboe, Alexei Starovoitov
The io_uring_enter() has a fixed order of execution: it submits
requests, waits for completions, and returns to the user. Allow to
optionally replace it with a custom loop driven by a callback called
loop_step. The basic requirements to the callback is that it should be
able to submit requests, wait for completions, parse them and repeat.
Most of the communication including parameter passing can be implemented
via shared memory.
The callback should return IOU_LOOP_CONTINUE to continue execution or
IOU_LOOP_STOP to return to the user space. Note that the kernel may
decide to prematurely terminate it as well, e.g. in case the process was
signalled or killed.
The hook takes a structure with parameters. It can be used to ask the
kernel to wait for CQEs by setting cq_wait_idx to the CQE index it wants
to wait for. Spurious wake ups are possible and even likely, the callback
is expected to handle it. There will be more parameters in the future
like timeout.
It can be used with kernel callbacks, for example, as a slow path
deprecation mechanism overwiting SQEs and emulating the wanted
behaviour, however it's more useful together with BPF programs
implemented in following patches.
Note that keeping it separately from the normal io_uring wait loop
makes things much simpler and cleaner. It keeps it in one place instead
of spreading a bunch of checks in different places including disabling
the submission path. It holds the lock by default, which is a better fit
for BPF synchronisation and the loop execution model. It nicely avoids
existing quirks like forced wake ups on timeout request completion. And
it should be easier to implement new features.
Signed-off-by: Pavel Begunkov <asml.silence@gmail.com>
---
include/linux/io_uring_types.h | 5 ++
io_uring/Makefile | 2 +-
io_uring/io_uring.c | 6 +++
io_uring/loop.c | 96 ++++++++++++++++++++++++++++++++++
io_uring/loop.h | 27 ++++++++++
5 files changed, 135 insertions(+), 1 deletion(-)
create mode 100644 io_uring/loop.c
create mode 100644 io_uring/loop.h
diff --git a/include/linux/io_uring_types.h b/include/linux/io_uring_types.h
index 3e4a82a6f817..cceac329fcfd 100644
--- a/include/linux/io_uring_types.h
+++ b/include/linux/io_uring_types.h
@@ -41,6 +41,8 @@ enum io_uring_cmd_flags {
IO_URING_F_COMPAT = (1 << 12),
};
+struct iou_loop_params;
+
struct io_wq_work_node {
struct io_wq_work_node *next;
};
@@ -355,6 +357,9 @@ struct io_ring_ctx {
struct io_alloc_cache rw_cache;
struct io_alloc_cache cmd_cache;
+ int (*loop_step)(struct io_ring_ctx *ctx,
+ struct iou_loop_params *);
+
/*
* Any cancelable uring_cmd is added to this list in
* ->uring_cmd() by io_uring_cmd_insert_cancelable()
diff --git a/io_uring/Makefile b/io_uring/Makefile
index 931f9156132a..1c1f47de32a4 100644
--- a/io_uring/Makefile
+++ b/io_uring/Makefile
@@ -14,7 +14,7 @@ obj-$(CONFIG_IO_URING) += io_uring.o opdef.o kbuf.o rsrc.o notif.o \
advise.o openclose.o statx.o timeout.o \
cancel.o waitid.o register.o \
truncate.o memmap.o alloc_cache.o \
- query.o
+ query.o loop.o
obj-$(CONFIG_IO_URING_ZCRX) += zcrx.o
obj-$(CONFIG_IO_WQ) += io-wq.o
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 3a7be1695c39..52f9a5c766c1 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -95,6 +95,7 @@
#include "eventfd.h"
#include "wait.h"
#include "bpf_filter.h"
+#include "loop.h"
#define SQE_COMMON_FLAGS (IOSQE_FIXED_FILE | IOSQE_IO_LINK | \
IOSQE_IO_HARDLINK | IOSQE_ASYNC)
@@ -2577,6 +2578,11 @@ SYSCALL_DEFINE6(io_uring_enter, unsigned int, fd, u32, to_submit,
if (unlikely(smp_load_acquire(&ctx->flags) & IORING_SETUP_R_DISABLED))
goto out;
+ if (io_has_loop_ops(ctx)) {
+ ret = io_run_loop(ctx);
+ goto out;
+ }
+
/*
* For SQ polling, the thread will do all submissions and completions.
* Just return the requested submit count, and wake the thread if
diff --git a/io_uring/loop.c b/io_uring/loop.c
new file mode 100644
index 000000000000..26c7e44ac4bf
--- /dev/null
+++ b/io_uring/loop.c
@@ -0,0 +1,96 @@
+#include "io_uring.h"
+#include "wait.h"
+#include "loop.h"
+
+struct iou_loop_state {
+ struct iou_loop_params p;
+ struct io_ring_ctx *ctx;
+};
+
+static inline int io_loop_nr_cqes(const struct io_ring_ctx *ctx,
+ const struct iou_loop_state *ls)
+{
+ return ls->p.cq_wait_idx - READ_ONCE(ctx->rings->cq.tail);
+}
+
+static inline void io_loop_wait_start(struct io_ring_ctx *ctx, unsigned nr_wait)
+{
+ atomic_set(&ctx->cq_wait_nr, nr_wait);
+ set_current_state(TASK_INTERRUPTIBLE);
+}
+
+static inline void io_loop_wait_finish(struct io_ring_ctx *ctx)
+{
+ __set_current_state(TASK_RUNNING);
+ atomic_set(&ctx->cq_wait_nr, IO_CQ_WAKE_INIT);
+}
+
+static void io_loop_wait(struct io_ring_ctx *ctx, struct iou_loop_state *ls,
+ unsigned nr_wait)
+{
+ io_loop_wait_start(ctx, nr_wait);
+
+ if (unlikely(io_local_work_pending(ctx) ||
+ io_loop_nr_cqes(ctx, ls) <= 0) ||
+ READ_ONCE(ctx->check_cq)) {
+ io_loop_wait_finish(ctx);
+ return;
+ }
+
+ mutex_unlock(&ctx->uring_lock);
+ schedule();
+ io_loop_wait_finish(ctx);
+ mutex_lock(&ctx->uring_lock);
+}
+
+static int __io_run_loop(struct io_ring_ctx *ctx)
+{
+ struct iou_loop_state ls = {};
+
+ while (true) {
+ unsigned nr_wait;
+ int step_res;
+
+ if (unlikely(!ctx->loop_step))
+ return -EFAULT;
+
+ step_res = ctx->loop_step(ctx, &ls.p);
+ if (step_res == IOU_LOOP_STOP)
+ break;
+ if (step_res != IOU_LOOP_CONTINUE)
+ return -EINVAL;
+
+ nr_wait = io_loop_nr_cqes(ctx, &ls);
+ if (nr_wait > 0)
+ io_loop_wait(ctx, &ls, nr_wait);
+
+ if (task_work_pending(current)) {
+ mutex_unlock(&ctx->uring_lock);
+ io_run_task_work();
+ mutex_lock(&ctx->uring_lock);
+ }
+ if (unlikely(task_sigpending(current)))
+ return -EINTR;
+
+ nr_wait = max(nr_wait, 0);
+ io_run_local_work_locked(ctx, nr_wait);
+
+ if (READ_ONCE(ctx->check_cq) & BIT(IO_CHECK_CQ_OVERFLOW_BIT))
+ io_cqring_do_overflow_flush(ctx);
+ }
+
+ return 0;
+}
+
+int io_run_loop(struct io_ring_ctx *ctx)
+{
+ int ret;
+
+ if (!io_allowed_run_tw(ctx))
+ return -EEXIST;
+
+ mutex_lock(&ctx->uring_lock);
+ ret = __io_run_loop(ctx);
+ mutex_unlock(&ctx->uring_lock);
+ return ret;
+}
diff --git a/io_uring/loop.h b/io_uring/loop.h
new file mode 100644
index 000000000000..d7718b9ce61e
--- /dev/null
+++ b/io_uring/loop.h
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-2.0
+#ifndef IOU_LOOP_H
+#define IOU_LOOP_H
+
+#include <linux/io_uring_types.h>
+
+struct iou_loop_params {
+ /*
+ * The CQE index to wait for. Only serves as a hint and can still be
+ * woken up earlier.
+ */
+ __u32 cq_wait_idx;
+};
+
+enum {
+ IOU_LOOP_CONTINUE = 0,
+ IOU_LOOP_STOP,
+};
+
+static inline bool io_has_loop_ops(struct io_ring_ctx *ctx)
+{
+ return data_race(ctx->loop_step);
+}
+
+int io_run_loop(struct io_ring_ctx *ctx);
+
+#endif
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v5 2/5] io_uring/bpf-ops: implement loop_step with BPF struct_ops
2026-02-11 14:32 [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 1/5] io_uring: introduce callback driven main loop Pavel Begunkov
@ 2026-02-11 14:32 ` Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 3/5] io_uring/bpf-ops: add kfunc helpers Pavel Begunkov
` (3 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 14:32 UTC (permalink / raw)
To: io-uring; +Cc: asml.silence, bpf, axboe, Alexei Starovoitov
Introduce io_uring BPF struct ops implementing the loop_step callback,
which will allow BPF to overwrite the default io_uring event loop logic.
The callback takes an io_uring context, the main role of which is to be
passed to io_uring kfuncs. The other argument is a struct iou_loop_params,
which BPF can use to request CQ waiting and communicate other parameters.
See the event loop description in the previous patch for more details.
Signed-off-by: Pavel Begunkov <asml.silence@gmail.com>
---
io_uring/Kconfig | 5 ++
io_uring/Makefile | 1 +
io_uring/bpf-ops.c | 127 ++++++++++++++++++++++++++++++++++++++++++++
io_uring/bpf-ops.h | 14 +++++
io_uring/io_uring.c | 1 +
5 files changed, 148 insertions(+)
create mode 100644 io_uring/bpf-ops.c
create mode 100644 io_uring/bpf-ops.h
diff --git a/io_uring/Kconfig b/io_uring/Kconfig
index a7ae23cf1035..a283d9e53787 100644
--- a/io_uring/Kconfig
+++ b/io_uring/Kconfig
@@ -14,3 +14,8 @@ config IO_URING_BPF
def_bool y
depends on BPF
depends on NET
+
+config IO_URING_BPF_OPS
+ def_bool y
+ depends on IO_URING
+ depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
diff --git a/io_uring/Makefile b/io_uring/Makefile
index 1c1f47de32a4..c54e328d1410 100644
--- a/io_uring/Makefile
+++ b/io_uring/Makefile
@@ -25,3 +25,4 @@ obj-$(CONFIG_NET) += net.o cmd_net.o
obj-$(CONFIG_PROC_FS) += fdinfo.o
obj-$(CONFIG_IO_URING_MOCK_FILE) += mock_file.o
obj-$(CONFIG_IO_URING_BPF) += bpf_filter.o
+obj-$(CONFIG_IO_URING_BPF_OPS) += bpf-ops.o
diff --git a/io_uring/bpf-ops.c b/io_uring/bpf-ops.c
new file mode 100644
index 000000000000..7db07eda5a48
--- /dev/null
+++ b/io_uring/bpf-ops.c
@@ -0,0 +1,127 @@
+#include <linux/mutex.h>
+#include <linux/bpf.h>
+#include <linux/bpf_verifier.h>
+
+#include "io_uring.h"
+#include "register.h"
+#include "bpf-ops.h"
+#include "loop.h"
+
+static const struct btf_type *loop_params_type;
+
+static int io_bpf_ops__loop_step(struct io_ring_ctx *ctx,
+ struct iou_loop_params *lp)
+{
+ return IOU_LOOP_STOP;
+}
+
+static struct io_uring_bpf_ops io_bpf_ops_stubs = {
+ .loop_step = io_bpf_ops__loop_step,
+};
+
+static bool bpf_io_is_valid_access(int off, int size,
+ enum bpf_access_type type,
+ const struct bpf_prog *prog,
+ struct bpf_insn_access_aux *info)
+{
+ if (type != BPF_READ)
+ return false;
+ if (off < 0 || off >= sizeof(__u64) * MAX_BPF_FUNC_ARGS)
+ return false;
+ if (off % size != 0)
+ return false;
+
+ return btf_ctx_access(off, size, type, prog, info);
+}
+
+static int bpf_io_btf_struct_access(struct bpf_verifier_log *log,
+ const struct bpf_reg_state *reg, int off,
+ int size)
+{
+ const struct btf_type *t = btf_type_by_id(reg->btf, reg->btf_id);
+
+ if (t == loop_params_type) {
+ if (off >= offsetof(struct iou_loop_params, cq_wait_idx) &&
+ off + size <= offsetofend(struct iou_loop_params, cq_wait_idx))
+ return SCALAR_VALUE;
+ }
+
+ return -EACCES;
+}
+
+static const struct bpf_verifier_ops bpf_io_verifier_ops = {
+ .get_func_proto = bpf_base_func_proto,
+ .is_valid_access = bpf_io_is_valid_access,
+ .btf_struct_access = bpf_io_btf_struct_access,
+};
+
+static const struct btf_type *
+io_lookup_struct_type(struct btf *btf, const char *name)
+{
+ s32 type_id;
+
+ type_id = btf_find_by_name_kind(btf, name, BTF_KIND_STRUCT);
+ if (type_id < 0)
+ return NULL;
+ return btf_type_by_id(btf, type_id);
+}
+
+static int bpf_io_init(struct btf *btf)
+{
+ loop_params_type = io_lookup_struct_type(btf, "iou_loop_params");
+ if (!loop_params_type) {
+ pr_err("io_uring: Failed to locate iou_loop_params\n");
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int bpf_io_check_member(const struct btf_type *t,
+ const struct btf_member *member,
+ const struct bpf_prog *prog)
+{
+ return 0;
+}
+
+static int bpf_io_init_member(const struct btf_type *t,
+ const struct btf_member *member,
+ void *kdata, const void *udata)
+{
+ return 0;
+}
+
+static int bpf_io_reg(void *kdata, struct bpf_link *link)
+{
+ return -EOPNOTSUPP;
+}
+
+static void bpf_io_unreg(void *kdata, struct bpf_link *link)
+{
+}
+
+static struct bpf_struct_ops bpf_ring_ops = {
+ .verifier_ops = &bpf_io_verifier_ops,
+ .reg = bpf_io_reg,
+ .unreg = bpf_io_unreg,
+ .check_member = bpf_io_check_member,
+ .init_member = bpf_io_init_member,
+ .init = bpf_io_init,
+ .cfi_stubs = &io_bpf_ops_stubs,
+ .name = "io_uring_bpf_ops",
+ .owner = THIS_MODULE,
+};
+
+static int __init io_uring_bpf_init(void)
+{
+ int ret;
+
+ ret = register_bpf_struct_ops(&bpf_ring_ops, io_uring_bpf_ops);
+ if (ret) {
+ pr_err("io_uring: Failed to register struct_ops (%d)\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+__initcall(io_uring_bpf_init);
diff --git a/io_uring/bpf-ops.h b/io_uring/bpf-ops.h
new file mode 100644
index 000000000000..e8a08ae2df0a
--- /dev/null
+++ b/io_uring/bpf-ops.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0
+#ifndef IOU_BPF_OPS_H
+#define IOU_BPF_OPS_H
+
+#include <linux/io_uring_types.h>
+
+struct io_uring_bpf_ops {
+ int (*loop_step)(struct io_ring_ctx *ctx, struct iou_loop_params *lp);
+
+ __u32 ring_fd;
+ void *priv;
+};
+
+#endif /* IOU_BPF_OPS_H */
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 52f9a5c766c1..1f7c03728083 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -87,6 +87,7 @@
#include "msg_ring.h"
#include "memmap.h"
#include "zcrx.h"
+#include "bpf-ops.h"
#include "timeout.h"
#include "poll.h"
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v5 3/5] io_uring/bpf-ops: add kfunc helpers
2026-02-11 14:32 [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 1/5] io_uring: introduce callback driven main loop Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 2/5] io_uring/bpf-ops: implement loop_step with BPF struct_ops Pavel Begunkov
@ 2026-02-11 14:32 ` Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 4/5] io_uring/bpf-ops: implement bpf ops registration Pavel Begunkov
` (2 subsequent siblings)
5 siblings, 0 replies; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 14:32 UTC (permalink / raw)
To: io-uring; +Cc: asml.silence, bpf, axboe, Alexei Starovoitov
Add two kfuncs that should cover most of the needs:
1. bpf_io_uring_submit_sqes(), which allows to submit io_uring requests.
It mirrors the normal user space submission path and follows all
related io_uring_enter(2) rules. i.e. SQEs are taken from the SQ
according to head/tail values. In case of IORING_SETUP_SQ_REWIND,
it'll submit first N entries.
2. bpf_io_uring_get_region() returns a pointer to the specified region,
where io_uring regions are kernel-userspace shared chunks of memory.
It takes the size as an argument, which should be a load time
constant. There are 3 types of regions:
- IOU_REGION_SQ returns the submission queue.
- IOU_REGION_CQ stores the CQ, SQ/CQ headers and the sqarray. In
other words, it gives same memory that would normally be mmap'ed
with IORING_FEAT_SINGLE_MMAP enabled IORING_OFF_SQ_RING.
- IOU_REGION_MEM represents the memory / parameter region. It can be
used to store request indirect parameters and for kernel - user
communication.
It intentionally provides a thin but flexible API and expects BPF
programs to implement CQ/SQ header parsing, CQ walking, etc. That
mirrors how the normal user space works with rings and should help
to minimise kernel / kfunc helpers changes while introducing new generic
io_uring features.
Signed-off-by: Pavel Begunkov <asml.silence@gmail.com>
---
io_uring/bpf-ops.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++
io_uring/bpf-ops.h | 6 +++++
2 files changed, 61 insertions(+)
diff --git a/io_uring/bpf-ops.c b/io_uring/bpf-ops.c
index 7db07eda5a48..66938514211f 100644
--- a/io_uring/bpf-ops.c
+++ b/io_uring/bpf-ops.c
@@ -4,11 +4,58 @@
#include "io_uring.h"
#include "register.h"
+#include "memmap.h"
#include "bpf-ops.h"
#include "loop.h"
static const struct btf_type *loop_params_type;
+__bpf_kfunc_start_defs();
+
+__bpf_kfunc int bpf_io_uring_submit_sqes(struct io_ring_ctx *ctx, u32 nr)
+{
+ return io_submit_sqes(ctx, nr);
+}
+
+__bpf_kfunc
+__u8 *bpf_io_uring_get_region(struct io_ring_ctx *ctx, __u32 region_id,
+ const size_t rdwr_buf_size)
+{
+ struct io_mapped_region *r;
+
+ lockdep_assert_held(&ctx->uring_lock);
+
+ switch (region_id) {
+ case IOU_REGION_MEM:
+ r = &ctx->param_region;
+ break;
+ case IOU_REGION_CQ:
+ r = &ctx->ring_region;
+ break;
+ case IOU_REGION_SQ:
+ r = &ctx->sq_region;
+ break;
+ default:
+ return NULL;
+ }
+
+ if (unlikely(rdwr_buf_size > io_region_size(r)))
+ return NULL;
+ return io_region_get_ptr(r);
+}
+
+__bpf_kfunc_end_defs();
+
+BTF_KFUNCS_START(io_uring_kfunc_set)
+BTF_ID_FLAGS(func, bpf_io_uring_submit_sqes, KF_SLEEPABLE);
+BTF_ID_FLAGS(func, bpf_io_uring_get_region, KF_RET_NULL);
+BTF_KFUNCS_END(io_uring_kfunc_set)
+
+static const struct btf_kfunc_id_set bpf_io_uring_kfunc_set = {
+ .owner = THIS_MODULE,
+ .set = &io_uring_kfunc_set,
+};
+
static int io_bpf_ops__loop_step(struct io_ring_ctx *ctx,
struct iou_loop_params *lp)
{
@@ -68,12 +115,20 @@ io_lookup_struct_type(struct btf *btf, const char *name)
static int bpf_io_init(struct btf *btf)
{
+ int ret;
+
loop_params_type = io_lookup_struct_type(btf, "iou_loop_params");
if (!loop_params_type) {
pr_err("io_uring: Failed to locate iou_loop_params\n");
return -EINVAL;
}
+ ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_STRUCT_OPS,
+ &bpf_io_uring_kfunc_set);
+ if (ret) {
+ pr_err("io_uring: Failed to register kfuncs (%d)\n", ret);
+ return ret;
+ }
return 0;
}
diff --git a/io_uring/bpf-ops.h b/io_uring/bpf-ops.h
index e8a08ae2df0a..b9e589ad519a 100644
--- a/io_uring/bpf-ops.h
+++ b/io_uring/bpf-ops.h
@@ -4,6 +4,12 @@
#include <linux/io_uring_types.h>
+enum {
+ IOU_REGION_MEM,
+ IOU_REGION_CQ,
+ IOU_REGION_SQ,
+};
+
struct io_uring_bpf_ops {
int (*loop_step)(struct io_ring_ctx *ctx, struct iou_loop_params *lp);
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v5 4/5] io_uring/bpf-ops: implement bpf ops registration
2026-02-11 14:32 [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Pavel Begunkov
` (2 preceding siblings ...)
2026-02-11 14:32 ` [PATCH v5 3/5] io_uring/bpf-ops: add kfunc helpers Pavel Begunkov
@ 2026-02-11 14:32 ` Pavel Begunkov
2026-02-11 15:21 ` Jens Axboe
2026-02-11 14:32 ` [PATCH v5 5/5] selftests/io_uring: add a bpf io_uring selftest Pavel Begunkov
2026-02-11 15:24 ` [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Jens Axboe
5 siblings, 1 reply; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 14:32 UTC (permalink / raw)
To: io-uring; +Cc: asml.silence, bpf, axboe, Alexei Starovoitov
Implement BPF struct ops registration. It's registered off the BPF
path, and can be removed by BPF as well as io_uring. To protect it,
introduce a global lock synchronising registration. ctx->uring_lock can
be nested under it. ctx->bpf_ops is write protected by both locks and
so it's safe to read it under either of them.
Signed-off-by: Pavel Begunkov <asml.silence@gmail.com>
---
include/linux/io_uring_types.h | 5 ++
io_uring/bpf-ops.c | 91 +++++++++++++++++++++++++++++++++-
io_uring/bpf-ops.h | 8 +++
io_uring/io_uring.c | 1 +
4 files changed, 104 insertions(+), 1 deletion(-)
diff --git a/include/linux/io_uring_types.h b/include/linux/io_uring_types.h
index cceac329fcfd..976d85f82f86 100644
--- a/include/linux/io_uring_types.h
+++ b/include/linux/io_uring_types.h
@@ -8,6 +8,9 @@
#include <linux/llist.h>
#include <uapi/linux/io_uring.h>
+struct iou_loop_params;
+struct io_uring_bpf_ops;
+
enum {
/*
* A hint to not wake right away but delay until there are enough of
@@ -481,6 +484,8 @@ struct io_ring_ctx {
DECLARE_HASHTABLE(napi_ht, 4);
#endif
+ struct io_uring_bpf_ops *bpf_ops;
+
/*
* Protection for resize vs mmap races - both the mmap and resize
* side will need to grab this lock, to prevent either side from
diff --git a/io_uring/bpf-ops.c b/io_uring/bpf-ops.c
index 66938514211f..c75a0b858715 100644
--- a/io_uring/bpf-ops.c
+++ b/io_uring/bpf-ops.c
@@ -4,10 +4,12 @@
#include "io_uring.h"
#include "register.h"
+#include "loop.h"
#include "memmap.h"
#include "bpf-ops.h"
#include "loop.h"
+static DEFINE_MUTEX(io_bpf_ctrl_mutex);
static const struct btf_type *loop_params_type;
__bpf_kfunc_start_defs();
@@ -143,16 +145,103 @@ static int bpf_io_init_member(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata)
{
+ u32 moff = __btf_member_bit_offset(t, member) / 8;
+ const struct io_uring_bpf_ops *uops = udata;
+ struct io_uring_bpf_ops *ops = kdata;
+
+ switch (moff) {
+ case offsetof(struct io_uring_bpf_ops, ring_fd):
+ ops->ring_fd = uops->ring_fd;
+ return 1;
+ }
+ return 0;
+}
+
+static int io_install_bpf(struct io_ring_ctx *ctx, struct io_uring_bpf_ops *ops)
+{
+ if (ctx->flags & (IORING_SETUP_SQPOLL | IORING_SETUP_IOPOLL))
+ return -EOPNOTSUPP;
+ if (!(ctx->flags & IORING_SETUP_DEFER_TASKRUN))
+ return -EOPNOTSUPP;
+
+ if (ctx->bpf_ops)
+ return -EBUSY;
+ if (WARN_ON_ONCE(!ops->loop_step))
+ return -EINVAL;
+
+ ops->priv = ctx;
+ ctx->bpf_ops = ops;
+ ctx->loop_step = ops->loop_step;
return 0;
}
static int bpf_io_reg(void *kdata, struct bpf_link *link)
{
- return -EOPNOTSUPP;
+ struct io_uring_bpf_ops *ops = kdata;
+ struct io_ring_ctx *ctx;
+ struct file *file;
+ int ret = -EBUSY;
+
+ file = io_uring_register_get_file(ops->ring_fd, false);
+ if (IS_ERR(file))
+ return PTR_ERR(file);
+ ctx = file->private_data;
+
+ scoped_guard(mutex, &io_bpf_ctrl_mutex) {
+ guard(mutex)(&ctx->uring_lock);
+ ret = io_install_bpf(ctx, ops);
+ }
+
+ fput(file);
+ return ret;
+}
+
+static void io_eject_bpf(struct io_ring_ctx *ctx)
+{
+ struct io_uring_bpf_ops *ops = ctx->bpf_ops;
+
+ if (!WARN_ON_ONCE(!ops))
+ return;
+ if (WARN_ON_ONCE(ops->priv != ctx))
+ return;
+
+ ops->priv = NULL;
+ ctx->bpf_ops = NULL;
+ ctx->loop_step = NULL;
}
static void bpf_io_unreg(void *kdata, struct bpf_link *link)
{
+ struct io_uring_bpf_ops *ops = kdata;
+ struct io_ring_ctx *ctx;
+
+ guard(mutex)(&io_bpf_ctrl_mutex);
+ ctx = ops->priv;
+ if (ctx) {
+ guard(mutex)(&ctx->uring_lock);
+ if (WARN_ON_ONCE(ctx->bpf_ops != ops))
+ return;
+
+ io_eject_bpf(ctx);
+ }
+}
+
+void io_unregister_bpf_ops(struct io_ring_ctx *ctx)
+{
+ /*
+ * ->bpf_ops is write protected by io_bpf_ctrl_mutex and uring_lock,
+ * and read protected by either. Try to avoid taking the global lock
+ * for rings that never had any bpf installed.
+ */
+ scoped_guard(mutex, &ctx->uring_lock) {
+ if (!ctx->bpf_ops)
+ return;
+ }
+
+ guard(mutex)(&io_bpf_ctrl_mutex);
+ guard(mutex)(&ctx->uring_lock);
+ if (ctx->bpf_ops)
+ io_eject_bpf(ctx);
}
static struct bpf_struct_ops bpf_ring_ops = {
diff --git a/io_uring/bpf-ops.h b/io_uring/bpf-ops.h
index b9e589ad519a..b39b3fd3acda 100644
--- a/io_uring/bpf-ops.h
+++ b/io_uring/bpf-ops.h
@@ -17,4 +17,12 @@ struct io_uring_bpf_ops {
void *priv;
};
+#ifdef CONFIG_IO_URING_BPF_OPS
+void io_unregister_bpf_ops(struct io_ring_ctx *ctx);
+#else
+static inline void io_unregister_bpf_ops(struct io_ring_ctx *ctx)
+{
+}
+#endif
+
#endif /* IOU_BPF_OPS_H */
diff --git a/io_uring/io_uring.c b/io_uring/io_uring.c
index 1f7c03728083..259412b6b072 100644
--- a/io_uring/io_uring.c
+++ b/io_uring/io_uring.c
@@ -2149,6 +2149,7 @@ static __cold void io_req_caches_free(struct io_ring_ctx *ctx)
static __cold void io_ring_ctx_free(struct io_ring_ctx *ctx)
{
+ io_unregister_bpf_ops(ctx);
io_sq_thread_finish(ctx);
mutex_lock(&ctx->uring_lock);
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v5 5/5] selftests/io_uring: add a bpf io_uring selftest
2026-02-11 14:32 [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Pavel Begunkov
` (3 preceding siblings ...)
2026-02-11 14:32 ` [PATCH v5 4/5] io_uring/bpf-ops: implement bpf ops registration Pavel Begunkov
@ 2026-02-11 14:32 ` Pavel Begunkov
2026-02-11 15:22 ` Jens Axboe
2026-02-11 15:24 ` [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Jens Axboe
5 siblings, 1 reply; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 14:32 UTC (permalink / raw)
To: io-uring; +Cc: asml.silence, bpf, axboe, Alexei Starovoitov
Add a simple io_uring BPF selftest, where the BPF program implemented in
basic.bpf.c executes a given number of NOP requests with QD=1, writes
some stats and returns back. The makefile is borrowed from sched_ext
tests.
Signed-off-by: Pavel Begunkov <asml.silence@gmail.com>
---
tools/testing/selftests/Makefile | 3 +-
tools/testing/selftests/io_uring/Makefile | 162 +++++++++++++++++++
tools/testing/selftests/io_uring/basic.bpf.c | 131 +++++++++++++++
tools/testing/selftests/io_uring/common.h | 6 +
tools/testing/selftests/io_uring/runner.c | 107 ++++++++++++
5 files changed, 408 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/io_uring/Makefile
create mode 100644 tools/testing/selftests/io_uring/basic.bpf.c
create mode 100644 tools/testing/selftests/io_uring/common.h
create mode 100644 tools/testing/selftests/io_uring/runner.c
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 56e44a98d6a5..5e965ba3697c 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -130,6 +130,7 @@ TARGETS += vfio
TARGETS += x86
TARGETS += x86/bugs
TARGETS += zram
+TARGETS += io_uring
#Please keep the TARGETS list alphabetically sorted
# Run "make quicktest=1 run_tests" or
# "make quicktest=1 kselftest" from top level Makefile
@@ -147,7 +148,7 @@ endif
# User can optionally provide a TARGETS skiplist. By default we skip
# targets using BPF since it has cutting edge build time dependencies
# which require more effort to install.
-SKIP_TARGETS ?= bpf sched_ext
+SKIP_TARGETS ?= bpf sched_ext io_uring
ifneq ($(SKIP_TARGETS),)
TMP := $(filter-out $(SKIP_TARGETS), $(TARGETS))
override TARGETS := $(TMP)
diff --git a/tools/testing/selftests/io_uring/Makefile b/tools/testing/selftests/io_uring/Makefile
new file mode 100644
index 000000000000..f3b9fb653973
--- /dev/null
+++ b/tools/testing/selftests/io_uring/Makefile
@@ -0,0 +1,162 @@
+# SPDX-License-Identifier: GPL-2.0
+include ../../../build/Build.include
+include ../../../scripts/Makefile.arch
+include ../../../scripts/Makefile.include
+
+TEST_GEN_PROGS := runner
+
+# override lib.mk's default rules
+OVERRIDE_TARGETS := 1
+include ../lib.mk
+
+CURDIR := $(abspath .)
+REPOROOT := $(abspath ../../../..)
+TOOLSDIR := $(REPOROOT)/tools
+LIBDIR := $(TOOLSDIR)/lib
+BPFDIR := $(LIBDIR)/bpf
+TOOLSINCDIR := $(TOOLSDIR)/include
+BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
+APIDIR := $(TOOLSINCDIR)/uapi
+GENDIR := $(REPOROOT)/include/generated
+GENHDR := $(GENDIR)/autoconf.h
+
+OUTPUT_DIR := $(OUTPUT)/build
+OBJ_DIR := $(OUTPUT_DIR)/obj
+INCLUDE_DIR := $(OUTPUT_DIR)/include
+BPFOBJ_DIR := $(OBJ_DIR)/libbpf
+IOUOBJ_DIR := $(OBJ_DIR)/io_uring
+LIBBPF_OUTPUT := $(OBJ_DIR)/libbpf/libbpf.a
+BPFOBJ := $(BPFOBJ_DIR)/libbpf.a
+
+DEFAULT_BPFTOOL := $(OUTPUT_DIR)/host/sbin/bpftool
+HOST_OBJ_DIR := $(OBJ_DIR)/host/bpftool
+HOST_LIBBPF_OUTPUT := $(OBJ_DIR)/host/libbpf/
+HOST_LIBBPF_DESTDIR := $(OUTPUT_DIR)/host/
+HOST_DESTDIR := $(OUTPUT_DIR)/host/
+
+VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux) \
+ $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux) \
+ ../../../../vmlinux \
+ /sys/kernel/btf/vmlinux \
+ /boot/vmlinux-$(shell uname -r)
+VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
+ifeq ($(VMLINUX_BTF),)
+$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
+endif
+
+BPFTOOL ?= $(DEFAULT_BPFTOOL)
+
+ifneq ($(wildcard $(GENHDR)),)
+ GENFLAGS := -DHAVE_GENHDR
+endif
+
+CFLAGS += -g -O2 -rdynamic -pthread -Wall -Werror $(GENFLAGS) \
+ -I$(INCLUDE_DIR) -I$(GENDIR) -I$(LIBDIR) \
+ -I$(TOOLSINCDIR) -I$(APIDIR) -I$(CURDIR)/include
+
+# Silence some warnings when compiled with clang
+ifneq ($(LLVM),)
+CFLAGS += -Wno-unused-command-line-argument
+endif
+
+LDFLAGS = -lelf -lz -lpthread -lzstd
+
+IS_LITTLE_ENDIAN = $(shell $(CC) -dM -E - </dev/null | \
+ grep 'define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__')
+
+# Get Clang's default includes on this system, as opposed to those seen by
+# '-target bpf'. This fixes "missing" files on some architectures/distros,
+# such as asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h etc.
+#
+# Use '-idirafter': Don't interfere with include mechanics except where the
+# build would have failed anyways.
+define get_sys_includes
+$(shell $(1) $(2) -v -E - </dev/null 2>&1 \
+ | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \
+$(shell $(1) $(2) -dM -E - </dev/null | grep '__riscv_xlen ' | awk '{printf("-D__riscv_xlen=%d -D__BITS_PER_LONG=%d", $$3, $$3)}')
+endef
+
+ifneq ($(CROSS_COMPILE),)
+CLANG_TARGET_ARCH = --target=$(notdir $(CROSS_COMPILE:%-=%))
+endif
+
+CLANG_SYS_INCLUDES = $(call get_sys_includes,$(CLANG),$(CLANG_TARGET_ARCH))
+
+BPF_CFLAGS = -g -D__TARGET_ARCH_$(SRCARCH) \
+ $(if $(IS_LITTLE_ENDIAN),-mlittle-endian,-mbig-endian) \
+ -I$(CURDIR)/include -I$(CURDIR)/include/bpf-compat \
+ -I$(INCLUDE_DIR) -I$(APIDIR) \
+ -I$(REPOROOT)/include \
+ $(CLANG_SYS_INCLUDES) \
+ -Wall -Wno-compare-distinct-pointer-types \
+ -Wno-incompatible-function-pointer-types \
+ -O2 -mcpu=v3
+
+# sort removes libbpf duplicates when not cross-building
+MAKE_DIRS := $(sort $(OBJ_DIR)/libbpf $(OBJ_DIR)/libbpf \
+ $(OBJ_DIR)/bpftool $(OBJ_DIR)/resolve_btfids \
+ $(HOST_OBJ_DIR) $(INCLUDE_DIR) $(IOUOBJ_DIR))
+
+$(MAKE_DIRS):
+ $(call msg,MKDIR,,$@)
+ $(Q)mkdir -p $@
+
+$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \
+ $(APIDIR)/linux/bpf.h \
+ | $(OBJ_DIR)/libbpf
+ $(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) OUTPUT=$(OBJ_DIR)/libbpf/ \
+ ARCH=$(ARCH) CC="$(CC)" CROSS_COMPILE=$(CROSS_COMPILE) \
+ EXTRA_CFLAGS='-g -O0 -fPIC' \
+ DESTDIR=$(OUTPUT_DIR) prefix= all install_headers
+
+$(DEFAULT_BPFTOOL): $(wildcard $(BPFTOOLDIR)/*.[ch] $(BPFTOOLDIR)/Makefile) \
+ $(LIBBPF_OUTPUT) | $(HOST_OBJ_DIR)
+ $(Q)$(MAKE) $(submake_extras) -C $(BPFTOOLDIR) \
+ ARCH= CROSS_COMPILE= CC=$(HOSTCC) LD=$(HOSTLD) \
+ EXTRA_CFLAGS='-g -O0' \
+ OUTPUT=$(HOST_OBJ_DIR)/ \
+ LIBBPF_OUTPUT=$(HOST_LIBBPF_OUTPUT) \
+ LIBBPF_DESTDIR=$(HOST_LIBBPF_DESTDIR) \
+ prefix= DESTDIR=$(HOST_DESTDIR) install-bin
+
+$(INCLUDE_DIR)/vmlinux.h: $(VMLINUX_BTF) $(BPFTOOL) | $(INCLUDE_DIR)
+ifeq ($(VMLINUX_H),)
+ $(call msg,GEN,,$@)
+ $(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
+else
+ $(call msg,CP,,$@)
+ $(Q)cp "$(VMLINUX_H)" $@
+endif
+
+$(IOUOBJ_DIR)/%.bpf.o: %.bpf.c $(INCLUDE_DIR)/vmlinux.h | $(BPFOBJ) $(IOUOBJ_DIR)
+ $(call msg,CLNG-BPF,,$(notdir $@))
+ $(Q)$(CLANG) $(BPF_CFLAGS) -target bpf -c $< -o $@
+
+$(INCLUDE_DIR)/%.bpf.skel.h: $(IOUOBJ_DIR)/%.bpf.o $(INCLUDE_DIR)/vmlinux.h $(BPFTOOL) | $(INCLUDE_DIR)
+ $(eval sched=$(notdir $@))
+ $(call msg,GEN-SKEL,,$(sched))
+ $(Q)$(BPFTOOL) gen object $(<:.o=.linked1.o) $<
+ $(Q)$(BPFTOOL) gen object $(<:.o=.linked2.o) $(<:.o=.linked1.o)
+ $(Q)$(BPFTOOL) gen object $(<:.o=.linked3.o) $(<:.o=.linked2.o)
+ $(Q)diff $(<:.o=.linked2.o) $(<:.o=.linked3.o)
+ $(Q)$(BPFTOOL) gen skeleton $(<:.o=.linked3.o) name $(subst .bpf.skel.h,,$(sched)) > $@
+ $(Q)$(BPFTOOL) gen subskeleton $(<:.o=.linked3.o) name $(subst .bpf.skel.h,,$(sched)) > $(@:.skel.h=.subskel.h)
+
+override define CLEAN
+ rm -rf $(OUTPUT_DIR)
+ rm -f $(TEST_GEN_PROGS)
+endef
+
+all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubst %.c,%.skel.h,$(prog)))
+
+$(IOUOBJ_DIR)/runner.o: runner.c $(all_test_bpfprogs) | $(IOUOBJ_DIR) $(BPFOBJ)
+ $(CC) $(CFLAGS) -c $< -o $@
+
+$(OUTPUT)/runner: $(IOUOBJ_DIR)/runner.o $(BPFOBJ)
+ $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
+
+.DEFAULT_GOAL := all
+
+.DELETE_ON_ERROR:
+
+.SECONDARY:
diff --git a/tools/testing/selftests/io_uring/basic.bpf.c b/tools/testing/selftests/io_uring/basic.bpf.c
new file mode 100644
index 000000000000..f1bccf3b5f42
--- /dev/null
+++ b/tools/testing/selftests/io_uring/basic.bpf.c
@@ -0,0 +1,131 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#include <linux/types.h>
+#include <linux/stddef.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include "vmlinux.h"
+#include "common.h"
+
+char LICENSE[] SEC("license") = "Dual BSD/GPL";
+
+#define REQ_TOKEN 0xabba1741
+
+const unsigned max_inflight = 8;
+const volatile unsigned cq_hdr_offset;
+const volatile unsigned sq_hdr_offset;
+const volatile unsigned cqes_offset;
+
+int reqs_to_run;
+unsigned inflight;
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 3);
+ __type(key, u32);
+ __type(value, s64);
+} res_map SEC(".maps");
+
+#define t_min(a, b) ((a) < (b) ? (a) : (b))
+
+static inline void set_cq_wait(struct iou_loop_params *lp,
+ struct io_uring *cq_hdr, unsigned to_wait)
+{
+ lp->cq_wait_idx = cq_hdr->head + to_wait;
+}
+
+static inline void write_result(int res)
+{
+ u32 key = SLOT_RES;
+ u64 *val;
+
+ val = bpf_map_lookup_elem(&res_map, &key);
+ if (val)
+ *val = res;
+}
+
+static inline void write_stats(int idx, unsigned int v)
+{
+ u32 key = idx;
+ u64 *val;
+
+ val = bpf_map_lookup_elem(&res_map, &key);
+ if (val)
+ *val += v;
+}
+
+SEC("struct_ops.s/link_loop")
+int BPF_PROG(link_loop, struct io_ring_ctx *ring, struct iou_loop_params *ls)
+{
+ struct io_uring *sq_hdr, *cq_hdr;
+ struct io_uring_sqe *sqes;
+ struct io_uring_cqe *cqes;
+ void *rings;
+ int ret;
+
+ sqes = (void *)bpf_io_uring_get_region(ring, IOU_REGION_SQ,
+ SQ_ENTRIES * sizeof(struct io_uring_sqe));
+ rings = (void *)bpf_io_uring_get_region(ring, IOU_REGION_CQ,
+ cqes_offset + CQ_ENTRIES * sizeof(struct io_uring_cqe));
+ if (!rings || !sqes) {
+ write_result(-1);
+ return IOU_LOOP_STOP;
+ }
+
+ sq_hdr = rings + (sq_hdr_offset & 63);
+ cq_hdr = rings + (cq_hdr_offset & 63);
+ cqes = rings + cqes_offset;
+
+ unsigned to_wait = cq_hdr->tail - cq_hdr->head;
+ to_wait = t_min(to_wait, CQ_ENTRIES);
+ for (int i = 0; i < to_wait; i++) {
+ struct io_uring_cqe *cqe = &cqes[cq_hdr->head & (CQ_ENTRIES - 1)];
+
+ if (cqe->user_data != REQ_TOKEN) {
+ write_result(-3);
+ return IOU_LOOP_STOP;
+ }
+ cq_hdr->head++;
+ }
+
+ reqs_to_run -= to_wait;
+ inflight -= to_wait;
+ write_stats(SLOT_NR_CQES, to_wait);
+
+ if (reqs_to_run <= 0) {
+ write_result(1);
+ return IOU_LOOP_STOP;
+ }
+
+ if (inflight < max_inflight) {
+ unsigned to_submit = max_inflight - inflight;
+
+ to_submit = t_min(to_submit, reqs_to_run);
+
+ for (int i = 0; i < to_submit; i++) {
+ struct io_uring_sqe *sqe = sqes + i;
+
+ sqe = &sqes[sq_hdr->tail & (SQ_ENTRIES - 1)];
+ *sqe = (struct io_uring_sqe){};
+ sqe->opcode = IORING_OP_NOP;
+ sqe->user_data = REQ_TOKEN;
+ sq_hdr->tail++;
+ }
+
+ ret = bpf_io_uring_submit_sqes(ring, to_submit);
+ if (ret != to_submit) {
+ write_result(-2);
+ return IOU_LOOP_STOP;
+ }
+
+ inflight += to_submit;
+ write_stats(SLOT_NR_SQES, to_submit);
+ }
+
+ set_cq_wait(ls, cq_hdr, 1);
+ return IOU_LOOP_CONTINUE;
+}
+
+SEC(".struct_ops.link")
+struct io_uring_bpf_ops basic_ops = {
+ .loop_step = (void *)link_loop,
+};
diff --git a/tools/testing/selftests/io_uring/common.h b/tools/testing/selftests/io_uring/common.h
new file mode 100644
index 000000000000..40e3182b8e5a
--- /dev/null
+++ b/tools/testing/selftests/io_uring/common.h
@@ -0,0 +1,6 @@
+#define CQ_ENTRIES 8
+#define SQ_ENTRIES 8
+
+#define SLOT_RES 0
+#define SLOT_NR_CQES 1
+#define SLOT_NR_SQES 2
diff --git a/tools/testing/selftests/io_uring/runner.c b/tools/testing/selftests/io_uring/runner.c
new file mode 100644
index 000000000000..5fc25ddc20e8
--- /dev/null
+++ b/tools/testing/selftests/io_uring/runner.c
@@ -0,0 +1,107 @@
+#include <linux/stddef.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdlib.h>
+
+#include <bpf/libbpf.h>
+#include <io_uring/mini_liburing.h>
+
+#include "basic.bpf.skel.h"
+#include "common.h"
+
+static struct io_uring_params params;
+static struct basic *skel;
+static struct bpf_link *basic_link;
+
+#define NR_ITERS 10
+
+static void setup_ring(struct io_uring *ring)
+{
+ int ret;
+
+ memset(¶ms, 0, sizeof(params));
+ params.cq_entries = CQ_ENTRIES;
+ params.flags = IORING_SETUP_SINGLE_ISSUER |
+ IORING_SETUP_DEFER_TASKRUN |
+ IORING_SETUP_NO_SQARRAY |
+ IORING_SETUP_CQSIZE;
+
+ ret = io_uring_queue_init_params(SQ_ENTRIES, ring, ¶ms);
+ if (ret) {
+ fprintf(stderr, "ring init failed\n");
+ exit(1);
+ }
+}
+
+static void setup_bpf_ops(struct io_uring *ring)
+{
+ int ret;
+
+ skel = basic__open();
+ if (!skel) {
+ fprintf(stderr, "can't generate skeleton\n");
+ exit(1);
+ }
+
+ skel->struct_ops.basic_ops->ring_fd = ring->ring_fd;
+ skel->bss->reqs_to_run = NR_ITERS;
+ skel->rodata->sq_hdr_offset = params.sq_off.head;
+ skel->rodata->cq_hdr_offset = params.cq_off.head;
+ skel->rodata->cqes_offset = params.cq_off.cqes;
+
+ ret = basic__load(skel);
+ if (ret) {
+ fprintf(stderr, "failed to load skeleton\n");
+ exit(1);
+ }
+
+ basic_link = bpf_map__attach_struct_ops(skel->maps.basic_ops);
+ if (!basic_link) {
+ fprintf(stderr, "failed to attach ops\n");
+ exit(1);
+ }
+}
+
+static void run_ring(struct io_uring *ring)
+{
+ __s64 res[3] = {};
+ int i, ret;
+
+ ret = io_uring_enter(ring->ring_fd, 0, 0, IORING_ENTER_GETEVENTS, NULL);
+ if (ret) {
+ fprintf(stderr, "run failed\n");
+ exit(1);
+ }
+
+ for (i = 0; i < 3; i++) {
+ __u32 key = i;
+
+ ret = bpf_map__lookup_elem(skel->maps.res_map,
+ &key, sizeof(key),
+ &res[i], sizeof(res[i]), 0);
+ if (ret)
+ fprintf(stderr, "can't read map idx %i: %i\n", i, ret);
+ }
+
+ if (res[SLOT_RES] != 1)
+ fprintf(stderr, "run failed: %i\n", (int)res[SLOT_RES]);
+ if (res[SLOT_NR_CQES] != NR_ITERS)
+ fprintf(stderr, "unexpected number of CQEs: %i\n",
+ (int)res[SLOT_NR_CQES]);
+ if (res[SLOT_NR_SQES] != NR_ITERS)
+ fprintf(stderr, "unexpected submitted number: %i\n",
+ (int)res[SLOT_NR_SQES]);
+}
+
+int main() {
+ struct io_uring ring;
+
+ setup_ring(&ring);
+ setup_bpf_ops(&ring);
+
+ run_ring(&ring);
+
+ bpf_link__destroy(basic_link);
+ basic__destroy(skel);
+ return 0;
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v5 4/5] io_uring/bpf-ops: implement bpf ops registration
2026-02-11 14:32 ` [PATCH v5 4/5] io_uring/bpf-ops: implement bpf ops registration Pavel Begunkov
@ 2026-02-11 15:21 ` Jens Axboe
2026-02-11 15:40 ` Pavel Begunkov
0 siblings, 1 reply; 11+ messages in thread
From: Jens Axboe @ 2026-02-11 15:21 UTC (permalink / raw)
To: Pavel Begunkov, io-uring; +Cc: bpf, Alexei Starovoitov
On 2/11/26 7:32 AM, Pavel Begunkov wrote:
> +static void io_eject_bpf(struct io_ring_ctx *ctx)
> +{
> + struct io_uring_bpf_ops *ops = ctx->bpf_ops;
> +
> + if (!WARN_ON_ONCE(!ops))
> + return;
if (WARN_ON_ONCE(!ops))
return;
?
--
Jens Axboe
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v5 5/5] selftests/io_uring: add a bpf io_uring selftest
2026-02-11 14:32 ` [PATCH v5 5/5] selftests/io_uring: add a bpf io_uring selftest Pavel Begunkov
@ 2026-02-11 15:22 ` Jens Axboe
0 siblings, 0 replies; 11+ messages in thread
From: Jens Axboe @ 2026-02-11 15:22 UTC (permalink / raw)
To: Pavel Begunkov, io-uring; +Cc: bpf, Alexei Starovoitov
On 2/11/26 7:32 AM, Pavel Begunkov wrote:
> + if (inflight < max_inflight) {
> + unsigned to_submit = max_inflight - inflight;
> +
> + to_submit = t_min(to_submit, reqs_to_run);
> +
> + for (int i = 0; i < to_submit; i++) {
> + struct io_uring_sqe *sqe = sqes + i;
> +
> + sqe = &sqes[sq_hdr->tail & (SQ_ENTRIES - 1)];
Nit: assign sqe and immediately reassign.
--
Jens Axboe
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring
2026-02-11 14:32 [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Pavel Begunkov
` (4 preceding siblings ...)
2026-02-11 14:32 ` [PATCH v5 5/5] selftests/io_uring: add a bpf io_uring selftest Pavel Begunkov
@ 2026-02-11 15:24 ` Jens Axboe
2026-02-11 18:58 ` Pavel Begunkov
5 siblings, 1 reply; 11+ messages in thread
From: Jens Axboe @ 2026-02-11 15:24 UTC (permalink / raw)
To: Pavel Begunkov, io-uring; +Cc: bpf, Alexei Starovoitov
On 2/11/26 7:32 AM, Pavel Begunkov wrote:
> This series introduces a way to override the standard io_uring_enter
> syscall execution with an extendible event loop, which can be controlled
> by BPF via new io_uring struct_ops or from within the kernel.
>
> There are multiple use cases I want to cover with this:
>
> - Syscall avoidance. Instead of returning to the userspace for
> CQE processing, a part of the logic can be moved into BPF to
> avoid excessive number of syscalls.
>
> - Access to in-kernel io_uring resources. For example, there are
> registered buffers that can't be directly accessed by the userspace,
> however we can give BPF the ability to peek at them. It can be used
> to take a look at in-buffer app level headers to decide what to do
> with data next and issuing IO using it.
>
> - Smarter request ordering and linking. Request links are pretty
> limited and inflexible as they can't pass information from one
> request to another. With BPF we can peek at CQEs and memory and
> compile a subsequent request.
>
> - Feature semi-deprecation. It can be used to simplify handling
> of deprecated features by moving it into the callback out core
> io_uring. For example, it should be trivial to simulate
> IOSQE_IO_DRAIN. Another target could be request linking logic.
>
> - It can serve as a base for custom algorithms and fine tuning.
> Often, it'd be impractical to introduce a generic feature because
> it's either niche or requires a lot of configuration. For example,
> there is support min-wait, however BPF can help to further fine tune
> it by doing it in multiple steps with different number of CQEs /
> timeouts. Another feature people were asking about is allowing
> to over queue SQEs but make the kernel to maintain a given QD.
>
> - Smarter polling. Napi polling is performed only once per syscall
> and then it switches to waiting. We can do smarter and intermix
> polling with waiting using the hook.
>
> It might need more specialised kfuncs in the future, but the core
> functionality is implemented with just two simple functions. One
> returns region memory, which gives BPF access to CQ/SQ/etc. And
> the second is for submitting requests. It's also given a structure
> as an argument, which is used to pass waiting parameters.
>
> It showed good numbers in a test that sequentially executes N nop
> requests, where BPF was more than twice as fast than a 2-nop
> request link implementation.
>
> I've got ideas on how the user space part while writing toy programs,
> mostly about simplifying life to BPF writers, but I want to turn it
> into something more cohesive before posting.
This looks nifty. Do you have a repo on the liburing side with some
examples to play with?
Nit on some of the new files added, not all of them have SPDX headers.
--
Jens Axboe
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v5 4/5] io_uring/bpf-ops: implement bpf ops registration
2026-02-11 15:21 ` Jens Axboe
@ 2026-02-11 15:40 ` Pavel Begunkov
0 siblings, 0 replies; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 15:40 UTC (permalink / raw)
To: Jens Axboe, io-uring; +Cc: bpf, Alexei Starovoitov
On 2/11/26 15:21, Jens Axboe wrote:
> On 2/11/26 7:32 AM, Pavel Begunkov wrote:
>> +static void io_eject_bpf(struct io_ring_ctx *ctx)
>> +{
>> + struct io_uring_bpf_ops *ops = ctx->bpf_ops;
>> +
>> + if (!WARN_ON_ONCE(!ops))
>> + return;
>
> if (WARN_ON_ONCE(!ops))
> return;
>
> ?
Good catch! I even lightly tested it before, but seems like
there is a delay before the program is actually destroyed
and you could still call the function without crashing.
I'll add it to selftests.
--
Pavel Begunkov
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring
2026-02-11 15:24 ` [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Jens Axboe
@ 2026-02-11 18:58 ` Pavel Begunkov
0 siblings, 0 replies; 11+ messages in thread
From: Pavel Begunkov @ 2026-02-11 18:58 UTC (permalink / raw)
To: Jens Axboe, io-uring; +Cc: bpf, Alexei Starovoitov
On 2/11/26 15:24, Jens Axboe wrote:
> On 2/11/26 7:32 AM, Pavel Begunkov wrote:
...
>> It might need more specialised kfuncs in the future, but the core
>> functionality is implemented with just two simple functions. One
>> returns region memory, which gives BPF access to CQ/SQ/etc. And
>> the second is for submitting requests. It's also given a structure
>> as an argument, which is used to pass waiting parameters.
>>
>> It showed good numbers in a test that sequentially executes N nop
>> requests, where BPF was more than twice as fast than a 2-nop
>> request link implementation.
>>
>> I've got ideas on how the user space part while writing toy programs,
>> mostly about simplifying life to BPF writers, but I want to turn it
>> into something more cohesive before posting.
>
> This looks nifty. Do you have a repo on the liburing side with some
> examples to play with?
Nope, it's all in a pretty dirty state yet. The selftest is pretty
good in that regard, it show cases a fixed QD workload. It's easy
to convert it to reads, and that works well, but I guess I need to
add something to show how to use the memory region to pass
parameters, e.g. iovs to readv reqs.
--
Pavel Begunkov
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-02-11 18:58 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-11 14:32 [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 1/5] io_uring: introduce callback driven main loop Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 2/5] io_uring/bpf-ops: implement loop_step with BPF struct_ops Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 3/5] io_uring/bpf-ops: add kfunc helpers Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 4/5] io_uring/bpf-ops: implement bpf ops registration Pavel Begunkov
2026-02-11 15:21 ` Jens Axboe
2026-02-11 15:40 ` Pavel Begunkov
2026-02-11 14:32 ` [PATCH v5 5/5] selftests/io_uring: add a bpf io_uring selftest Pavel Begunkov
2026-02-11 15:22 ` Jens Axboe
2026-02-11 15:24 ` [PATCH io_uring-7.1 v5 0/5] BPF controlled io_uring Jens Axboe
2026-02-11 18:58 ` Pavel Begunkov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox