[PATCH] test: add io_uring bpf-ops double-registration regression test
Woraphat Khiaodaeng <[email protected]> Sat, 18 Jul 2026 09:30:41 +0700
| Newsgroups | org.kernel.vger.io-uring |
|---|---|
| Message-ID | <[email protected]> |
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 <[email protected]> --- 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