[RFC PATCH v3 6/8] selftests/landlock: Add tests for UDP sendmsg
Matthieu Buffet
matthieu at buffet.re
Fri Dec 12 16:37:02 UTC 2025
Add tests specific to UDP sendmsg(), orthogonal to whether the process
is allowed to bind()/connect(). Make this part of the protocol_*
variants to ensure behaviour is consistent across AF_INET, AF_INET6 and
AF_UNIX.
Signed-off-by: Matthieu Buffet <matthieu at buffet.re>
---
tools/testing/selftests/landlock/net_test.c | 306 +++++++++++++++++++-
1 file changed, 299 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 977d82eb9934..79ce52907ef3 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -268,9 +268,68 @@ static int connect_variant(const int sock_fd,
return connect_variant_addrlen(sock_fd, srv, get_addrlen(srv, false));
}
+static int sendto_variant_addrlen(const int sock_fd,
+ const struct service_fixture *const srv,
+ const socklen_t addrlen, void *buf,
+ size_t len, size_t flags)
+{
+ const struct sockaddr *dst = NULL;
+ ssize_t ret;
+
+ /*
+ * We never want our processes to be killed by SIGPIPE: we check
+ * return codes and errno, so that we have actual error messages.
+ */
+ flags |= MSG_NOSIGNAL;
+
+ if (srv != NULL) {
+ switch (srv->protocol.domain) {
+ case AF_UNSPEC:
+ case AF_INET:
+ dst = (const struct sockaddr *)&srv->ipv4_addr;
+ break;
+
+ case AF_INET6:
+ dst = (const struct sockaddr *)&srv->ipv6_addr;
+ break;
+
+ case AF_UNIX:
+ dst = (const struct sockaddr *)&srv->unix_addr;
+ break;
+
+ default:
+ errno = -EAFNOSUPPORT;
+ return -errno;
+ }
+ }
+
+ ret = sendto(sock_fd, buf, len, flags, dst, addrlen);
+ if (ret < 0)
+ return -errno;
+
+ /* errno is not set in cases of partial writes. */
+ if (ret != len)
+ return -EINTR;
+
+ return 0;
+}
+
+static int sendto_variant(const int sock_fd,
+ const struct service_fixture *const srv, void *buf,
+ size_t len, size_t flags)
+{
+ socklen_t addrlen = 0;
+
+ if (srv != NULL)
+ addrlen = get_addrlen(srv, false);
+
+ return sendto_variant_addrlen(sock_fd, srv, addrlen, buf, len, flags);
+}
+
FIXTURE(protocol)
{
- struct service_fixture srv0, srv1, srv2, unspec_any0, unspec_srv0;
+ struct service_fixture srv0, srv1, srv2;
+ struct service_fixture unspec_any0, unspec_srv0, unspec_srv1;
};
FIXTURE_VARIANT(protocol)
@@ -292,6 +351,7 @@ FIXTURE_SETUP(protocol)
ASSERT_EQ(0, set_service(&self->srv2, variant->prot, 2));
ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0));
+ ASSERT_EQ(0, set_service(&self->unspec_srv1, prot_unspec, 1));
ASSERT_EQ(0, set_service(&self->unspec_any0, prot_unspec, 0));
self->unspec_any0.ipv4_addr.sin_addr.s_addr = htonl(INADDR_ANY);
@@ -1075,6 +1135,185 @@ TEST_F(protocol, connect_unspec)
EXPECT_EQ(0, close(bind_fd));
}
+TEST_F(protocol, sendmsg)
+{
+ /* Arbitrary value just to not block other tests indefinitely. */
+ const struct timeval read_timeout = {
+ .tv_sec = 0,
+ .tv_usec = 100000,
+ };
+ const bool sandboxed = variant->sandbox == UDP_SANDBOX &&
+ (variant->prot.domain == AF_INET ||
+ variant->prot.domain == AF_INET6);
+ int res, srv1_fd, srv0_fd, client_fd;
+ char read_buf[1] = { 0 };
+
+ if (variant->prot.type != SOCK_DGRAM)
+ return;
+
+ disable_caps(_metadata);
+
+ /* Prepare server on port #0 to be denied */
+ ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
+ srv0_fd = socket_variant(&self->srv0);
+ ASSERT_LE(0, srv0_fd);
+ ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0));
+
+ /* And another server on port #1 to be allowed */
+ ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1));
+ srv1_fd = socket_variant(&self->srv1);
+ ASSERT_LE(0, srv1_fd);
+ ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1));
+
+ EXPECT_EQ(0, setsockopt(srv0_fd, SOL_SOCKET, SO_RCVTIMEO, &read_timeout,
+ sizeof(read_timeout)));
+ EXPECT_EQ(0, setsockopt(srv1_fd, SOL_SOCKET, SO_RCVTIMEO, &read_timeout,
+ sizeof(read_timeout)));
+
+ client_fd = socket_variant(&self->srv0);
+ ASSERT_LE(0, client_fd);
+
+ if (sandboxed) {
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_SENDTO_UDP |
+ LANDLOCK_ACCESS_NET_BIND_UDP,
+ };
+ const struct landlock_net_port_attr allow_one_server = {
+ .allowed_access = LANDLOCK_ACCESS_NET_SENDTO_UDP,
+ .port = self->srv1.port,
+ };
+ const int ruleset_fd = landlock_create_ruleset(
+ &ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0,
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &allow_one_server, 0));
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+ }
+
+ /* No connect(), NULL address */
+ EXPECT_EQ(-1, write(client_fd, "A", 1));
+ if (variant->prot.domain == AF_UNIX) {
+ EXPECT_EQ(ENOTCONN, errno);
+ } else {
+ EXPECT_EQ(EDESTADDRREQ, errno);
+ }
+
+ /* No connect(), non-NULL but too small explicit address */
+ EXPECT_EQ(-EINVAL,
+ sendto_variant_addrlen(client_fd, &self->srv0,
+ get_addrlen(&self->srv0, true) - 1,
+ "B", 1, 0));
+
+ /* No connect(), explicit denied port */
+ res = sendto_variant(client_fd, &self->srv0, "C", 1, 0);
+ if (sandboxed) {
+ EXPECT_EQ(-EACCES, res);
+ } else {
+ EXPECT_EQ(0, res);
+ EXPECT_EQ(1, read(srv0_fd, read_buf, 1));
+ EXPECT_EQ(read_buf[0], 'C');
+ }
+
+ /* No connect(), explicit allowed port */
+ EXPECT_EQ(0, sendto_variant(client_fd, &self->srv1, "D", 1, 0));
+ EXPECT_EQ(1, read(srv1_fd, read_buf, 1));
+ EXPECT_EQ(read_buf[0], 'D');
+
+ /* Explicit AF_UNSPEC address but truncated */
+ EXPECT_EQ(-EINVAL, sendto_variant_addrlen(
+ client_fd, &self->unspec_srv0,
+ get_addrlen(&self->unspec_srv0, true) - 1,
+ "E", 1, 0));
+
+ /* Explicit AF_UNSPEC address, should always be denied */
+ res = sendto_variant(client_fd, &self->unspec_srv1, "F", 1, 0);
+ if (sandboxed) {
+ EXPECT_EQ(-EACCES, res);
+ } else {
+ if (variant->prot.domain == AF_INET) {
+ /* IPv4 sockets treat AF_UNSPEC as AF_INET */
+ EXPECT_EQ(0, res);
+ EXPECT_EQ(1, read(srv1_fd, read_buf, 1))
+ {
+ TH_LOG("read() failed: %s", strerror(errno));
+ }
+ EXPECT_EQ(read_buf[0], 'F');
+ } else if (variant->prot.domain == AF_INET6) {
+ /* IPv6 sockets treat AF_UNSPEC as a NULL address */
+ EXPECT_EQ(-EDESTADDRREQ, res);
+ } else {
+ /* Unix sockets don't accept AF_UNSPEC */
+ EXPECT_EQ(-EINVAL, res);
+ }
+ }
+
+ /* With connect() on an allowed explicit port, no explicit address */
+ ASSERT_EQ(0, connect_variant(client_fd, &self->srv1));
+ EXPECT_EQ(0, sendto_variant(client_fd, NULL, "G", 1, 0));
+ EXPECT_EQ(1, read(srv1_fd, read_buf, 1));
+ EXPECT_EQ(read_buf[0], 'G');
+
+ /* With connect() on a denied explicit port, no explicit address */
+ ASSERT_EQ(0, connect_variant(client_fd, &self->srv0));
+ EXPECT_EQ(0, sendto_variant(client_fd, NULL, "H", 1, 0));
+ EXPECT_EQ(1, read(srv0_fd, read_buf, 1));
+ EXPECT_EQ(read_buf[0], 'H');
+
+ /* Explicit AF_UNSPEC minimal address (just the sa_family_t field) */
+ res = sendto_variant_addrlen(client_fd, &self->unspec_srv0,
+ get_addrlen(&self->unspec_srv0, true), "I",
+ 1, 0);
+ if (sandboxed) {
+ EXPECT_EQ(-EACCES, res);
+ } else if (variant->prot.domain == AF_INET6) {
+ /*
+ * IPv6 sockets treat AF_UNSPEC as a NULL address,
+ * falling back to the connected address
+ */
+ EXPECT_EQ(0, res);
+ EXPECT_EQ(1, read(srv0_fd, read_buf, 1));
+ EXPECT_EQ(read_buf[0], 'I');
+ } else {
+ /*
+ * IPv4 socket will expect a struct sockaddr_in, our address
+ * is considered truncated.
+ * And Unix sockets don't accept AF_UNSPEC at all.
+ */
+ EXPECT_EQ(-EINVAL, res);
+ }
+
+ /* Explicit AF_UNSPEC address, should always be denied */
+ res = sendto_variant(client_fd, &self->unspec_srv1, "J", 1, 0);
+ if (sandboxed) {
+ EXPECT_EQ(-EACCES, res);
+ } else if (variant->prot.domain == AF_INET ||
+ variant->prot.domain == AF_INET6) {
+ int read_fd;
+
+ EXPECT_EQ(0, res);
+ if (variant->prot.domain == AF_INET) {
+ /* IPv4 sockets treat AF_UNSPEC as AF_INET */
+ read_fd = srv1_fd;
+ } else {
+ /*
+ * IPv6 sockets treat AF_UNSPEC as a NULL address,
+ * falling back to the connected address
+ */
+ read_fd = srv0_fd;
+ }
+ EXPECT_EQ(1, read(read_fd, read_buf, 1))
+ {
+ TH_LOG("read failed: %s", strerror(errno));
+ }
+ EXPECT_EQ(read_buf[0], 'J');
+ } else {
+ /* Unix sockets don't accept AF_UNSPEC */
+ EXPECT_EQ(-EINVAL, res);
+ }
+}
+
FIXTURE(ipv4)
{
struct service_fixture srv0, srv1;
@@ -1470,13 +1709,14 @@ FIXTURE_TEARDOWN(mini)
/* clang-format off */
-#define ACCESS_LAST LANDLOCK_ACCESS_NET_CONNECT_UDP
+#define ACCESS_LAST LANDLOCK_ACCESS_NET_SENDTO_UDP
#define ACCESS_ALL ( \
LANDLOCK_ACCESS_NET_BIND_TCP | \
LANDLOCK_ACCESS_NET_CONNECT_TCP | \
LANDLOCK_ACCESS_NET_BIND_UDP | \
- LANDLOCK_ACCESS_NET_CONNECT_UDP)
+ LANDLOCK_ACCESS_NET_CONNECT_UDP | \
+ LANDLOCK_ACCESS_NET_SENDTO_UDP)
/* clang-format on */
@@ -2078,19 +2318,26 @@ static int matches_log_prot(const int audit_fd, const char *const blockers,
const char *const dir_addr, const char *const addr,
const char *const dir_port)
{
- static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ static const char tmpl_with_addr_port[] = REGEX_LANDLOCK_PREFIX
" blockers=%s %s=%s %s=1024$";
+ static const char tmpl_no_addr_port[] = REGEX_LANDLOCK_PREFIX
+ " blockers=%s$";
/*
* Max strlen(blockers): 16
* Max strlen(dir_addr): 5
* Max strlen(addr): 12
* Max strlen(dir_port): 4
*/
- char log_match[sizeof(log_template) + 37];
+ char log_match[sizeof(tmpl_with_addr_port) + 37];
int log_match_len;
- log_match_len = snprintf(log_match, sizeof(log_match), log_template,
- blockers, dir_addr, addr, dir_port);
+ if (addr != NULL)
+ log_match_len = snprintf(log_match, sizeof(log_match), tmpl_with_addr_port,
+ blockers, dir_addr, addr, dir_port);
+ else
+ log_match_len = snprintf(log_match, sizeof(log_match), tmpl_no_addr_port,
+ blockers);
+
if (log_match_len > sizeof(log_match))
return -E2BIG;
@@ -2101,6 +2348,7 @@ static int matches_log_prot(const int audit_fd, const char *const blockers,
FIXTURE(audit)
{
struct service_fixture srv0;
+ struct service_fixture unspec_srv0;
struct audit_filter audit_filter;
int audit_fd;
};
@@ -2153,7 +2401,13 @@ FIXTURE_VARIANT_ADD(audit, ipv6_udp) {
FIXTURE_SETUP(audit)
{
+ struct protocol_variant prot_unspec = variant->prot;
+
+ prot_unspec.domain = AF_UNSPEC;
+
ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
+ ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0));
+
setup_loopback(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
@@ -2241,4 +2495,42 @@ TEST_F(audit, connect)
EXPECT_EQ(0, close(sock_fd));
}
+TEST_F(audit, sendmsg)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_SENDTO_UDP,
+ };
+ struct audit_records records;
+ int ruleset_fd, sock_fd;
+
+ if (variant->prot.type != SOCK_DGRAM)
+ return;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ EXPECT_EQ(0, close(ruleset_fd));
+
+ sock_fd = socket_variant(&self->srv0);
+ ASSERT_LE(0, sock_fd);
+ EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv0, "A", 1, 0));
+ EXPECT_EQ(0, matches_log_prot(self->audit_fd, "net.sendto_udp", "daddr",
+ variant->addr, "dest"));
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+
+ EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->unspec_srv0, "B", 1, 0));
+ EXPECT_EQ(0, matches_log_prot(self->audit_fd, "net.sendto_udp", "daddr",
+ NULL, "dest"));
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(0, records.domain);
+
+ EXPECT_EQ(0, close(sock_fd));
+}
+
TEST_HARNESS_MAIN
--
2.47.3
More information about the Linux-security-module-archive
mailing list