#include "../config-host.h" /* SPDX-License-Identifier: MIT */ /* * Description: test per-task io_uring restrictions * * Per-task restrictions are registered via io_uring_register(2) with fd=-1 * and IORING_REGISTER_RESTRICTIONS opcode. Once registered, they apply to * all rings created by that task and are inherited across fork. */ #include #include #include #include #include #include #include #include #include "liburing.h" #include "helpers.h" #include "../src/syscall.h" static int register_task_restrictions(struct io_uring_restriction *res, unsigned int nr_res) { struct { __u16 flags; __u16 nr_res; __u32 resv[3]; struct io_uring_restriction restrictions[]; } *arg; size_t sz; int ret; sz = sizeof(*arg) + nr_res * sizeof(struct io_uring_restriction); arg = calloc(1, sz); if (!arg) return -ENOMEM; arg->nr_res = nr_res; memcpy(arg->restrictions, res, nr_res * sizeof(*res)); ret = __sys_io_uring_register(-1, IORING_REGISTER_RESTRICTIONS, arg, 1); free(arg); return ret; } /* * Test that per-task restrictions restrict SQE ops on newly created rings */ static int test_task_restrict_sqe_op(void) { struct io_uring_restriction res[1]; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; struct io_uring ring; int ret; /* Allow only NOP */ res[0].opcode = IORING_RESTRICTION_SQE_OP; res[0].sqe_op = IORING_OP_NOP; /* * Task restrictions need to be tested in a child process since * once set they can't be removed for the current task. */ pid_t pid = fork(); if (pid < 0) { perror("fork"); return T_EXIT_FAIL; } if (pid == 0) { /* Child: set no_new_privs (required like seccomp) */ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); /* Register task restriction, then create ring */ ret = register_task_restrictions(res, 1); if (ret == -EINVAL || ret == -EBADF) { /* Kernel doesn't support per-task restrictions */ _exit(T_EXIT_SKIP); } if (ret) { fprintf(stderr, "register task restrictions: %d\n", ret); _exit(T_EXIT_FAIL); } /* Create a new ring - should inherit task restrictions */ ret = io_uring_queue_init(8, &ring, 0); if (ret) { fprintf(stderr, "ring setup failed: %d\n", ret); _exit(T_EXIT_FAIL); } /* NOP should be allowed */ sqe = io_uring_get_sqe(&ring); io_uring_prep_nop(sqe); sqe->user_data = 1; /* Read should be denied */ sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, 0, NULL, 0, 0); sqe->user_data = 2; ret = io_uring_submit(&ring); if (ret != 2) { fprintf(stderr, "submit: %d\n", ret); _exit(T_EXIT_FAIL); } for (int i = 0; i < 2; i++) { ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "wait: %d\n", ret); _exit(T_EXIT_FAIL); } switch (cqe->user_data) { case 1: /* nop - should succeed */ if (cqe->res != 0) { fprintf(stderr, "nop res: %d\n", cqe->res); _exit(T_EXIT_FAIL); } break; case 2: /* read - should be denied */ if (cqe->res != -EACCES) { fprintf(stderr, "read res: %d (expected -EACCES)\n", cqe->res); _exit(T_EXIT_FAIL); } break; } io_uring_cqe_seen(&ring, cqe); } io_uring_queue_exit(&ring); _exit(T_EXIT_PASS); } /* Parent: wait for child */ int status; waitpid(pid, &status, 0); if (!WIFEXITED(status)) return T_EXIT_FAIL; return WEXITSTATUS(status); } /* * Test that per-task restrictions are inherited across fork */ static int test_task_restrict_fork_inherit(void) { struct io_uring_restriction res[2]; int ret; /* Allow only NOP and WRITE */ res[0].opcode = IORING_RESTRICTION_SQE_OP; res[0].sqe_op = IORING_OP_NOP; res[1].opcode = IORING_RESTRICTION_SQE_OP; res[1].sqe_op = IORING_OP_WRITE; pid_t pid = fork(); if (pid < 0) { perror("fork"); return T_EXIT_FAIL; } if (pid == 0) { /* Child: set no_new_privs (required like seccomp) */ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); /* Register task restriction then fork again */ ret = register_task_restrictions(res, 2); if (ret == -EINVAL) { _exit(T_EXIT_SKIP); } if (ret) { fprintf(stderr, "register task restrictions: %d\n", ret); _exit(T_EXIT_FAIL); } /* Fork a grandchild - restrictions should be inherited */ pid_t gpid = fork(); if (gpid < 0) { perror("fork"); _exit(T_EXIT_FAIL); } if (gpid == 0) { /* Grandchild: create ring and verify restrictions */ struct io_uring ring; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; ret = io_uring_queue_init(8, &ring, 0); if (ret) { fprintf(stderr, "grandchild ring setup: %d\n", ret); _exit(T_EXIT_FAIL); } /* NOP should be allowed */ sqe = io_uring_get_sqe(&ring); io_uring_prep_nop(sqe); sqe->user_data = 1; /* Read should be denied */ sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, 0, NULL, 0, 0); sqe->user_data = 2; ret = io_uring_submit(&ring); if (ret != 2) { fprintf(stderr, "grandchild submit: %d\n", ret); _exit(T_EXIT_FAIL); } for (int i = 0; i < 2; i++) { ret = io_uring_wait_cqe(&ring, &cqe); if (ret) { fprintf(stderr, "grandchild wait: %d\n", ret); _exit(T_EXIT_FAIL); } switch (cqe->user_data) { case 1: if (cqe->res != 0) { fprintf(stderr, "grandchild nop: %d\n", cqe->res); _exit(T_EXIT_FAIL); } break; case 2: if (cqe->res != -EACCES) { fprintf(stderr, "grandchild read: %d\n", cqe->res); _exit(T_EXIT_FAIL); } break; } io_uring_cqe_seen(&ring, cqe); } io_uring_queue_exit(&ring); _exit(T_EXIT_PASS); } int status; waitpid(gpid, &status, 0); if (!WIFEXITED(status)) _exit(T_EXIT_FAIL); _exit(WEXITSTATUS(status)); } int status; waitpid(pid, &status, 0); if (!WIFEXITED(status)) return T_EXIT_FAIL; return WEXITSTATUS(status); } /* * Test that registering task restrictions twice fails */ static int test_task_restrict_double_register(void) { struct io_uring_restriction res[1]; int ret; res[0].opcode = IORING_RESTRICTION_SQE_OP; res[0].sqe_op = IORING_OP_NOP; pid_t pid = fork(); if (pid < 0) { perror("fork"); return T_EXIT_FAIL; } if (pid == 0) { prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); ret = register_task_restrictions(res, 1); if (ret == -EINVAL) { _exit(T_EXIT_SKIP); } if (ret) { fprintf(stderr, "first register: %d\n", ret); _exit(T_EXIT_FAIL); } /* Second registration should fail with -EPERM */ ret = register_task_restrictions(res, 1); if (ret != -EPERM) { fprintf(stderr, "second register: %d (expected -EPERM)\n", ret); _exit(T_EXIT_FAIL); } _exit(T_EXIT_PASS); } int status; waitpid(pid, &status, 0); if (!WIFEXITED(status)) return T_EXIT_FAIL; return WEXITSTATUS(status); } /* * Test per-task register op restrictions */ static int test_task_restrict_register_op(void) { struct io_uring_restriction res[1]; int ret; /* Allow only IORING_REGISTER_FILES */ res[0].opcode = IORING_RESTRICTION_REGISTER_OP; res[0].register_op = IORING_REGISTER_FILES; pid_t pid = fork(); if (pid < 0) { perror("fork"); return T_EXIT_FAIL; } if (pid == 0) { prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); ret = register_task_restrictions(res, 1); if (ret == -EINVAL) { _exit(T_EXIT_SKIP); } if (ret) { fprintf(stderr, "register task restrictions: %d\n", ret); _exit(T_EXIT_FAIL); } struct io_uring ring; int pipe1[2]; if (pipe(pipe1) != 0) { perror("pipe"); _exit(T_EXIT_FAIL); } ret = io_uring_queue_init(8, &ring, 0); if (ret) { fprintf(stderr, "ring setup: %d\n", ret); _exit(T_EXIT_FAIL); } /* Register files should be allowed */ ret = io_uring_register_files(&ring, pipe1, 2); if (ret) { fprintf(stderr, "register files: %d\n", ret); _exit(T_EXIT_FAIL); } /* Register buffers should be denied */ uint64_t ptr; struct iovec vec = { .iov_base = &ptr, .iov_len = sizeof(ptr) }; ret = io_uring_register_buffers(&ring, &vec, 1); if (ret != -EACCES) { fprintf(stderr, "register buffers: %d (expected -EACCES)\n", ret); _exit(T_EXIT_FAIL); } io_uring_queue_exit(&ring); close(pipe1[0]); close(pipe1[1]); _exit(T_EXIT_PASS); } int status; waitpid(pid, &status, 0); if (!WIFEXITED(status)) return T_EXIT_FAIL; return WEXITSTATUS(status); } int main(int argc, char *argv[]) { int ret; if (argc > 1) return T_EXIT_SKIP; ret = test_task_restrict_sqe_op(); if (ret == T_EXIT_SKIP) { printf("Per-task restrictions not supported, skipping\n"); return T_EXIT_SKIP; } else if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_task_restrict_sqe_op failed\n"); return T_EXIT_FAIL; } ret = test_task_restrict_fork_inherit(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_task_restrict_fork_inherit failed\n"); return T_EXIT_FAIL; } ret = test_task_restrict_double_register(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_task_restrict_double_register failed\n"); return T_EXIT_FAIL; } ret = test_task_restrict_register_op(); if (ret == T_EXIT_FAIL) { fprintf(stderr, "test_task_restrict_register_op failed\n"); return T_EXIT_FAIL; } return T_EXIT_PASS; }