[RFC PATCH v3 5/8] landlock: Add UDP sendmsg access control

Matthieu Buffet matthieu at buffet.re
Fri Dec 12 16:37:01 UTC 2025


Add support for a LANDLOCK_ACCESS_NET_SENDTO_UDP access right, providing
control over specifying a UDP datagram's destination address explicitly
in sendto(), sendmsg(), and sendmmsg().
This complements the previous control of connect() via
LANDLOCK_ACCESS_NET_CONNECT_UDP.

Signed-off-by: Matthieu Buffet <matthieu at buffet.re>
---
 include/uapi/linux/landlock.h | 13 ++++++++
 security/landlock/audit.c     |  1 +
 security/landlock/limits.h    |  2 +-
 security/landlock/net.c       | 61 +++++++++++++++++++++++++++++++++--
 4 files changed, 74 insertions(+), 3 deletions(-)

diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 8f748fcf79dd..c43586e02216 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -352,12 +352,25 @@ struct landlock_net_port_attr {
  * - %LANDLOCK_ACCESS_NET_CONNECT_UDP: Connect UDP sockets to remote
  *   addresses with the given remote port. Support added in Landlock ABI
  *   version 8.
+ * - %LANDLOCK_ACCESS_NET_SENDTO_UDP: Send datagrams on UDP sockets with
+ *   an explicit destination address set to the given remote port.
+ *   Support added in Landlock ABI version 8. Note: this access right
+ *   does not control sending datagrams with no explicit destination
+ *   (e.g. via :manpage:`send(2)` or ``sendto(..., NULL, 0)``, so this
+ *   access right is not necessary when specifying a destination address
+ *   once and for all in :manpage:`connect(2)`.
+ *
+ *   Note: sending datagrams to an explicit ``AF_UNSPEC`` destination
+ *   address family is not supported. For IPv4 sockets, you will need to
+ *   use an ``AF_INET`` address instead, and for IPv6 sockets, you will
+ *   need to use a ``NULL`` address.
  */
 /* clang-format off */
 #define LANDLOCK_ACCESS_NET_BIND_TCP			(1ULL << 0)
 #define LANDLOCK_ACCESS_NET_CONNECT_TCP			(1ULL << 1)
 #define LANDLOCK_ACCESS_NET_BIND_UDP			(1ULL << 2)
 #define LANDLOCK_ACCESS_NET_CONNECT_UDP			(1ULL << 3)
+#define LANDLOCK_ACCESS_NET_SENDTO_UDP			(1ULL << 4)
 /* clang-format on */
 
 /**
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 23d8dee320ef..e0c030727dab 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -46,6 +46,7 @@ static const char *const net_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp",
 	[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_UDP)] = "net.bind_udp",
 	[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_UDP)] = "net.connect_udp",
+	[BIT_INDEX(LANDLOCK_ACCESS_NET_SENDTO_UDP)] = "net.sendto_udp",
 };
 
 static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 13dd5503e471..b6d26bc5c49e 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -23,7 +23,7 @@
 #define LANDLOCK_MASK_ACCESS_FS		((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_FS		__const_hweight64(LANDLOCK_MASK_ACCESS_FS)
 
-#define LANDLOCK_LAST_ACCESS_NET	LANDLOCK_ACCESS_NET_CONNECT_UDP
+#define LANDLOCK_LAST_ACCESS_NET	LANDLOCK_ACCESS_NET_SENDTO_UDP
 #define LANDLOCK_MASK_ACCESS_NET	((LANDLOCK_LAST_ACCESS_NET << 1) - 1)
 #define LANDLOCK_NUM_ACCESS_NET		__const_hweight64(LANDLOCK_MASK_ACCESS_NET)
 
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 9bddcf466ce9..061a531339de 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -121,6 +121,34 @@ static int current_check_access_socket(struct socket *const sock,
 				else
 					return -EAFNOSUPPORT;
 			}
+		} else if (access_request == LANDLOCK_ACCESS_NET_SENDTO_UDP) {
+			/*
+			 * We cannot allow LANDLOCK_ACCESS_NET_SENDTO_UDP on an
+			 * explicit AF_UNSPEC address. That's because semantics
+			 * of AF_UNSPEC change between socket families (e.g.
+			 * IPv6 treat it as "no address" in the sendmsg()
+			 * syscall family, so we should always allow, whilst
+			 * IPv4 treat it as AF_INET, so we should filter based
+			 * on port, and future address families might even do
+			 * something else), and the socket's family can change
+			 * under our feet due to setsockopt(IPV6_ADDRFORM).
+			 */
+			audit_net.family = AF_UNSPEC;
+			landlock_init_layer_masks(subject->domain,
+						  access_request, &layer_masks,
+						  LANDLOCK_KEY_NET_PORT);
+			landlock_log_denial(
+				subject,
+				&(struct landlock_request){
+					.type = LANDLOCK_REQUEST_NET_ACCESS,
+					.audit.type = LSM_AUDIT_DATA_NET,
+					.audit.u.net = &audit_net,
+					.access = access_request,
+					.layer_masks = &layer_masks,
+					.layer_masks_size =
+						ARRAY_SIZE(layer_masks),
+				});
+			return -EACCES;
 		} else {
 			WARN_ON_ONCE(1);
 		}
@@ -136,7 +164,8 @@ static int current_check_access_socket(struct socket *const sock,
 		port = addr4->sin_port;
 
 		if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP ||
-		    access_request == LANDLOCK_ACCESS_NET_CONNECT_UDP) {
+		    access_request == LANDLOCK_ACCESS_NET_CONNECT_UDP ||
+		    access_request == LANDLOCK_ACCESS_NET_SENDTO_UDP) {
 			audit_net.dport = port;
 			audit_net.v4info.daddr = addr4->sin_addr.s_addr;
 		} else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
@@ -160,7 +189,8 @@ static int current_check_access_socket(struct socket *const sock,
 		port = addr6->sin6_port;
 
 		if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP ||
-		    access_request == LANDLOCK_ACCESS_NET_CONNECT_UDP) {
+		    access_request == LANDLOCK_ACCESS_NET_CONNECT_UDP ||
+		    access_request == LANDLOCK_ACCESS_NET_SENDTO_UDP) {
 			audit_net.dport = port;
 			audit_net.v6info.daddr = addr6->sin6_addr;
 		} else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
@@ -248,9 +278,36 @@ static int hook_socket_connect(struct socket *const sock,
 					   access_request);
 }
 
+static int hook_socket_sendmsg(struct socket *const sock,
+			       struct msghdr *const msg, const int size)
+{
+	struct sockaddr *const address = msg->msg_name;
+	const int addrlen = msg->msg_namelen;
+	access_mask_t access_request;
+
+	/*
+	 * If there is no explicit address in the message, we have no
+	 * policy to enforce here because either:
+	 * - the socket has a remote address assigned, so the appropriate
+	 *   access check has already been done back then at assignment time;
+	 * - or, we can let the networking stack reply -EDESTADDRREQ.
+	 */
+	if (!address)
+		return 0;
+
+	if (sk_is_udp(sock->sk))
+		access_request = LANDLOCK_ACCESS_NET_SENDTO_UDP;
+	else
+		return 0;
+
+	return current_check_access_socket(sock, address, addrlen,
+					   access_request);
+}
+
 static struct security_hook_list landlock_hooks[] __ro_after_init = {
 	LSM_HOOK_INIT(socket_bind, hook_socket_bind),
 	LSM_HOOK_INIT(socket_connect, hook_socket_connect),
+	LSM_HOOK_INIT(socket_sendmsg, hook_socket_sendmsg),
 };
 
 __init void landlock_add_net_hooks(void)
-- 
2.47.3




More information about the Linux-security-module-archive mailing list