]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
selftests/landlock: Add tests for UDP send
authorMatthieu Buffet <matthieu@buffet.re>
Thu, 11 Jun 2026 16:21:04 +0000 (18:21 +0200)
committerMickaël Salaün <mic@digikod.net>
Sat, 13 Jun 2026 21:15:07 +0000 (23:15 +0200)
Add tests specific to UDP sendmsg() in the protocol_* variants to ensure
behaviour is consistent across AF_INET, AF_INET6 and AF_UNIX.

Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
Link: https://patch.msgid.link/20260611162107.49278-5-matthieu@buffet.re
[mic: Fix comment formatting, rebase]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
tools/testing/selftests/landlock/net_test.c

index 72e6fbdc2ea2aa4a19164239237d805f8bca3efe..fb244aaef86fbd657998fff259f00088a5e9706c 100644 (file)
@@ -289,9 +289,163 @@ 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);
+}
+
+static int test_sendmsg(struct __test_metadata *const _metadata,
+                       const struct protocol_variant *prot, int client_fd,
+                       int server_fd, const struct service_fixture *srv,
+                       bool bind_denied, bool send_denied)
+{
+       int ret;
+       socklen_t opt_len;
+       int sock_type;
+       int addr_family;
+       struct sockaddr_storage peer_addr = { 0 };
+       bool has_remote_port;
+       bool needs_autobind;
+       char read_buf[1] = { 0 };
+
+       /*
+        * Prepare the test by inspecting the socket type and whether it has a
+        * local/remote address set (all of which determine the expected
+        * outcomes).
+        */
+       opt_len = sizeof(sock_type);
+       ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_TYPE, &sock_type,
+                               &opt_len));
+       opt_len = sizeof(addr_family);
+       ASSERT_EQ(0, getsockopt(client_fd, SOL_SOCKET, SO_DOMAIN, &addr_family,
+                               &opt_len));
+       opt_len = sizeof(peer_addr);
+       has_remote_port = (getpeername(client_fd, (struct sockaddr *)&peer_addr,
+                                      &opt_len) == 0);
+       needs_autobind = (addr_family == AF_INET || addr_family == AF_INET6) &&
+                        get_binded_port(client_fd, prot) == 0;
+
+       /* First, check error code with truncated explicit address. */
+       if (srv != NULL) {
+               ret = sendto_variant_addrlen(
+                       client_fd, srv, get_addrlen(srv, true) - 1, "A", 1, 0);
+               if (sock_type == SOCK_STREAM && !has_remote_port) {
+                       EXPECT_EQ(-EPIPE, ret)
+                       {
+                               return -1;
+                       }
+               } else if (bind_denied && needs_autobind) {
+                       EXPECT_EQ(-EACCES, ret)
+                       {
+                               return -1;
+                       }
+               } else {
+                       EXPECT_EQ(-EINVAL, ret)
+                       {
+                               return -1;
+                       }
+               }
+       }
+
+       /* With or without explicit destination address (srv can be NULL). */
+       ret = sendto_variant(client_fd, srv, "B", 1, 0);
+       if (sock_type == SOCK_STREAM && !has_remote_port) {
+               EXPECT_EQ(-EPIPE, ret)
+               {
+                       return -1;
+               }
+       } else if ((send_denied && srv != NULL) ||
+                  (bind_denied && needs_autobind)) {
+               ASSERT_EQ(-EACCES, ret)
+               {
+                       return -1;
+               }
+       } else if (srv == NULL && !has_remote_port) {
+               if (addr_family == AF_UNIX) {
+                       ASSERT_EQ(-ENOTCONN, ret)
+                       {
+                               return -1;
+                       }
+               } else if (sock_type == SOCK_STREAM) {
+                       ASSERT_EQ(-EPIPE, ret)
+                       {
+                               return -1;
+                       }
+               } else {
+                       ASSERT_EQ(-EDESTADDRREQ, ret)
+                       {
+                               return -1;
+                       }
+               }
+       } else {
+               ASSERT_EQ(0, ret);
+               ASSERT_EQ(1, recv(server_fd, read_buf, 1, 0));
+               ASSERT_EQ(read_buf[0], 'B')
+               {
+                       return -1;
+               }
+       }
+
+       return 0;
+}
+
 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)
@@ -313,6 +467,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);
@@ -1126,6 +1281,441 @@ TEST_F(protocol, connect_unspec)
        EXPECT_EQ(0, close(bind_fd));
 }
 
+TEST_F(protocol, sendmsg_stream)
+{
+       int srv0_fd, tmp_fd, client_fd, res;
+       char read_buf[1] = { 0 };
+
+       /*
+        * Simple test for stream sockets: just deny all connect()/
+        * send(explicit addr)/bind(), and make sure we don't interfere with any
+        * operation.
+        */
+       if (variant->prot.type != SOCK_STREAM)
+               return;
+
+       if (variant->sandbox == UDP_SANDBOX) {
+               const struct landlock_ruleset_attr ruleset_attr = {
+                       .handled_access_net =
+                               LANDLOCK_ACCESS_NET_BIND_UDP |
+                               LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+               };
+               const int 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));
+       }
+
+       ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+       ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0));
+       ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0));
+       ASSERT_EQ(0, listen(srv0_fd, backlog));
+
+       /* Send on a non-connected socket. */
+       res = sendto_variant(client_fd, NULL, "A", 1, 0);
+       if (variant->prot.domain == AF_UNIX) {
+               EXPECT_EQ(-ENOTCONN, res);
+       } else {
+               EXPECT_EQ(-EPIPE, res);
+       }
+
+       /* Send to a truncated (invalid) address on a non-connected socket. */
+       res = sendto_variant_addrlen(client_fd, &self->srv0,
+                                    get_addrlen(&self->srv0, true) - 1, "B", 1,
+                                    0);
+       if (variant->prot.domain == AF_UNIX) {
+               EXPECT_EQ(-EOPNOTSUPP, res);
+       } else {
+               EXPECT_EQ(-EPIPE, res);
+       }
+
+       /* Connect. */
+       ASSERT_EQ(0, connect_variant(client_fd, &self->srv0));
+       tmp_fd = accept(srv0_fd, NULL, 0);
+       ASSERT_LE(0, tmp_fd);
+       EXPECT_EQ(0, close(srv0_fd));
+       srv0_fd = tmp_fd;
+
+       /* Send without an explicit address. */
+       EXPECT_EQ(0, sendto_variant(client_fd, NULL, "C", 1, 0));
+       EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0))
+       {
+               TH_LOG("recv() failed: %s", strerror(errno));
+       }
+       EXPECT_EQ(read_buf[0], 'C');
+
+       /* Send to a truncated (invalid) address. */
+       res = sendto_variant_addrlen(client_fd, &self->srv0,
+                                    get_addrlen(&self->srv0, true) - 1, "D", 1,
+                                    0);
+       if (variant->prot.domain == AF_UNIX) {
+               EXPECT_EQ(-EISCONN, res);
+       } else {
+               ASSERT_EQ(0, res);
+               EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0))
+               {
+                       TH_LOG("recv() failed: %s", strerror(errno));
+               }
+               EXPECT_EQ(read_buf[0], 'D');
+       }
+
+       /* Send to a valid but different address. */
+       res = sendto_variant(client_fd, &self->srv1, "E", 1, 0);
+       if (variant->prot.domain == AF_UNIX) {
+               EXPECT_EQ(-EISCONN, res);
+       } else {
+               ASSERT_EQ(0, res);
+               EXPECT_EQ(1, recv(srv0_fd, read_buf, 1, 0))
+               {
+                       TH_LOG("recv() failed: %s", strerror(errno));
+               }
+               EXPECT_EQ(read_buf[0], 'E');
+       }
+
+       EXPECT_EQ(0, close(client_fd));
+}
+
+TEST_F(protocol, sendmsg_dgram)
+{
+       const bool restricted = is_restricted(&variant->prot, variant->sandbox);
+       int srv0_fd, srv1_fd, client_fd, child, status, res;
+
+       if (variant->prot.type != SOCK_DGRAM)
+               return;
+
+       /* Prepare server on port #0 to be allowed. */
+       ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0));
+       ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0));
+
+       /* And another server on port #1 to be denied. */
+       ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1));
+       ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1));
+
+       /*
+        * Check that sockets connected before restrictions are not impacted in
+        * any way.
+        */
+       child = fork();
+       ASSERT_LE(0, child);
+       if (child == 0) {
+               ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+               ASSERT_EQ(0, connect_variant(client_fd, &self->srv0));
+               if (variant->sandbox == UDP_SANDBOX) {
+                       /* Deny all connect()/send(explicit addr)/bind(). */
+                       const struct landlock_ruleset_attr ruleset_attr = {
+                               .handled_access_net =
+                                       LANDLOCK_ACCESS_NET_BIND_UDP |
+                                       LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+                       };
+                       const int 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));
+               }
+               EXPECT_EQ(0,
+                         test_sendmsg(_metadata, &variant->prot, client_fd,
+                                      srv0_fd, NULL, restricted, restricted));
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         srv0_fd, &self->srv0, restricted,
+                                         restricted));
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         srv1_fd, &self->srv1, restricted,
+                                         restricted));
+               EXPECT_EQ(0, close(client_fd));
+               _exit(_metadata->exit_code);
+       }
+       EXPECT_EQ(child, waitpid(child, &status, 0));
+       EXPECT_EQ(1, WIFEXITED(status));
+       EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+       /*
+        * Restrict connect/send, but not bind(). Then try sending with no
+        * destination (and no remote peer set), an allowed destination, then a
+        * denied destination.
+        */
+       child = fork();
+       ASSERT_LE(0, child);
+       if (child == 0) {
+               if (variant->sandbox == UDP_SANDBOX) {
+                       const struct landlock_ruleset_attr ruleset_attr = {
+                               .handled_access_net =
+                                       LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+                       };
+                       const struct landlock_net_port_attr send_p0 = {
+                               .allowed_access =
+                                       LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+                               .port = self->srv0.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,
+                                                      &send_p0, 0));
+                       enforce_ruleset(_metadata, ruleset_fd);
+                       EXPECT_EQ(0, close(ruleset_fd));
+               }
+               ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         -1, NULL, false, false));
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         srv0_fd, &self->srv0, false, false));
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         srv1_fd, &self->srv1, false,
+                                         restricted));
+               EXPECT_EQ(0, close(client_fd));
+               _exit(_metadata->exit_code);
+               return;
+       }
+       EXPECT_EQ(child, waitpid(child, &status, 0));
+       EXPECT_EQ(1, WIFEXITED(status));
+       EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+       /*
+        * Rest of this test is just for autobind enforcement, which only exists
+        * in IP sockets.
+        */
+       if (variant->prot.domain != AF_INET && variant->prot.domain != AF_INET6)
+               return;
+
+       /* Restrict bind() to explicit calls with an arbitrary (non-0) port. */
+       child = fork();
+       ASSERT_LE(0, child);
+       if (child == 0) {
+               const uint16_t allowed_src_port = 42424;
+               struct service_fixture allowed_src;
+
+               allowed_src = self->srv0;
+               set_port(&allowed_src, allowed_src_port);
+               if (variant->sandbox == UDP_SANDBOX) {
+                       const struct landlock_ruleset_attr ruleset_attr = {
+                               .handled_access_net =
+                                       LANDLOCK_ACCESS_NET_BIND_UDP,
+                       };
+                       const struct landlock_net_port_attr rule = {
+                               .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+                               .port = allowed_src_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,
+                                                      &rule, 0));
+                       enforce_ruleset(_metadata, ruleset_fd);
+                       EXPECT_EQ(0, close(ruleset_fd));
+               }
+               ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+
+               /* Check that implicit bind(0) in sendmsg() is denied. */
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         srv0_fd, &self->srv0, restricted,
+                                         false));
+
+               /* Same thing for autobind in connect(). */
+               res = connect_variant(client_fd, &self->srv0);
+               if (restricted) {
+                       EXPECT_EQ(-EACCES, res);
+               } else {
+                       EXPECT_EQ(0, res);
+               }
+               EXPECT_EQ(0, close(client_fd));
+
+               /* Make sendmsg() work by explicitly binding to the only allowed port. */
+               ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+               EXPECT_EQ(0, bind_variant(client_fd, &allowed_src));
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         srv0_fd, &self->srv0, restricted,
+                                         false));
+               EXPECT_EQ(0, close(client_fd));
+
+               /* Make connect() work by explicitly binding to the only allowed port. */
+               ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+               EXPECT_EQ(0, bind_variant(client_fd, &allowed_src));
+               EXPECT_EQ(0, connect_variant(client_fd, &self->srv0));
+               EXPECT_EQ(0, close(client_fd));
+
+               _exit(_metadata->exit_code);
+               return;
+       }
+       EXPECT_EQ(child, waitpid(child, &status, 0));
+       EXPECT_EQ(1, WIFEXITED(status));
+       EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+       /*
+        * Check that %LANDLOCK_ACCESS_NET_BIND_UDP on port 0 allows implicit
+        * autobinds.
+        */
+       child = fork();
+       ASSERT_LE(0, child);
+       if (child == 0) {
+               if (variant->sandbox == UDP_SANDBOX) {
+                       const struct landlock_ruleset_attr ruleset_attr = {
+                               .handled_access_net =
+                                       LANDLOCK_ACCESS_NET_BIND_UDP,
+                       };
+                       const struct landlock_net_port_attr rule = {
+                               .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+                               .port = 0,
+                       };
+                       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,
+                                                      &rule, 0));
+                       enforce_ruleset(_metadata, ruleset_fd);
+                       EXPECT_EQ(0, close(ruleset_fd));
+               }
+               ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+               EXPECT_EQ(0, test_sendmsg(_metadata, &variant->prot, client_fd,
+                                         srv0_fd, &self->srv0, false, false));
+               EXPECT_EQ(0, close(client_fd));
+               _exit(_metadata->exit_code);
+       }
+       EXPECT_EQ(child, waitpid(child, &status, 0));
+       EXPECT_EQ(1, WIFEXITED(status));
+       EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+TEST_F(protocol, sendmsg_unspec)
+{
+       const bool restricted = is_restricted(&variant->prot, variant->sandbox);
+       int client_fd, srv0_fd, srv1_fd, res;
+       char read_buf[1] = { 0 };
+
+       /*
+        * We already test for the absence of influence on sendmsg for other
+        * socket types and other address families, there's no point in adapting
+        * this test for stream sockets too.
+        */
+       if (variant->prot.type != SOCK_DGRAM)
+               return;
+
+       /* Prepare client of the right family. */
+       ASSERT_LE(0, client_fd = socket_variant(&self->srv0));
+
+       /* Prepare server on port #0 to be allowed. */
+       ASSERT_LE(0, srv0_fd = socket_variant(&self->srv0));
+       ASSERT_EQ(0, bind_variant(srv0_fd, &self->srv0));
+
+       /* And another server on port #1 to be denied. */
+       ASSERT_LE(0, srv1_fd = socket_variant(&self->srv1));
+       ASSERT_EQ(0, bind_variant(srv1_fd, &self->srv1));
+
+       if (variant->sandbox == UDP_SANDBOX) {
+               const struct landlock_ruleset_attr ruleset_attr = {
+                       .handled_access_net =
+                               LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+               };
+               const struct landlock_net_port_attr rule = {
+                       .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+                       .port = self->srv0.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,
+                                           &rule, 0));
+               enforce_ruleset(_metadata, ruleset_fd);
+               EXPECT_EQ(0, close(ruleset_fd));
+       }
+
+       /* Explicit AF_UNSPEC address but truncated. */
+       EXPECT_EQ(-EINVAL, sendto_variant_addrlen(
+                                  client_fd, &self->unspec_srv0,
+                                  get_addrlen(&self->unspec_srv0, true) - 1,
+                                  "A", 1, 0));
+
+       /*
+        * Explicit AF_UNSPEC address, should be treated as AF_INET by IPv4
+        * sockets (and thus map to srv0, allowed), but be denied by IPv6
+        * sockets.
+        */
+       res = sendto_variant(client_fd, &self->unspec_srv0, "B", 1, 0);
+       if (variant->prot.domain == AF_INET6) {
+               if (restricted) {
+                       /* Always denied on IPv6 socket. */
+                       EXPECT_EQ(-EACCES, res);
+               } else {
+                       /* IPv6 sockets treat AF_UNSPEC as a NULL address. */
+                       EXPECT_EQ(-EDESTADDRREQ, res);
+               }
+       } else if (variant->prot.domain == AF_INET) {
+               ASSERT_EQ(0, res);
+               EXPECT_EQ(1, read(srv0_fd, read_buf, 1))
+               {
+                       TH_LOG("read() failed: %s", strerror(errno));
+               }
+               EXPECT_EQ(read_buf[0], 'B');
+       } else {
+               /* Unix sockets don't accept AF_UNSPEC. */
+               EXPECT_EQ(-EINVAL, res);
+       }
+
+       /*
+        * Explicit AF_UNSPEC address, should be treated as AF_INET on IPv4
+        * sockets (and thus map to srv1, denied), and be denied on IPv6 sockets
+        * as always.
+        */
+       res = sendto_variant(client_fd, &self->unspec_srv1, "C", 1, 0);
+       if (variant->prot.domain == AF_INET6) {
+               if (restricted) {
+                       /* Always denied on IPv6 socket. */
+                       EXPECT_EQ(-EACCES, res);
+               } else {
+                       /* IPv6 sockets treat AF_UNSPEC as a NULL address. */
+                       EXPECT_EQ(-EDESTADDRREQ, res);
+               }
+       } else if (variant->prot.domain == AF_INET) {
+               if (restricted) {
+                       /* Sending to srv1 is not allowed, only srv0. */
+                       EXPECT_EQ(-EACCES, res);
+               } else {
+                       ASSERT_EQ(0, res);
+                       EXPECT_EQ(1, read(srv1_fd, read_buf, 1))
+                       {
+                               TH_LOG("read() failed: %s", strerror(errno));
+                       }
+                       EXPECT_EQ(read_buf[0], 'C');
+               }
+       } else {
+               /* Unix sockets don't accept AF_UNSPEC. */
+               EXPECT_EQ(-EINVAL, res);
+       }
+
+       ASSERT_EQ(0, connect_variant(client_fd, &self->srv0));
+
+       /* Minimal explicit AF_UNSPEC address (just the sa_family_t field) */
+       res = sendto_variant_addrlen(client_fd, &self->unspec_srv0,
+                                    get_addrlen(&self->unspec_srv0, true), "D",
+                                    1, 0);
+       if (variant->prot.domain == AF_INET6) {
+               if (restricted) {
+                       /* AF_UNSPEC is always denied in IPv6. */
+                       EXPECT_EQ(-EACCES, res);
+               } else {
+                       /*
+                        * IPv6 sockets treat AF_UNSPEC as a NULL address,
+                        * falling back to the connected address.
+                        */
+                       ASSERT_EQ(0, res);
+                       EXPECT_EQ(1, read(srv0_fd, read_buf, 1));
+                       EXPECT_EQ(read_buf[0], 'D');
+               }
+       } 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);
+       }
+}
+
 FIXTURE(ipv4)
 {
        struct service_fixture srv0, srv1;
@@ -2187,6 +2777,7 @@ FIXTURE(audit)
 {
        struct service_fixture srv0;
        struct service_fixture srv1;
+       struct service_fixture unspec_srv0;
        struct audit_filter audit_filter;
        int audit_fd;
 };
@@ -2239,8 +2830,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->srv1, variant->prot, 1));
+       ASSERT_EQ(0, set_service(&self->unspec_srv0, prot_unspec, 0));
 
        setup_loopback(_metadata);
 
@@ -2419,4 +3015,59 @@ TEST_F(audit, connect_bound)
        EXPECT_EQ(0, close(sock_fd));
 }
 
+TEST_F(audit, sendmsg)
+{
+       const struct landlock_ruleset_attr ruleset_attr = {
+               .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP |
+                                     LANDLOCK_ACCESS_NET_BIND_UDP,
+       };
+       const struct landlock_net_port_attr rule = {
+               .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+               .port = self->srv1.port,
+       };
+       struct audit_records records;
+       int ruleset_fd;
+       int sock_fd;
+
+       /* Sendmsg on stream sockets is never denied. */
+       if (variant->prot.type != SOCK_DGRAM)
+               return;
+
+       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,
+                                      &rule, 0));
+       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_auditlog(self->audit_fd, "net\\.connect_send_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);
+
+       /* Check that autobind generates a denied bind event. */
+       EXPECT_EQ(-EACCES, sendto_variant(sock_fd, &self->srv1, "A", 1, 0));
+       EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp", NULL,
+                                     NULL, NULL));
+       EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+       EXPECT_EQ(0, records.access);
+       EXPECT_EQ(0, records.domain);
+
+       EXPECT_EQ(-EACCES,
+                 sendto_variant(sock_fd, &self->unspec_srv0, "B", 1, 0));
+       EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.connect_send_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