public inbox for io-uring@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH liburing] tests: add cBPF filter tests for IORING_OP_CONNECT
@ 2026-05-13 12:10 Shouvik Kar
  2026-05-13 17:06 ` Jens Axboe
  0 siblings, 1 reply; 2+ messages in thread
From: Shouvik Kar @ 2026-05-13 12:10 UTC (permalink / raw)
  To: io-uring
  Cc: Jens Axboe, Pavel Begunkov, Kees Cook, Christian Brauner,
	Shouvik Kar

Add subtests for IORING_OP_CONNECT to test/cbpf_filter.c, exercising
the io_connect_bpf_populate() helper added in the companion kernel
patch ("io_uring/net: allow filtering on IORING_OP_CONNECT").

Coverage spans both blacklist and whitelist filters for each
connect-specific data field (family, v4 address, v6 address, port),
plus v4 and v6 subnet matching, and a test for the addr_len guard
in io_connect_bpf_populate that prevents stale io_async_msghdr
cache from leaking through to the filter on short connects.

Signed-off-by: Shouvik Kar <auxcorelabs@gmail.com>
---
 test/cbpf_filter.c | 1594 +++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 1439 insertions(+), 155 deletions(-)

diff --git a/test/cbpf_filter.c b/test/cbpf_filter.c
index b80b1503..9194a7f1 100644
--- a/test/cbpf_filter.c
+++ b/test/cbpf_filter.c
@@ -15,6 +15,8 @@
 #include <sys/wait.h>
 #include <sys/prctl.h>
 #include <linux/filter.h>
+#include <netinet/in.h>
+#include <sys/un.h>
 
 #include "liburing.h"
 #include "liburing/io_uring/bpf_filter.h"
@@ -43,6 +45,61 @@
 #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.
@@ -127,6 +184,193 @@ static struct sock_filter deny_resolve_in_root_filter[] = {
 	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)
@@ -340,49 +584,862 @@ static int test_openat2(struct io_uring *ring, const char *path,
 		return ret;
 	}
 
-	ret = io_uring_wait_cqe(ring, &cqe);
-	if (ret < 0) {
-		printf("FAIL (wait: %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 (should_succeed) {
-		if (cqe->res >= 0) {
-			close(cqe->res);
-			ret = 0;
-		} else {
-			printf("FAIL (expected success, got %s)\n",
-			       strerror(-cqe->res));
-			ret = -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);
 		}
-	} 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;
+
+		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);
 	}
 
-	if (ret)
-		fprintf(stderr, "%s: %s: failed\n", __FUNCTION__, desc);
-	io_uring_cqe_seen(ring, cqe);
-	return ret;
+	waitpid(pid, &status, 0);
+	if (WIFEXITED(status))
+		return WEXITSTATUS(status);
+	return 1;
 }
 
-static int test_deny_nop(void)
+static int test_connect_allow_v4_addr(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");
@@ -390,12 +1447,29 @@ static int test_deny_nop(void)
 	}
 
 	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);
+		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\n");
+			fprintf(stderr, "Child: register failed: %s\n",
+				strerror(-ret));
 			exit(ret == -EINVAL ? 0 : 1);
 		}
 
@@ -405,28 +1479,40 @@ static int test_deny_nop(void)
 			exit(1);
 		}
 
-		if (test_nop(&ring, "NOP should be denied", 0) != 0)
+		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);
 	}
 
-	/* Parent waits for child */
 	waitpid(pid, &status, 0);
 	if (WIFEXITED(status))
 		return WEXITSTATUS(status);
 	return 1;
 }
 
-static int test_allow_inet_only(void)
+/*
+ * 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;
 
-	/* Fork to get fresh task restrictions */
 	pid = fork();
 	if (pid < 0) {
 		perror("fork");
@@ -434,12 +1520,41 @@ static int test_allow_inet_only(void)
 	}
 
 	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);
+		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\n");
+			fprintf(stderr, "Child: register failed: %s\n",
+				strerror(-ret));
 			exit(ret == -EINVAL ? 0 : 1);
 		}
 
@@ -449,30 +1564,27 @@ static int test_allow_inet_only(void)
 			exit(1);
 		}
 
-		if (test_socket(&ring, AF_INET, SOCK_STREAM,
-				"AF_INET TCP should succeed", 1) != 0)
+		if (test_connect(&ring, (struct sockaddr *)&banned, sizeof(banned),
+				 "2001:db8::dead should be denied", 0) != 0)
 			failed++;
-
-		if (test_socket(&ring, AF_INET6, SOCK_STREAM,
-				"AF_INET6 TCP should be denied", 0) != 0)
+		if (test_connect(&ring, (struct sockaddr *)&other_lo, sizeof(other_lo),
+				 "::1 should be allowed", 1) != 0)
 			failed++;
-
-		if (test_socket(&ring, AF_UNIX, SOCK_STREAM,
-				"AF_UNIX should be denied", 0) != 0)
+		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);
 	}
 
-	/* Parent waits for child */
 	waitpid(pid, &status, 0);
 	if (WIFEXITED(status))
 		return WEXITSTATUS(status);
 	return 1;
 }
 
-static int test_allow_tcp_only(void)
+static int test_connect_allow_v6_addr(void)
 {
 	struct io_uring ring;
 	int ret, failed = 0;
@@ -486,11 +1598,35 @@ static int test_allow_tcp_only(void)
 	}
 
 	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);
+		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 -- reserved per RFC 4291, non-routable */
+		denied_lo.sin6_addr.s6_addr[15] = 0x02;
+		/* 2001:db8::1 -- documentation prefix, RFC 3849 */
+		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\n");
+			fprintf(stderr, "Child: register failed: %s\n",
+				strerror(-ret));
 			exit(ret == -EINVAL ? 0 : 1);
 		}
 
@@ -500,16 +1636,14 @@ static int test_allow_tcp_only(void)
 			exit(1);
 		}
 
-		if (test_socket(&ring, AF_INET, SOCK_STREAM,
-				"TCP should succeed", 1) != 0)
+		if (test_connect(&ring, (struct sockaddr *)&allowed, sizeof(allowed),
+				 "::1 should be allowed", 1) != 0)
 			failed++;
-
-		if (test_socket(&ring, AF_INET, SOCK_DGRAM,
-				"UDP should be denied", 0) != 0)
+		if (test_connect(&ring, (struct sockaddr *)&denied_lo, sizeof(denied_lo),
+				 "::2 should be denied", 0) != 0)
 			failed++;
-
-		if (test_socket(&ring, AF_INET6, SOCK_STREAM,
-				"IPv6 TCP should succeed", 1) != 0)
+		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);
@@ -522,7 +1656,7 @@ static int test_allow_tcp_only(void)
 	return 1;
 }
 
-static int test_deny_rest(void)
+static int test_connect_allow_port(void)
 {
 	struct io_uring ring;
 	int ret, failed = 0;
@@ -536,13 +1670,29 @@ static int test_deny_rest(void)
 	}
 
 	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 */
+		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\n");
+			fprintf(stderr, "Child: register failed: %s\n",
+				strerror(-ret));
 			exit(ret == -EINVAL ? 0 : 1);
 		}
 
@@ -552,11 +1702,14 @@ static int test_deny_rest(void)
 			exit(1);
 		}
 
-		if (test_nop(&ring, "NOP should succeed", 1) != 0)
+		if (test_connect(&ring, (struct sockaddr *)&allowed, sizeof(allowed),
+				 "port 80 should be allowed", 1) != 0)
 			failed++;
-
-		if (test_socket(&ring, AF_INET, SOCK_STREAM,
-				"Socket should be denied (DENY_REST)", 0) != 0)
+		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);
@@ -569,28 +1722,12 @@ static int test_deny_rest(void)
 	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)
+static int test_connect_deny_v4_subnet(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) {
@@ -599,52 +1736,112 @@ static int test_deny_openat_creat(void)
 	}
 
 	if (pid == 0) {
-		/* Test that O_CREAT works BEFORE installing filter */
+		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_openat(&ring, tmpfile, O_CREAT | O_RDWR, 0644,
-				"O_CREAT should succeed before filter", 1) != 0)
+		if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a),
+				 "127.42.0.1 should be denied", 0) != 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)
+		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);
+	}
 
-		/* 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);
+	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);
 		}
 
-		/* 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");
+			fprintf(stderr, "Child: queue_init 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)
+		if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a),
+				 "127.0.0.1 should be allowed", 1) != 0)
 			failed++;
-
-		/* Test that regular open still works */
-		if (test_openat(&ring, "/dev/null", O_RDONLY, 0,
-				"regular open should still succeed", 1) != 0)
+		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);
@@ -657,30 +1854,12 @@ static int test_deny_openat_creat(void)
 	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)
+static int test_connect_deny_v6_subnet(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) {
@@ -689,49 +1868,139 @@ static int test_deny_openat2_resolve_in_root(void)
 	}
 
 	if (pid == 0) {
-		/* Test that RESOLVE_IN_ROOT works BEFORE installing filter */
+		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_openat2(&ring, ".", &how_with_resolve,
-				 "RESOLVE_IN_ROOT should succeed before filter", 1) != 0)
+		if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a),
+				 "2001:db8::1 should be denied", 0) != 0)
 			failed++;
-
-		/* Test that normal openat2 works */
-		if (test_openat2(&ring, ".", &how_normal,
-				 "normal openat2 should succeed before filter", 1) != 0)
+		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);
+	}
 
-		/* 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);
+	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);
 		}
 
-		/* 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");
+			fprintf(stderr, "Child: queue_init 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)
+		if (test_connect(&ring, (struct sockaddr *)&in_subnet_a, sizeof(in_subnet_a),
+				 "fe80::1 should be allowed", 1) != 0)
 			failed++;
-
-		/* Test that normal openat2 still works */
-		if (test_openat2(&ring, ".", &how_normal,
-				 "normal openat2 should still succeed", 1) != 0)
+		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);
@@ -1431,6 +2700,21 @@ int main(int argc, char *argv[])
 	total_failed += test_deny_openat_creat();
 	total_failed += test_deny_openat2_resolve_in_root();
 
+	/* Task-level connect filter tests */
+	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();
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH liburing] tests: add cBPF filter tests for IORING_OP_CONNECT
  2026-05-13 12:10 [PATCH liburing] tests: add cBPF filter tests for IORING_OP_CONNECT Shouvik Kar
@ 2026-05-13 17:06 ` Jens Axboe
  0 siblings, 0 replies; 2+ messages in thread
From: Jens Axboe @ 2026-05-13 17:06 UTC (permalink / raw)
  To: Shouvik Kar, io-uring; +Cc: Pavel Begunkov, Kees Cook, Christian Brauner

On 5/13/26 6:10 AM, Shouvik Kar wrote:
> Add subtests for IORING_OP_CONNECT to test/cbpf_filter.c, exercising
> the io_connect_bpf_populate() helper added in the companion kernel
> patch ("io_uring/net: allow filtering on IORING_OP_CONNECT").
> 
> Coverage spans both blacklist and whitelist filters for each
> connect-specific data field (family, v4 address, v6 address, port),
> plus v4 and v6 subnet matching, and a test for the addr_len guard
> in io_connect_bpf_populate that prevents stale io_async_msghdr
> cache from leaking through to the filter on short connects.

If you run this on a kernel that doesn't have your connect changes,
then you get a lot of:

Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long
Child: register failed: Message too long

when the test is run. It's important that any liburing test cases
handles older kernels appropriately. You get some of it for free with
this test case, as previous tests will have already checked if cbpf
filters are supported in the first place. But you still need to handle
the case where cbpf filters are supported by io_uring, yet the kernel
doesn't support your filter yet.

It should just check for the error on the first case and skip testing
the rest of them.

-- 
Jens Axboe

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-05-13 17:06 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-13 12:10 [PATCH liburing] tests: add cBPF filter tests for IORING_OP_CONNECT Shouvik Kar
2026-05-13 17:06 ` Jens Axboe

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox