#include "../config-host.h" /* SPDX-License-Identifier: MIT */ /* * Test classic BPF (cBPF) filtering for io_uring operations. * * This test demonstrates using cBPF filters to restrict io_uring operations. * Unlike eBPF which requires a separate compiled program, cBPF filters can * be defined inline as an array of sock_filter instructions. */ #include #include #include #include #include #include #include #include #include #include #include #include "liburing.h" #include "liburing/io_uring/bpf_filter.h" #include "helpers.h" #ifndef RESOLVE_IN_ROOT #define RESOLVE_IN_ROOT 0x10 #endif /* * cBPF filter context layout (struct io_uring_bpf_ctx): * offset 0: user_data (u64) * offset 8: opcode (u8) * offset 9: sqe_flags (u8) * offset 10: pdu_size (u8) * offset 11: pad[5] * offset 16: union (socket: family/type/protocol at 16/20/24) * (open: flags/mode/resolve at 16/24/32 - all u64) */ #define CTX_OFF_USER_DATA 0 #define CTX_OFF_OPCODE 8 #define CTX_OFF_SQE_FLAGS 9 #define CTX_OFF_SOCKET_FAMILY 16 #define CTX_OFF_SOCKET_TYPE 20 #define CTX_OFF_SOCKET_PROTO 24 #define CTX_OFF_OPEN_FLAGS 16 /* u64, use low 32 bits */ #define CTX_OFF_OPEN_MODE 24 /* u64 */ #define CTX_OFF_OPEN_RESOLVE 32 /* u64, use low 32 bits */ /* * connect: family @16 (u32), port @20 (__be16) + 2 pad, * v4_addr @24 (__be32) / v6_addr @24 (u8[16]). * pdu_size = 24 (one __u32 + one __be16 + 2 pad + 16 bytes). * v6_addr is 16 bytes, accessed as four 4-byte words at offsets 24, * 28, 32, 36 via BPF_LD|BPF_W|BPF_ABS. */ #define CTX_OFF_CONNECT_FAMILY 16 #define CTX_OFF_CONNECT_PORT 20 #define CTX_OFF_CONNECT_V4_ADDR 24 #define CTX_OFF_CONNECT_V6_ADDR_W0 24 /* v6 bytes 0-3 */ #define CTX_OFF_CONNECT_V6_ADDR_W1 28 /* v6 bytes 4-7 */ #define CTX_OFF_CONNECT_V6_ADDR_W2 32 /* v6 bytes 8-11 */ #define CTX_OFF_CONNECT_V6_ADDR_W3 36 /* v6 bytes 12-15 */ #define CONNECT_PDU_SIZE 24 /* * Compile-time __be16 swap. htons() is a function call and is not * usable in static initializers like BPF_JUMP K constants. */ #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ # define CT_HTONS(x) ((__u16)(x)) #else # define CT_HTONS(x) ((__u16)((((x) & 0xff) << 8) | (((x) >> 8) & 0xff))) #endif /* * Compile-time K-constant for matching the __be16 port field via a * BPF_LD|BPF_W|BPF_ABS load at CTX_OFF_CONNECT_PORT. The kernel * populator writes port (__be16) at offset 20 with 2 zero pad bytes * at offset 22-23, and bpf_prog_run reads in native host byte order. * On LE the port lands in the low 16 bits; on BE the port lands in * the high 16 bits. Pad bytes are guaranteed zero by the framework's * memset, so no AND-mask is required. */ #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ # define CT_PORT_K(p) ((__u32)(p) << 16) #else # define CT_PORT_K(p) ((__u32)CT_HTONS(p)) #endif /* * Compile-time K-constant for matching a 4-byte address slice (one v4 * address, one dword of a v6 address, or a /N subnet mask/base) via a * BPF_LD|BPF_W|BPF_ABS load. Pass the bytes in their on-the-wire * (network byte order) order; the macro emits the host-order u32 that * the BPF interpreter will see after loading those bytes. */ #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ # define CT_ADDR_K(a, b, c, d) \ (((__u32)(a) << 24) | ((__u32)(b) << 16) | ((__u32)(c) << 8) | (__u32)(d)) #else # define CT_ADDR_K(a, b, c, d) \ (((__u32)(d) << 24) | ((__u32)(c) << 16) | ((__u32)(b) << 8) | (__u32)(a)) #endif /* * Simple cBPF filter that allows all operations. * Returns 1 (non-zero) to allow. */ static struct sock_filter allow_all_filter[] = { /* return 1 (allow) */ BPF_STMT(BPF_RET | BPF_K, 1), }; /* * Simple cBPF filter that denies all operations. * Returns 0 to deny. */ static struct sock_filter deny_all_filter[] = { /* return 0 (deny) */ BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that only allows AF_INET sockets (denies AF_INET6, etc). * Checks the socket family field in the context. */ static struct sock_filter allow_inet_only_filter[] = { /* Load socket family (32-bit at offset 16) */ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_SOCKET_FAMILY), /* Jump if family == AF_INET (2), allow; else deny */ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1), /* Allow: return 1 */ BPF_STMT(BPF_RET | BPF_K, 1), /* Deny: return 0 */ BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that only allows TCP sockets (SOCK_STREAM). */ static struct sock_filter allow_tcp_only_filter[] = { /* Load socket type (32-bit at offset 20) */ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_SOCKET_TYPE), /* Mask off SOCK_CLOEXEC/SOCK_NONBLOCK flags */ BPF_STMT(BPF_ALU | BPF_AND | BPF_K, 0xf), /* Jump if type == SOCK_STREAM (1), allow; else deny */ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, SOCK_STREAM, 0, 1), /* Allow: return 1 */ BPF_STMT(BPF_RET | BPF_K, 1), /* Deny: return 0 */ BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that denies O_CREAT flag for openat operations. * Checks the flags field in the open context. */ static struct sock_filter deny_o_creat_filter[] = { /* Load open flags (low 32 bits at offset 16) */ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_OPEN_FLAGS), /* Check if O_CREAT bit is set */ BPF_STMT(BPF_ALU | BPF_AND | BPF_K, O_CREAT), /* If result is non-zero (O_CREAT set), deny */ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 0), /* Deny: return 0 */ BPF_STMT(BPF_RET | BPF_K, 0), /* Allow: return 1 */ BPF_STMT(BPF_RET | BPF_K, 1), }; /* * cBPF filter that denies RESOLVE_IN_ROOT flag for openat2 operations. * Checks the resolve field in the open context. */ static struct sock_filter deny_resolve_in_root_filter[] = { /* Load resolve flags (low 32 bits at offset 32) */ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_OPEN_RESOLVE), /* Check if RESOLVE_IN_ROOT bit is set */ BPF_STMT(BPF_ALU | BPF_AND | BPF_K, RESOLVE_IN_ROOT), /* If result is non-zero (RESOLVE_IN_ROOT set), deny */ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 1, 0), /* Deny: return 0 */ BPF_STMT(BPF_RET | BPF_K, 0), /* Allow: return 1 */ BPF_STMT(BPF_RET | BPF_K, 1), }; /* * cBPF filter that allows only AF_INET CONNECTs and denies everything * else (a family-whitelist of AF_INET). */ static struct sock_filter connect_allow_family_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that denies AF_UNIX CONNECTs and allows everything else * (a family-blacklist of AF_UNIX). */ static struct sock_filter connect_deny_family_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_UNIX, 1, 0), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * Deny AF_INET CONNECTs to 127.0.0.127 and allow the rest. The test * address is byte-palindromic, so the K constant is endian-symmetric * and CT_ADDR_K() is not needed here. */ static struct sock_filter connect_deny_v4_addr_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 2), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V4_ADDR), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x7f00007f, 1, 0), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * Deny AF_INET CONNECTs to port 22 and allow the rest. Non-AF_INET * traffic falls through to allow. Matches the port via CT_PORT_K(). */ static struct sock_filter connect_deny_port_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 3), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_PORT), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_PORT_K(22), 0, 1), BPF_STMT(BPF_RET | BPF_K, 0), BPF_STMT(BPF_RET | BPF_K, 1), }; /* * cBPF filter that denies AF_INET CONNECTs outright. Used by the * stale-cache test: poisons the async msghdr with valid * AF_INET state, then submits a short-len CONNECT and verifies the * second one does NOT inherit AF_INET. When the framework zero-fill * remains intact (the populator returns early via the addr_len * guard), the filter sees family=0, falls through to allow, and the * kernel net path returns -EINVAL for the short addr_len. */ static struct sock_filter connect_deny_inet_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 1), BPF_STMT(BPF_RET | BPF_K, 0), BPF_STMT(BPF_RET | BPF_K, 1), }; /* * cBPF filter that allows only AF_INET CONNECTs to 127.0.0.1 and * denies everything else (a v4-address whitelist). */ static struct sock_filter connect_allow_v4_addr_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 3), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V4_ADDR), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(127, 0, 0, 1), 0, 1), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * Deny AF_INET6 CONNECTs to 2001:db8::dead and allow the rest. * Walks the v6 address as four 4-byte word loads at offsets 24, 28, * 32, 36. */ static struct sock_filter connect_deny_v6_addr_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET6, 0, 8), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W0), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(0x20, 0x01, 0x0d, 0xb8), 0, 6), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W1), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 4), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W2), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 2), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W3), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(0, 0, 0xde, 0xad), 1, 0), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * Allow only AF_INET6 CONNECTs to ::1 and deny everything else. Walks * the v6 address as four 4-byte word loads at offsets 24, 28, 32, 36. */ static struct sock_filter connect_allow_v6_addr_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET6, 0, 9), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W0), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 7), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W1), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 5), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W2), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 0, 3), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W3), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(0, 0, 0, 1), 0, 1), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that allows only AF_INET CONNECTs to port 80 and denies * everything else (a port whitelist). */ static struct sock_filter connect_allow_port_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 3), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_PORT), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_PORT_K(80), 0, 1), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * Deny AF_INET CONNECTs in 127.42.0.0/24 and allow the rest. CIDR * matching via load-mask-compare on the v4 address. */ static struct sock_filter connect_deny_v4_subnet_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 3), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V4_ADDR), BPF_STMT(BPF_ALU | BPF_AND | BPF_K, CT_ADDR_K(0xff, 0xff, 0xff, 0x00)), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(127, 42, 0, 0), 1, 0), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that allows only AF_INET CONNECTs in the 127.0.0.0/24 * subnet and denies everything else (a v4 subnet whitelist). */ static struct sock_filter connect_allow_v4_subnet_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET, 0, 4), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V4_ADDR), BPF_STMT(BPF_ALU | BPF_AND | BPF_K, CT_ADDR_K(0xff, 0xff, 0xff, 0x00)), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(127, 0, 0, 0), 0, 1), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that denies AF_INET6 CONNECTs in the 2001:db8::/32 * subnet and allows everything else. /32 falls on a word boundary, so * an exact-match JEQ on the first v6 word suffices. */ static struct sock_filter connect_deny_v6_subnet_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET6, 0, 2), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W0), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(0x20, 0x01, 0x0d, 0xb8), 1, 0), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* * cBPF filter that allows only AF_INET6 CONNECTs in the fe80::/16 * subnet (link-local) and denies everything else. /16 falls within * the first v6 word, so we AND-mask the first 16 bits and compare. */ static struct sock_filter connect_allow_v6_subnet_filter[] = { BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_FAMILY), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AF_INET6, 0, 4), BPF_STMT(BPF_LD | BPF_W | BPF_ABS, CTX_OFF_CONNECT_V6_ADDR_W0), BPF_STMT(BPF_ALU | BPF_AND | BPF_K, CT_ADDR_K(0xff, 0xff, 0x00, 0x00)), BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, CT_ADDR_K(0xfe, 0x80, 0x00, 0x00), 0, 1), BPF_STMT(BPF_RET | BPF_K, 1), BPF_STMT(BPF_RET | BPF_K, 0), }; /* Register a BPF filter on a task */ static int register_bpf_filter(struct sock_filter *filter, unsigned int len, __u32 opcode, __u8 pdu_size, int deny_rest) { struct io_uring_bpf bpf = { .cmd_type = IO_URING_BPF_CMD_FILTER, .filter = { .opcode = opcode, .flags = deny_rest ? IO_URING_BPF_FILTER_DENY_REST : 0, .filter_len = len, .filter_ptr = (unsigned long) (uintptr_t) filter, .pdu_size = pdu_size, }, }; return io_uring_register_bpf_filter_task(&bpf); } /* Register a BPF filter on a ring */ static int register_bpf_filter_ring(struct io_uring *ring, struct sock_filter *filter, unsigned int len, __u32 opcode, __u8 pdu_size, int deny_rest) { struct io_uring_bpf bpf = { .cmd_type = IO_URING_BPF_CMD_FILTER, .filter = { .opcode = opcode, .flags = deny_rest ? IO_URING_BPF_FILTER_DENY_REST : 0, .filter_len = len, .filter_ptr = (unsigned long) (uintptr_t) filter, .pdu_size = pdu_size, }, }; return io_uring_register_bpf_filter(ring, &bpf); } /* Test NOP operation */ static int test_nop(struct io_uring *ring, const char *desc, int should_succeed) { struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; int ret; sqe = io_uring_get_sqe(ring); if (!sqe) { printf("FAIL (get_sqe)\n"); return -1; } io_uring_prep_nop(sqe); sqe->user_data = 0x1234; ret = io_uring_submit(ring); if (ret < 0) { printf("FAIL (submit: %s)\n", strerror(-ret)); return ret; } ret = io_uring_wait_cqe(ring, &cqe); if (ret < 0) { printf("FAIL (wait: %s)\n", strerror(-ret)); return ret; } if (should_succeed) { if (cqe->res >= 0) { ret = 0; } else { printf("FAIL (expected success, got %s)\n", strerror(-cqe->res)); ret = -1; } } else { if (cqe->res == -EACCES) { ret = 0; } else { printf("FAIL (expected -EACCES, got %d)\n", cqe->res); ret = -1; } } if (ret) fprintf(stderr, "%s: %s: failed\n", __FUNCTION__, desc); io_uring_cqe_seen(ring, cqe); return ret; } /* Test socket operation */ static int test_socket(struct io_uring *ring, int family, int type, const char *desc, int should_succeed) { struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; int ret; sqe = io_uring_get_sqe(ring); io_uring_prep_socket(sqe, family, type, 0, 0); sqe->user_data = 0x5678; ret = io_uring_submit(ring); if (ret < 0) { printf("FAIL (submit: %s)\n", strerror(-ret)); return ret; } ret = io_uring_wait_cqe(ring, &cqe); if (ret < 0) { printf("FAIL (wait: %s)\n", strerror(-ret)); return ret; } if (should_succeed) { if (cqe->res >= 0) { close(cqe->res); ret = 0; } else { printf("FAIL (expected success, got %s)\n", strerror(-cqe->res)); ret = -1; } } else { if (cqe->res == -EACCES) { ret = 0; } else if (cqe->res < 0) { printf("FAIL (expected -EACCES, got %s)\n", strerror(-cqe->res)); ret = -1; } else { printf("FAIL (expected denial, got fd=%d)\n", cqe->res); close(cqe->res); ret = -1; } } if (ret) fprintf(stderr, "%s: %s: failed\n", __FUNCTION__, desc); io_uring_cqe_seen(ring, cqe); return ret; } /* Test openat operation */ static int test_openat(struct io_uring *ring, const char *path, int flags, mode_t mode, const char *desc, int should_succeed) { struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; int ret; sqe = io_uring_get_sqe(ring); io_uring_prep_openat(sqe, AT_FDCWD, path, flags, mode); sqe->user_data = 0xabcd; ret = io_uring_submit(ring); if (ret < 0) { printf("FAIL (submit: %s)\n", strerror(-ret)); return ret; } ret = io_uring_wait_cqe(ring, &cqe); if (ret < 0) { printf("FAIL (wait: %s)\n", strerror(-ret)); return ret; } if (should_succeed) { if (cqe->res >= 0) { close(cqe->res); ret = 0; } else { printf("FAIL (expected success, got %s)\n", strerror(-cqe->res)); ret = -1; } } else { if (cqe->res == -EACCES) { ret = 0; } else if (cqe->res < 0) { printf("FAIL (expected -EACCES, got %s)\n", strerror(-cqe->res)); ret = -1; } else { printf("FAIL (expected denial, got fd=%d)\n", cqe->res); close(cqe->res); ret = -1; } } if (ret) fprintf(stderr, "%s: %s: failed\n", __FUNCTION__, desc); io_uring_cqe_seen(ring, cqe); return ret; } /* Test openat2 operation */ static int test_openat2(struct io_uring *ring, const char *path, struct open_how *how, const char *desc, int should_succeed) { struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; int ret; sqe = io_uring_get_sqe(ring); io_uring_prep_openat2(sqe, AT_FDCWD, path, how); sqe->user_data = 0xef01; ret = io_uring_submit(ring); if (ret < 0) { printf("FAIL (submit: %s)\n", strerror(-ret)); return ret; } ret = io_uring_wait_cqe(ring, &cqe); if (ret < 0) { printf("FAIL (wait: %s)\n", strerror(-ret)); return ret; } if (should_succeed) { if (cqe->res >= 0) { close(cqe->res); ret = 0; } else { printf("FAIL (expected success, got %s)\n", strerror(-cqe->res)); ret = -1; } } else { if (cqe->res == -EACCES) { ret = 0; } else if (cqe->res < 0) { printf("FAIL (expected -EACCES, got %s)\n", strerror(-cqe->res)); ret = -1; } else { printf("FAIL (expected denial, got fd=%d)\n", cqe->res); close(cqe->res); ret = -1; } } if (ret) fprintf(stderr, "%s: %s: failed\n", __FUNCTION__, desc); io_uring_cqe_seen(ring, cqe); return ret; } /* * Submit an IORING_OP_CONNECT to @sa/@slen. should_succeed == 1 means * the filter must allow the op through (cqe->res != -EACCES); the * connect itself may still fail, typically with -ECONNREFUSED on * closed loopback ports. Any non--EACCES result means the kernel net * path ran. should_succeed == 0 means the filter must deny * (cqe->res == -EACCES). The socket fd is consumed. */ static int test_connect(struct io_uring *ring, const struct sockaddr *sa, socklen_t slen, const char *desc, int should_succeed) { struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; int fd, ret; fd = socket(sa->sa_family, SOCK_STREAM, 0); if (fd < 0) { printf("FAIL (socket: %s)\n", strerror(errno)); return -1; } sqe = io_uring_get_sqe(ring); io_uring_prep_connect(sqe, fd, sa, slen); sqe->user_data = 0x9abc; ret = io_uring_submit(ring); if (ret < 0) { printf("FAIL (submit: %s)\n", strerror(-ret)); close(fd); return ret; } ret = io_uring_wait_cqe(ring, &cqe); if (ret < 0) { printf("FAIL (wait: %s)\n", strerror(-ret)); close(fd); return ret; } ret = 0; if (should_succeed && cqe->res == -EACCES) { printf("FAIL (expected allow, got -EACCES)\n"); ret = -1; } else if (!should_succeed && cqe->res != -EACCES) { printf("FAIL (expected -EACCES, got %s)\n", strerror(cqe->res < 0 ? -cqe->res : 0)); ret = -1; } if (ret) fprintf(stderr, "%s: %s: failed\n", __FUNCTION__, desc); io_uring_cqe_seen(ring, cqe); close(fd); return ret; } static int test_deny_nop(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; /* Fork to get fresh task restrictions */ pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* Child process */ ret = register_bpf_filter(deny_all_filter, sizeof(deny_all_filter) / sizeof(deny_all_filter[0]), IORING_OP_NOP, 0, 0); if (ret < 0) { fprintf(stderr, "Child: register failed\n"); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_nop(&ring, "NOP should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } /* Parent waits for child */ waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_allow_inet_only(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; /* Fork to get fresh task restrictions */ pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* Child process */ ret = register_bpf_filter(allow_inet_only_filter, sizeof(allow_inet_only_filter) / sizeof(allow_inet_only_filter[0]), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "Child: register failed\n"); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_socket(&ring, AF_INET, SOCK_STREAM, "AF_INET TCP should succeed", 1) != 0) failed++; if (test_socket(&ring, AF_INET6, SOCK_STREAM, "AF_INET6 TCP should be denied", 0) != 0) failed++; if (test_socket(&ring, AF_UNIX, SOCK_STREAM, "AF_UNIX should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } /* Parent waits for child */ waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_allow_tcp_only(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { ret = register_bpf_filter(allow_tcp_only_filter, sizeof(allow_tcp_only_filter) / sizeof(allow_tcp_only_filter[0]), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "Child: register failed\n"); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_socket(&ring, AF_INET, SOCK_STREAM, "TCP should succeed", 1) != 0) failed++; if (test_socket(&ring, AF_INET, SOCK_DGRAM, "UDP should be denied", 0) != 0) failed++; if (test_socket(&ring, AF_INET6, SOCK_STREAM, "IPv6 TCP should succeed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_deny_rest(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* Register allow filter for NOP with DENY_REST flag */ ret = register_bpf_filter(allow_all_filter, sizeof(allow_all_filter) / sizeof(allow_all_filter[0]), IORING_OP_NOP, 0, 1); /* deny_rest = true */ if (ret < 0) { fprintf(stderr, "Child: register failed\n"); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_nop(&ring, "NOP should succeed", 1) != 0) failed++; if (test_socket(&ring, AF_INET, SOCK_STREAM, "Socket should be denied (DENY_REST)", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test denying O_CREAT flag for IORING_OP_OPENAT. * Verifies the operation works before filter installation, * then fails with -EACCES after. */ static int test_deny_openat_creat(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; char tmpfile[] = "/tmp/cbpf_test_XXXXXX"; int tmpfd; /* Create a temp file path we can use for testing */ tmpfd = mkstemp(tmpfile); if (tmpfd < 0) { perror("mkstemp"); return 1; } close(tmpfd); unlink(tmpfile); pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* Test that O_CREAT works BEFORE installing filter */ ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_openat(&ring, tmpfile, O_CREAT | O_RDWR, 0644, "O_CREAT should succeed before filter", 1) != 0) failed++; /* Clean up created file */ unlink(tmpfile); /* Test that regular open (no O_CREAT) works */ if (test_openat(&ring, "/dev/null", O_RDONLY, 0, "regular open should succeed before filter", 1) != 0) failed++; io_uring_queue_exit(&ring); /* Now install the O_CREAT deny filter */ ret = register_bpf_filter(deny_o_creat_filter, sizeof(deny_o_creat_filter) / sizeof(deny_o_creat_filter[0]), IORING_OP_OPENAT, 24, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } /* Create new ring after filter is installed */ ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init 2 failed\n"); exit(1); } /* Test that O_CREAT is now denied */ if (test_openat(&ring, tmpfile, O_CREAT | O_RDWR, 0644, "O_CREAT should be denied after filter", 0) != 0) failed++; /* Test that regular open still works */ if (test_openat(&ring, "/dev/null", O_RDONLY, 0, "regular open should still succeed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test denying RESOLVE_IN_ROOT flag for IORING_OP_OPENAT2. * Verifies the operation works before filter installation, * then fails with -EACCES after. * * Note: RESOLVE_IN_ROOT requires a relative path since it treats dfd as root. * We use "." with O_DIRECTORY to test this. */ static int test_deny_openat2_resolve_in_root(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; struct open_how how_with_resolve = { .flags = O_RDONLY | O_DIRECTORY, .mode = 0, .resolve = RESOLVE_IN_ROOT, }; struct open_how how_normal = { .flags = O_RDONLY | O_DIRECTORY, .mode = 0, .resolve = 0, }; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* Test that RESOLVE_IN_ROOT works BEFORE installing filter */ ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_openat2(&ring, ".", &how_with_resolve, "RESOLVE_IN_ROOT should succeed before filter", 1) != 0) failed++; /* Test that normal openat2 works */ if (test_openat2(&ring, ".", &how_normal, "normal openat2 should succeed before filter", 1) != 0) failed++; io_uring_queue_exit(&ring); /* Now install the RESOLVE_IN_ROOT deny filter */ ret = register_bpf_filter(deny_resolve_in_root_filter, sizeof(deny_resolve_in_root_filter) / sizeof(deny_resolve_in_root_filter[0]), IORING_OP_OPENAT2, 24, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } /* Create new ring after filter is installed */ ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init 2 failed\n"); exit(1); } /* Test that RESOLVE_IN_ROOT is now denied */ if (test_openat2(&ring, ".", &how_with_resolve, "RESOLVE_IN_ROOT should be denied after filter", 0) != 0) failed++; /* Test that normal openat2 still works */ if (test_openat2(&ring, ".", &how_normal, "normal openat2 should still succeed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_allow_family(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in v4 = { .sin_family = AF_INET, .sin_port = htons(1), }; struct sockaddr_in6 v6 = { .sin6_family = AF_INET6, .sin6_port = htons(1), }; struct sockaddr_un un = { .sun_family = AF_UNIX }; v4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); v6.sin6_addr = in6addr_loopback; strncpy(un.sun_path, "/tmp/cbpf_filter_no_such_socket", sizeof(un.sun_path) - 1); ret = register_bpf_filter(connect_allow_family_filter, sizeof(connect_allow_family_filter) / sizeof(connect_allow_family_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&v4, sizeof(v4), "AF_INET should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&v6, sizeof(v6), "AF_INET6 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&un, sizeof(un), "AF_UNIX should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_deny_v4_addr(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in banned = { .sin_family = AF_INET, .sin_port = htons(1), }; struct sockaddr_in other = { .sin_family = AF_INET, .sin_port = htons(1), }; banned.sin_addr.s_addr = htonl(0x7f00007f); other.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ret = register_bpf_filter(connect_deny_v4_addr_filter, sizeof(connect_deny_v4_addr_filter) / sizeof(connect_deny_v4_addr_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&banned, sizeof(banned), "127.0.0.127 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&other, sizeof(other), "127.0.0.1 should be allowed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_deny_port(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in ssh = { .sin_family = AF_INET, .sin_port = htons(22), }; struct sockaddr_in http = { .sin_family = AF_INET, .sin_port = htons(80), }; ssh.sin_addr.s_addr = htonl(INADDR_LOOPBACK); http.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ret = register_bpf_filter(connect_deny_port_filter, sizeof(connect_deny_port_filter) / sizeof(connect_deny_port_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&ssh, sizeof(ssh), "port 22 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&http, sizeof(http), "port 80 should be allowed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test for io_connect_bpf_populate's addr_len handling. * Two kernel-side mechanisms cooperate: the framework's caller-side * memset in io_uring_populate_bpf_ctx() zero-fills bctx before the * populator runs, and the populator returns early when addr_len does * not cover the family discriminator (sizeof(sa_family_t)) so the * zero-fill stays intact. Step 1 poisons iomsg->addr with a denied * AF_INET CONNECT. Step 2 submits CONNECT with addr_len=1: the * filter must see family=0 and fall through to the kernel net path, * which returns -EINVAL for the sub-minimum addr_len. If the * populator read the stale AF_INET cache instead, the filter would * deny with -EACCES -- the failure mode this test catches. */ static int test_connect_stale_addr_len(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in sa = { .sin_family = AF_INET, .sin_port = htons(1), }; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe; int fd; sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ret = register_bpf_filter(connect_deny_inet_filter, sizeof(connect_deny_inet_filter) / sizeof(connect_deny_inet_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } /* * Step 1: poison iomsg->addr by submitting a fully-formed * AF_INET CONNECT. The submit path's move_addr_to_kernel() * copies the user sockaddr into the async msghdr before the * filter runs; the filter then denies based on the populated * family, leaving the AF_INET state cached in iomsg->addr. */ fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("stale: socket step1"); exit(1); } sqe = io_uring_get_sqe(&ring); if (!sqe) { fprintf(stderr, "stale: get_sqe step1 failed\n"); close(fd); exit(1); } io_uring_prep_connect(sqe, fd, (struct sockaddr *)&sa, sizeof(sa)); ret = io_uring_submit(&ring); if (ret < 0) { fprintf(stderr, "stale: submit step1: %s\n", strerror(-ret)); close(fd); exit(1); } ret = io_uring_wait_cqe(&ring, &cqe); if (ret < 0) { fprintf(stderr, "stale: wait step1: %s\n", strerror(-ret)); close(fd); exit(1); } if (cqe->res != -EACCES) { fprintf(stderr, "stale: poison expected -EACCES, got %d\n", cqe->res); failed++; } io_uring_cqe_seen(&ring, cqe); close(fd); /* * Step 2: short-len CONNECT. Without the guard, this would * reuse stale AF_INET from step 1 and be denied with * -EACCES. With the guard, the filter sees family=0, allows * the op through, and the kernel net path rejects the * sub-minimum addr_len with -EINVAL -- which is the * specific result we assert. */ fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) { perror("stale: socket step2"); exit(1); } sqe = io_uring_get_sqe(&ring); if (!sqe) { fprintf(stderr, "stale: get_sqe step2 failed\n"); close(fd); exit(1); } io_uring_prep_connect(sqe, fd, (struct sockaddr *)&sa, 1); ret = io_uring_submit(&ring); if (ret < 0) { fprintf(stderr, "stale: submit step2: %s\n", strerror(-ret)); close(fd); exit(1); } ret = io_uring_wait_cqe(&ring, &cqe); if (ret < 0) { fprintf(stderr, "stale: wait step2: %s\n", strerror(-ret)); close(fd); exit(1); } if (cqe->res != -EINVAL) { fprintf(stderr, "stale: short-len expected -EINVAL, got %d\n", cqe->res); failed++; } io_uring_cqe_seen(&ring, cqe); close(fd); io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_deny_family(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in v4 = { .sin_family = AF_INET, .sin_port = htons(1), }; struct sockaddr_in6 v6 = { .sin6_family = AF_INET6, .sin6_port = htons(1), }; struct sockaddr_un un = { .sun_family = AF_UNIX }; v4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); v6.sin6_addr = in6addr_loopback; strncpy(un.sun_path, "/tmp/cbpf_filter_no_such_socket", sizeof(un.sun_path) - 1); ret = register_bpf_filter(connect_deny_family_filter, sizeof(connect_deny_family_filter) / sizeof(connect_deny_family_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&v4, sizeof(v4), "AF_INET should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&v6, sizeof(v6), "AF_INET6 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&un, sizeof(un), "AF_UNIX should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_allow_v4_addr(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in allowed = { .sin_family = AF_INET, .sin_port = htons(80), }; struct sockaddr_in denied_a = { .sin_family = AF_INET, .sin_port = htons(80), }; struct sockaddr_in denied_b = { .sin_family = AF_INET, .sin_port = htons(80), }; allowed.sin_addr.s_addr = htonl(INADDR_LOOPBACK); denied_a.sin_addr.s_addr = htonl(0x7f00007f); denied_b.sin_addr.s_addr = htonl(0x7f000002); ret = register_bpf_filter(connect_allow_v4_addr_filter, sizeof(connect_allow_v4_addr_filter) / sizeof(connect_allow_v4_addr_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&allowed, sizeof(allowed), "127.0.0.1 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&denied_a, sizeof(denied_a), "127.0.0.127 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&denied_b, sizeof(denied_b), "127.0.0.2 should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test blacklisting the v6 address 2001:db8::dead for * IORING_OP_CONNECT. Other v6 addresses (including those sharing the * 2001:db8::/32 prefix) are allowed. Non-AF_INET6 sockaddrs fall * through to allow as well, since this is purely a v6-address * blacklist. */ static int test_connect_deny_v6_addr(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in6 banned = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 other_lo = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 other_doc = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; /* 2001:db8::dead -- banned */ banned.sin6_addr.s6_addr[0] = 0x20; banned.sin6_addr.s6_addr[1] = 0x01; banned.sin6_addr.s6_addr[2] = 0x0d; banned.sin6_addr.s6_addr[3] = 0xb8; banned.sin6_addr.s6_addr[14] = 0xde; banned.sin6_addr.s6_addr[15] = 0xad; /* ::1 -- loopback, outside the banned exact address */ other_lo.sin6_addr = in6addr_loopback; /* 2001:db8::1 -- same /32 prefix, different exact addr */ other_doc.sin6_addr.s6_addr[0] = 0x20; other_doc.sin6_addr.s6_addr[1] = 0x01; other_doc.sin6_addr.s6_addr[2] = 0x0d; other_doc.sin6_addr.s6_addr[3] = 0xb8; other_doc.sin6_addr.s6_addr[15] = 0x01; ret = register_bpf_filter(connect_deny_v6_addr_filter, sizeof(connect_deny_v6_addr_filter) / sizeof(connect_deny_v6_addr_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&banned, sizeof(banned), "2001:db8::dead should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&other_lo, sizeof(other_lo), "::1 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&other_doc, sizeof(other_doc), "2001:db8::1 should be allowed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_allow_v6_addr(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in6 allowed = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 denied_lo = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 denied_doc = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; allowed.sin6_addr = in6addr_loopback; /* ::2 -- test target */ denied_lo.sin6_addr.s6_addr[15] = 0x02; /* 2001:db8::1 -- test target */ denied_doc.sin6_addr.s6_addr[0] = 0x20; denied_doc.sin6_addr.s6_addr[1] = 0x01; denied_doc.sin6_addr.s6_addr[2] = 0x0d; denied_doc.sin6_addr.s6_addr[3] = 0xb8; denied_doc.sin6_addr.s6_addr[15] = 0x01; ret = register_bpf_filter(connect_allow_v6_addr_filter, sizeof(connect_allow_v6_addr_filter) / sizeof(connect_allow_v6_addr_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&allowed, sizeof(allowed), "::1 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&denied_lo, sizeof(denied_lo), "::2 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&denied_doc, sizeof(denied_doc), "2001:db8::1 should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_allow_port(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in allowed = { .sin_family = AF_INET, .sin_port = htons(80), }; struct sockaddr_in denied_ssh = { .sin_family = AF_INET, .sin_port = htons(22), }; struct sockaddr_in denied_https = { .sin_family = AF_INET, .sin_port = htons(443), }; allowed.sin_addr.s_addr = htonl(INADDR_LOOPBACK); denied_ssh.sin_addr.s_addr = htonl(INADDR_LOOPBACK); denied_https.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ret = register_bpf_filter(connect_allow_port_filter, sizeof(connect_allow_port_filter) / sizeof(connect_allow_port_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&allowed, sizeof(allowed), "port 80 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&denied_ssh, sizeof(denied_ssh), "port 22 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&denied_https, sizeof(denied_https), "port 443 should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_deny_v4_subnet(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in in_subnet_a = { .sin_family = AF_INET, .sin_port = htons(80), }; struct sockaddr_in in_subnet_b = { .sin_family = AF_INET, .sin_port = htons(80), }; struct sockaddr_in out_subnet = { .sin_family = AF_INET, .sin_port = htons(80), }; in_subnet_a.sin_addr.s_addr = htonl(0x7f2a0001); /* 127.42.0.1 */ in_subnet_b.sin_addr.s_addr = htonl(0x7f2a0063); /* 127.42.0.99 */ out_subnet.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ret = register_bpf_filter(connect_deny_v4_subnet_filter, sizeof(connect_deny_v4_subnet_filter) / sizeof(connect_deny_v4_subnet_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a), "127.42.0.1 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&in_subnet_b, sizeof(in_subnet_b), "127.42.0.99 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&out_subnet, sizeof(out_subnet), "127.0.0.1 should be allowed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_allow_v4_subnet(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in in_subnet_a = { .sin_family = AF_INET, .sin_port = htons(80), }; struct sockaddr_in in_subnet_b = { .sin_family = AF_INET, .sin_port = htons(80), }; struct sockaddr_in out_subnet = { .sin_family = AF_INET, .sin_port = htons(80), }; in_subnet_a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* 127.0.0.1 */ in_subnet_b.sin_addr.s_addr = htonl(0x7f000063); /* 127.0.0.99 */ out_subnet.sin_addr.s_addr = htonl(0x7f2a0001); /* 127.42.0.1 */ ret = register_bpf_filter(connect_allow_v4_subnet_filter, sizeof(connect_allow_v4_subnet_filter) / sizeof(connect_allow_v4_subnet_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a), "127.0.0.1 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&in_subnet_b, sizeof(in_subnet_b), "127.0.0.99 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&out_subnet, sizeof(out_subnet), "127.42.0.1 should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_deny_v6_subnet(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in6 in_subnet_a = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 in_subnet_b = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 out_subnet = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; /* 2001:db8::1 */ in_subnet_a.sin6_addr.s6_addr[0] = 0x20; in_subnet_a.sin6_addr.s6_addr[1] = 0x01; in_subnet_a.sin6_addr.s6_addr[2] = 0x0d; in_subnet_a.sin6_addr.s6_addr[3] = 0xb8; in_subnet_a.sin6_addr.s6_addr[15] = 0x01; /* 2001:db8:dead::1 -- same /32 prefix, different remainder */ in_subnet_b.sin6_addr.s6_addr[0] = 0x20; in_subnet_b.sin6_addr.s6_addr[1] = 0x01; in_subnet_b.sin6_addr.s6_addr[2] = 0x0d; in_subnet_b.sin6_addr.s6_addr[3] = 0xb8; in_subnet_b.sin6_addr.s6_addr[4] = 0xde; in_subnet_b.sin6_addr.s6_addr[5] = 0xad; in_subnet_b.sin6_addr.s6_addr[15] = 0x01; /* ::1 -- loopback, outside the /32 */ out_subnet.sin6_addr = in6addr_loopback; ret = register_bpf_filter(connect_deny_v6_subnet_filter, sizeof(connect_deny_v6_subnet_filter) / sizeof(connect_deny_v6_subnet_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a), "2001:db8::1 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&in_subnet_b, sizeof(in_subnet_b), "2001:db8:dead::1 should be denied", 0) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&out_subnet, sizeof(out_subnet), "::1 should be allowed", 1) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } static int test_connect_allow_v6_subnet(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { struct sockaddr_in6 in_subnet_a = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 in_subnet_b = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; struct sockaddr_in6 out_subnet = { .sin6_family = AF_INET6, .sin6_port = htons(80), }; /* fe80::1 */ in_subnet_a.sin6_addr.s6_addr[0] = 0xfe; in_subnet_a.sin6_addr.s6_addr[1] = 0x80; in_subnet_a.sin6_addr.s6_addr[15] = 0x01; /* fe80:cafe::beef -- same /16 prefix */ in_subnet_b.sin6_addr.s6_addr[0] = 0xfe; in_subnet_b.sin6_addr.s6_addr[1] = 0x80; in_subnet_b.sin6_addr.s6_addr[2] = 0xca; in_subnet_b.sin6_addr.s6_addr[3] = 0xfe; in_subnet_b.sin6_addr.s6_addr[14] = 0xbe; in_subnet_b.sin6_addr.s6_addr[15] = 0xef; /* 2001:db8::1 -- documentation prefix, outside fe80::/16 */ out_subnet.sin6_addr.s6_addr[0] = 0x20; out_subnet.sin6_addr.s6_addr[1] = 0x01; out_subnet.sin6_addr.s6_addr[2] = 0x0d; out_subnet.sin6_addr.s6_addr[3] = 0xb8; out_subnet.sin6_addr.s6_addr[15] = 0x01; ret = register_bpf_filter(connect_allow_v6_subnet_filter, sizeof(connect_allow_v6_subnet_filter) / sizeof(connect_allow_v6_subnet_filter[0]), IORING_OP_CONNECT, CONNECT_PDU_SIZE, 0); if (ret < 0) { fprintf(stderr, "Child: register failed: %s\n", strerror(-ret)); exit(ret == -EINVAL ? 0 : 1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Child: queue_init failed\n"); exit(1); } if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a), "fe80::1 should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&in_subnet_b, sizeof(in_subnet_b), "fe80:cafe::beef should be allowed", 1) != 0) failed++; if (test_connect(&ring, (struct sockaddr *)&out_subnet, sizeof(out_subnet), "2001:db8::1 should be denied", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Ring-level filter tests - these test filters registered on a specific ring * rather than on the task. Ring filters don't require forking. */ static int test_deny_nop_ring(void) { struct io_uring ring; int ret, failed = 0; ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "queue_init failed: %s\n", strerror(-ret)); return 1; } ret = register_bpf_filter_ring(&ring, deny_all_filter, ARRAY_SIZE(deny_all_filter), IORING_OP_NOP, 0, 0); if (ret < 0) { fprintf(stderr, "register failed: %s\n", strerror(-ret)); io_uring_queue_exit(&ring); return ret == -EINVAL ? 0 : 1; } if (test_nop(&ring, "NOP should be denied (ring)", 0) != 0) failed++; io_uring_queue_exit(&ring); return failed; } static int test_allow_inet_only_ring(void) { struct io_uring ring; int ret, failed = 0; ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "queue_init failed: %s\n", strerror(-ret)); return 1; } ret = register_bpf_filter_ring(&ring, allow_inet_only_filter, ARRAY_SIZE(allow_inet_only_filter), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "register failed: %s\n", strerror(-ret)); io_uring_queue_exit(&ring); return ret == -EINVAL ? 0 : 1; } if (test_socket(&ring, AF_INET, SOCK_STREAM, "AF_INET TCP should succeed (ring)", 1) != 0) failed++; if (test_socket(&ring, AF_INET6, SOCK_STREAM, "AF_INET6 TCP should be denied (ring)", 0) != 0) failed++; if (test_socket(&ring, AF_UNIX, SOCK_STREAM, "AF_UNIX should be denied (ring)", 0) != 0) failed++; io_uring_queue_exit(&ring); return failed; } static int test_allow_tcp_only_ring(void) { struct io_uring ring; int ret, failed = 0; ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "queue_init failed: %s\n", strerror(-ret)); return 1; } ret = register_bpf_filter_ring(&ring, allow_tcp_only_filter, ARRAY_SIZE(allow_tcp_only_filter), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "register failed: %s\n", strerror(-ret)); io_uring_queue_exit(&ring); return ret == -EINVAL ? 0 : 1; } if (test_socket(&ring, AF_INET, SOCK_STREAM, "TCP should succeed (ring)", 1) != 0) failed++; if (test_socket(&ring, AF_INET, SOCK_DGRAM, "UDP should be denied (ring)", 0) != 0) failed++; if (test_socket(&ring, AF_INET6, SOCK_STREAM, "IPv6 TCP should succeed (ring)", 1) != 0) failed++; io_uring_queue_exit(&ring); return failed; } static int test_deny_rest_ring(void) { struct io_uring ring; int ret, failed = 0; ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "queue_init failed: %s\n", strerror(-ret)); return 1; } /* Register allow filter for NOP with DENY_REST flag */ ret = register_bpf_filter_ring(&ring, allow_all_filter, ARRAY_SIZE(allow_all_filter), IORING_OP_NOP, 0, 1); if (ret < 0) { fprintf(stderr, "register failed: %s\n", strerror(-ret)); io_uring_queue_exit(&ring); return ret == -EINVAL ? 0 : 1; } if (test_nop(&ring, "NOP should succeed (ring)", 1) != 0) failed++; if (test_socket(&ring, AF_INET, SOCK_STREAM, "Socket should be denied DENY_REST (ring)", 0) != 0) failed++; io_uring_queue_exit(&ring); return failed; } /* * Test pdu_size validation for filter registration. * * IORING_OP_SOCKET has a kernel pdu_size of 12 (3x __u32). Test: * 1) pdu_size too big (24) - should fail with -EMSGSIZE * 2) pdu_size too small (8) without strict - should succeed, kernel * writes back actual pdu_size (12) * 3) pdu_size too small (8) with IO_URING_BPF_FILTER_SZ_STRICT - * should fail with -EMSGSIZE, kernel writes back actual pdu_size (12) */ static int test_pdu_size_ring(void) { struct io_uring ring; struct io_uring_bpf bpf; int ret, failed = 0; ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "queue_init failed: %s\n", strerror(-ret)); return 1; } /* Test 1: pdu_size too big, should fail with -EMSGSIZE */ memset(&bpf, 0, sizeof(bpf)); bpf.cmd_type = IO_URING_BPF_CMD_FILTER; bpf.filter.opcode = IORING_OP_SOCKET; bpf.filter.filter_len = ARRAY_SIZE(allow_all_filter); bpf.filter.filter_ptr = (unsigned long) (uintptr_t) allow_all_filter; bpf.filter.pdu_size = 24; ret = io_uring_register_bpf_filter(&ring, &bpf); if (ret != -EMSGSIZE) { fprintf(stderr, "pdu too big: expected -EMSGSIZE, got %d\n", ret); failed++; } else if (bpf.filter.pdu_size != 12) { fprintf(stderr, "pdu too big: expected writeback 12, got %u\n", bpf.filter.pdu_size); failed++; } /* Test 2: pdu_size smaller without strict, should succeed */ memset(&bpf, 0, sizeof(bpf)); bpf.cmd_type = IO_URING_BPF_CMD_FILTER; bpf.filter.opcode = IORING_OP_SOCKET; bpf.filter.filter_len = ARRAY_SIZE(allow_all_filter); bpf.filter.filter_ptr = (unsigned long) (uintptr_t) allow_all_filter; bpf.filter.pdu_size = 8; ret = io_uring_register_bpf_filter(&ring, &bpf); if (ret) { fprintf(stderr, "pdu smaller no strict: expected success, " "got %d\n", ret); failed++; } else if (bpf.filter.pdu_size != 12) { fprintf(stderr, "pdu smaller no strict: expected writeback " "12, got %u\n", bpf.filter.pdu_size); failed++; } io_uring_queue_exit(&ring); /* * Test 3: pdu_size smaller with strict, should fail. * Use a fresh ring since test 2 registered a filter. */ ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "queue_init 2 failed: %s\n", strerror(-ret)); return 1; } memset(&bpf, 0, sizeof(bpf)); bpf.cmd_type = IO_URING_BPF_CMD_FILTER; bpf.filter.opcode = IORING_OP_SOCKET; bpf.filter.flags = IO_URING_BPF_FILTER_SZ_STRICT; bpf.filter.filter_len = ARRAY_SIZE(allow_all_filter); bpf.filter.filter_ptr = (unsigned long) (uintptr_t) allow_all_filter; bpf.filter.pdu_size = 8; ret = io_uring_register_bpf_filter(&ring, &bpf); if (ret != -EMSGSIZE) { fprintf(stderr, "pdu smaller strict: expected -EMSGSIZE, " "got %d\n", ret); failed++; } else if (bpf.filter.pdu_size != 12) { fprintf(stderr, "pdu smaller strict: expected writeback 12, " "got %u\n", bpf.filter.pdu_size); failed++; } io_uring_queue_exit(&ring); return failed; } /* * Test that child processes inherit parent's restrictions. * Parent registers a filter, forks, child verifies the restriction applies. */ static int test_inherit_restrictions(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* First child: register deny filter for NOP, then fork */ ret = register_bpf_filter(deny_all_filter, sizeof(deny_all_filter) / sizeof(deny_all_filter[0]), IORING_OP_NOP, 0, 0); if (ret < 0) { fprintf(stderr, "Child1: register failed: %s\n", strerror(-ret)); exit(1); } /* Fork grandchild to test inheritance */ pid_t grandchild = fork(); if (grandchild < 0) { perror("fork grandchild"); exit(1); } if (grandchild == 0) { /* Grandchild: should inherit parent's NOP denial */ ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Grandchild: queue_init failed\n"); exit(1); } /* NOP should be denied due to inherited restriction */ if (test_nop(&ring, "inherited NOP denial", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(grandchild, &status, 0); exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test that child can add new restrictions on top of inherited ones. * Parent allows only AF_INET, child adds TCP-only filter. * Result: only AF_INET + TCP should be allowed. */ static int test_stack_restrictions(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* First child: register AF_INET only filter */ ret = register_bpf_filter(allow_inet_only_filter, sizeof(allow_inet_only_filter) / sizeof(allow_inet_only_filter[0]), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "Child1: register failed: %s\n", strerror(-ret)); exit(1); } /* Fork grandchild to add more restrictions */ pid_t grandchild = fork(); if (grandchild < 0) { perror("fork grandchild"); exit(1); } if (grandchild == 0) { /* Grandchild: add TCP-only filter on top */ ret = register_bpf_filter(allow_tcp_only_filter, sizeof(allow_tcp_only_filter) / sizeof(allow_tcp_only_filter[0]), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "Grandchild: register failed: %s\n", strerror(-ret)); exit(1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Grandchild: queue_init failed\n"); exit(1); } /* AF_INET + TCP: allowed by both filters */ if (test_socket(&ring, AF_INET, SOCK_STREAM, "AF_INET TCP (both filters allow)", 1) != 0) failed++; /* AF_INET + UDP: allowed by parent, denied by child */ if (test_socket(&ring, AF_INET, SOCK_DGRAM, "AF_INET UDP (child denies)", 0) != 0) failed++; /* AF_INET6 + TCP: denied by parent, allowed by child */ if (test_socket(&ring, AF_INET6, SOCK_STREAM, "AF_INET6 TCP (parent denies)", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(grandchild, &status, 0); exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test that child cannot loosen parent's restrictions. * Parent denies NOP, child tries to allow it - should still be denied. */ static int test_cannot_loosen_restrictions(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* First child: deny NOP */ ret = register_bpf_filter(deny_all_filter, sizeof(deny_all_filter) / sizeof(deny_all_filter[0]), IORING_OP_NOP, 0, 0); if (ret < 0) { fprintf(stderr, "Child1: register failed: %s\n", strerror(-ret)); exit(1); } /* Fork grandchild that tries to allow NOP */ pid_t grandchild = fork(); if (grandchild < 0) { perror("fork grandchild"); exit(1); } if (grandchild == 0) { /* Grandchild: try to allow NOP (inherits no_new_privs) */ ret = register_bpf_filter(allow_all_filter, sizeof(allow_all_filter) / sizeof(allow_all_filter[0]), IORING_OP_NOP, 0, 0); if (ret < 0) { fprintf(stderr, "Grandchild: register failed: %s\n", strerror(-ret)); exit(1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Grandchild: queue_init failed\n"); exit(1); } /* * NOP should still be denied - child's allow filter * runs first (returns 1), but parent's deny filter * is stacked and runs second (returns 0). */ if (test_nop(&ring, "NOP still denied (can't loosen)", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(grandchild, &status, 0); exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test multi-level inheritance (parent -> child -> grandchild -> great-grandchild). * Each level adds more restrictions. */ static int test_multi_level_inherit(void) { struct io_uring ring; int ret, failed = 0; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { /* Level 1: allow only AF_INET for sockets */ ret = register_bpf_filter(allow_inet_only_filter, sizeof(allow_inet_only_filter) / sizeof(allow_inet_only_filter[0]), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "Level1: register failed\n"); exit(1); } pid_t level2 = fork(); if (level2 < 0) exit(1); if (level2 == 0) { /* Level 2: add TCP-only restriction */ ret = register_bpf_filter(allow_tcp_only_filter, sizeof(allow_tcp_only_filter) / sizeof(allow_tcp_only_filter[0]), IORING_OP_SOCKET, 12, 0); if (ret < 0) { fprintf(stderr, "Level2: register failed\n"); exit(1); } pid_t level3 = fork(); if (level3 < 0) exit(1); if (level3 == 0) { /* Level 3: allow NOP with DENY_REST */ ret = register_bpf_filter(allow_all_filter, sizeof(allow_all_filter) / sizeof(allow_all_filter[0]), IORING_OP_NOP, 0, 1); if (ret < 0) { fprintf(stderr, "Level3: register failed\n"); exit(1); } ret = io_uring_queue_init(8, &ring, 0); if (ret < 0) { fprintf(stderr, "Level3: queue_init failed\n"); exit(1); } /* NOP: allowed (explicit filter) */ if (test_nop(&ring, "NOP allowed", 1) != 0) failed++; /* * Socket still governed by inherited filters. * DENY_REST only denies opcodes with no filter * in the entire chain - ancestors have socket * filters so those still apply. * * AF_INET + TCP: allowed by both ancestors */ if (test_socket(&ring, AF_INET, SOCK_STREAM, "AF_INET TCP (inherited filters)", 1) != 0) failed++; /* AF_INET + UDP: denied by Level 2's TCP filter */ if (test_socket(&ring, AF_INET, SOCK_DGRAM, "AF_INET UDP (denied by L2)", 0) != 0) failed++; /* AF_INET6 + TCP: denied by Level 1's AF_INET filter */ if (test_socket(&ring, AF_INET6, SOCK_STREAM, "AF_INET6 TCP (denied by L1)", 0) != 0) failed++; io_uring_queue_exit(&ring); exit(failed); } waitpid(level3, &status, 0); exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1); } waitpid(level2, &status, 0); exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1); } waitpid(pid, &status, 0); if (WIFEXITED(status)) return WEXITSTATUS(status); return 1; } /* * Test that registering a filter without no_new_privs returns -EACCES. * This must be called before prctl(PR_SET_NO_NEW_PRIVS) in main(). */ static int test_no_new_privs_required(void) { struct io_uring_bpf io_bpf = { .cmd_type = IO_URING_BPF_CMD_FILTER, .filter = { .opcode = IORING_OP_NOP, .flags = 0, .filter_len = sizeof(allow_all_filter) / sizeof(allow_all_filter[0]), .filter_ptr = (unsigned long)allow_all_filter, }, }; pid_t pid; int status; pid = fork(); if (pid < 0) { perror("fork"); return 1; } if (pid == 0) { int ret; /* Try to register without no_new_privs - should fail with EACCES */ ret = io_uring_register(-1, IORING_REGISTER_BPF_FILTER, &io_bpf, 1); if (ret == -EACCES) { if (!geteuid()) exit(1); exit(0); /* Expected */ } else if (!ret) { if (!geteuid()) exit(0); exit(1); } if (ret == -EINVAL || ret == -ENOSYS) exit(2); /* Not supported */ fprintf(stderr, "Expected -EACCES, got %d\n", ret); exit(1); } waitpid(pid, &status, 0); if (WIFEXITED(status)) { int code = WEXITSTATUS(status); if (code == 0) return 0; /* Test passed */ if (code == 2) return -1; /* Not supported, skip */ } fprintf(stderr, "test_no_new_privs_required failed\n"); return 1; } static int probe_bpf_filter_support(void) { struct io_uring_bpf io_bpf = { .cmd_type = IO_URING_BPF_CMD_FILTER, .filter = { .opcode = IORING_OP_NOP, .flags = 0, .filter_len = sizeof(allow_all_filter) / sizeof(allow_all_filter[0]), .filter_ptr = (unsigned long)allow_all_filter, }, }; pid_t pid; int status; /* Fork so we don't pollute the main process */ pid = fork(); if (pid < 0) return -1; if (pid == 0) { int ret; ret = io_uring_register(-1, IORING_REGISTER_BPF_FILTER, &io_bpf, 1); exit(ret < 0 ? -ret : 0); } waitpid(pid, &status, 0); if (WIFEXITED(status)) { int code = WEXITSTATUS(status); if (code == EINVAL || code == ENOSYS) return -1; /* Not supported */ return 0; /* Supported (or other error we'll catch later) */ } return -1; } static int probe_connect_filter_support(void) { struct io_uring_bpf io_bpf = { .cmd_type = IO_URING_BPF_CMD_FILTER, .filter = { .opcode = IORING_OP_CONNECT, .flags = 0, .filter_len = sizeof(allow_all_filter) / sizeof(allow_all_filter[0]), .filter_ptr = (unsigned long)allow_all_filter, .pdu_size = CONNECT_PDU_SIZE, }, }; pid_t pid; int status; pid = fork(); if (pid < 0) return -1; if (pid == 0) { int ret; ret = io_uring_register(-1, IORING_REGISTER_BPF_FILTER, &io_bpf, 1); exit(ret < 0 ? -ret : 0); } waitpid(pid, &status, 0); if (WIFEXITED(status)) { int code = WEXITSTATUS(status); if (code == EMSGSIZE) return -1; /* No populator for IORING_OP_CONNECT */ return 0; /* Populator present (or unrelated error we will catch later) */ } return -1; } int main(int argc, char *argv[]) { int total_failed = 0; int ret; if (argc > 1) return T_EXIT_SKIP; if (probe_bpf_filter_support() < 0) return T_EXIT_SKIP; /* * Test that filter registration fails without no_new_privs. * Must run before we call prctl() below. */ ret = test_no_new_privs_required(); if (ret < 0) return T_EXIT_SKIP; if (ret > 0) total_failed++; /* * Must set no_new_privs to register BPF filters without CAP_SYS_ADMIN. * This is inherited by all child processes. */ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) { perror("prctl"); return T_EXIT_SKIP; } /* Task-level filter tests */ total_failed += test_deny_nop(); total_failed += test_allow_inet_only(); total_failed += test_allow_tcp_only(); total_failed += test_deny_rest(); /* Task-level openat/openat2 filter tests */ total_failed += test_deny_openat_creat(); total_failed += test_deny_openat2_resolve_in_root(); /* Task-level connect filter tests */ /* * Probe whether the kernel exposes a filter populator for * IORING_OP_CONNECT. */ if (probe_connect_filter_support() == 0) { total_failed += test_connect_allow_family(); total_failed += test_connect_deny_family(); total_failed += test_connect_deny_v4_addr(); total_failed += test_connect_deny_port(); total_failed += test_connect_stale_addr_len(); total_failed += test_connect_allow_v4_addr(); total_failed += test_connect_deny_v6_addr(); total_failed += test_connect_allow_v6_addr(); total_failed += test_connect_allow_port(); total_failed += test_connect_deny_v4_subnet(); total_failed += test_connect_allow_v4_subnet(); total_failed += test_connect_deny_v6_subnet(); total_failed += test_connect_allow_v6_subnet(); } /* Ring-level filter tests */ total_failed += test_deny_nop_ring(); total_failed += test_allow_inet_only_ring(); total_failed += test_allow_tcp_only_ring(); total_failed += test_deny_rest_ring(); total_failed += test_pdu_size_ring(); /* Per-task inheritance tests */ total_failed += test_inherit_restrictions(); total_failed += test_stack_restrictions(); total_failed += test_cannot_loosen_restrictions(); total_failed += test_multi_level_inherit(); return total_failed; }