[PATCH 3/6] landlock: Add MPTCP bind and connect access rights
From: Günther Noack <hidden>
Date: 2026-08-30 20:17:07
Also in:
linux-security-module, mptcp
Subsystem:
kernel selftest framework, landlock security module, security subsystem, the rest · Maintainers:
Shuah Khan, Shuah Khan, Mickaël Salaün, Paul Moore, James Morris, "Serge E. Hallyn", Linus Torvalds
MPTCP sockets have equivalent bind(2) and connect(2) operations as TCP
sockets, but can not currently be restricted with Landlock without
explicit MPTCP access rights. As MPTCP operates on the same TCP port
number space as TCP, this is a gap in Landlock's policies.
Add access rights for MPTCP bind(2) and connect(2) operations
and document them in the header.
Treat TCP Fast Open the same as done for plain TCP in
commit 33cb713db016 ("landlock: Fix TCP Fast Open connection bypass")
The port numbers used in MPTCP subflows are negotiated by the kernel
and therefore not subject to these access rights.
Bump the Landlock ABI version to 12.
Closes: https://github.com/landlock-lsm/linux/issues/54
Signed-off-by: Günther Noack <redacted>
---
include/linux/landlock.h | 5 +-
include/uapi/linux/landlock.h | 24 +++++++
security/landlock/limits.h | 2 +-
security/landlock/net.c | 68 ++++++++++++++------
security/landlock/syscalls.c | 2 +-
tools/testing/selftests/landlock/base_test.c | 2 +-
6 files changed, 79 insertions(+), 24 deletions(-)
diff --git a/include/linux/landlock.h b/include/linux/landlock.h
index 004cbd0b9298..b04ffc7caa21 100644
--- a/include/linux/landlock.h
+++ b/include/linux/landlock.h@@ -46,7 +46,10 @@ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_TCP, "connect_tcp"), \ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_UDP, "bind_udp"), \ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, \ - "connect_send_udp") + "connect_send_udp"), \ + _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_MPTCP, "bind_mptcp"), \ + _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_MPTCP, \ + "connect_mptcp") #define _LANDLOCK_SCOPE_NAMES \ _LANDLOCK_NAME_ENTRY(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, \
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index cceda3b3b961..2a953ba7ce25 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h@@ -448,6 +448,9 @@ struct landlock_net_port_attr { * - %LANDLOCK_ACCESS_NET_CONNECT_TCP: Connect TCP sockets to the given * remote port. Support added in Landlock ABI version 4. * + * .. note:: These rights do not apply to MPTCP sockets, which have their own + * access rights (see below). + * * And similarly for UDP port numbers: * * - %LANDLOCK_ACCESS_NET_BIND_UDP: Bind UDP sockets to the given local
@@ -474,12 +477,33 @@ struct landlock_net_port_attr { * .. note:: Sending datagrams to an ``AF_UNSPEC`` destination address * family is not supported for IPv6 UDP sockets: you will need to use a * ``NULL`` address instead. + * + * MPTCP sockets (created with ``IPPROTO_MPTCP``) use TCP port numbers, but + * they are controlled by their own access rights: + * + * - %LANDLOCK_ACCESS_NET_BIND_MPTCP: Bind MPTCP sockets to the given local + * port. Support added in Landlock ABI version 12. + * - %LANDLOCK_ACCESS_NET_CONNECT_MPTCP: Connect MPTCP sockets to the given + * remote port. Support added in Landlock ABI version 12. + * + * .. note:: The TCP and the MPTCP access rights are independent, even though + * they refer to the same port number space. Handling only + * %LANDLOCK_ACCESS_NET_BIND_TCP and %LANDLOCK_ACCESS_NET_CONNECT_TCP leaves + * MPTCP sockets unrestricted, and vice versa. A sandbox that wants to + * control all TCP-based traffic needs to handle both sets. + * + * .. note:: These MPTCP access rights restrict the ports passed to + * :manpage:`bind(2)` and :manpage:`connect(2)`. The ports used in MPTCP + * subflows are negotiated in the MPTCP protocol by the kernel and are not + * subject to these restrictions. */ /* 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_SEND_UDP (1ULL << 3) +#define LANDLOCK_ACCESS_NET_BIND_MPTCP (1ULL << 4) +#define LANDLOCK_ACCESS_NET_CONNECT_MPTCP (1ULL << 5) /* clang-format on */ /**
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 1a7c5fb8f6fd..d25e056b7ca2 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_SEND_UDP +#define LANDLOCK_LAST_ACCESS_NET LANDLOCK_ACCESS_NET_CONNECT_MPTCP #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 8f2aaac54b33..8541b0c07d64 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c@@ -11,6 +11,7 @@ #include <linux/net.h> #include <linux/socket.h> #include <net/ipv6.h> +#include <net/mptcp.h> #include "common.h" #include "cred.h"
@@ -53,6 +54,26 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset, return err; } +static bool sk_is_mptcp_socket(const struct sock *sk) +{ + return sk_is_inet(sk) && sk->sk_type == SOCK_STREAM && + sk->sk_protocol == IPPROTO_MPTCP; +} + +static bool is_connect_access(const access_mask_t access_request) +{ + return access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP || + access_request == LANDLOCK_ACCESS_NET_CONNECT_MPTCP || + access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP; +} + +static bool is_bind_access(const access_mask_t access_request) +{ + return access_request == LANDLOCK_ACCESS_NET_BIND_TCP || + access_request == LANDLOCK_ACCESS_NET_BIND_MPTCP || + access_request == LANDLOCK_ACCESS_NET_BIND_UDP; +} + static bool unmask_layers_net(const struct landlock_domain *const domain, const struct landlock_id id, struct layer_masks *masks,
@@ -104,6 +125,7 @@ static int current_check_access_socket(struct socket *const sock, switch (address->sa_family) { case AF_UNSPEC: if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP || + access_request == LANDLOCK_ACCESS_NET_CONNECT_MPTCP || (access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP && connecting)) { /*
@@ -147,17 +169,15 @@ static int current_check_access_socket(struct socket *const sock, }); return -EACCES; } - } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP || - access_request == LANDLOCK_ACCESS_NET_BIND_UDP) { + } else if (is_bind_access(access_request)) { /* * Binding to an AF_UNSPEC address is treated * differently by IPv4 and IPv6 sockets. The socket's * family may change under our feet due to * setsockopt(IPV6_ADDRFORM), but that's ok: we either - * reject entirely for IPv6 or require - * %LANDLOCK_ACCESS_NET_BIND_TCP or - * %LANDLOCK_ACCESS_NET_BIND_UDP for IPv4, so it cannot - * be used to bypass the policy. + * reject entirely for IPv6 or require the relevant bind + * access right for IPv4, so it cannot be used to bypass + * the policy. * * IPv4 sockets map AF_UNSPEC to AF_INET for * retrocompatibility for bind accesses, only if the
@@ -204,12 +224,10 @@ static int current_check_access_socket(struct socket *const sock, addr4 = (struct sockaddr_in *)address; port = addr4->sin_port; - if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP || - access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) { + if (is_connect_access(access_request)) { audit_net.dport = port; audit_net.v4info.daddr = addr4->sin_addr.s_addr; - } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP || - access_request == LANDLOCK_ACCESS_NET_BIND_UDP) { + } else if (is_bind_access(access_request)) { audit_net.sport = port; audit_net.v4info.saddr = addr4->sin_addr.s_addr; } else {
@@ -228,12 +246,10 @@ static int current_check_access_socket(struct socket *const sock, addr6 = (struct sockaddr_in6 *)address; port = addr6->sin6_port; - if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP || - access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) { + if (is_connect_access(access_request)) { audit_net.dport = port; audit_net.v6info.daddr = addr6->sin6_addr; - } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP || - access_request == LANDLOCK_ACCESS_NET_BIND_UDP) { + } else if (is_bind_access(access_request)) { audit_net.sport = port; audit_net.v6info.saddr = addr6->sin6_addr; } else {
@@ -331,6 +347,8 @@ static int hook_socket_bind(struct socket *const sock, if (sk_is_tcp(sock->sk)) access_request = LANDLOCK_ACCESS_NET_BIND_TCP; + else if (sk_is_mptcp_socket(sock->sk)) + access_request = LANDLOCK_ACCESS_NET_BIND_MPTCP; else if (sk_is_udp(sock->sk)) access_request = LANDLOCK_ACCESS_NET_BIND_UDP; else
@@ -349,6 +367,8 @@ static int hook_socket_connect(struct socket *const sock, if (sk_is_tcp(sock->sk)) access_request = LANDLOCK_ACCESS_NET_CONNECT_TCP; + else if (sk_is_mptcp_socket(sock->sk)) + access_request = LANDLOCK_ACCESS_NET_CONNECT_MPTCP; else if (sk_is_udp(sock->sk)) access_request = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP; else
@@ -377,12 +397,20 @@ static int hook_socket_sendmsg(struct socket *const sock, access_mask_t access_request; int ret = 0; - if ((msg->msg_flags & MSG_FASTOPEN) && address && sk_is_tcp(sock->sk)) { - ret = current_check_access_socket( - sock, address, addrlen, LANDLOCK_ACCESS_NET_CONNECT_TCP, - true); - if (ret != 0) - return ret; + if ((msg->msg_flags & MSG_FASTOPEN) && address) { + access_mask_t fastopen_access = 0; + + if (sk_is_tcp(sock->sk)) + fastopen_access = LANDLOCK_ACCESS_NET_CONNECT_TCP; + else if (sk_is_mptcp_socket(sock->sk)) + fastopen_access = LANDLOCK_ACCESS_NET_CONNECT_MPTCP; + + if (fastopen_access) { + ret = current_check_access_socket( + sock, address, addrlen, fastopen_access, true); + if (ret != 0) + return ret; + } } if (sk_is_udp(sock->sk))
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 1d02d57f4c48..cc54d4f1d502 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c@@ -172,7 +172,7 @@ static const struct file_operations ruleset_fops = { * If the change involves a fix that requires userspace awareness, also update * the errata documentation in Documentation/userspace-api/landlock.rst . */ -const int landlock_abi_version = 11; +const int landlock_abi_version = 12; /** * sys_landlock_create_ruleset - Create a new ruleset
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index d20ab8f0862c..58fe322d8637 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c@@ -76,7 +76,7 @@ TEST(abi_version) const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, }; - ASSERT_EQ(11, landlock_create_ruleset(NULL, 0, + ASSERT_EQ(12, landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)); ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
--
2.55.0