BPF controlled io_uring
v2 cover BPF controlled io_uring
This series adds io_uring BPF struct_ops, which allows processing events and submitting requests from BPF without returning to user. There is only one callback for now, it's called from the io_uring CQ waiting loop when there is an event to be processed. It also has access to waiting parameters like batching and timeouts.
It's tested with a program that queues a nop request, waits for its completion and then queues another request, repeating it N times. The baseline to compare with is traditional io_uring application doing same without BPF and using 2 requests links, with the same total number of requests.
# ./link 0 100000000 type 2-LINK, requests to run 100000000 sec 20, total (ms) 20374 # ./link 1 100000000 type BPF, requests to run 100000000 sec 13, total (ms) 13700
The BPF version works ~50% faster on a mitigated kernel, while it's not even a completely fair comparison as links are restrictive and can't always be used. Without links the speedup reaches ~80%.
This allows arbitrary relations between requests including using a result from one request to configure the following one. There are other use cases in mind that need access to in-kernel resources and can't be implemented from userspace. On top, it can be extended with more callbacks to get finer control over task work batching.
It's a prototype, I intend to remake the kfunc helpers, enchance program verification, and fix some mild io_uring waiting edge cases.
Kernel branch:
https://github.com/isilence/linux/tree/io-uring-bpf/v2
git https://github.com/isilence/linux.git io-uring-bpf/v2
Liburing + bpf bootsrap examples:
https://github.com/isilence/liburing/tree/bpf-struct-ops-examples
git [email protected]:isilence/liburing.git bpf-struct-ops-examples
Pavel Begunkov (5):
io_uring: add struct for state controlling cqwait io_uring/bpf: add stubs for bpf struct_ops io_uring/bpf: implement struct_ops registration io_uring/bpf: add handle events callback io_uring/bpf: add basic kfunc helpers
include/linux/io_uring_types.h | 4 + io_uring/Kconfig | 5 + io_uring/Makefile | 1 + io_uring/bpf.c | 277 +++++++++++++++++++++++++++++++++ io_uring/bpf.h | 45 ++++++ io_uring/io_uring.c | 45 ++++-- io_uring/io_uring.h | 11 +- io_uring/napi.c | 4 +- 8 files changed, 376 insertions(+), 16 deletions(-) create mode 100644 io_uring/bpf.c create mode 100644 io_uring/bpf.h
Signature · 1 line
2.49.0
RFC v2 1/5 io_uring: add struct for state controlling cqwait
Add struct iou_loop_state and place there parameter controlling the flow of normal CQ waiting. It will be exposed to BPF for api of the helpers, and while I could've used struct io_wait_queue, the name is not ideal, and keeping only necessary bits makes further development a bit cleaner.
--- io_uring/io_uring.c | 20 ++++++++++---------- io_uring/io_uring.h | 11 ++++++++--- io_uring/napi.c | 4 ++-- 3 files changed, 20 insertions(+), 15 deletions(-)
io_uring/io_uring.c +10 -10
@@ -2404,8 +2404,8 @@ static enum hrtimer_restart io_cqring_min_timer_wakeup(struct hrtimer *timer)struct io_ring_ctx *ctx = iowq->ctx;/* no general timeout, or shorter (or equal), we are done */ -if (iowq->timeout ==KTIME_MAX || -ktime_compare (iowq->min_timeout, iowq->timeout) >=0 ) +if (iowq->state.timeout ==KTIME_MAX || +ktime_compare (iowq->min_timeout, iowq->state.timeout) >=0 ) goto out_wake;/* work we may need to run, wake function will see if we need to wake */ if (io_has_work (ctx))
@@ -2431,7 +2431,7 @@ static enum hrtimer_restart io_cqring_min_timer_wakeup(struct hrtimer *timer) }hrtimer_update_function (&iowq->t, io_cqring_timer_wakeup); -hrtimer_set_expires (timer, iowq->timeout); +hrtimer_set_expires (timer, iowq->state.timeout);return HRTIMER_RESTART ; out_wake:return io_cqring_timer_wakeup (timer);
@@ -2447,7 +2447,7 @@ static int io_cqring_schedule_timeout(struct io_wait_queue *iowq,hrtimer_setup_on_stack (&iowq->t, io_cqring_min_timer_wakeup, clock_id,HRTIMER_MODE_ABS ); }else { - timeout = iowq->timeout; + timeout = iowq->state.timeout;hrtimer_setup_on_stack (&iowq->t, io_cqring_timer_wakeup, clock_id,HRTIMER_MODE_ABS ); }
@@ -2488,7 +2488,7 @@ static int __io_cqring_wait_schedule(struct io_ring_ctx *ctx,*/ if (ext_arg->iowait &¤t_pending_io ()) current->in_iowait =1 ; -if (iowq->timeout !=KTIME_MAX || iowq->min_timeout) +if (iowq->state.timeout !=KTIME_MAX || iowq->min_timeout) ret =io_cqring_schedule_timeout (iowq, ctx->clockid, start_time);else schedule ();
@@ -2546,18 +2546,18 @@ static int io_cqring_wait(struct io_ring_ctx *ctx, int min_events, u32 flags, iowq.wq.private = current;INIT_LIST_HEAD (&iowq.wq.entry); iowq.ctx = ctx; - iowq.cq_tail =READ_ONCE (ctx->rings->cq.head) + min_events; + iowq.state.target_cq_tail =READ_ONCE (ctx->rings->cq.head) + min_events; iowq.cq_min_tail =READ_ONCE (ctx->rings->cq.tail); iowq.nr_timeouts =atomic_read (&ctx->cq_timeouts); iowq.hit_timeout =0 ; iowq.min_timeout = ext_arg->min_time; - iowq.timeout =KTIME_MAX ; + iowq.state.timeout =KTIME_MAX ; start_time =io_get_time (ctx);if (ext_arg->ts_set) { - iowq.timeout =timespec64_to_ktime (ext_arg->ts); + iowq.state.timeout =timespec64_to_ktime (ext_arg->ts);if (!(flags &IORING_ENTER_ABS_TIMER )) - iowq.timeout =ktime_add (iowq.timeout, start_time); + iowq.state.timeout =ktime_add (iowq.state.timeout, start_time); }if (ext_arg->sig) {
@@ -2582,7 +2582,7 @@ static int io_cqring_wait(struct io_ring_ctx *ctx, int min_events, u32 flags,/* if min timeout has been hit, don't reset wait count */ if (!iowq.hit_timeout) - nr_wait = (int ) iowq.cq_tail - + nr_wait = (int ) iowq.state.target_cq_tail -READ_ONCE (ctx->rings->cq.tail);else nr_wait =1 ;
io_uring/io_uring.h +8 -3
@@ -39,15 +39,19 @@ enum { IOU_REQUEUE = -3072, }; +struct iou_loop_state { +__u32 target_cq_tail; +ktime_t timeout; +}; +struct io_wait_queue { +struct iou_loop_state state;struct wait_queue_entry wq;struct io_ring_ctx *ctx; -unsigned cq_tail;unsigned cq_min_tail;unsigned nr_timeouts;int hit_timeout;ktime_t min_timeout; -ktime_t timeout;struct hrtimer t;#ifdef CONFIG_NET_RX_BUSY_POLL
@@ -59,7 +63,8 @@ struct io_wait_queue {static inline bool io_should_wake (struct io_wait_queue *iowq) {struct io_ring_ctx *ctx = iowq->ctx; -int dist =READ_ONCE (ctx->rings->cq.tail) - (int ) iowq->cq_tail; +u32 target = iowq->state.target_cq_tail; +int dist =READ_ONCE (ctx->rings->cq.tail) - target;/* * Wake up if we have enough events, or if a timeout occurred since we
io_uring/napi.c +2 -2
@@ -360,8 +360,8 @@ void __io_napi_busy_loop(struct io_ring_ctx *ctx, struct io_wait_queue *iowq)return ; iowq->napi_busy_poll_dt =READ_ONCE (ctx->napi_busy_poll_dt); -if (iowq->timeout !=KTIME_MAX ) { -ktime_t dt =ktime_sub (iowq->timeout,io_get_time (ctx)); +if (iowq->state.timeout !=KTIME_MAX ) { +ktime_t dt =ktime_sub (iowq->state.timeout,io_get_time (ctx)); iowq->napi_busy_poll_dt =min_t (u64, iowq->napi_busy_poll_dt, dt); }
Signature · 1 line
2.49.0
RFC v2 2/5 io_uring/bpf: add stubs for bpf struct_ops
In reply to Pavel BegunkovAdd some basic helpers and definitions for implementing bpf struct_ops. There are no callbaack yet, and registration will always fail.
--- include/linux/io_uring_types.h | 4 ++ io_uring/Kconfig | 5 ++ io_uring/Makefile | 1 + io_uring/bpf.c | 93 ++++++++++++++++++++++++++++++++++ io_uring/bpf.h | 26 ++++++++++ io_uring/io_uring.c | 3 ++ 6 files changed, 132 insertions(+) create mode 100644 io_uring/bpf.c create mode 100644 io_uring/bpf.h
include/linux/io_uring_types.h +4 -0
@@ -8,6 +8,8 @@#include <linux/llist.h> #include <uapi/linux/io_uring.h> +struct io_uring_ops ; +enum {/* * A hint to not wake right away but delay until there are enough of
@@ -344,6 +346,8 @@ struct io_ring_ctx {void *cq_wait_arg;size_t cq_wait_size; + +struct io_uring_ops *bpf_ops; }____cacheline_aligned_in_smp ;/*
io_uring/Kconfig +5 -0
@@ -9,3 +9,8 @@ config IO_URING_ZCRX depends on PAGE_POOL depends on INET depends on NET_RX_BUSY_POLL + +config IO_URING_BPF + def_bool y + depends on IO_URING + depends on BPF_SYSCALL && BPF_JIT && DEBUG_INFO_BTF
io_uring/Makefile +1 -0
@@ -21,3 +21,4 @@ obj-$(CONFIG_EPOLL) += epoll.o obj-$(CONFIG_NET_RX_BUSY_POLL) += napi.o obj-$(CONFIG_NET) += net.o cmd_net.o obj-$(CONFIG_PROC_FS) += fdinfo.o +obj-$(CONFIG_IO_URING_BPF) += bpf.o
io_uring/bpf.c new file +93 -0
93 lines not on this page · raw message
io_uring/bpf.h new file +26 -0
@@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef IOU_BPF_H +#define IOU_BPF_H + +#include <linux/io_uring_types.h> +#include <linux/bpf.h> + +#include "io_uring.h" + +struct io_uring_ops { +}; + +static inline bool io_bpf_attached (struct io_ring_ctx *ctx) +{ +return IS_ENABLED (CONFIG_BPF ) && ctx->bpf_ops !=NULL ; +} + +#ifdef CONFIG_BPF +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
io_uring/io_uring.c +3 -0
@@ -98,6 +98,7 @@#include "msg_ring.h" #include "memmap.h" #include "zcrx.h" +#include "bpf.h" #include "timeout.h" #include "poll.h"
@@ -2870,6 +2871,8 @@ static __cold void io_ring_exit_work(struct work_struct *work)struct io_tctx_node *node;int ret; +io_unregister_bpf_ops (ctx); +/* * If we're doing polled IO and end up having requests being * submitted async (out-of-line), then completions can come in while
Signature · 1 line
2.49.0
Jens Axboe
maintainerRe: [RFC v2 2/5] io_uring/bpf: add stubs for bpf struct_ops
Quoted 20 lines · Pavel Begunkov
io_uring/bpf.h new file +14
-0@@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef IOU_BPF_H +#define IOU_BPF_H + +#include <linux/io_uring_types.h> +#include <linux/bpf.h> + +#include "io_uring.h" + +struct io_uring_ops { +}; + +static inline bool io_bpf_attached (struct io_ring_ctx *ctx) +{
io_uring/bpf.h line 24
Quoted 10 lines
io_uring/bpf.h new file +10
-0+return IS_ENABLED (CONFIG_BPF ) && ctx->bpf_ops !=NULL ; +} + +#ifdef CONFIG_BPF +void io_unregister_bpf_ops (struct io_ring_ctx *ctx); +#else +static inline void io_unregister_bpf_ops (struct io_ring_ctx *ctx) +{ +} +#endif
Should be
#ifdef IO_URING_BPF
here.
Signature · 1 line
Jens Axboe
Jens Axboe
maintainerRe: [RFC v2 2/5] io_uring/bpf: add stubs for bpf struct_ops
Quoted 34 lines · Jens Axboe
Quoted 20 lines · Pavel Begunkov
io_uring/bpf.h new file +14
-0@@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0 +#ifndef IOU_BPF_H +#define IOU_BPF_H + +#include <linux/io_uring_types.h> +#include <linux/bpf.h> + +#include "io_uring.h" + +struct io_uring_ops { +}; + +static inline bool io_bpf_attached (struct io_ring_ctx *ctx) +{io_uring/bpf.h line 24
Quoted 10 lines
io_uring/bpf.h new file +10
-0+return IS_ENABLED (CONFIG_BPF ) && ctx->bpf_ops !=NULL ; +} + +#ifdef CONFIG_BPF +void io_unregister_bpf_ops (struct io_ring_ctx *ctx); +#else +static inline void io_unregister_bpf_ops (struct io_ring_ctx *ctx) +{ +} +#endif Should be
Quoted 3 lines
#ifdef
IO_URING_BPFhere.
CONFIG_IO_URING_BPF of course...
Signature · 1 line
Jens Axboe
RFC v2 3/5 io_uring/bpf: implement struct_ops registration
In reply to Pavel BegunkovAdd ring_fd to the struct_ops and implement [un]registration.
--- io_uring/bpf.c | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- io_uring/bpf.h | 3 +++ 2 files changed, 69 insertions(+), 1 deletion(-)
io_uring/bpf.c +66 -1
@@ -3,6 +3,8 @@#include "bpf.h" #include "register.h" +DEFINE_MUTEX (io_bpf_ctrl_mutex); +static struct io_uring_ops io_bpf_ops_stubs = { };
@@ -50,20 +52,83 @@ 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_ops *uops = udata; +struct io_uring_ops *ops = kdata; + +switch (moff) { +case offsetof(struct io_uring_ops , ring_fd): + ops->ring_fd = uops->ring_fd; +return 1 ; + } +return 0 ; +} + +static int io_register_bpf_ops (struct io_ring_ctx *ctx,struct io_uring_ops *ops) +{ +if (ctx->bpf_ops) +return -EBUSY ; +if (!(ctx->flags &IORING_SETUP_DEFER_TASKRUN )) +return -EOPNOTSUPP ; + +percpu_ref_get (&ctx->refs); + ops->ctx = ctx; + ctx->bpf_ops = ops;return 0 ; }static int bpf_io_reg (void *kdata,struct bpf_link *link) { -return -EOPNOTSUPP ; +struct io_uring_ops *ops = kdata; +struct io_ring_ctx *ctx; +struct file *file; +int ret; + + 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, &ctx->uring_lock) + ret = io_register_bpf_ops(ctx, ops); + +fput (file); +return ret; }static void bpf_io_unreg (void *kdata,struct bpf_link *link) { +struct io_uring_ops *ops = kdata; +struct io_ring_ctx *ctx; + +guard (mutex)(&io_bpf_ctrl_mutex); + + ctx = ops->ctx; + ops->ctx =NULL ; + +if (ctx) { + scoped_guard(mutex, &ctx->uring_lock) { +if (ctx->bpf_ops == ops) + ctx->bpf_ops =NULL ; + } +percpu_ref_put (&ctx->refs); + } }void io_unregister_bpf_ops (struct io_ring_ctx *ctx) { +struct io_uring_ops *ops; + +guard (mutex)(&io_bpf_ctrl_mutex); +guard (mutex)(&ctx->uring_lock); + + ops = ctx->bpf_ops; + ctx->bpf_ops =NULL ; + +if (ops && ops->ctx) { +percpu_ref_put (&ctx->refs); + ops->ctx =NULL ; + } } static struct bpf_struct_ops bpf_io_uring_ops = {
io_uring/bpf.h +3 -0
@@ -8,6 +8,9 @@#include "io_uring.h" struct io_uring_ops { +__u32 ring_fd; + +struct io_ring_ctx *ctx; };static inline bool io_bpf_attached (struct io_ring_ctx *ctx)
Signature · 1 line
2.49.0
Jens Axboe
maintainerRe: [RFC v2 3/5] io_uring/bpf: implement struct_ops registration
Quoted 25 lines · Pavel Begunkov
Add
ring_fdto thestruct_opsand implement [un]registration.Signed-off-by Pavel Begunkov <[email protected]>--- io_uring/bpf.c | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- io_uring/bpf.h | 3 +++ 2 files changed, 69 insertions(+), 1 deletion(-)io_uring/bpf.c +2
-0@@ -3,6 +3,8 @@#include "bpf.h" #include "register.h" +DEFINE_MUTEX (io_bpf_ctrl_mutex); +static struct io_uring_ops io_bpf_ops_stubs = { };@@ -50,20 +52,83 @@ static int bpf_io_init_member(const struct btf_type *t,const struct btf_member *member, void *kdata, const void *udata) {
io_uring/bpf.c static int bpf_io_init_member(const struct btf_type *t, · line 64
Quoted 10 lines
io_uring/bpf.c +10
-0+u32 moff =__btf_member_bit_offset (t, member) /8 ; +const struct io_uring_ops *uops = udata; +struct io_uring_ops *ops = kdata; + +switch (moff) { +case offsetof(struct io_uring_ops , ring_fd): + ops->ring_fd = uops->ring_fd; +return 1 ; + } +return 0 ;
Possible to pass in here whether the ring fd is registered or not? Such that it can be used in bpf_io_reg() as well.
io_uring/bpf.c static int bpf_io_init_member(const struct btf_type *t, · line 78
Quoted 12 lines
io_uring/bpf.c +10
-0+static int io_register_bpf_ops (struct io_ring_ctx *ctx,struct io_uring_ops *ops) +{ +if (ctx->bpf_ops) +return -EBUSY ; +if (!(ctx->flags &IORING_SETUP_DEFER_TASKRUN )) +return -EOPNOTSUPP ; + +percpu_ref_get (&ctx->refs); + ops->ctx = ctx; + ctx->bpf_ops = ops;return 0 ; }
Haven't looked too deeply yet, but what's the dependency with DEFER_TASKRUN?
Signature · 1 line
Jens Axboe
Re: [RFC v2 3/5] io_uring/bpf: implement struct_ops registration
On 6/6/25 15:57, Jens Axboe wrote:
...>> @@ -50,20 +52,83 @@ static int bpf_io_init_member(const struct btf_type *t,
Quoted 8 lines
Quoted 8 lines
const struct
btf_member*member, void *kdata, const void *udata)
{io_uring/bpf.c +5
-0+u32 moff =__btf_member_bit_offset (t, member) /8 ; +const struct io_uring_ops *uops = udata; +struct io_uring_ops *ops = kdata; + +switch (moff) {
Quoted 8 lines
io_uring/bpf.c static int bpf_io_init_member(const struct btf_type *t, · line 64
Quoted 5 lines
io_uring/bpf.c +5
-0+case offsetof(struct io_uring_ops , ring_fd): + ops->ring_fd = uops->ring_fd; +return 1 ; + } +return 0 ;Possible to pass in here whether the ring fd is registered or not? Such that it can be used in
bpf_io_reg()as well.
That requires registration to be done off the syscall path (e.g. no workers), which is low risk and I'm pretty sure that's how it's done, but in either case that's not up to io_uring and should be vetted by bpf. It's not important to performance, and leaking that to other syscalls is a bad idea as well, so in the meantime it's just left unsupported.
Quoted 7 lines
Quoted 7 lines
io_uring/bpf.c +7
-0+static int io_register_bpf_ops (struct io_ring_ctx *ctx,struct io_uring_ops *ops) +{ +if (ctx->bpf_ops) +return -EBUSY ; +if (!(ctx->flags &IORING_SETUP_DEFER_TASKRUN )) +return -EOPNOTSUPP ; +
Quoted 8 lines
io_uring/bpf.c static int bpf_io_init_member(const struct btf_type *t, · line 78
Quoted 5 lines
io_uring/bpf.c +3
-0+percpu_ref_get (&ctx->refs); + ops->ctx = ctx; + ctx->bpf_ops = ops;return 0 ; }Haven't looked too deeply yet, but what's the dependency with
DEFER_TASKRUN?
Unregistration needs to be sync'ed with waiters, and that can easily become a problem. Taking the lock like in this set in not necessarily the right solution. I plan to wait and see where it goes rather than shooting myself in the leg right away.
Signature · 1 line
Pavel Begunkov
Jens Axboe
maintainerRe: [RFC v2 3/5] io_uring/bpf: implement struct_ops registration
Quoted 19 lines · Pavel Begunkov
On 6/6/25 15:57, Jens Axboe wrote:
...>> @@ -50,20 +52,83 @@ static int bpf_io_init_member(const structbtf_type*t,Quoted 14 lines
io_uring/bpf.c static int bpf_io_init_member(const struct btf_type *t, · line 64
Quoted 13 lines
const struct
btf_member*member, void *kdata, const void *udata)
{io_uring/bpf.c +10
-0+u32 moff =__btf_member_bit_offset (t, member) /8 ; +const struct io_uring_ops *uops = udata; +struct io_uring_ops *ops = kdata; + +switch (moff) { +case offsetof(struct io_uring_ops , ring_fd): + ops->ring_fd = uops->ring_fd; +return 1 ; + } +return 0 ;Quoted 2 lines
Possible to pass in here whether the ring fd is registered or not? Such that it can be used in
bpf_io_reg()as well.
Quoted 6 lines
That requires registration to be done off the syscall path (e.g. no workers), which is low risk and I'm pretty sure that's how it's done, but in either case that's not up to
io_uringand should be vetted by bpf. It's not important to performance, and leaking that to other syscalls is a bad idea as well, so in the meantime it's just left unsupported.
Don't care about the performance as much as it being a weird crinkle. Obviously not a huge deal, and can always get sorted out down the line.
Quoted 15 lines
Quoted 7 lines
Quoted 7 lines
io_uring/bpf.c +7
-0+static int io_register_bpf_ops (struct io_ring_ctx *ctx,struct io_uring_ops *ops) +{ +if (ctx->bpf_ops) +return -EBUSY ; +if (!(ctx->flags &IORING_SETUP_DEFER_TASKRUN )) +return -EOPNOTSUPP ; +Quoted 8 lines
io_uring/bpf.c static int bpf_io_init_member(const struct btf_type *t, · line 78
Quoted 5 lines
io_uring/bpf.c +3
-0+percpu_ref_get (&ctx->refs); + ops->ctx = ctx; + ctx->bpf_ops = ops;return 0 ; }Haven't looked too deeply yet, but what's the dependency with
DEFER_TASKRUN?
Quoted 4 lines
Unregistration needs to be sync'ed with waiters, and that can easily become a problem. Taking the lock like in this set in not necessarily the right solution. I plan to wait and see where it goes rather than shooting myself in the leg right away.
That's fine, would be nice with a comment or something in the commit message to that effect at least for the time being.
Signature · 1 line
Jens Axboe
RFC v2 4/5 io_uring/bpf: add handle events callback
In reply to Pavel BegunkovAdd a struct_ops callback called handle_events, which will be called off the CQ waiting loop every time there is an event that might be interesting to the program. The program takes the io_uring ctx and also a loop state, which it can use to set the number of events it wants to wait for as well as the timeout value.
--- io_uring/bpf.c | 33 +++++++++++++++++++++++++++++++++ io_uring/bpf.h | 16 ++++++++++++++++ io_uring/io_uring.c | 22 +++++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-)
io_uring/bpf.c +33 -0
@@ -1,11 +1,20 @@#include <linux/mutex.h> +#include <linux/bpf_verifier.h> #include "bpf.h" #include "register.h" +static const struct btf_type *loop_state_type;DEFINE_MUTEX (io_bpf_ctrl_mutex); +static int io_bpf_ops__handle_events (struct io_ring_ctx *ctx, +struct iou_loop_state *state) +{ +return IOU_EVENTS_STOP ; +} +static struct io_uring_ops io_bpf_ops_stubs = { + .handle_events = io_bpf_ops__handle_events, }; static bool bpf_io_is_valid_access(int off, int size,
@@ -27,6 +36,16 @@ 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_state_type) { +if (off >= offsetof(struct iou_loop_state , target_cq_tail) && + off + size <= offsetofend(struct iou_loop_state, target_cq_tail)) +return SCALAR_VALUE ; +if (off >= offsetof(struct iou_loop_state , timeout) && + off + size <= offsetofend(struct iou_loop_state, timeout)) +return SCALAR_VALUE ; + }return -EACCES ; }
@@ -36,8 +55,22 @@ static const struct bpf_verifier_ops bpf_io_verifier_ops = { .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_state_type =io_lookup_struct_type (btf,"iou_loop_state" ); +if (!loop_state_type) +return -EINVAL ;return 0 ; }
io_uring/bpf.h +16 -0
@@ -7,12 +7,28 @@#include "io_uring.h" +enum { +IOU_EVENTS_WAIT , +IOU_EVENTS_STOP , +}; +struct io_uring_ops {__u32 ring_fd; +int (*handle_events)(struct io_ring_ctx *ctx,struct iou_loop_state *state); +struct io_ring_ctx *ctx; }; +static inline int io_run_bpf (struct io_ring_ctx *ctx,struct iou_loop_state *state) +{ + scoped_guard(mutex, &ctx->uring_lock) { +if (!ctx->bpf_ops) +return IOU_EVENTS_STOP ; +return ctx->bpf_ops->handle_events (ctx, state); + } +} +static inline bool io_bpf_attached (struct io_ring_ctx *ctx) {return IS_ENABLED (CONFIG_BPF ) && ctx->bpf_ops !=NULL ;
io_uring/io_uring.c +21 -1
@@ -2540,8 +2540,13 @@ static int io_cqring_wait(struct io_ring_ctx *ctx, int min_events, u32 flags,if (unlikely (test_bit (IO_CHECK_CQ_OVERFLOW_BIT , &ctx->check_cq)))io_cqring_do_overflow_flush (ctx); -if (__io_cqring_events_user (ctx) >= min_events) + +if (io_bpf_attached (ctx)) { +if (ext_arg->min_time) +return -EINVAL ; + }else if (__io_cqring_events_user (ctx) >= min_events) {return 0 ; + }init_waitqueue_func_entry (&iowq.wq, io_wake_function); iowq.wq.private = current;
@@ -2621,6 +2626,21 @@ static int io_cqring_wait(struct io_ring_ctx *ctx, int min_events, u32 flags,if (ret <0 )break ; +if (io_bpf_attached (ctx)) { + ret =io_run_bpf (ctx, &iowq.state); +if (ret !=IOU_EVENTS_WAIT ) +break ; + +if (unlikely (read_thread_flags ())) { +if (task_sigpending (current)) { + ret = -EINTR ; +break ; + } +cond_resched (); + } +continue ; + } + check_cq =READ_ONCE (ctx->check_cq);if (unlikely (check_cq)) {/* let the caller flush overflows, retry */
Signature · 1 line
2.49.0
Re: [RFC v2 4/5] io_uring/bpf: add handle events callback
io_uring/bpf.h line 30
Quoted 9 lines · Pavel Begunkov
io_uring/bpf.h +8
-0+static inline int io_run_bpf (struct io_ring_ctx *ctx,struct iou_loop_state *state) +{ + scoped_guard(mutex, &ctx->uring_lock) { +if (!ctx->bpf_ops) +return IOU_EVENTS_STOP ; +return ctx->bpf_ops->handle_events (ctx, state); + } +}
you're grabbing the mutex before calling bpf prog and doing it in a loop million times a second?
Looks like massive overhead for program invocation. I'm surprised it's fast.
Re: [RFC v2 4/5] io_uring/bpf: add handle events callback
Quoted 15 lines · Alexei Starovoitov
io_uring/bpf.h line 30
Quoted 9 lines · Pavel Begunkov
io_uring/bpf.h +8
-0+static inline int io_run_bpf (struct io_ring_ctx *ctx,struct iou_loop_state *state) +{ + scoped_guard(mutex, &ctx->uring_lock) { +if (!ctx->bpf_ops) +return IOU_EVENTS_STOP ; +return ctx->bpf_ops->handle_events (ctx, state); + } +}you're grabbing the mutex before calling bpf prog and doing it in a loop million times a second?
Looks like massive overhead for program invocation. I'm surprised it's fast.
You need the lock to submit anything with io_uring, so there is a parity with how it already is. And the program is just a test and pretty silly in nature, normally you'd either get higher batching, and the user (incl bpf) can specifically specify to wait for more, or it'll be intermingled with sleeping at which point the mutex is not a problem. I'll write a storage IO example for the next time.
If there will be a good use case, I can try to relax it for programs that don't issue requests, but that might make sync more complicated, especially on the reg/unreg side.
Signature · 1 line
Pavel Begunkov
Jens Axboe
maintainerRe: [RFC v2 4/5] io_uring/bpf: add handle events callback
In reply to Alexei StarovoitovQuoted 15 lines · Alexei Starovoitov
io_uring/bpf.h line 30
Quoted 9 lines · On Fri, Jun 6, 2025 at 6:58?AM Pavel Begunkov <[email protected]> wrote:
io_uring/bpf.h +8
-0+static inline int io_run_bpf (struct io_ring_ctx *ctx,struct iou_loop_state *state) +{ + scoped_guard(mutex, &ctx->uring_lock) { +if (!ctx->bpf_ops) +return IOU_EVENTS_STOP ; +return ctx->bpf_ops->handle_events (ctx, state); + } +}you're grabbing the mutex before calling bpf prog and doing it in a loop million times a second?
Looks like massive overhead for program invocation. I'm surprised it's fast.
Grabbing a mutex is only expensive if it's contended, or obviously if it's already held. Repeatedly grabbing it on submission where submission is the only one expected to grab it (or off that path, at least) means it should be very cheap.
Signature · 1 line
Jens Axboe
RFC v2 5/5 io_uring/bpf: add basic kfunc helpers
In reply to Pavel BegunkovA handle_events program should be able to parse the CQ and submit new requests, add kfuncs to cover that. The only essential kfunc here is bpf_io_uring_submit_sqes, and the rest are likely be removed in a non-RFC version in favour of a more general approach.
--- io_uring/bpf.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+)
io_uring/bpf.c +86 -0
@@ -1,12 +1,92 @@#include <linux/mutex.h> #include <linux/bpf_verifier.h> +#include "io_uring.h" #include "bpf.h" #include "register.h" static const struct btf_type *loop_state_type;DEFINE_MUTEX (io_bpf_ctrl_mutex); +__bpf_kfunc_start_defs (); + +__bpf_kfunc int bpf_io_uring_submit_sqes(struct io_ring_ctx *ctx, +unsigned nr) +{ +return io_submit_sqes (ctx, nr); +} + +__bpf_kfunc int bpf_io_uring_post_cqe(struct io_ring_ctx *ctx, +u64 data,u32 res,u32 cflags) +{ +bool posted; + + posted =io_post_aux_cqe (ctx, data, res, cflags); +return posted ?0 : -ENOMEM ; +} + +__bpf_kfunc int bpf_io_uring_queue_sqe(struct io_ring_ctx *ctx, +void *bpf_sqe,int mem__sz) +{ +unsigned tail = ctx->rings->sq.tail; +struct io_uring_sqe *sqe; + +if (mem__sz !=sizeof (*sqe)) +return -EINVAL ; + + ctx->rings->sq.tail++; + tail &= (ctx->sq_entries -1 ); +/* double index for 128-byte SQEs, twice as long */ +if (ctx->flags &IORING_SETUP_SQE128 ) + tail <<=1 ; + sqe = &ctx->sq_sqes[tail]; +memcpy (sqe, bpf_sqe,sizeof (*sqe)); +return 0 ; +} + +__bpf_kfunc +struct io_uring_cqe *bpf_io_uring_get_cqe(struct io_ring_ctx *ctx, u32 idx) +{ +unsigned max_entries = ctx->cq_entries; +struct io_uring_cqe *cqe_array = ctx->rings->cqes; + +if (ctx->flags &IORING_SETUP_CQE32 ) + max_entries *=2 ; +return &cqe_array[idx & (max_entries -1 )]; +} + +__bpf_kfunc +struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx) +{ +struct io_rings *rings = ctx->rings; +unsigned int mask = ctx->cq_entries -1 ; +unsigned head = rings->cq.head; +struct io_uring_cqe *cqe; + +/* TODO CQE32 */ +if (head == rings->cq.tail) +return NULL ; + + cqe = &rings->cqes[head & mask]; + rings->cq.head++; +return cqe; +} + +__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_post_cqe,KF_SLEEPABLE ); +BTF_ID_FLAGS (func, bpf_io_uring_queue_sqe,KF_SLEEPABLE ); +BTF_ID_FLAGS (func, bpf_io_uring_get_cqe,0 ); +BTF_ID_FLAGS (func, bpf_io_uring_extract_next_cqe,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__handle_events (struct io_ring_ctx *ctx,struct iou_loop_state *state) {
@@ -186,6 +266,12 @@ static int __init io_uring_bpf_init(void)return ret; } + 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 ; }__initcall (io_uring_bpf_init);
Signature · 1 line
2.49.0
Re: [RFC v2 5/5] io_uring/bpf: add basic kfunc helpers
Quoted 89 lines · Pavel Begunkov
A
handle_eventsprogram should be able to parse the CQ and submit new requests, add kfuncs to cover that. The only essential kfunc here isbpf_io_uring_submit_sqes, and the rest are likely be removed in a non-RFC version in favour of a more general approach.Signed-off-by Pavel Begunkov <[email protected]>--- io_uring/bpf.c | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+)io_uring/bpf.c +64
-0@@ -1,12 +1,92 @@#include <linux/mutex.h> #include <linux/bpf_verifier.h> +#include "io_uring.h" #include "bpf.h" #include "register.h" static const struct btf_type *loop_state_type;DEFINE_MUTEX (io_bpf_ctrl_mutex); +__bpf_kfunc_start_defs (); + +__bpf_kfunc int bpf_io_uring_submit_sqes(struct io_ring_ctx *ctx, +unsigned nr) +{ +return io_submit_sqes (ctx, nr); +} + +__bpf_kfunc int bpf_io_uring_post_cqe(struct io_ring_ctx *ctx, +u64 data,u32 res,u32 cflags) +{ +bool posted; + + posted =io_post_aux_cqe (ctx, data, res, cflags); +return posted ?0 : -ENOMEM ; +} + +__bpf_kfunc int bpf_io_uring_queue_sqe(struct io_ring_ctx *ctx, +void *bpf_sqe,int mem__sz) +{ +unsigned tail = ctx->rings->sq.tail; +struct io_uring_sqe *sqe; + +if (mem__sz !=sizeof (*sqe)) +return -EINVAL ; + + ctx->rings->sq.tail++; + tail &= (ctx->sq_entries -1 ); +/* double index for 128-byte SQEs, twice as long */ +if (ctx->flags &IORING_SETUP_SQE128 ) + tail <<=1 ; + sqe = &ctx->sq_sqes[tail]; +memcpy (sqe, bpf_sqe,sizeof (*sqe)); +return 0 ; +} + +__bpf_kfunc +struct io_uring_cqe *bpf_io_uring_get_cqe(struct io_ring_ctx *ctx, u32 idx) +{ +unsigned max_entries = ctx->cq_entries; +struct io_uring_cqe *cqe_array = ctx->rings->cqes; + +if (ctx->flags &IORING_SETUP_CQE32 ) + max_entries *=2 ; +return &cqe_array[idx & (max_entries -1 )]; +} + +__bpf_kfunc +struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx) +{ +struct io_rings *rings = ctx->rings; +unsigned int mask = ctx->cq_entries -1 ; +unsigned head = rings->cq.head; +struct io_uring_cqe *cqe; + +/* TODO CQE32 */ +if (head == rings->cq.tail) +return NULL ; + + cqe = &rings->cqes[head & mask]; + rings->cq.head++; +return cqe; +}
io_uring/bpf.c line 83
Quoted 10 lines
io_uring/bpf.c +10
-0+ +__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_post_cqe,KF_SLEEPABLE ); +BTF_ID_FLAGS (func, bpf_io_uring_queue_sqe,KF_SLEEPABLE ); +BTF_ID_FLAGS (func, bpf_io_uring_get_cqe,0 ); +BTF_ID_FLAGS (func, bpf_io_uring_extract_next_cqe,KF_RET_NULL ); +BTF_KFUNCS_END (io_uring_kfunc_set)
This is not safe in general.
The verifier doesn't enforce argument safety here.
As a minimum you need to add KF_TRUSTED_ARGS flag to all kfunc. And once you do that you'll see that the verifier doesn't recognize the cqe returned from bpf_io_uring_get_cqe*() as trusted.
Looking at your example:
https://github.com/axboe/liburing/commit/706237127f03e15b4cc9c7c31c16d34dbff37cdc
it doesn't care about contents of cqe and doesn't pass it further. So sort-of ok-ish right now,
but if you need to pass cqe to another kfunc
you would need to add an open coded iterator for cqe-s with appropriate KF_ITER* flags
or maybe add acquire/release semantics for cqe.
Like, get_cqe will be KF_ACQUIRE, and you'd need matching KF_RELEASE kfunc,
so that 'cqe' is not lost.
Then 'cqe' will be trusted and you can pass it as actual 'cqe' into another kfunc.
Without KF_ACQUIRE the verifier sees that get_cqe*() kfuncs return 'struct io_uring_cqe *' and it's ok for tracing or passing into kfuncs like bpf_io_uring_queue_sqe() that don't care about a particular type,
but not ok for full tracking of objects.
For next revision please post all selftest, examples, and bpf progs on the list,
so people don't need to search github.
Re: [RFC v2 5/5] io_uring/bpf: add basic kfunc helpers
Quoted 1 line · Alexei Starovoitov
On Fri, Jun 6, 2025 at 6:58 AM Pavel Begunkov <[email protected]> wrote:
...>> +__bpf_kfunc
Quoted 24 lines
Quoted 24 lines
(quoted diff) +24
-0+struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx) +{ + struct io_rings *rings = ctx->rings; + unsigned int mask = ctx->cq_entries - 1; + unsigned head = rings->cq.head; + struct io_uring_cqe *cqe; + + /* TODO CQE32 */ + if (head == rings->cq.tail) + return NULL; + + cqe = &rings->cqes[head & mask]; + rings->cq.head++; + return cqe; +} + +__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_post_cqe, KF_SLEEPABLE); +BTF_ID_FLAGS(func, bpf_io_uring_queue_sqe, KF_SLEEPABLE); +BTF_ID_FLAGS(func, bpf_io_uring_get_cqe, 0); +BTF_ID_FLAGS(func, bpf_io_uring_extract_next_cqe, KF_RET_NULL);
Quoted 8 lines
Quoted 1 line
+BTF_KFUNCS_END(io_uring_kfunc_set)
This is not safe in general.
The verifier doesn't enforce argument safety here.
As a minimum you need to addKF_TRUSTED_ARGSflag to all kfunc. And once you do that you'll see that the verifier doesn't recognize the cqe returned from bpf_io_uring_get_cqe*() as trusted.
Thanks, will add it. If I read it right, without the flag the program can, for example, create a struct io_ring_ctx on stack, fill it with nonsense and pass to kfuncs. Is that right?
Quoted 10 lines
Looking at your example:
https://github.com/axboe/liburing/commit/706237127f03e15b4cc9c7c31c16d34dbff37cdc
it doesn't care about contents of cqe and doesn't pass it further. So sort-of ok-ish right now,
but if you need to pass cqe to another kfunc
you would need to add an open coded iterator for cqe-s with appropriate KF_ITER* flags
or maybe add acquire/release semantics for cqe.
Like,get_cqewill beKF_ACQUIRE, and you'd need matchingKF_RELEASEkfunc,
Quoted 8 lines
so that 'cqe' is not lost.
Then 'cqe' will be trusted and you can pass it as actual 'cqe' into another kfunc.
WithoutKF_ACQUIREthe verifier sees that get_cqe*() kfuncs return 'structio_uring_cqe*' and it's ok for tracing or passing into kfuncs likebpf_io_uring_queue_sqe()that don't care about a particular type,
but not ok for full tracking of objects.
I don't need type safety for SQEs / CQEs, they're supposed to be simple memory blobs containing userspace data only. SQ / CQ are shared with userspace, and the kfuncs can leak the content of passed CQE / SQE to userspace. But I'd like to find a way to reject programs stashing kernel pointers / data into them.
BPF_PROG(name, struct io_ring_ctx *io_ring)
{
struct io_uring_sqe *cqe = ...;
cqe->user_data = io_ring;
cqe->res = io_ring->private_field;
}And I mentioned in the message, I rather want to get rid of half of the kfuncs, and give BPF direct access to the SQ/CQ instead. Schematically it should look like this:
BPF_PROG(name, struct io_ring_ctx *ring)
{
struct io_uring_sqe *sqes = get_SQ(ring);
sqes[ring->sq_tail]->opcode = OP_NOP;
bpf_kfunc_submit_sqes(ring, 1);
struct io_uring_cqe *cqes = get_CQ(ring);
print_cqe(&cqes[ring->cq_head]);
}I hacked up RET_PTR_TO_MEM for kfuncs, the diff is below, but it'd be great to get rid of the constness of the size argument. I need to digest arenas first as conceptually they look very close.
Quoted 3 lines
For next revision please post all selftest, examples, and bpf progs on the list,
so people don't need to search github.
Did the link in the cover letter not work for you? I'm confused since it's all in a branch in my tree, but you linked to the same patches but in Jens' tree, and I have zero clue what they're doing there or how you found them.
io_uring/bpf.c +10 -0
@@ -2,6 +2,7 @@#include <linux/bpf_verifier.h> #include "io_uring.h" +#include "memmap.h" #include "bpf.h" #include "register.h"
@@ -72,6 +73,14 @@ struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx)return cqe; } +__bpf_kfunc +void *bpf_io_uring_get_region(struct io_ring_ctx *ctx, u64 size__retsz) +{ +if (size__retsz > ((u64 )ctx->ring_region.nr_pages <<PAGE_SHIFT )) +return NULL ; +return io_region_get_ptr (&ctx->ring_region); +} +__bpf_kfunc_end_defs ();BTF_KFUNCS_START (io_uring_kfunc_set)
@@ -80,6 +89,7 @@ BTF_ID_FLAGS(func, bpf_io_uring_post_cqe, KF_SLEEPABLE);BTF_ID_FLAGS (func, bpf_io_uring_queue_sqe,KF_SLEEPABLE );BTF_ID_FLAGS (func, bpf_io_uring_get_cqe,0 );BTF_ID_FLAGS (func, bpf_io_uring_extract_next_cqe,KF_RET_NULL ); +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 = {
kernel/bpf/verifier.c +27 -1
@@ -343,6 +343,7 @@ struct bpf_kfunc_call_arg_meta {int uid; } map;u64 mem_size; +bool mem_size_found; };struct btf *btf_vmlinux;
@@ -11862,6 +11863,11 @@ static bool is_kfunc_arg_ignore(const struct btf *btf, const struct btf_param *areturn btf_param_match_suffix (btf, arg,"__ign" ); } +static bool is_kfunc_arg_ret_size (const struct btf *btf,const struct btf_param *arg) +{ +return btf_param_match_suffix (btf, arg,"__retsz" ); +} +static bool is_kfunc_arg_map (const struct btf *btf,const struct btf_param *arg) {return btf_param_match_suffix (btf, arg,"__map" );
@@ -12912,7 +12918,21 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_return -EINVAL ; } -if (is_kfunc_arg_constant (meta->btf, &args[i])) { +if (is_kfunc_arg_ret_size (btf, &args[i])) { +if (!tnum_is_const (reg->var_off)) { +verbose (env,"R%d must be a known constant\n" , regno); +return -EINVAL ; + } +if (meta->mem_size_found) { +verbose (env,"Only one return size argument is permitted\n" ); +return -EINVAL ; + } + meta->mem_size = reg->var_off.value; + meta->mem_size_found = true; + ret =mark_chain_precision (env, regno); +if (ret) +return ret; + }else if (is_kfunc_arg_constant (meta->btf, &args[i])) {if (meta->arg_constant.found) {verbose (env,"verifier internal error: only one constant argument permitted\n" );return -EFAULT ;
@@ -13816,6 +13836,12 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn, }else if (btf_type_is_void (ptr_type)) {/* kfunc returning 'void *' is equivalent to returning scalar */ mark_reg_unknown (env, regs,BPF_REG_0 ); + +if (meta.mem_size_found) { +mark_reg_known_zero (env, regs,BPF_REG_0 ); + regs[BPF_REG_0 ].type =PTR_TO_MEM ; + regs[BPF_REG_0 ].mem_size = meta.mem_size; + } }else if (!__btf_type_is_struct (ptr_type)) {if (!meta.r0_size) {__u32 sz;
Signature · 1 line
Pavel Begunkov
Jens Axboe
maintainerRe: [RFC v2 5/5] io_uring/bpf: add basic kfunc helpers
Quoted 8 lines · Pavel Begunkov
Quoted 3 lines
For next revision please post all selftest, examples, and bpf progs on the list,
so people don't need to search github.Did the link in the cover letter not work for you? I'm confused since it's all in a branch in my tree, but you linked to the same patches but in Jens' tree, and I have zero clue what they're doing there or how you found them.
Puzzled me too, but if you go there, github will say:
"This commit does not belong to any branch on this repository, and may
belong to a fork outside of the repository."
which is exactly because it's not in my tree, but in your fork of my tree. Pretty wonky GH behavior if you ask me, but there it is.
Signature · 1 line
Jens Axboe
Re: [RFC v2 5/5] io_uring/bpf: add basic kfunc helpers
In reply to Pavel BegunkovQuoted 37 lines · Pavel Begunkov
Quoted 1 line · Alexei Starovoitov
On Fri, Jun 6, 2025 at 6:58 AM Pavel Begunkov <[email protected]> wrote:
...>> +__bpf_kfunc
Quoted 24 lines
Quoted 24 lines
(quoted diff) +24
-0+struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx) +{ + struct io_rings *rings = ctx->rings; + unsigned int mask = ctx->cq_entries - 1; + unsigned head = rings->cq.head; + struct io_uring_cqe *cqe; + + /* TODO CQE32 */ + if (head == rings->cq.tail) + return NULL; + + cqe = &rings->cqes[head & mask]; + rings->cq.head++; + return cqe; +} + +__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_post_cqe, KF_SLEEPABLE); +BTF_ID_FLAGS(func, bpf_io_uring_queue_sqe, KF_SLEEPABLE); +BTF_ID_FLAGS(func, bpf_io_uring_get_cqe, 0); +BTF_ID_FLAGS(func, bpf_io_uring_extract_next_cqe, KF_RET_NULL);Quoted 8 lines
Quoted 1 line
+BTF_KFUNCS_END(io_uring_kfunc_set)
This is not safe in general.
The verifier doesn't enforce argument safety here.
As a minimum you need to addKF_TRUSTED_ARGSflag to all kfunc. And once you do that you'll see that the verifier doesn't recognize the cqe returned from bpf_io_uring_get_cqe*() as trusted.
Quoted 3 lines
Thanks, will add it. If I read it right, without the flag the program can, for example, create a struct
io_ring_ctxon stack, fill it with nonsense and pass to kfuncs. Is that right?
No. The verifier will only allow a pointer to struct io_ring_ctx to be passed, but it may not be fully trusted.
The verifier has 3 types of pointers to kernel structures:
1. ptr_to_btf_id
2. ptr_to_btf_id | trusted
3. ptr_to_btf_id | untrusted
1st was added long ago for tracing and gradually got adopted for non-tracing needs, but it has a foot gun, since all pointer walks keep ptr_to_btf_id type.
It's fine in some cases to follow pointers, but not in all. Hence 2nd variant was added and therefoo->bar dereference needs to be explicitly allowed instead of allowed by default like for 1st kind.
All loads through 1 and 3 are implemented as probe_read_kernel. while loads from 2 are direct loads.
So kfuncs without KF_TRUSTED_ARGS with struct io_ring_ctx *ctx argument are likely fine and safe, since it's impossible to get this io_ring_ctx pointer by dereferencing some other pointer. But better to tighten safety from the start.
We recommend KF_TRUSTED_ARGS for all kfuncs and eventually it will be the default.
Quoted 19 lines
Quoted 10 lines
Looking at your example:
https://github.com/axboe/liburing/commit/706237127f03e15b4cc9c7c31c16d34dbff37cdc
it doesn't care about contents of cqe and doesn't pass it further. So sort-of ok-ish right now,
but if you need to pass cqe to another kfunc
you would need to add an open coded iterator for cqe-s with appropriate KF_ITER* flags
or maybe add acquire/release semantics for cqe.
Like,get_cqewill beKF_ACQUIRE, and you'd need matchingKF_RELEASEkfunc,Quoted 8 lines
so that 'cqe' is not lost.
Then 'cqe' will be trusted and you can pass it as actual 'cqe' into another kfunc.
WithoutKF_ACQUIREthe verifier sees that get_cqe*() kfuncs return 'structio_uring_cqe*' and it's ok for tracing or passing into kfuncs likebpf_io_uring_queue_sqe()that don't care about a particular type,
but not ok for full tracking of objects.
Quoted 5 lines
I don't need type safety for SQEs / CQEs, they're supposed to be simple memory blobs containing userspace data only. SQ / CQ are shared with userspace, and the kfuncs can leak the content of passed CQE / SQE to userspace. But I'd like to find a way to reject programs stashing kernel pointers / data into them.
That's impossible.
If you're worried about bpf prog exposing kernel addresses to user space then abort the whole thing. CAP_PERFMON is required for the majority of bpf progs.
Quoted 23 lines
BPF_PROG(name, struct io_ring_ctx *io_ring){ struct io_uring_sqe *cqe = ...; cqe->user_data = io_ring; cqe->res = io_ring->private_field; }And I mentioned in the message, I rather want to get rid of half of the kfuncs, and give BPF direct access to the SQ/CQ instead. Schematically it should look like this:
BPF_PROG(name, struct io_ring_ctx *ring){ struct io_uring_sqe *sqes = get_SQ(ring); sqes[ring->sq_tail]->opcode = OP_NOP; bpf_kfunc_submit_sqes(ring, 1); struct io_uring_cqe *cqes = get_CQ(ring); print_cqe(&cqes[ring->cq_head]); }
Quoted 3 lines
I hacked up
RET_PTR_TO_MEMfor kfuncs, the diff is below, but it'd be great to get rid of the constness of the size argument. I need to digest arenas first as conceptually they look very close.
arena is a special memory region where every byte is writeable by user space.
Quoted 9 lines
Quoted 4 lines
For next revision please post all selftest, examples, and bpf progs on the list,
so people don't need to search github.Did the link in the cover letter not work for you? I'm confused since it's all in a branch in my tree, but you linked to the same patches but in Jens' tree, and I have zero clue what they're doing there or how you found them.
External links can disappear. It's not good for reviewers and for keeping the history of conversation.
Quoted 14 lines
io_uring/bpf.c +1
-0@@ -2,6 +2,7 @@#include <linux/bpf_verifier.h> #include "io_uring.h" +#include "memmap.h" #include "bpf.h" #include "register.h"
io_uring/bpf.c struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx) · line 82
Quoted 10 lines
io_uring/bpf.c +7
-0@@ -72,6 +73,14 @@ struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx)return cqe; } +__bpf_kfunc +void *bpf_io_uring_get_region(struct io_ring_ctx *ctx, u64 size__retsz) +{ +if (size__retsz > ((u64 )ctx->ring_region.nr_pages <<PAGE_SHIFT )) +return NULL ; +return io_region_get_ptr (&ctx->ring_region); +}
and bpf prog should be able to read/write anything in [ctx->ring_region->ptr, ..ptr + size] region ?
Populating (creating) dynptr is probably better.
See bpf_dynptr_from*()
but what is the lifetime of that memory ?
Re: [RFC v2 5/5] io_uring/bpf: add basic kfunc helpers
Quoted 1 line · Alexei Starovoitov
On Thu, Jun 12, 2025 at 6:25 AM Pavel Begunkov <[email protected]> wrote:
...>>>> +BTF_ID_FLAGS(func, bpf_io_uring_extract_next_cqe, KF_RET_NULL);
Quoted 32 lines
Quoted 9 lines
Quoted 8 lines
Quoted 1 line
+BTF_KFUNCS_END(io_uring_kfunc_set)
This is not safe in general.
The verifier doesn't enforce argument safety here.
As a minimum you need to addKF_TRUSTED_ARGSflag to all kfunc. And once you do that you'll see that the verifier doesn't recognize the cqe returned from bpf_io_uring_get_cqe*() as trusted.Quoted 3 lines
Thanks, will add it. If I read it right, without the flag the program can, for example, create a struct
io_ring_ctxon stack, fill it with nonsense and pass to kfuncs. Is that right?No. The verifier will only allow a pointer to struct
io_ring_ctxto be passed, but it may not be fully trusted.The verifier has 3 types of pointers to kernel structures:
1.ptr_to_btf_id
2.ptr_to_btf_id| trusted
3.ptr_to_btf_id| untrusted1st was added long ago for tracing and gradually got adopted for non-tracing needs, but it has a foot gun, since all pointer walks keep
ptr_to_btf_idtype.
It's fine in some cases to follow pointers, but not in all. Hence 2nd variant was added and therefoo->bardereference needs to be explicitly allowed instead of allowed by default like for 1st kind.All loads through 1 and 3 are implemented as
probe_read_kernel. while loads from 2 are direct loads.
Quoted 6 lines
So kfuncs without
KF_TRUSTED_ARGSwith structio_ring_ctx*ctx argument are likely fine and safe, since it's impossible to get thisio_ring_ctxpointer by dereferencing some other pointer. But better to tighten safety from the start.
We recommendKF_TRUSTED_ARGSfor all kfuncs and eventually it will be the default.
Sure, I'll add it, thanks for the explanation
...>> diff --git a/io_uring/bpf.c b/io_uring/bpf.c
Quoted 15 lines
Quoted 15 lines
index
9494e4289605..400a06a74b5d100644io_uring/bpf.c +1
-0@@ -2,6 +2,7 @@#include <linux/bpf_verifier.h> #include "io_uring.h" +#include "memmap.h" #include "bpf.h" #include "register.h" @@ -72,6 +73,14 @@ struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx)return cqe; }
Quoted 10 lines
io_uring/bpf.c struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx) · line 82
Quoted 7 lines
io_uring/bpf.c +7
-0+__bpf_kfunc +void *bpf_io_uring_get_region(struct io_ring_ctx *ctx, u64 size__retsz) +{ +if (size__retsz > ((u64 )ctx->ring_region.nr_pages <<PAGE_SHIFT )) +return NULL ; +return io_region_get_ptr (&ctx->ring_region); +}and bpf prog should be able to read/write anything in [
ctx->ring_region->ptr, ..ptr + size] region ?
Right, and it's already rw mmap'ed into the user space.
Quoted 4 lines
Populating (creating) dynptr is probably better.
See bpf_dynptr_from*()but what is the lifetime of that memory ?
It's valid within a single run of the callback but shouldn't cross into another invocation. Specifically, it's protected by the lock, but that can be tuned. Does that match with what PTR_TO_MEM expects?
I can add refcounting for longer term pinning, maybe to store it as a bpf map or whatever is the right way, but I'd rather avoid anything expensive in the kfunc as that'll likely be called on every program run.
Signature · 1 line
Pavel Begunkov
Re: [RFC v2 5/5] io_uring/bpf: add basic kfunc helpers
Quoted 74 lines · Pavel Begunkov
Quoted 1 line · Alexei Starovoitov
On Thu, Jun 12, 2025 at 6:25 AM Pavel Begunkov <[email protected]> wrote:
...>>>> +BTF_ID_FLAGS(func,
bpf_io_uring_extract_next_cqe,KF_RET_NULL);Quoted 32 lines
Quoted 9 lines
Quoted 8 lines
Quoted 1 line
+BTF_KFUNCS_END(io_uring_kfunc_set)
This is not safe in general.
The verifier doesn't enforce argument safety here.
As a minimum you need to addKF_TRUSTED_ARGSflag to all kfunc. And once you do that you'll see that the verifier doesn't recognize the cqe returned from bpf_io_uring_get_cqe*() as trusted.Quoted 3 lines
Thanks, will add it. If I read it right, without the flag the program can, for example, create a struct
io_ring_ctxon stack, fill it with nonsense and pass to kfuncs. Is that right?No. The verifier will only allow a pointer to struct
io_ring_ctxto be passed, but it may not be fully trusted.The verifier has 3 types of pointers to kernel structures:
1.ptr_to_btf_id
2.ptr_to_btf_id| trusted
3.ptr_to_btf_id| untrusted1st was added long ago for tracing and gradually got adopted for non-tracing needs, but it has a foot gun, since all pointer walks keep
ptr_to_btf_idtype.
It's fine in some cases to follow pointers, but not in all. Hence 2nd variant was added and therefoo->bardereference needs to be explicitly allowed instead of allowed by default like for 1st kind.All loads through 1 and 3 are implemented as
probe_read_kernel. while loads from 2 are direct loads.Quoted 6 lines
So kfuncs without
KF_TRUSTED_ARGSwith structio_ring_ctx*ctx argument are likely fine and safe, since it's impossible to get thisio_ring_ctxpointer by dereferencing some other pointer. But better to tighten safety from the start.
We recommendKF_TRUSTED_ARGSfor all kfuncs and eventually it will be the default.Sure, I'll add it, thanks for the explanation
...>> diff --git
a/io_uring/bpf.cb/io_uring/bpf.cQuoted 15 lines
Quoted 15 lines
index
9494e4289605..400a06a74b5d100644io_uring/bpf.c +1
-0@@ -2,6 +2,7 @@#include <linux/bpf_verifier.h> #include "io_uring.h" +#include "memmap.h" #include "bpf.h" #include "register.h" @@ -72,6 +73,14 @@ struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx)return cqe; }Quoted 10 lines
io_uring/bpf.c struct io_uring_cqe *bpf_io_uring_extract_next_cqe(struct io_ring_ctx *ctx) · line 82
Quoted 7 lines
io_uring/bpf.c +7
-0+__bpf_kfunc +void *bpf_io_uring_get_region(struct io_ring_ctx *ctx, u64 size__retsz) +{ +if (size__retsz > ((u64 )ctx->ring_region.nr_pages <<PAGE_SHIFT )) +return NULL ; +return io_region_get_ptr (&ctx->ring_region); +}and bpf prog should be able to read/write anything in [
ctx->ring_region->ptr, ..ptr + size] region ?Right, and it's already rw mmap'ed into the user space.
Quoted 8 lines
Quoted 4 lines
Populating (creating) dynptr is probably better.
See bpf_dynptr_from*()but what is the lifetime of that memory ?
It's valid within a single run of the callback but shouldn't cross into another invocation. Specifically, it's protected by the lock, but that can be tuned. Does that match with what
PTR_TO_MEMexpects?
yes. PTR_TO_MEM lasts for duration of the prog.
Quoted 4 lines
I can add refcounting for longer term pinning, maybe to store it as a bpf map or whatever is the right way, but I'd rather avoid anything expensive in the kfunc as that'll likely be called on every program run.
yeah. let's not add any refcounting.
It sounds like you want something similar to__bpf_kfunc __u8 *hid_bpf_get_data(struct hid_bpf_ctx *ctx, unsigned int offset, const size_t rdwr_buf_size)
we have a special hack for it already in the verifier. The argument need to be called rdwr_buf_size, then it will be used to establish the range of PTR_TO_MEM. It has to be run-time constant.
What you're proposing with "__retsz" is a cleaner version of the same. But consider bpf_dynptr_from_io_uring(struct io_ring_ctx *ctx) it can create a dynamically sized region,
and later use bpf_dynptr_slice_rdwr() to get writeable chunk of it.
I feel that __retsz approach may actually be a better fit at the end, if you're ok with constant arg.
Re: [RFC v2 5/5] io_uring/bpf: add basic kfunc helpers
Quoted 1 line · Alexei Starovoitov
On Fri, Jun 13, 2025 at 9:11 AM Pavel Begunkov <[email protected]> wrote:
...>>
Quoted 18 lines
Quoted 3 lines
It's valid within a single run of the callback but shouldn't cross into another invocation. Specifically, it's protected by the lock, but that can be tuned. Does that match with what
PTR_TO_MEMexpects?yes.
PTR_TO_MEMlasts for duration of the prog.Quoted 4 lines
I can add refcounting for longer term pinning, maybe to store it as a bpf map or whatever is the right way, but I'd rather avoid anything expensive in the kfunc as that'll likely be called on every program run.
yeah. let's not add any refcounting.
It sounds like you want something similar to
__bpf_kfunc__u8*hid_bpf_get_data(struct hid_bpf_ctx *ctx, unsigned int offset, const size_t rdwr_buf_size)
Quoted 4 lines
we have a special hack for it already in the verifier. The argument need to be called
rdwr_buf_size, then it will be used to establish the range ofPTR_TO_MEM. It has to be run-time constant.
Great, I can just use that
Quoted 7 lines
What you're proposing with "
__retsz" is a cleaner version of the same. But considerbpf_dynptr_from_io_uring(struct io_ring_ctx *ctx)it can create a dynamically sized region,
and later usebpf_dynptr_slice_rdwr()to get writeable chunk of it.I feel that
__retszapproach may actually be a better fit at the end, if you're ok with constant arg.
I took a quick look, 16MB sounds a bit restrictive long term. I'll just go for rdwr_buf_size while experimenting and hopefully will be able to make a more educated choice later
Signature · 1 line
Pavel Begunkov
Jens Axboe
maintainerQuoted 19 lines · Pavel Begunkov
This series adds
io_uringBPFstruct_ops, which allows processing events and submitting requests from BPF without returning to user. There is only one callback for now, it's called from theio_uringCQ waiting loop when there is an event to be processed. It also has access to waiting parameters like batching and timeouts.It's tested with a program that queues a nop request, waits for its completion and then queues another request, repeating it N times. The baseline to compare with is traditional
io_uringapplication doing same without BPF and using 2 requests links, with the same total number of requests.# ./link 0 100000000 type 2-LINK, requests to run 100000000 sec 20, total (ms) 20374 # ./link 1 100000000 type BPF, requests to run 100000000 sec 13, total (ms) 13700
Quoted 3 lines
The BPF version works ~50% faster on a mitigated kernel, while it's not even a completely fair comparison as links are restrictive and can't always be used. Without links the speedup reaches ~80%.
Nifty! Great to see the BPF side taking shape, I can think of many cool things we could do with that. Out of curiosity, tested this on my usual arm64 vm on the laptop:
axboe@m2max-kvm ~/g/l/examples-bpf (bpf) [1]> ./link 0 100000000 type 2-LINK, requests to run 100000000
sec 13, total (ms) 13868
axboe@m2max-kvm ~/g/l/examples-bpf (bpf)> sudo ./link 1 100000000 type BPF, requests to run 100000000
sec 4, total (ms) 4929
No mitigations or anything configured in this kernel.
I'll take a closer look at the patches.
Signature · 1 line
Jens Axboe