* [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops [not found] <PAVEL_MESSAGE_ID> @ 2026-07-17 15:45 ` Woraphat Khiaodaeng 2026-07-17 16:05 ` Gabriel Krisman Bertazi ` (2 more replies) 0 siblings, 3 replies; 5+ messages in thread From: Woraphat Khiaodaeng @ 2026-07-17 15:45 UTC (permalink / raw) To: Jens Axboe, Pavel Begunkov; +Cc: io-uring, security, Woraphat Khiaodaeng io_install_bpf() only rejects a second registration on the ctx side (ctx->bpf_ops) and sets the per-map back-pointer ops->priv unconditionally. The struct_ops link path never advances a map past BPF_STRUCT_OPS_STATE_READY, so the same io_uring_bpf_ops map can be registered more than once, and bpf_io_reg() re-resolves the target ring via fget(ops->ring_fd) on every call. A caller can therefore point the same ring_fd at a different io_ring_ctx between two BPF_LINK_CREATE calls. The second registration passes the ctx->bpf_ops check (the new ctx has none) and overwrites ops->priv, orphaning the first ctx. Teardown (io_eject_bpf()/bpf_io_unreg()) only reaches a ctx through ops->priv, so the orphaned ctx is never torn down: its ctx->loop_step keeps pointing into the struct_ops trampoline, which is freed once the map is gone. A later io_uring_enter() on the orphaned ring then calls the dangling ctx->loop_step from io_run_loop() -- a use-after-free of freed executable memory, reachable by a task with CAP_BPF + CAP_PERFMON. Reject registration when ops->priv is already set, as hid_bpf_reg() does for its struct_ops. Fixes: 98f37634b12b ("io_uring/bpf-ops: implement bpf ops registration") Signed-off-by: Woraphat Khiaodaeng <worapat.kd2@gmail.com> --- io_uring/bpf-ops.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/io_uring/bpf-ops.c b/io_uring/bpf-ops.c index 5a50f0675..cf2bd068e 100644 --- a/io_uring/bpf-ops.c +++ b/io_uring/bpf-ops.c @@ -168,6 +168,8 @@ static int io_install_bpf(struct io_ring_ctx *ctx, struct io_uring_bpf_ops *ops) if (ctx->bpf_ops) return -EBUSY; + if (ops->priv) + return -EBUSY; if (WARN_ON_ONCE(!ops->loop_step)) return -EINVAL; -- 2.43.0 ^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops 2026-07-17 15:45 ` [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops Woraphat Khiaodaeng @ 2026-07-17 16:05 ` Gabriel Krisman Bertazi 2026-07-18 2:30 ` [PATCH] test: add io_uring bpf-ops double-registration regression test Woraphat Khiaodaeng 2026-07-17 17:05 ` [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops Pavel Begunkov 2026-07-17 17:05 ` Jens Axboe 2 siblings, 1 reply; 5+ messages in thread From: Gabriel Krisman Bertazi @ 2026-07-17 16:05 UTC (permalink / raw) To: Woraphat Khiaodaeng, Jens Axboe, Pavel Begunkov Cc: io-uring, security, Woraphat Khiaodaeng Woraphat Khiaodaeng <worapat.kd2@gmail.com> writes: > io_install_bpf() only rejects a second registration on the ctx side > (ctx->bpf_ops) and sets the per-map back-pointer ops->priv > unconditionally. The struct_ops link path never advances a map past > BPF_STRUCT_OPS_STATE_READY, so the same io_uring_bpf_ops map can be > registered more than once, and bpf_io_reg() re-resolves the target ring > via fget(ops->ring_fd) on every call. A caller can therefore point the > same ring_fd at a different io_ring_ctx between two BPF_LINK_CREATE > calls. > > The second registration passes the ctx->bpf_ops check (the new ctx has > none) and overwrites ops->priv, orphaning the first ctx. Teardown > (io_eject_bpf()/bpf_io_unreg()) only reaches a ctx through ops->priv, so > the orphaned ctx is never torn down: its ctx->loop_step keeps pointing > into the struct_ops trampoline, which is freed once the map is gone. A > later io_uring_enter() on the orphaned ring then calls the dangling > ctx->loop_step from io_run_loop() -- a use-after-free of freed > executable memory, reachable by a task with CAP_BPF + CAP_PERFMON. > > Reject registration when ops->priv is already set, as hid_bpf_reg() > does for its struct_ops. > > Fixes: 98f37634b12b ("io_uring/bpf-ops: implement bpf ops registration") > Signed-off-by: Woraphat Khiaodaeng <worapat.kd2@gmail.com> Feel free to add, Reviewed-by: Gabriel Krisman Bertazi <krisman@suse.de> can you send a testcase to add to liburing? > --- > io_uring/bpf-ops.c | 2 ++ > 1 file changed, 2 insertions(+) > > diff --git a/io_uring/bpf-ops.c b/io_uring/bpf-ops.c > index 5a50f0675..cf2bd068e 100644 > --- a/io_uring/bpf-ops.c > +++ b/io_uring/bpf-ops.c > @@ -168,6 +168,8 @@ static int io_install_bpf(struct io_ring_ctx *ctx, struct io_uring_bpf_ops *ops) > > if (ctx->bpf_ops) > return -EBUSY; > + if (ops->priv) > + return -EBUSY; > if (WARN_ON_ONCE(!ops->loop_step)) > return -EINVAL; > > -- > 2.43.0 > -- Gabriel Krisman Bertazi ^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH] test: add io_uring bpf-ops double-registration regression test 2026-07-17 16:05 ` Gabriel Krisman Bertazi @ 2026-07-18 2:30 ` Woraphat Khiaodaeng 0 siblings, 0 replies; 5+ messages in thread From: Woraphat Khiaodaeng @ 2026-07-18 2:30 UTC (permalink / raw) To: Jens Axboe, Gabriel Krisman Bertazi Cc: Pavel Begunkov, io-uring, Woraphat Khiaodaeng Exercise the io_uring BPF struct_ops registration path fixed by the kernel commit "io_uring/bpf-ops: reject re-registration of an already-bound ops". The same io_uring_bpf_ops struct_ops map is registered twice. Between the two BPF_LINK_CREATEs the ring_fd is repointed at a second ring, so the second registration targets a different io_ring_ctx whose ->bpf_ops is NULL and passes the per-ctx check; only the per-map ops->priv guard rejects it. The test verifies the second registration is refused with -EBUSY. On a vulnerable kernel it is accepted, the first ctx is orphaned and its loop_step dangles into the freed struct_ops trampoline. The test reuses the existing bpf test infrastructure and only checks the registration result; it never triggers the dangling call. Signed-off-by: Woraphat Khiaodaeng <worapat.kd2@gmail.com> --- test/Makefile | 2 +- test/bpf-progs/double_reg.bpf.c | 16 ++++ test/bpf_double_reg.c | 158 ++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 test/bpf-progs/double_reg.bpf.c create mode 100644 test/bpf_double_reg.c diff --git a/test/Makefile b/test/Makefile index d88a428..5f6794d 100644 --- a/test/Makefile +++ b/test/Makefile @@ -335,7 +335,7 @@ ifdef CONFIG_HAVE_CXX endif all_targets += sq-full-cpp.t -bpf_test_srcs := bpf_nops.c bpf_cp.c +bpf_test_srcs := bpf_nops.c bpf_cp.c bpf_double_reg.c bpf_progs := $(patsubst bpf_%.c, %.bpf.c, $(bpf_test_srcs)) bpf_test_targets := diff --git a/test/bpf-progs/double_reg.bpf.c b/test/bpf-progs/double_reg.bpf.c new file mode 100644 index 0000000..20617d0 --- /dev/null +++ b/test/bpf-progs/double_reg.bpf.c @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#include "../bpf_defs.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +/* Minimal loop_step: the test only exercises registration, never runs it. */ +SEC("struct_ops.s/reg_loop_step") +int BPF_PROG(reg_loop_step, struct io_ring_ctx *ring, struct iou_loop_params *ls) +{ + return IOU_LOOP_STOP; +} + +SEC(".struct_ops.link") +struct io_uring_bpf_ops reg_ops = { + .loop_step = (void *)reg_loop_step, +}; diff --git a/test/bpf_double_reg.c b/test/bpf_double_reg.c new file mode 100644 index 0000000..93c3f38 --- /dev/null +++ b/test/bpf_double_reg.c @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Regression test for the io_uring BPF struct_ops double-registration UAF. + * + * The same io_uring_bpf_ops struct_ops map is registered twice. Between the + * two BPF_LINK_CREATEs the ring_fd is repointed at a second ring, so the + * second registration targets a *different* io_ring_ctx whose ->bpf_ops is + * NULL and thus passes the per-ctx guard in io_install_bpf(). Only the + * per-map ops->priv guard rejects it. Without that guard the first ctx is + * orphaned and its ctx->loop_step dangles into the freed struct_ops + * trampoline (use-after-free, called from io_run_loop()). + * + * The test only checks that the second registration is refused with -EBUSY; + * it never triggers the dangling call. + */ +#include <linux/stddef.h> +#include <errno.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/syscall.h> +#include <bpf/libbpf.h> + +#include "liburing.h" +#include "double_reg.skel.h" +#include "helpers.h" + +#ifndef __NR_io_uring_setup +#define __NR_io_uring_setup 425 +#endif + +#define CQ_ENTRIES 8 +#define SQ_ENTRIES 8 + +static struct double_reg_bpf *skel; + +/* A bare io_ring_ctx fd is all the struct_ops .reg path needs (fget()). */ +static int mk_ring(void) +{ + struct io_uring_params p; + + memset(&p, 0, sizeof(p)); + p.cq_entries = CQ_ENTRIES; + p.flags = IORING_SETUP_SINGLE_ISSUER | + IORING_SETUP_DEFER_TASKRUN | + IORING_SETUP_NO_SQARRAY | + IORING_SETUP_CQSIZE | + IORING_SETUP_SQ_REWIND; + return (int)syscall(__NR_io_uring_setup, SQ_ENTRIES, &p); +} + +static int test_double_reg(void) +{ + struct bpf_link *link1, *link2; + int ret, fd_a, fd_b, fd_keep; + + fd_a = mk_ring(); + if (fd_a < 0) { + if (fd_a == -EINVAL) + return T_EXIT_SKIP; + fprintf(stderr, "io_uring_setup: %d\n", fd_a); + return T_EXIT_FAIL; + } + + skel = double_reg_bpf__open(); + if (!skel) { + fprintf(stderr, "can't open skeleton\n"); + return T_EXIT_FAIL; + } + skel->struct_ops.reg_ops->ring_fd = fd_a; + + ret = double_reg_bpf__load(skel); + if (ret) { + if (ret == -ESRCH) { + printf("io_uring BPF ops are not supported\n"); + return T_EXIT_SKIP; + } + if (ret == -EPERM || ret == -EACCES) { + printf("no permission to load struct_ops, skip\n"); + return T_EXIT_SKIP; + } + fprintf(stderr, "failed to load skeleton: %d\n", ret); + return T_EXIT_FAIL; + } + + /* reg #1: binds the ops to fd_a's ctx, ops->priv = ctxA */ + link1 = bpf_map__attach_struct_ops(skel->maps.reg_ops); + if (!link1) { + fprintf(stderr, "first attach failed: %d\n", errno); + return T_EXIT_FAIL; + } + + /* + * Keep ctxA alive on another fd, then repoint fd_a at a brand new + * ring (ctxB). ops->ring_fd still holds the integer fd_a, which reg + * re-resolves with fget() on every call. + */ + fd_keep = dup(fd_a); + if (fd_keep < 0) { + perror("dup"); + ret = T_EXIT_FAIL; + goto destroy1; + } + fd_b = mk_ring(); + if (fd_b < 0) { + fprintf(stderr, "second io_uring_setup: %d\n", fd_b); + ret = T_EXIT_FAIL; + goto close_keep; + } + if (dup2(fd_b, fd_a) < 0) { + perror("dup2"); + ret = T_EXIT_FAIL; + goto close_b; + } + + /* + * reg #2: reg re-resolves fd_a to ctxB (->bpf_ops == NULL, passes the + * per-ctx check). It must be refused by the per-map ops->priv guard + * with -EBUSY. If it is accepted, ctxA is orphaned and the kernel is + * vulnerable. + */ + errno = 0; + link2 = bpf_map__attach_struct_ops(skel->maps.reg_ops); + if (link2) { + fprintf(stderr, "second registration was accepted; ctxA is orphaned (vulnerable)\n"); + bpf_link__destroy(link2); + ret = T_EXIT_FAIL; + } else if (errno == EBUSY) { + ret = T_EXIT_PASS; + } else { + fprintf(stderr, "second registration failed with %d (%s), expected EBUSY\n", + errno, strerror(errno)); + ret = T_EXIT_FAIL; + } + +close_b: + close(fd_b); +close_keep: + close(fd_keep); +destroy1: + bpf_link__destroy(link1); + close(fd_a); + return ret; +} + +int main(int argc, char *argv[]) +{ + int ret; + + if (argc > 1) + return T_EXIT_SKIP; + + ret = test_double_reg(); + + if (skel) + double_reg_bpf__destroy(skel); + return ret; +} -- 2.43.0 ^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops 2026-07-17 15:45 ` [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops Woraphat Khiaodaeng 2026-07-17 16:05 ` Gabriel Krisman Bertazi @ 2026-07-17 17:05 ` Pavel Begunkov 2026-07-17 17:05 ` Jens Axboe 2 siblings, 0 replies; 5+ messages in thread From: Pavel Begunkov @ 2026-07-17 17:05 UTC (permalink / raw) To: Woraphat Khiaodaeng, Jens Axboe; +Cc: io-uring, security On 7/17/26 16:45, Woraphat Khiaodaeng wrote: > io_install_bpf() only rejects a second registration on the ctx side > (ctx->bpf_ops) and sets the per-map back-pointer ops->priv > unconditionally. The struct_ops link path never advances a map past > BPF_STRUCT_OPS_STATE_READY, so the same io_uring_bpf_ops map can be > registered more than once, and bpf_io_reg() re-resolves the target ring > via fget(ops->ring_fd) on every call. A caller can therefore point the > same ring_fd at a different io_ring_ctx between two BPF_LINK_CREATE > calls. > > The second registration passes the ctx->bpf_ops check (the new ctx has > none) and overwrites ops->priv, orphaning the first ctx. Teardown > (io_eject_bpf()/bpf_io_unreg()) only reaches a ctx through ops->priv, so > the orphaned ctx is never torn down: its ctx->loop_step keeps pointing > into the struct_ops trampoline, which is freed once the map is gone. A > later io_uring_enter() on the orphaned ring then calls the dangling > ctx->loop_step from io_run_loop() -- a use-after-free of freed > executable memory, reachable by a task with CAP_BPF + CAP_PERFMON. > > Reject registration when ops->priv is already set, as hid_bpf_reg() > does for its struct_ops. > > Fixes: 98f37634b12b ("io_uring/bpf-ops: implement bpf ops registration") > Signed-off-by: Woraphat Khiaodaeng <worapat.kd2@gmail.com> Thanks for the patch. As mentioned, the patch is simple and should be safe to apply. Reviewed-by: Pavel Begunkov <asml.silence@gmail.com> > --- > io_uring/bpf-ops.c | 2 ++ > 1 file changed, 2 insertions(+) > > diff --git a/io_uring/bpf-ops.c b/io_uring/bpf-ops.c > index 5a50f0675..cf2bd068e 100644 > --- a/io_uring/bpf-ops.c > +++ b/io_uring/bpf-ops.c > @@ -168,6 +168,8 @@ static int io_install_bpf(struct io_ring_ctx *ctx, struct io_uring_bpf_ops *ops) > > if (ctx->bpf_ops) > return -EBUSY; > + if (ops->priv) > + return -EBUSY; > if (WARN_ON_ONCE(!ops->loop_step)) > return -EINVAL; > -- Pavel Begunkov ^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops 2026-07-17 15:45 ` [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops Woraphat Khiaodaeng 2026-07-17 16:05 ` Gabriel Krisman Bertazi 2026-07-17 17:05 ` [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops Pavel Begunkov @ 2026-07-17 17:05 ` Jens Axboe 2 siblings, 0 replies; 5+ messages in thread From: Jens Axboe @ 2026-07-17 17:05 UTC (permalink / raw) To: Pavel Begunkov, Woraphat Khiaodaeng; +Cc: io-uring, security On Fri, 17 Jul 2026 22:45:37 +0700, Woraphat Khiaodaeng wrote: > io_install_bpf() only rejects a second registration on the ctx side > (ctx->bpf_ops) and sets the per-map back-pointer ops->priv > unconditionally. The struct_ops link path never advances a map past > BPF_STRUCT_OPS_STATE_READY, so the same io_uring_bpf_ops map can be > registered more than once, and bpf_io_reg() re-resolves the target ring > via fget(ops->ring_fd) on every call. A caller can therefore point the > same ring_fd at a different io_ring_ctx between two BPF_LINK_CREATE > calls. > > [...] Applied, thanks! [1/1] io_uring/bpf-ops: reject re-registration of an already-bound ops commit: 83d2280089bd45e5f782011275b5c7414d85cb2b Best regards, -- Jens Axboe ^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-07-18 2:32 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <PAVEL_MESSAGE_ID>
2026-07-17 15:45 ` [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops Woraphat Khiaodaeng
2026-07-17 16:05 ` Gabriel Krisman Bertazi
2026-07-18 2:30 ` [PATCH] test: add io_uring bpf-ops double-registration regression test Woraphat Khiaodaeng
2026-07-17 17:05 ` [PATCH] io_uring/bpf-ops: reject re-registration of an already-bound ops Pavel Begunkov
2026-07-17 17:05 ` Jens Axboe
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox