Thread (19 messages) flat view 19 messages, 2 authors, 4d ago
COOLING4d

[PATCH v1 8/9] selftests/landlock: Test network denial context

From: Mickaël Salaün <mic@digikod.net>
Date: 2026-09-18 18:50:56
Also in: bpf, linux-security-module
Subsystem: kernel selftest framework, landlock security module, the rest · Maintainers: Shuah Khan, Shuah Khan, Mickaël Salaün, Linus Torvalds

Network denial events now report the port from their one authoritative
validated address through one signed field.  Verify this value directly
without inferring a source or destination role.

Exercise a nonzero IPv4 TCP bind, an explicit-zero IPv4 UDP bind, a
synthetic-zero IPv6 UDP autobind, and a family-only AF_UNSPEC UDP send.
Require exactly one event with the exact policy blocker for each shape.
The value -1 distinguishes a family-only address with no validated port
from the two valid port-zero cases.

Cc: Günther Noack <gnoack@google.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
 tools/testing/selftests/landlock/net_test.c | 259 +++++++++++---------
 1 file changed, 147 insertions(+), 112 deletions(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 28942438e154..16afbfdf06bb 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -3481,13 +3481,14 @@ TEST_F(trace_net, deny_access_net_bind)
 }
 
 /*
- * Anchors the denial fields shared by every deny_access_net event so a field
- * test proves more than the checked endpoint: the denying domain, the same-exec
- * bit, the audit-logging verdict, and the blocked access all stay populated.
+ * Anchors the denial fields shared by every deny_access_net event so a port
+ * test also proves the denying domain, execution status, logging verdict, and
+ * exact blocked access.
  */
 static void
 expect_net_deny_common_fields(struct __test_metadata *const _metadata,
-			      const char *const buf)
+			      const char *const buf,
+			      const char *const expected_blockers)
 {
 	char field[64];
 
@@ -3511,18 +3512,21 @@ expect_net_deny_common_fields(struct __test_metadata *const _metadata,
 	ASSERT_EQ(0,
 		  tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
 					"blockers", field, sizeof(field)));
-	EXPECT_STRNE("", field);
+	EXPECT_STREQ(expected_blockers, field);
 }
 
-/* Connect and field-check tests use a separate fixture without variants. */
+enum trace_net_operation {
+	TRACE_NET_BIND,
+	TRACE_NET_SEND,
+};
 
 /* clang-format off */
-FIXTURE(trace_net_connect) {
+FIXTURE(trace_net_address) {
 	/* clang-format on */
 	int tracefs_ok;
 };
 
-FIXTURE_SETUP(trace_net_connect)
+FIXTURE_SETUP(trace_net_address)
 {
 	int ret;
 
@@ -3547,7 +3551,7 @@ FIXTURE_SETUP(trace_net_connect)
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 }
 
-FIXTURE_TEARDOWN(trace_net_connect)
+FIXTURE_TEARDOWN(trace_net_address)
 {
 	if (!self->tracefs_ok)
 		return;
@@ -3559,146 +3563,177 @@ FIXTURE_TEARDOWN(trace_net_connect)
 }
 
 /* clang-format off */
-FIXTURE_VARIANT(trace_net_connect) {
+FIXTURE_VARIANT(trace_net_address) {
 	/* clang-format on */
-	/* handled_access_net, also the access allowed on the base port. */
-	__u64 handled;
-	/* Bind the allowed base port before the denied operation. */
-	bool bind_base_first;
-	/* Denied operation on the next port: connect (true) or bind (false). */
-	bool deny_connect;
+	int socket_family;
+	int socket_type;
+	enum trace_net_operation operation;
+	int address_family;
+	socklen_t addrlen;
+	__u64 handled_access;
+	const char *expected_blockers;
+	bool address_port_zero;
+	bool expected_address_port;
+	int expected_port;
 };
 
-/* Denied connect() to the next IPv4 port. */
 /* clang-format off */
-FIXTURE_VARIANT_ADD(trace_net_connect, connect_denied) {
+FIXTURE_VARIANT_ADD(trace_net_address, ipv4_tcp_bind) {
 	/* clang-format on */
-	.handled = LANDLOCK_ACCESS_NET_CONNECT_TCP,
-	.bind_base_first = false,
-	.deny_connect = true,
+	.socket_family = AF_INET,
+	.socket_type = SOCK_STREAM,
+	.operation = TRACE_NET_BIND,
+	.address_family = AF_INET,
+	.addrlen = sizeof(struct sockaddr_in),
+	.handled_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+	.expected_blockers = "bind_tcp",
+	.expected_address_port = true,
 };
 
-/* Denied bind() to the next IPv4 port. */
+/* Explicit bind(0) has a checked zero port. */
 /* clang-format off */
-FIXTURE_VARIANT_ADD(trace_net_connect, bind_fields) {
+FIXTURE_VARIANT_ADD(trace_net_address, ipv4_udp_bind_zero) {
 	/* clang-format on */
-	.handled = LANDLOCK_ACCESS_NET_BIND_TCP,
-	.bind_base_first = false,
-	.deny_connect = false,
+	.socket_family = AF_INET,
+	.socket_type = SOCK_DGRAM,
+	.operation = TRACE_NET_BIND,
+	.address_family = AF_INET,
+	.addrlen = sizeof(struct sockaddr_in),
+	.handled_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+	.expected_blockers = "bind_udp",
+	.address_port_zero = true,
+	.expected_port = 0,
 };
 
-/* Denied connect() after an allowed bind() uses the checked destination. */
+/* A UDP send can deny its synthetic unspecified bind endpoint. */
 /* clang-format off */
-FIXTURE_VARIANT_ADD(trace_net_connect, connect_after_bind) {
+FIXTURE_VARIANT_ADD(trace_net_address, ipv6_udp_autobind) {
 	/* clang-format on */
-	.handled = LANDLOCK_ACCESS_NET_BIND_TCP |
-		   LANDLOCK_ACCESS_NET_CONNECT_TCP,
-	.bind_base_first = true,
-	.deny_connect = true,
+	.socket_family = AF_INET6,
+	.socket_type = SOCK_DGRAM,
+	.operation = TRACE_NET_SEND,
+	.address_family = AF_INET6,
+	.addrlen = sizeof(struct sockaddr_in6),
+	.handled_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+	.expected_blockers = "bind_udp",
+	.expected_port = 0,
 };
 
-/*
- * A denied TCP bind(2) or connect(2) emits one deny_access_net event with the
- * checked IPv4 port in host endianness (the UAPI landlock_net_port_attr.port
- * convention).  A prior allowed bind does not change a connect denial's port.
- */
-TEST_F(trace_net_connect, deny_access_net)
+/* A family-only address has no checked port. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_net_address, ipv6_unspec_udp_send_min) {
+	/* clang-format on */
+	.socket_family = AF_INET6,
+	.socket_type = SOCK_DGRAM,
+	.operation = TRACE_NET_SEND,
+	.address_family = AF_UNSPEC,
+	.addrlen = sizeof(sa_family_t),
+	.handled_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+	.expected_blockers = "connect_send_udp",
+	.expected_port = -1,
+};
+
+static void set_trace_net_address(struct sockaddr_storage *const storage,
+				  const int socket_family,
+				  const int address_family,
+				  const unsigned short port)
 {
-	pid_t child;
-	int status;
-	char *buf;
+	memset(storage, 0, sizeof(*storage));
+
+	if (socket_family == AF_INET) {
+		struct sockaddr_in *const addr4 = (struct sockaddr_in *)storage;
+
+		addr4->sin_family = address_family;
+		addr4->sin_port = htons(port);
+		addr4->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+	} else {
+		struct sockaddr_in6 *const addr6 =
+			(struct sockaddr_in6 *)storage;
+
+		addr6->sin6_family = address_family;
+		addr6->sin6_port = htons(port);
+		addr6->sin6_addr = in6addr_loopback;
+	}
+}
+
+/* Verifies the actionable signed port for representative checked shapes. */
+TEST_F(trace_net_address, deny_access_net)
+{
+	const char *const event_regex = REGEX_DENY_ACCESS_NET(TRACE_TASK);
+	const unsigned short address_port =
+		variant->address_port_zero ? 0 : sock_port_start + 1;
+	const int expected_port = variant->expected_address_port ?
+					  address_port :
+					  variant->expected_port;
+	const struct landlock_ruleset_attr ruleset_attr = {
+		.handled_access_net = variant->handled_access,
+	};
+	struct sockaddr_storage address;
 	char field[64], expected[16];
+	char *buf;
+	int count, ret, ruleset_fd, socket_fd, status;
+	pid_t child;
 
 	if (!self->tracefs_ok)
 		SKIP(return, "tracefs not available");
 
+	set_trace_net_address(&address, variant->socket_family,
+			      variant->address_family, address_port);
+	socket_fd = socket(variant->socket_family,
+			   variant->socket_type | SOCK_CLOEXEC, 0);
+	ASSERT_LE(0, socket_fd);
+	ruleset_fd =
+		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+	ASSERT_LE(0, ruleset_fd);
+	ASSERT_EQ(0, tracefs_clear_buf());
+
 	child = fork();
 	ASSERT_LE(0, child);
-
 	if (child == 0) {
-		struct landlock_ruleset_attr ruleset_attr = {
-			.handled_access_net = variant->handled,
-		};
-		struct landlock_net_port_attr port_attr = {
-			.allowed_access = variant->handled,
-			.port = sock_port_start,
-		};
-		struct sockaddr_in addr = {
-			.sin_family = AF_INET,
-			.sin_addr.s_addr = htonl(INADDR_LOOPBACK),
-		};
-		int ruleset_fd, sock_fd, optval = 1, ret;
-
-		ruleset_fd = landlock_create_ruleset(&ruleset_attr,
-						     sizeof(ruleset_attr), 0);
-		if (ruleset_fd < 0)
+		if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
 			_exit(1);
-		if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
-				      &port_attr, 0)) {
-			close(ruleset_fd);
-			_exit(1);
-		}
-		prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
-		if (landlock_restrict_self(ruleset_fd, 0)) {
-			close(ruleset_fd);
-			_exit(1);
-		}
+		if (landlock_restrict_self(ruleset_fd, 0))
+			_exit(2);
 		close(ruleset_fd);
 
-		sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
-		if (sock_fd < 0)
-			_exit(1);
-
-		/* Bind the allowed base port first (succeeds, no event). */
-		if (variant->bind_base_first) {
-			setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval,
-				   sizeof(optval));
-			addr.sin_port = htons(sock_port_start);
-			if (bind(sock_fd, (struct sockaddr *)&addr,
-				 sizeof(addr))) {
-				close(sock_fd);
-				_exit(1);
-			}
-		}
-
-		/* Denied operation on the next port. */
-		addr.sin_port = htons(sock_port_start + 1);
-		if (variant->deny_connect)
-			ret = connect(sock_fd, (struct sockaddr *)&addr,
-				      sizeof(addr));
-		else
-			ret = bind(sock_fd, (struct sockaddr *)&addr,
-				   sizeof(addr));
-		if (ret == 0) {
-			close(sock_fd);
-			_exit(2);
-		}
-		if (errno != EACCES) {
-			close(sock_fd);
+		switch (variant->operation) {
+		case TRACE_NET_BIND:
+			ret = bind(socket_fd, (const struct sockaddr *)&address,
+				   variant->addrlen);
+			break;
+		case TRACE_NET_SEND:
+			ret = sendto(socket_fd, "A", 1, MSG_NOSIGNAL,
+				     (const struct sockaddr *)&address,
+				     variant->addrlen);
+			break;
+		default:
 			_exit(3);
 		}
-		close(sock_fd);
+		if (ret >= 0 || errno != EACCES)
+			_exit(4);
+		close(socket_fd);
+
 		_exit(0);
 	}
+	close(ruleset_fd);
+	close(socket_fd);
 
 	ASSERT_EQ(child, waitpid(child, &status, 0));
 	ASSERT_TRUE(WIFEXITED(status));
-	EXPECT_EQ(0, WEXITSTATUS(status));
+	ASSERT_EQ(0, WEXITSTATUS(status));
 
 	buf = tracefs_read_buf();
 	ASSERT_NE(NULL, buf);
-
-	EXPECT_EQ(1, tracefs_count_matches(buf,
-					   REGEX_DENY_ACCESS_NET(TRACE_TASK)));
-
-	expect_net_deny_common_fields(_metadata, buf);
-
-	snprintf(expected, sizeof(expected), "%llu",
-		 (unsigned long long)(sock_port_start + 1));
-
-	ASSERT_EQ(0,
-		  tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
-					"port", field, sizeof(field)));
+	count = tracefs_count_matches(buf, event_regex);
+	if (count != 1)
+		TH_LOG("Expected 1 denial event, got %d\n%s", count, buf);
+	ASSERT_EQ(1, count);
+	expect_net_deny_common_fields(_metadata, buf,
+				      variant->expected_blockers);
+
+	ASSERT_EQ(0, tracefs_extract_field(buf, event_regex, "port", field,
+					   sizeof(field)));
+	snprintf(expected, sizeof(expected), "%d", expected_port);
 	EXPECT_STREQ(expected, field);
 
 	free(buf);
-- 
2.55.0
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help