* [syzbot] Monthly wireless report (May 2026)
From: syzbot @ 2026-05-02 12:32 UTC (permalink / raw)
To: linux-kernel, linux-wireless, netdev, syzkaller-bugs
Hello wireless maintainers/developers,
This is a 31-day syzbot report for the wireless subsystem.
All related reports/information can be found at:
https://syzkaller.appspot.com/upstream/s/wireless
During the period, 2 new issues were detected and 0 were fixed.
In total, 47 issues are still open and 174 have already been fixed.
Some of the still happening issues:
Ref Crashes Repro Title
<1> 28976 Yes WARNING in rate_control_rate_init (3)
https://syzkaller.appspot.com/bug?extid=9bdc0c5998ab45b05030
<2> 18964 No WARNING in kcov_remote_start (6)
https://syzkaller.appspot.com/bug?extid=3f51ad7ac3ae57a6fdcc
<3> 10661 Yes WARNING in __rate_control_send_low (3)
https://syzkaller.appspot.com/bug?extid=34463a129786910405dd
<4> 6955 Yes WARNING in __cfg80211_ibss_joined (2)
https://syzkaller.appspot.com/bug?extid=7f064ba1704c2466e36d
<5> 1227 Yes WARNING in ieee80211_start_next_roc
https://syzkaller.appspot.com/bug?extid=c3a167b5615df4ccd7fb
<6> 1049 Yes INFO: task hung in reg_process_self_managed_hints
https://syzkaller.appspot.com/bug?extid=1f16507d9ec05f64210a
<7> 903 Yes INFO: task hung in reg_check_chans_work (7)
https://syzkaller.appspot.com/bug?extid=a2de4763f84f61499210
<8> 827 Yes INFO: rcu detected stall in ieee80211_handle_queued_frames
https://syzkaller.appspot.com/bug?extid=1c991592da3ef18957c0
<9> 624 Yes INFO: task hung in crda_timeout_work (8)
https://syzkaller.appspot.com/bug?extid=d41f74db64598e0b5016
<10> 382 Yes WARNING in ieee80211_tx_skb_tid
https://syzkaller.appspot.com/bug?extid=8bd4574e8c52c48c2595
---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.
To disable reminders for individual bugs, reply with the following command:
#syz set <Ref> no-reminders
To change bug's subsystems, reply with:
#syz set <Ref> subsystems: new-subsystem
You may send multiple commands in a single email message.
^ permalink raw reply
* [PATCH net] net: prevent possible UAF in rtnl_prop_list_size()
From: Eric Dumazet @ 2026-05-02 12:41 UTC (permalink / raw)
To: David S . Miller, Jakub Kicinski, Paolo Abeni
Cc: Simon Horman, netdev, eric.dumazet, Eric Dumazet
I was mistaken by synchronize_rcu() [1] call in netdev_name_node_alt_destroy(),
giving a false sense of RCU safety at delete times.
We have to use list_del_rcu() to not confuse potential readers
in rtnl_prop_list_size().
[1] This synchronize_rcu() call was later removed in commit 723de3ebef03
("net: free altname using an RCU callback").
Fixes: 9f30831390ed ("net: add rcu safety to rtnl_prop_list_size()")
Signed-off-by: Eric Dumazet <edumazet@google.com>
---
net/core/dev.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/net/core/dev.c b/net/core/dev.c
index 06c195906231a39ef58dfff299964a3419d87c88..8bfa8313ef62eda9fe6aa037c6a5408df54e91cf 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -371,7 +371,7 @@ static void netdev_name_node_alt_free(struct rcu_head *head)
static void __netdev_name_node_alt_destroy(struct netdev_name_node *name_node)
{
netdev_name_node_del(name_node);
- list_del(&name_node->list);
+ list_del_rcu(&name_node->list);
call_rcu(&name_node->rcu, netdev_name_node_alt_free);
}
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related
* [PATCH v4 2/7] landlock: Add UDP connect() access control
From: Matthieu Buffet @ 2026-05-02 12:43 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260502124306.3975990-1-matthieu@buffet.re>
Add support for a second fine-grained UDP access right.
This first half of LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP controls the
ability to set the remote port of a socket (via connect()). It will be
useful for applications that send datagrams, and for some servers too
(those creating per-client sockets, which want to receive traffic only
from a specific address).
Similarly as for bind(), this access control is performed when
configuring sockets, not in hot code paths.
Include detection of when autobind is about to be required, and check if
the process would be allowed to call bind(0) explicitly. Autobind can
only be performed when sending a first datagram, when connect()ing, and
in some splice() EOF edge case which, afaiu, can only happen after a
remote peer has been set (which is already covered).
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
include/uapi/linux/landlock.h | 19 +++++
security/landlock/audit.c | 2 +
security/landlock/limits.h | 2 +-
security/landlock/net.c | 79 +++++++++++++++++----
tools/testing/selftests/landlock/net_test.c | 5 +-
5 files changed, 92 insertions(+), 15 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 045b251ff1b4..22c8cc63f30e 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -378,11 +378,30 @@ struct landlock_net_port_attr {
*
* - %LANDLOCK_ACCESS_NET_BIND_UDP: Bind UDP sockets to the given local
* port. Support added in Landlock ABI version 10.
+ * - %LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP: Set the remote port of UDP
+ * sockets to the given port, or send datagrams to the given remote port
+ * ignoring any destination pre-set on a socket. Support added in
+ * Landlock ABI version 10.
+ *
+ * .. note:: Setting a remote address or sending a first datagram
+ * auto-binds UDP sockets to an ephemeral local source port if not
+ * already bound. To allow this if both %LANDLOCK_ACCESS_NET_BIND_UDP
+ * and %LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP are handled, you need to
+ * either:
+ *
+ * - use a socket already bound to a port before the ruleset started
+ * being enforced;
+ * - or grant %LANDLOCK_ACCESS_NET_BIND_UDP on port 0, meaning "any
+ * port in the ephemeral port range";
+ * - or grant %LANDLOCK_ACCESS_NET_BIND_UDP on a specific port, and
+ * call :manpage:`bind(2)` on that port before trying to
+ * :manpage:`connect(2)` or send datagrams.
*/
/* 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)
/* clang-format on */
/**
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index e676ebffeebe..851647197a01 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -46,6 +46,8 @@ static const char *const net_access_strings[] = {
[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp",
[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_SEND_UDP)] =
+ "net.connect_send_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 c0f30a4591b8..a4d908b240a2 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_BIND_UDP
+#define LANDLOCK_LAST_ACCESS_NET LANDLOCK_ACCESS_NET_CONNECT_SEND_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 f9ccb52e7d45..045881f81295 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -68,16 +68,17 @@ 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) {
+ if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP ||
+ access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) {
/*
* Connecting to an address with AF_UNSPEC dissolves
- * the TCP association, which have the same effect as
- * closing the connection while retaining the socket
- * object (i.e., the file descriptor). As for dropping
- * privileges, closing connections is always allowed.
- *
- * For a TCP access control system, this request is
- * legitimate. Let the network stack handle potential
+ * the remote association while retaining the socket
+ * object (i.e., the file descriptor). For TCP, it has
+ * the same effect as closing the connection. For UDP,
+ * it removes any preset remote address. As for
+ * dropping privileges, these actions are always
+ * allowed.
+ * Let the network stack handle potential
* inconsistencies and return -EINVAL if needed.
*/
return 0;
@@ -134,7 +135,8 @@ 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) {
+ if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP ||
+ access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) {
audit_net.dport = port;
audit_net.v4info.daddr = addr4->sin_addr.s_addr;
} else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
@@ -157,7 +159,8 @@ 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) {
+ if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP ||
+ access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) {
audit_net.dport = port;
audit_net.v6info.daddr = addr6->sin6_addr;
} else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
@@ -213,6 +216,50 @@ static int current_check_access_socket(struct socket *const sock,
return -EACCES;
}
+static int current_check_autobind_udp_socket(struct socket *const sock)
+{
+ struct sockaddr_storage port0 = { 0 };
+
+ /*
+ * On UDP sockets, if a local port has not already been bound,
+ * calling connect() or sending a first datagram has the side
+ * effect of autobinding an ephemeral port: we also have to check
+ * that the process would have had the right to bind(0) explicitly.
+ * Note: socket is not locked, so another thread could do an
+ * explicit bind(!=0) on this socket, changing inet_num to non-zero
+ * after we read it, but this would only have us enforce an
+ * additional bind(0) access check and would not bypass policy.
+ */
+ if (inet_sk(sock->sk)->inet_num != 0)
+ return 0;
+
+ /*
+ * Construct a struct sockaddr* with port 0 to pretend the
+ * process tried to bind() on that address.
+ */
+ port0.ss_family = sock->sk->__sk_common.skc_family;
+ switch (port0.ss_family) {
+ case AF_INET: {
+ ((struct sockaddr_in *)&port0)->sin_port = 0;
+ break;
+ }
+
+#if IS_ENABLED(CONFIG_IPV6)
+ case AF_INET6: {
+ ((struct sockaddr_in6 *)&port0)->sin6_port = 0;
+ break;
+ }
+#endif /* IS_ENABLED(CONFIG_IPV6) */
+
+ default:
+ return 0;
+ }
+
+ return current_check_access_socket(sock, (struct sockaddr *)&port0,
+ sizeof(port0),
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+}
+
static int hook_socket_bind(struct socket *const sock,
struct sockaddr *const address, const int addrlen)
{
@@ -234,14 +281,22 @@ static int hook_socket_connect(struct socket *const sock,
const int addrlen)
{
access_mask_t access_request;
+ int ret = 0;
if (sk_is_tcp(sock->sk))
access_request = LANDLOCK_ACCESS_NET_CONNECT_TCP;
+ else if (sk_is_udp(sock->sk))
+ access_request = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP;
else
return 0;
- return current_check_access_socket(sock, address, addrlen,
- access_request);
+ ret = current_check_access_socket(sock, address, addrlen,
+ access_request);
+
+ if (ret == 0 && sk_is_udp(sock->sk))
+ ret = current_check_autobind_udp_socket(sock);
+
+ return ret;
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index ec392d971ea3..016c7277e370 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -1326,12 +1326,13 @@ FIXTURE_TEARDOWN(mini)
/* clang-format off */
-#define ACCESS_LAST LANDLOCK_ACCESS_NET_BIND_UDP
+#define ACCESS_LAST LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP
#define ACCESS_ALL ( \
LANDLOCK_ACCESS_NET_BIND_TCP | \
LANDLOCK_ACCESS_NET_CONNECT_TCP | \
- LANDLOCK_ACCESS_NET_BIND_UDP)
+ LANDLOCK_ACCESS_NET_BIND_UDP | \
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)
/* clang-format on */
--
2.39.5
^ permalink raw reply related
* [PATCH v4 3/7] landlock: Add UDP send access control
From: Matthieu Buffet @ 2026-05-02 12:43 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260502124306.3975990-1-matthieu@buffet.re>
Add the second half of LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP: control the
ability to specify an explicit destination when sending a datagram, to
override any remote peer set on a UDP socket (in sendto(), sendmsg(), and
sendmmsg()). It will make the right useful for clients which want to
send datagrams while specifying a destination address each time.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
include/uapi/linux/landlock.h | 4 ++
security/landlock/net.c | 70 ++++++++++++++++++++++++++++++++---
2 files changed, 68 insertions(+), 6 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 22c8cc63f30e..b147223efc97 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -396,6 +396,10 @@ struct landlock_net_port_attr {
* - or grant %LANDLOCK_ACCESS_NET_BIND_UDP on a specific port, and
* call :manpage:`bind(2)` on that port before trying to
* :manpage:`connect(2)` or send datagrams.
+ *
+ * .. 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.
*/
/* clang-format off */
#define LANDLOCK_ACCESS_NET_BIND_TCP (1ULL << 0)
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 045881f81295..8a53aebdb8c6 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -44,7 +44,8 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
static int current_check_access_socket(struct socket *const sock,
struct sockaddr *const address,
const int addrlen,
- access_mask_t access_request)
+ access_mask_t access_request,
+ bool connecting)
{
__be16 port;
struct layer_access_masks layer_masks = {};
@@ -69,7 +70,8 @@ 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_SEND_UDP) {
+ (access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP &&
+ connecting)) {
/*
* Connecting to an address with AF_UNSPEC dissolves
* the remote association while retaining the socket
@@ -82,6 +84,35 @@ static int current_check_access_socket(struct socket *const sock,
* inconsistencies and return -EINVAL if needed.
*/
return 0;
+ } else if (access_request ==
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) {
+ if (sock->sk->__sk_common.skc_family == AF_INET6) {
+ /*
+ * We cannot allow sending UDP datagrams to an
+ * explicit AF_UNSPEC address on IPv6 sockets,
+ * even if AF_UNSPEC is treated as "no address"
+ * on such sockets (so it should always be allowed).
+ * That's because the socket's family can change under
+ * our feet (if another thread calls setsockopt(IPV6_ADDRFORM))
+ * to IPv4, which would then treat AF_UNSPEC as
+ * AF_INET.
+ */
+ 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,
+ });
+ return -EACCES;
+ }
} else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
access_request == LANDLOCK_ACCESS_NET_BIND_UDP) {
/*
@@ -124,7 +155,10 @@ static int current_check_access_socket(struct socket *const sock,
} else {
WARN_ON_ONCE(1);
}
- /* Only for bind(AF_UNSPEC+INADDR_ANY) on IPv4 socket. */
+ /*
+ * For bind(AF_UNSPEC+INADDR_ANY) on IPv4 socket and
+ * for sending to AF_UNSPEC addresses on IPv4 socket.
+ */
fallthrough;
case AF_INET: {
const struct sockaddr_in *addr4;
@@ -257,7 +291,7 @@ static int current_check_autobind_udp_socket(struct socket *const sock)
return current_check_access_socket(sock, (struct sockaddr *)&port0,
sizeof(port0),
- LANDLOCK_ACCESS_NET_BIND_UDP);
+ LANDLOCK_ACCESS_NET_BIND_UDP, false);
}
static int hook_socket_bind(struct socket *const sock,
@@ -273,7 +307,7 @@ static int hook_socket_bind(struct socket *const sock,
return 0;
return current_check_access_socket(sock, address, addrlen,
- access_request);
+ access_request, false);
}
static int hook_socket_connect(struct socket *const sock,
@@ -291,7 +325,7 @@ static int hook_socket_connect(struct socket *const sock,
return 0;
ret = current_check_access_socket(sock, address, addrlen,
- access_request);
+ access_request, true);
if (ret == 0 && sk_is_udp(sock->sk))
ret = current_check_autobind_udp_socket(sock);
@@ -299,9 +333,33 @@ static int hook_socket_connect(struct socket *const sock,
return ret;
}
+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;
+ int ret = 0;
+
+ if (sk_is_udp(sock->sk))
+ access_request = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP;
+ else
+ return 0;
+
+ if (address != NULL)
+ ret = current_check_access_socket(sock, address, addrlen,
+ access_request, false);
+
+ if (ret == 0)
+ ret = current_check_autobind_udp_socket(sock);
+
+ return ret;
+}
+
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.39.5
^ permalink raw reply related
* [PATCH v4 4/7] selftests/landlock: Add UDP bind/connect tests
From: Matthieu Buffet @ 2026-05-02 12:43 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260502124306.3975990-1-matthieu@buffet.re>
Make basic changes to the existing bind() and connect() test suite to
cover UDP restriction.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
tools/testing/selftests/landlock/net_test.c | 488 ++++++++++++++++----
1 file changed, 401 insertions(+), 87 deletions(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 016c7277e370..568a6ed7139c 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -35,6 +35,7 @@ enum sandbox_type {
NO_SANDBOX,
/* This may be used to test rules that allow *and* deny accesses. */
TCP_SANDBOX,
+ UDP_SANDBOX,
};
static int set_service(struct service_fixture *const srv,
@@ -93,23 +94,53 @@ static bool prot_is_tcp(const struct protocol_variant *const prot)
(prot->protocol == IPPROTO_TCP || prot->protocol == IPPROTO_IP);
}
+static bool prot_is_udp(const struct protocol_variant *const prot)
+{
+ return (prot->domain == AF_INET || prot->domain == AF_INET6) &&
+ prot->type == SOCK_DGRAM &&
+ (prot->protocol == IPPROTO_UDP || prot->protocol == IPPROTO_IP);
+}
+
static bool is_restricted(const struct protocol_variant *const prot,
const enum sandbox_type sandbox)
{
if (sandbox == TCP_SANDBOX)
return prot_is_tcp(prot);
+ else if (sandbox == UDP_SANDBOX)
+ return prot_is_udp(prot);
return false;
}
static int socket_variant(const struct service_fixture *const srv)
{
+ /* Arbitrary value just to not block other tests indefinitely. */
+ const struct timeval timeout = {
+ .tv_sec = 0,
+ .tv_usec = 100000,
+ };
+ int sockfd;
int ret;
- ret = socket(srv->protocol.domain, srv->protocol.type | SOCK_CLOEXEC,
- srv->protocol.protocol);
- if (ret < 0)
+ sockfd = socket(srv->protocol.domain, srv->protocol.type | SOCK_CLOEXEC,
+ srv->protocol.protocol);
+ if (sockfd < 0)
return -errno;
- return ret;
+
+ ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout,
+ sizeof(timeout));
+ if (ret != 0) {
+ ret = -errno;
+ close(sockfd);
+ return ret;
+ }
+ ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout,
+ sizeof(timeout));
+ if (ret != 0) {
+ ret = -errno;
+ close(sockfd);
+ return ret;
+ }
+ return sockfd;
}
#ifndef SIN6_LEN_RFC2133
@@ -271,10 +302,9 @@ FIXTURE_VARIANT(protocol)
FIXTURE_SETUP(protocol)
{
- const struct protocol_variant prot_unspec = {
- .domain = AF_UNSPEC,
- .type = SOCK_STREAM,
- };
+ struct protocol_variant prot_unspec = variant->prot;
+
+ prot_unspec.domain = AF_UNSPEC;
disable_caps(_metadata);
@@ -510,6 +540,92 @@ FIXTURE_VARIANT_ADD(protocol, tcp_sandbox_with_unix_datagram) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp1) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ .protocol = IPPROTO_UDP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_udp2) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ /* IPPROTO_IP == 0 */
+ .protocol = IPPROTO_IP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp1) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ .protocol = IPPROTO_UDP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_udp2) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ /* IPPROTO_IP == 0 */
+ .protocol = IPPROTO_IP,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv4_tcp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_STREAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_ipv6_tcp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_STREAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_stream) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_UNIX,
+ .type = SOCK_STREAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(protocol, udp_sandbox_with_unix_datagram) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_UNIX,
+ .type = SOCK_DGRAM,
+ },
+};
+
static void test_bind_and_connect(struct __test_metadata *const _metadata,
const struct service_fixture *const srv,
const bool deny_bind, const bool deny_connect)
@@ -602,7 +718,7 @@ static void test_bind_and_connect(struct __test_metadata *const _metadata,
ret = connect_variant(connect_fd, srv);
if (deny_connect) {
EXPECT_EQ(-EACCES, ret);
- } else if (deny_bind) {
+ } else if (deny_bind && srv->protocol.type == SOCK_STREAM) {
/* No listening server. */
EXPECT_EQ(-ECONNREFUSED, ret);
} else {
@@ -641,18 +757,25 @@ static void test_bind_and_connect(struct __test_metadata *const _metadata,
TEST_F(protocol, bind)
{
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const __u64 bind_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const __u64 conn_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = bind_access | conn_access,
};
- const struct landlock_net_port_attr tcp_bind_connect_p0 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_p0 = {
+ .allowed_access = bind_access | conn_access,
.port = self->srv0.port,
};
- const struct landlock_net_port_attr tcp_connect_p1 = {
- .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr connect_p1 = {
+ .allowed_access = conn_access,
.port = self->srv1.port,
};
int ruleset_fd;
@@ -664,12 +787,26 @@ TEST_F(protocol, bind)
/* Allows connect and bind for the first port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_p0, 0));
+ &bind_connect_p0, 0));
/* Allows connect and denies bind for the second port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_connect_p1, 0));
+ &connect_p1, 0));
+
+ /*
+ * For UDP sockets, allows binding to ephemeral ports
+ * (required to connect or send a first datagram)
+ */
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_net_port_attr bind_ephemeral = {
+ .allowed_access = bind_access,
+ .port = 0,
+ };
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_NET_PORT,
+ &bind_ephemeral, 0));
+ }
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -691,18 +828,25 @@ TEST_F(protocol, bind)
TEST_F(protocol, connect)
{
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const __u64 bind_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const __u64 conn_access =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = bind_access | conn_access,
};
- const struct landlock_net_port_attr tcp_bind_connect_p0 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_p0 = {
+ .allowed_access = bind_access | conn_access,
.port = self->srv0.port,
};
- const struct landlock_net_port_attr tcp_bind_p1 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ const struct landlock_net_port_attr bind_p1 = {
+ .allowed_access = bind_access,
.port = self->srv1.port,
};
int ruleset_fd;
@@ -714,12 +858,26 @@ TEST_F(protocol, connect)
/* Allows connect and bind for the first port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_p0, 0));
+ &bind_connect_p0, 0));
/* Allows bind and denies connect for the second port. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_p1, 0));
+ &bind_p1, 0));
+
+ /*
+ * For UDP sockets, allows binding to ephemeral ports
+ * (required to connect or send a first datagram)
+ */
+ if (variant->sandbox == UDP_SANDBOX) {
+ const struct landlock_net_port_attr bind_ephemeral = {
+ .allowed_access = bind_access,
+ .port = 0,
+ };
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_NET_PORT,
+ &bind_ephemeral, 0));
+ }
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -737,16 +895,20 @@ TEST_F(protocol, connect)
TEST_F(protocol, bind_unspec)
{
+ const int bind_access = (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .handled_access_net = bind_access,
};
- const struct landlock_net_port_attr tcp_bind = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ const struct landlock_net_port_attr rule_bind = {
+ .allowed_access = bind_access,
.port = self->srv0.port,
};
int bind_fd, ret;
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
@@ -754,7 +916,7 @@ TEST_F(protocol, bind_unspec)
/* Allows bind. */
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind, 0));
+ &rule_bind, 0));
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
@@ -782,7 +944,8 @@ TEST_F(protocol, bind_unspec)
}
EXPECT_EQ(0, close(bind_fd));
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
@@ -828,11 +991,15 @@ TEST_F(protocol, bind_unspec)
TEST_F(protocol, connect_unspec)
{
+ const __u64 connect_right =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = connect_right,
};
- const struct landlock_net_port_attr tcp_connect = {
- .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr rule_connect = {
+ .allowed_access = connect_right,
.port = self->srv0.port,
};
int bind_fd, client_fd, status;
@@ -865,7 +1032,8 @@ TEST_F(protocol, connect_unspec)
EXPECT_EQ(0, ret);
}
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
@@ -873,7 +1041,7 @@ TEST_F(protocol, connect_unspec)
/* Allows connect. */
ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
LANDLOCK_RULE_NET_PORT,
- &tcp_connect, 0));
+ &rule_connect, 0));
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
}
@@ -896,7 +1064,8 @@ TEST_F(protocol, connect_unspec)
EXPECT_EQ(0, ret);
}
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
const int ruleset_fd = landlock_create_ruleset(
&ruleset_attr, sizeof(ruleset_attr), 0);
ASSERT_LE(0, ruleset_fd);
@@ -975,6 +1144,13 @@ FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_tcp) {
.type = SOCK_STREAM,
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_tcp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .type = SOCK_STREAM,
+};
+
/* clang-format off */
FIXTURE_VARIANT_ADD(ipv4, no_sandbox_with_udp) {
/* clang-format on */
@@ -989,6 +1165,13 @@ FIXTURE_VARIANT_ADD(ipv4, tcp_sandbox_with_udp) {
.type = SOCK_DGRAM,
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(ipv4, udp_sandbox_with_udp) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .type = SOCK_DGRAM,
+};
+
FIXTURE_SETUP(ipv4)
{
const struct protocol_variant prot = {
@@ -1012,14 +1195,19 @@ TEST_F(ipv4, from_unix_to_inet)
{
int unix_stream_fd, unix_dgram_fd;
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const int access_rights =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = access_rights,
};
const struct landlock_net_port_attr tcp_bind_connect_p0 = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .allowed_access = access_rights,
.port = self->srv0.port,
};
int ruleset_fd;
@@ -1680,6 +1868,7 @@ TEST_F(ipv4_tcp, with_fs)
FIXTURE(port_specific)
{
struct service_fixture srv0;
+ struct service_fixture cli1;
};
FIXTURE_VARIANT(port_specific)
@@ -1699,7 +1888,7 @@ FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv4) {
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv4) {
+FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv4) {
/* clang-format on */
.sandbox = TCP_SANDBOX,
.prot = {
@@ -1708,6 +1897,16 @@ FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv4) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv4) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ },
+};
+
/* clang-format off */
FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv6) {
/* clang-format on */
@@ -1719,7 +1918,7 @@ FIXTURE_VARIANT_ADD(port_specific, no_sandbox_with_ipv6) {
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv6) {
+FIXTURE_VARIANT_ADD(port_specific, tcp_sandbox_with_ipv6) {
/* clang-format on */
.sandbox = TCP_SANDBOX,
.prot = {
@@ -1728,11 +1927,22 @@ FIXTURE_VARIANT_ADD(port_specific, sandbox_with_ipv6) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(port_specific, udp_sandbox_with_ipv6) {
+ /* clang-format on */
+ .sandbox = UDP_SANDBOX,
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ },
+};
+
FIXTURE_SETUP(port_specific)
{
disable_caps(_metadata);
ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
+ ASSERT_EQ(0, set_service(&self->cli1, variant->prot, 1));
setup_loopback(_metadata);
};
@@ -1747,14 +1957,19 @@ TEST_F(port_specific, bind_connect_zero)
uint16_t port;
/* Adds a rule layer with bind and connect actions. */
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const int access_rights =
+ (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP
+ .handled_access_net = access_rights,
};
- const struct landlock_net_port_attr tcp_bind_connect_zero = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_zero = {
+ .allowed_access = access_rights,
.port = 0,
};
int ruleset_fd;
@@ -1766,7 +1981,7 @@ TEST_F(port_specific, bind_connect_zero)
/* Checks zero port value on bind and connect actions. */
EXPECT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_zero, 0));
+ &bind_connect_zero, 0));
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -1787,11 +2002,16 @@ TEST_F(port_specific, bind_connect_zero)
ret = bind_variant(bind_fd, &self->srv0);
EXPECT_EQ(0, ret);
- EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_STREAM)
+ EXPECT_EQ(0, listen(bind_fd, backlog));
/* Connects on port 0. */
ret = connect_variant(connect_fd, &self->srv0);
- EXPECT_EQ(-ECONNREFUSED, ret);
+ if (variant->prot.type == SOCK_STREAM) {
+ EXPECT_EQ(-ECONNREFUSED, ret);
+ } else {
+ EXPECT_EQ(0, ret);
+ }
/* Sets binded port for both protocol families. */
port = get_binded_port(bind_fd, &variant->prot);
@@ -1815,23 +2035,35 @@ TEST_F(port_specific, bind_connect_1023)
int bind_fd, connect_fd, ret;
/* Adds a rule layer with bind and connect actions. */
- if (variant->sandbox == TCP_SANDBOX) {
+ if (variant->sandbox == TCP_SANDBOX ||
+ variant->sandbox == UDP_SANDBOX) {
+ const int bind_right = (variant->sandbox == TCP_SANDBOX ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const int access_rights =
+ (variant->sandbox == TCP_SANDBOX ?
+ (LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP) :
+ (LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP));
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP
+ .handled_access_net = access_rights,
};
/* A rule with port value less than 1024. */
- const struct landlock_net_port_attr tcp_bind_connect_low_range = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect_low_range = {
+ .allowed_access = access_rights,
.port = 1023,
};
/* A rule with 1024 port. */
- const struct landlock_net_port_attr tcp_bind_connect = {
- .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ const struct landlock_net_port_attr bind_connect = {
+ .allowed_access = access_rights,
.port = 1024,
};
+ /* A rule with cli1's port, to use as source port. */
+ const struct landlock_net_port_attr srcport = {
+ .allowed_access = bind_right,
+ .port = self->cli1.port,
+ };
int ruleset_fd;
ruleset_fd = landlock_create_ruleset(&ruleset_attr,
@@ -1840,10 +2072,15 @@ TEST_F(port_specific, bind_connect_1023)
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect_low_range, 0));
+ &bind_connect_low_range, 0));
ASSERT_EQ(0,
landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &tcp_bind_connect, 0));
+ &bind_connect, 0));
+ if (variant->sandbox == UDP_SANDBOX) {
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_NET_PORT,
+ &srcport, 0));
+ }
enforce_ruleset(_metadata, ruleset_fd);
EXPECT_EQ(0, close(ruleset_fd));
@@ -1867,8 +2104,19 @@ TEST_F(port_specific, bind_connect_1023)
ret = bind_variant(bind_fd, &self->srv0);
clear_cap(_metadata, CAP_NET_BIND_SERVICE);
EXPECT_EQ(0, ret);
- EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_STREAM)
+ EXPECT_EQ(0, listen(bind_fd, backlog));
+ connect_fd = socket_variant(&self->srv0);
+ ASSERT_LE(0, connect_fd);
+ if (variant->prot.type == SOCK_DGRAM) {
+ /*
+ * We are about to connect(), but bind() is restricted, so for
+ * UDP sockets we need to use cli1's port as source port (the
+ * only one we are allowed to use).
+ */
+ EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1));
+ }
/* Connects on the binded port 1023. */
ret = connect_variant(connect_fd, &self->srv0);
EXPECT_EQ(0, ret);
@@ -1887,7 +2135,10 @@ TEST_F(port_specific, bind_connect_1023)
/* Binds on port 1024. */
ret = bind_variant(bind_fd, &self->srv0);
EXPECT_EQ(0, ret);
- EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_STREAM)
+ EXPECT_EQ(0, listen(bind_fd, backlog));
+ if (variant->prot.type == SOCK_DGRAM)
+ EXPECT_EQ(0, bind_variant(connect_fd, &self->cli1));
/* Connects on the binded port 1024. */
ret = connect_variant(connect_fd, &self->srv0);
@@ -1897,23 +2148,30 @@ TEST_F(port_specific, bind_connect_1023)
EXPECT_EQ(0, close(bind_fd));
}
-static int matches_log_tcp(const int audit_fd, const char *const blockers,
- const char *const dir_addr, const char *const addr,
- const char *const dir_port)
+static int matches_auditlog(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 log_with_addrport_tmpl[] = REGEX_LANDLOCK_PREFIX
" blockers=%s %s=%s %s=1024$";
+ static const char log_without_addrport_tmpl[] = 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(log_with_addrport_tmpl) + 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),
+ log_without_addrport_tmpl, blockers);
+ else
+ log_match_len = snprintf(log_match, sizeof(log_match),
+ log_with_addrport_tmpl, blockers,
+ dir_addr, addr, dir_port);
if (log_match_len > sizeof(log_match))
return -E2BIG;
@@ -1924,6 +2182,7 @@ static int matches_log_tcp(const int audit_fd, const char *const blockers,
FIXTURE(audit)
{
struct service_fixture srv0;
+ struct service_fixture srv1;
struct audit_filter audit_filter;
int audit_fd;
};
@@ -1935,7 +2194,7 @@ FIXTURE_VARIANT(audit)
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(audit, ipv4) {
+FIXTURE_VARIANT_ADD(audit, ipv4_tcp) {
/* clang-format on */
.addr = "127\\.0\\.0\\.1",
.prot = {
@@ -1945,7 +2204,17 @@ FIXTURE_VARIANT_ADD(audit, ipv4) {
};
/* clang-format off */
-FIXTURE_VARIANT_ADD(audit, ipv6) {
+FIXTURE_VARIANT_ADD(audit, ipv4_udp) {
+ /* clang-format on */
+ .addr = "127\\.0\\.0\\.1",
+ .prot = {
+ .domain = AF_INET,
+ .type = SOCK_DGRAM,
+ },
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit, ipv6_tcp) {
/* clang-format on */
.addr = "::1",
.prot = {
@@ -1954,9 +2223,21 @@ FIXTURE_VARIANT_ADD(audit, ipv6) {
},
};
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit, ipv6_udp) {
+ /* clang-format on */
+ .addr = "::1",
+ .prot = {
+ .domain = AF_INET6,
+ .type = SOCK_DGRAM,
+ },
+};
+
FIXTURE_SETUP(audit)
{
ASSERT_EQ(0, set_service(&self->srv0, variant->prot, 0));
+ ASSERT_EQ(0, set_service(&self->srv1, variant->prot, 1));
+
setup_loopback(_metadata);
set_cap(_metadata, CAP_AUDIT_CONTROL);
@@ -1974,9 +2255,17 @@ FIXTURE_TEARDOWN(audit)
TEST_F(audit, bind)
{
+ const char *audit_evt = (variant->prot.type == SOCK_STREAM ?
+ "net\\.bind_tcp" :
+ "net\\.bind_udp");
+ const int access_rights =
+ (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = access_rights,
};
struct audit_records records;
int ruleset_fd, sock_fd;
@@ -1990,8 +2279,8 @@ TEST_F(audit, bind)
sock_fd = socket_variant(&self->srv0);
ASSERT_LE(0, sock_fd);
EXPECT_EQ(-EACCES, bind_variant(sock_fd, &self->srv0));
- EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.bind_tcp", "saddr",
- variant->addr, "src"));
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "saddr",
+ variant->addr, "src"));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
@@ -2002,9 +2291,22 @@ TEST_F(audit, bind)
TEST_F(audit, connect)
{
+ const char *audit_evt = (variant->prot.type == SOCK_STREAM ?
+ "net\\.connect_tcp" :
+ "net\\.connect_send_udp");
+ const int bind_right = (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_BIND_TCP :
+ LANDLOCK_ACCESS_NET_BIND_UDP);
+ const int conn_right = (variant->prot.type == SOCK_STREAM ?
+ LANDLOCK_ACCESS_NET_CONNECT_TCP :
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+ const int access_rights = bind_right | conn_right;
const struct landlock_ruleset_attr ruleset_attr = {
- .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .handled_access_net = access_rights,
+ };
+ const struct landlock_net_port_attr rule_connect_p1 = {
+ .allowed_access = conn_right,
+ .port = self->srv1.port,
};
struct audit_records records;
int ruleset_fd, sock_fd;
@@ -2012,19 +2314,31 @@ TEST_F(audit, connect)
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_connect_p1, 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, connect_variant(sock_fd, &self->srv0));
- EXPECT_EQ(0, matches_log_tcp(self->audit_fd, "net\\.connect_tcp",
- "daddr", variant->addr, "dest"));
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, audit_evt, "daddr",
+ variant->addr, "dest"));
EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
EXPECT_EQ(0, records.access);
EXPECT_EQ(1, records.domain);
+ if (variant->prot.type == SOCK_DGRAM) {
+ /* Check that autobind generates a denied bind event. */
+ EXPECT_EQ(-EACCES, connect_variant(sock_fd, &self->srv1));
+
+ EXPECT_EQ(0, matches_auditlog(self->audit_fd, "net\\.bind_udp",
+ NULL, NULL, NULL));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(1, records.domain);
+ }
+
EXPECT_EQ(0, close(sock_fd));
}
--
2.39.5
^ permalink raw reply related
* [PATCH v4 5/7] selftests/landlock: Add tests for sendmsg()
From: Matthieu Buffet @ 2026-05-02 12:43 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260502124306.3975990-1-matthieu@buffet.re>
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>
---
tools/testing/selftests/landlock/net_test.c | 652 +++++++++++++++++++-
1 file changed, 651 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 568a6ed7139c..2c72fda3c606 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -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);
@@ -1119,6 +1274,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 {
+ EXPECT_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 {
+ EXPECT_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) {
+ EXPECT_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 {
+ 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], '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.
+ */
+ EXPECT_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;
@@ -2183,6 +2773,7 @@ FIXTURE(audit)
{
struct service_fixture srv0;
struct service_fixture srv1;
+ struct service_fixture unspec_srv0;
struct audit_filter audit_filter;
int audit_fd;
};
@@ -2235,8 +2826,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);
@@ -2342,4 +2938,58 @@ 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_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,
+ };
+ const int ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ struct audit_records records;
+ int sock_fd;
+
+ /* Sendmsg on stream sockets is never denied. */
+ if (variant->prot.type != SOCK_DGRAM)
+ return;
+
+ 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, records.access);
+ EXPECT_EQ(1, 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
--
2.39.5
^ permalink raw reply related
* [PATCH v4 6/7] samples/landlock: Add sandboxer UDP access control
From: Matthieu Buffet @ 2026-05-02 12:43 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260502124306.3975990-1-matthieu@buffet.re>
Add environment variables to control associated access rights:
- LL_UDP_BIND
- LL_UDP_CONNECT_SEND
Each one takes a list of ports separated by colons, like other list
options.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
samples/landlock/sandboxer.c | 40 ++++++++++++++++++++++++++++++++++--
1 file changed, 38 insertions(+), 2 deletions(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 66e56ae275c6..94e399e6b146 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -62,6 +62,8 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_SCOPED_NAME "LL_SCOPED"
#define ENV_FORCE_LOG_NAME "LL_FORCE_LOG"
+#define ENV_UDP_BIND_NAME "LL_UDP_BIND"
+#define ENV_UDP_CONNECT_SEND_NAME "LL_UDP_CONNECT_SEND"
#define ENV_DELIMITER ":"
static int str2num(const char *numstr, __u64 *num_dst)
@@ -301,7 +303,7 @@ static bool check_ruleset_scope(const char *const env_var,
/* clang-format on */
-#define LANDLOCK_ABI_LAST 9
+#define LANDLOCK_ABI_LAST 10
#define XSTR(s) #s
#define STR(s) XSTR(s)
@@ -324,6 +326,10 @@ static const char help[] =
"means an empty list):\n"
"* " ENV_TCP_BIND_NAME ": ports allowed to bind (server)\n"
"* " ENV_TCP_CONNECT_NAME ": ports allowed to connect (client)\n"
+ "* " ENV_UDP_BIND_NAME ": local UDP ports allowed to bind (server: "
+ "prepare to receive on port / client: set as source port)\n"
+ "* " ENV_UDP_CONNECT_SEND_NAME ": remote UDP ports allowed to connect "
+ "or sendmsg (client: use as destination port / server: receive only from it)\n"
"* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n"
" - \"a\" to restrict opening abstract unix sockets\n"
" - \"s\" to restrict sending signals\n"
@@ -336,6 +342,7 @@ static const char help[] =
ENV_FS_RW_NAME "=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
ENV_TCP_BIND_NAME "=\"9418\" "
ENV_TCP_CONNECT_NAME "=\"80:443\" "
+ ENV_UDP_CONNECT_SEND_NAME "=\"53\" "
ENV_SCOPED_NAME "=\"a:s\" "
"%1$s bash -i\n"
"\n"
@@ -356,7 +363,9 @@ int main(const int argc, char *const argv[], char *const *const envp)
struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = access_fs_rw,
.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ LANDLOCK_ACCESS_NET_CONNECT_TCP |
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
.scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
};
@@ -444,6 +453,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
/* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
ruleset_attr.handled_access_fs &=
~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
+ __attribute__((fallthrough));
+ case 9:
+ /* Removes UDP support for ABI < 10 */
+ ruleset_attr.handled_access_net &=
+ ~(LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
"Hint: You should update the running kernel "
@@ -475,6 +491,18 @@ int main(const int argc, char *const argv[], char *const *const envp)
ruleset_attr.handled_access_net &=
~LANDLOCK_ACCESS_NET_CONNECT_TCP;
}
+ /* Removes UDP bind access control if not supported by a user. */
+ env_port_name = getenv(ENV_UDP_BIND_NAME);
+ if (!env_port_name) {
+ ruleset_attr.handled_access_net &=
+ ~LANDLOCK_ACCESS_NET_BIND_UDP;
+ }
+ /* Removes UDP connect/send access control if not supported by a user. */
+ env_port_name = getenv(ENV_UDP_CONNECT_SEND_NAME);
+ if (!env_port_name) {
+ ruleset_attr.handled_access_net &=
+ ~LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP;
+ }
if (check_ruleset_scope(ENV_SCOPED_NAME, &ruleset_attr))
return 1;
@@ -519,6 +547,14 @@ int main(const int argc, char *const argv[], char *const *const envp)
LANDLOCK_ACCESS_NET_CONNECT_TCP)) {
goto err_close_ruleset;
}
+ if (populate_ruleset_net(ENV_UDP_BIND_NAME, ruleset_fd,
+ LANDLOCK_ACCESS_NET_BIND_UDP)) {
+ goto err_close_ruleset;
+ }
+ if (populate_ruleset_net(ENV_UDP_CONNECT_SEND_NAME, ruleset_fd,
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)) {
+ goto err_close_ruleset;
+ }
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
--
2.39.5
^ permalink raw reply related
* [PATCH v4 7/7] landlock: Add documentation for UDP support
From: Matthieu Buffet @ 2026-05-02 12:43 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260502124306.3975990-1-matthieu@buffet.re>
Add example of UDP usage, without detailing the two access right.
Slightly change the example used in code blocks: build a ruleset for a
DNS client, so that it uses both TCP and UDP.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
Documentation/userspace-api/landlock.rst | 89 ++++++++++++++++++------
1 file changed, 68 insertions(+), 21 deletions(-)
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index fd8b78c31f2f..9d5da9896628 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -40,8 +40,8 @@ Filesystem rules
and the related filesystem actions are defined with
`filesystem access rights`.
-Network rules (since ABI v4)
- For these rules, the object is a TCP port,
+Network rules (since ABI v4 for TCP and v10 for UDP)
+ For these rules, the object is a TCP or UDP port,
and the related actions are defined with `network access rights`.
Defining and enforcing a security policy
@@ -49,11 +49,11 @@ Defining and enforcing a security policy
We first need to define the ruleset that will contain our rules.
-For this example, the ruleset will contain rules that only allow filesystem
-read actions and establish a specific TCP connection. Filesystem write
-actions and other TCP actions will be denied.
+For this example, the ruleset will contain rules that only allow some
+filesystem read actions and some specific UDP and TCP actions. Filesystem
+write actions and other TCP/UDP actions will be denied.
-The ruleset then needs to handle both these kinds of actions. This is
+The ruleset then needs to handle all these kinds of actions. This is
required for backward and forward compatibility (i.e. the kernel and user
space may not know each other's supported restrictions), hence the need
to be explicit about the denied-by-default access rights.
@@ -81,7 +81,9 @@ to be explicit about the denied-by-default access rights.
LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
.handled_access_net =
LANDLOCK_ACCESS_NET_BIND_TCP |
- LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ LANDLOCK_ACCESS_NET_CONNECT_TCP |
+ LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
.scoped =
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
LANDLOCK_SCOPE_SIGNAL,
@@ -132,6 +134,12 @@ version, and only use the available subset of access rights:
case 6 ... 8:
/* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
+ __attribute__((fallthrough));
+ case 9:
+ /* Removes LANDLOCK_ACCESS_*_UDP for ABI < 10 */
+ ruleset_attr.handled_access_net &=
+ ~(LANDLOCK_ACCESS_NET_BIND_UDP |
+ LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
}
This enables the creation of an inclusive ruleset that will contain our rules.
@@ -180,21 +188,50 @@ this file descriptor.
It may also be required to create rules following the same logic as explained
for the ruleset creation, by filtering access rights according to the Landlock
-ABI version. In this example, this is not required because all of the requested
-``allowed_access`` rights are already available in ABI 1.
+ABI version. So far, this was not required because all of the requested
+``allowed_access`` rights have always been available, from ABI 1.
-For network access-control, we can add a set of rules that allow to use a port
-number for a specific action: HTTPS connections.
+For network access-control, we will add a set of rules to allow DNS
+queries, which requires both UDP and TCP. For TCP, we need to allow
+outbound connections to port 53, which can be handled and granted starting
+with ABI 4:
.. code-block:: c
- struct landlock_net_port_attr net_port = {
- .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
- .port = 443,
- };
+ if (ruleset_attr.handled_access_net & LANDLOCK_ACCESS_NET_CONNECT_TCP) {
+ struct landlock_net_port_attr net_port = {
+ .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .port = 53,
+ };
- err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
- &net_port, 0);
+ err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &net_port, 0);
+
+We also need to be able to send UDP datagrams to port 53, which requires
+granting ``LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP``. Since our DNS client will
+emit datagrams without explicitly binding to a specific source port, its UDP
+socket will automatically bind an ephemeral port. To allow this behaviour,
+we also need to grant ``LANDLOCK_ACCESS_NET_BIND_UDP`` on port 0, as if
+the program explicitly called :manpage:`bind(2)` on port 0.
+
+.. code-block:: c
+
+ if (ruleset_attr.handled_access_net & LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) {
+ const struct landlock_net_port_attr send_dst_port = {
+ .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
+ .port = 53,
+ };
+ err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &send_dst_port, 0);
+ [...]
+
+ if (ruleset_attr.handled_access_net & LANDLOCK_ACCESS_NET_BIND_UDP) {
+ const struct landlock_net_port_attr bind_src_port = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_UDP,
+ .port = 0,
+ };
+ err = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &bind_src_port, 0);
When passing a non-zero ``flags`` argument to ``landlock_restrict_self()``, a
similar backwards compatibility check is needed for the restrict flags
@@ -228,7 +265,7 @@ similar backwards compatibility check is needed for the restrict flags
The next step is to restrict the current thread from gaining more privileges
(e.g. through a SUID binary). We now have a ruleset with the first rule
allowing read and execute access to ``/usr`` while denying all other handled
-accesses for the filesystem, and a second rule allowing HTTPS connections.
+accesses for the filesystem, and two more rules allowing DNS queries.
.. code-block:: c
@@ -716,6 +753,16 @@ Starting with the Landlock ABI version 9, it is possible to restrict
connections to pathname UNIX domain sockets (:manpage:`unix(7)`) using
the new ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` right.
+UDP bind, connect, sendto, sendmsg and sendmmsg (ABI < 10)
+----------------------------------------------------------
+
+Starting with the Landlock ABI version 10, it is possible to restrict
+setting the local port of UDP sockets with the
+``LANDLOCK_ACCESS_NET_BIND_UDP`` right.
+The ``LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP`` right controls setting the
+remote port of UDP sockets, and sending datagrams to an explicit remote
+port (ignoring any destination set on UDP sockets).
+
.. _kernel_support:
Kernel support
@@ -778,10 +825,10 @@ the boot loader.
Network support
---------------
-To be able to explicitly allow TCP operations (e.g., adding a network rule with
-``LANDLOCK_ACCESS_NET_BIND_TCP``), the kernel must support TCP
+To be able to explicitly allow TCP or UDP operations (e.g., adding a network rule with
+``LANDLOCK_ACCESS_NET_BIND_TCP``), the kernel must support the TCP/IP protocol suite
(``CONFIG_INET=y``). Otherwise, sys_landlock_add_rule() returns an
-``EAFNOSUPPORT`` error, which can safely be ignored because this kind of TCP
+``EAFNOSUPPORT`` error, which can safely be ignored because this kind of TCP or UDP
operation is already not possible.
Questions and answers
--
2.39.5
^ permalink raw reply related
* [PATCH v4 0/7] landlock: Add UDP access control support
From: Matthieu Buffet @ 2026-05-02 12:42 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
Hi,
This is V4 of UDP access control in Landlock. Thanks to the round of
review of v3, access rights have changed to something that seems easier
to use and understand. It adds only two access rights, to restrict
configuring local and remote addresses on UDP sockets. The one that
restricts setting a remote address also controls sending datagrams to
explicit remote addresses -ignoring any remote address preset on the
socket-. The one that restricts binding to a local port also applies
when the kernel auto-binds an ephemeral port.
v1:
Link: https://lore.kernel.org/all/20240916122230.114800-1-matthieu@buffet.re/
v2:
Link: https://lore.kernel.org/all/20241214184540.3835222-1-matthieu@buffet.re/
v3:
Link: https://lore.kernel.org/all/20251212163704.142301-1-matthieu@buffet.re/
The limitation around allowing a process to send but not receive is
still there, and could warrant another patch if there is a real user
need.
I'm just not super happy about the clarity of logs generated for denied
autobinds ("domain=xxxxxx blockers=net.bind_udp"), due to the fact that
addresses and ports are currently only logged if they are non-0. A later
(coordinated LSM-wide) patch could improve readability by replacing != 0
checks with new booleans in struct lsm_network_audit. I'm also not
exactly happy with the integration in existing TCP selftests, but
refactoring them has already been discussed earlier.
Changes v1->v2
==============
- recvmsg hook is gone and sendmsg hook doesn't apply when sending to a
remote address pre-set on socket, to improve performance
- don't add a get_addr_port() helper function, which required a weird
"am I in IPv4 or IPv6 context"
- reorder hook prologue for consistency: check domain, then type and
family
Changes v2->v3
==============
- removed support for sending datagrams with explicit destination
address of family AF_UNSPEC, which allowed to bypass restrictions with
a race condition
- rebased on linux-mic/next => add support for auditing
- fixed mistake in selftests when using unspec_srv variables, which were
implicitly of type SOCK_STREAM and did not actually test UDP code
- add tests for IPPROTO_IP
- improved docs, split off TCP-related refactoring
Changes v3->v4
==============
- merge LANDLOCK_ACCESS_NET_CONNECT_UDP and
LANDLOCK_ACCESS_NET_SENDTO_UDP into
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP (everything that might set the
destination of a datagram)
- make LANDLOCK_ACCESS_NET_BIND_UDP apply when kernel is about to
auto-bind an ephemeral port for the caller. Block it if policy would
not allow an explicit call to bind(0)
- only deny sending AF_UNSPEC datagrams on IPv6 sockets, where there is
a risk of the address family changing midway
Patch is based on https://git.kernel.org/pub/scm/linux/kernel/git/mic/linux.git
3457a5ccacd3 ("landlock: Document fallocate(2) as another truncation corner case")
All lines added are covered with selftests, except the "default: return
0" in current_check_autobind_udp_socket() which is not currently
reachable (net.c goes from 92.9%->94.6% line coverage).
Let me know what you think!
Closes: https://github.com/landlock-lsm/linux/issues/10
Matthieu Buffet (7):
landlock: Add UDP bind() access control
landlock: Add UDP connect() access control
landlock: Add UDP send access control
selftests/landlock: Add UDP bind/connect tests
selftests/landlock: Add tests for sendmsg()
samples/landlock: Add sandboxer UDP access control
landlock: Add documentation for UDP support
Documentation/userspace-api/landlock.rst | 89 +-
include/uapi/linux/landlock.h | 35 +-
samples/landlock/sandboxer.c | 40 +-
security/landlock/audit.c | 3 +
security/landlock/limits.h | 2 +-
security/landlock/net.c | 161 ++-
security/landlock/syscalls.c | 2 +-
tools/testing/selftests/landlock/base_test.c | 4 +-
tools/testing/selftests/landlock/net_test.c | 1146 ++++++++++++++++--
9 files changed, 1341 insertions(+), 141 deletions(-)
base-commit: 3457a5ccacd34fdd5ebd3a4745e721b5a1239690
--
2.39.5
^ permalink raw reply
* [PATCH v4 1/7] landlock: Add UDP bind() access control
From: Matthieu Buffet @ 2026-05-02 12:43 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, linux-security-module, Mikhail Ivanov,
konstantin.meskhidze, Tingmao Wang, netdev, Matthieu Buffet
In-Reply-To: <20260502124306.3975990-1-matthieu@buffet.re>
Add support for a first fine-grained UDP access right.
LANDLOCK_ACCESS_NET_BIND_UDP controls the ability to set the local port
of a UDP socket (via bind()). It will be useful for servers (to start
receiving datagrams), and for some clients that need to use a specific
source port (e.g. mDNS requires to use port 5353)
For obvious performance concerns, access control is only enforced when
configuring sockets, not when using them for common send/recv
operations.
Bump ABI to allow userspace to detect and use this new right.
Signed-off-by: Matthieu Buffet <matthieu@buffet.re>
---
include/uapi/linux/landlock.h | 12 +++++++++---
security/landlock/audit.c | 1 +
security/landlock/limits.h | 2 +-
security/landlock/net.c | 18 ++++++++++++------
security/landlock/syscalls.c | 2 +-
tools/testing/selftests/landlock/base_test.c | 4 ++--
tools/testing/selftests/landlock/net_test.c | 5 +++--
7 files changed, 29 insertions(+), 15 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 10a346e55e95..045b251ff1b4 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -201,9 +201,9 @@ struct landlock_net_port_attr {
* with ``setsockopt(IP_LOCAL_PORT_RANGE)``.
*
* A Landlock rule with port 0 and the %LANDLOCK_ACCESS_NET_BIND_TCP
- * right means that requesting to bind on port 0 is allowed and it will
- * automatically translate to binding on a kernel-assigned ephemeral
- * port.
+ * or %LANDLOCK_ACCESS_NET_BIND_UDP right means that requesting to bind
+ * on port 0 is allowed and it will automatically translate to binding
+ * on a kernel-assigned ephemeral port.
*/
__u64 port;
};
@@ -373,10 +373,16 @@ struct landlock_net_port_attr {
* port. Support added in Landlock ABI version 4.
* - %LANDLOCK_ACCESS_NET_CONNECT_TCP: Connect TCP sockets to the given
* remote port. Support added in Landlock ABI version 4.
+ *
+ * And similarly for UDP port numbers:
+ *
+ * - %LANDLOCK_ACCESS_NET_BIND_UDP: Bind UDP sockets to the given local
+ * port. Support added in Landlock ABI version 10.
*/
/* 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)
/* clang-format on */
/**
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 8d0edf94037d..e676ebffeebe 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -45,6 +45,7 @@ static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
static const char *const net_access_strings[] = {
[BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp",
[BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp",
+ [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_UDP)] = "net.bind_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 b454ad73b15e..c0f30a4591b8 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_TCP
+#define LANDLOCK_LAST_ACCESS_NET LANDLOCK_ACCESS_NET_BIND_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 c368649985c5..f9ccb52e7d45 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -81,15 +81,17 @@ static int current_check_access_socket(struct socket *const sock,
* inconsistencies and return -EINVAL if needed.
*/
return 0;
- } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) {
+ } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
+ access_request == LANDLOCK_ACCESS_NET_BIND_UDP) {
/*
* 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 or require
- * %LANDLOCK_ACCESS_NET_BIND_TCP for the given port, so
- * it cannot be used to bypass the policy.
+ * 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.
*
* IPv4 sockets map AF_UNSPEC to AF_INET for
* retrocompatibility for bind accesses, only if the
@@ -135,7 +137,8 @@ static int current_check_access_socket(struct socket *const sock,
if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) {
audit_net.dport = port;
audit_net.v4info.daddr = addr4->sin_addr.s_addr;
- } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) {
+ } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
+ access_request == LANDLOCK_ACCESS_NET_BIND_UDP) {
audit_net.sport = port;
audit_net.v4info.saddr = addr4->sin_addr.s_addr;
} else {
@@ -157,7 +160,8 @@ static int current_check_access_socket(struct socket *const sock,
if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) {
audit_net.dport = port;
audit_net.v6info.daddr = addr6->sin6_addr;
- } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP) {
+ } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP ||
+ access_request == LANDLOCK_ACCESS_NET_BIND_UDP) {
audit_net.sport = port;
audit_net.v6info.saddr = addr6->sin6_addr;
} else {
@@ -216,6 +220,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_udp(sock->sk))
+ access_request = LANDLOCK_ACCESS_NET_BIND_UDP;
else
return 0;
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index accfd2e5a0cd..d45469d5d464 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -166,7 +166,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 = 9;
+const int landlock_abi_version = 10;
/**
* 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 30d37234086c..6c8113c2ded1 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,8 +76,8 @@ TEST(abi_version)
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
- ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
- LANDLOCK_CREATE_RULESET_VERSION));
+ ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
+ LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
LANDLOCK_CREATE_RULESET_VERSION));
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 4c528154ea92..ec392d971ea3 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -1326,11 +1326,12 @@ FIXTURE_TEARDOWN(mini)
/* clang-format off */
-#define ACCESS_LAST LANDLOCK_ACCESS_NET_CONNECT_TCP
+#define ACCESS_LAST LANDLOCK_ACCESS_NET_BIND_UDP
#define ACCESS_ALL ( \
LANDLOCK_ACCESS_NET_BIND_TCP | \
- LANDLOCK_ACCESS_NET_CONNECT_TCP)
+ LANDLOCK_ACCESS_NET_CONNECT_TCP | \
+ LANDLOCK_ACCESS_NET_BIND_UDP)
/* clang-format on */
--
2.39.5
^ permalink raw reply related
* [WITHDRAW] Documentation: Fix typo "eXtention" -> "Extension" and duplicated words
From: Wang Zihan @ 2026-05-02 13:04 UTC (permalink / raw)
To: loongarch, netdev, linux-doc, linux-media, linux-um
Cc: chenhuacai, kernel, alexs, si.yanteng, corbet, davem, kuba,
mchehab, richard, linux-kernel, Wang Zihan
From: Wang Zihan <3772548978@qq.com>
I apologize for submitting incorrect patches. After review by Björn Persson,
I realized that my changes were not actually fixing errors:
1. Patch "Documentation: loongarch: Fix typo eXtention -> Extension"
The capitalization of 'X' in 'eXtention' was intentional to highlight
the letter, not a spelling mistake.
2. Patch "Documentation: Fix duplicated words" (the "later on on" part)
"Later on" is an established English expression. The original text
was not an error, just a stylistic choice.
I am withdrawing these patches:
- Message-ID: <20260502111912.143743-1-3772548978@qq.com>
- Message-ID: <20260502111916.145238-1-3772548978@qq.com>
Thank you for the correction and education. I will be more careful in the future.
Apologies for the noise.
Wang Zihan
^ permalink raw reply
* [WITHDRAW] Documentation: Fix duplicated words - "in in" was not an error
From: Wang Zihan @ 2026-05-02 13:09 UTC (permalink / raw)
To: netdev, linux-doc, linux-media, linux-um
Cc: davem, kuba, corbet, mchehab, richard, linux-kernel, Wang Zihan
From: Wang Zihan <3772548978@qq.com>
I apologize for another incorrect patch. After review by Björn Persson,
I realized that "in in" in switchdev.rst was not an error:
Original: "what position a port is in in the overall topology"
This is correct English:
- "what position a port is in" = the position that a port occupies
- "in the overall topology" = within the overall topology
My change incorrectly altered the meaning to suggest that a port IS a position,
when in fact a port is IN a position IN the topology.
I am withdrawing the "in in" part of this patch:
- Message-ID: <20260502111916.145238-1-3772548978@qq.com>
The other parts ("The the" -> "The" and "on on" -> "on") were also incorrect
as previously acknowledged.
Thank you for the correction. I will be more careful in analyzing English
grammar before submitting patches.
Apologies for the noise.
Wang Zihan
^ permalink raw reply
* [PATCH net-next v6 0/5] net: bridge: mcast: support exponential field encoding
From: Ujjal Roy @ 2026-05-02 13:19 UTC (permalink / raw)
To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Nikolay Aleksandrov, Ido Schimmel, David Ahern,
Shuah Khan, Andy Roulin, Yong Wang, Petr Machata
Cc: Ujjal Roy, bridge, netdev, linux-kernel, linux-kselftest
Description:
This series addresses a mismatch in how multicast query
intervals and response codes are handled across IPv4 (IGMPv3)
and IPv6 (MLDv2). While decoding logic currently exists,
the corresponding encoding logic is missing during query
packet generation. This leads to incorrect intervals being
transmitted when values exceed their linear thresholds.
The patches introduce a unified floating-point encoding
approach based on RFC3376 and RFC3810, ensuring that large
intervals are correctly represented in QQIC and MRC fields
using the exponent-mantissa format.
Key Changes:
* ipv4: igmp: get rid of IGMPV3_{QQIC,MRC} and simplify calculation
Removes legacy macros in favor of a cleaner, unified
calculation for retrieving intervals from encoded fields,
improving code maintainability.
* ipv6: mld: rename mldv2_mrc() and add mldv2_qqi()
Standardizes MLDv2 terminology by renaming mldv2_mrc()
to mldv2_mrd() (Maximum Response Delay) and introducing
a new API mldv2_qqi for QQI calculation, improving code
readability.
* ipv4: igmp: encode multicast exponential fields
Introduces the logic to dynamically calculate the exponent
and mantissa using bit-scan (fls). This ensures QQIC and
MRC fields (8-bit) are properly encoded when transmitting
query packets with intervals that exceed their respective
linear threshold value of 128 (for QQI/MRT).
* ipv6: mld: encode multicast exponential fields
Applies similar encoding logic for MLDv2. This ensures
QQIC (8-bit) and MRC (16-bit) fields are properly encoded
when transmitting query packets with intervals that exceed
their respective linear thresholds (128 for QQI; 32768
for MRD).
* selftests: net: bridge: add MRC and QQIC field encoding tests
Updates bridge selftests to validate both linear and non-linear
(exponential) encoding for MRC and QQIC fields, ensuring
protocol compliance across IGMPv3 and MLDv2.
Impact:
These changes ensure that multicast queriers and listeners
stay synchronized on timing intervals, preventing protocol
timeouts or premature group membership expiration caused
by incorrectly formatted packet headers.
Test results:
Giving results only from modified API vlmc_query_intvl_test() and
vlmc_query_response_intvl_test() of the script bridge_vlan_mcast.sh.
Using old Linux code, here is the result:
- TEST: Vlan multicast snooping enable [ OK ]
- TEST: Vlan mcast_query_interval global option default value [ OK ]
- TEST: Number of tagged IGMPv2 general query [ OK ]
- TEST: IGMPv3 QQIC linear value 60(s) [ OK ]
- TEST: MLDv2 QQIC linear value 60(s) [ OK ]
- TEST: IGMPv3 QQIC non linear value 160(s) [FAIL]
- Wrong QQIC in generated IGMPv3 general queries
- TEST: MLDv2 QQIC non linear value 160(s) [FAIL]
- Wrong QQIC in generated MLDv2 general queries
- TEST: Vlan mcast_query_response_interval global option default value [ OK ]
- TEST: IGMPv3 MRC linear value of 60(x0.1s) [ OK ]
- TEST: MLDv2 MRC linear value of 24000(ms) [ OK ]
- TEST: IGMPv3 MRC non linear value of 240(x0.1s) [FAIL]
- Wrong MRC in generated IGMPv3 general queries
- TEST: MLDv2 MRC non linear value of 48000(ms) [FAIL]
- Wrong MRC in generated MLDv2 general queries
Linux code with this patchset, here is the result:
* TEST: Vlan multicast snooping enable [ OK ]
* TEST: Vlan mcast_query_interval global option default value [ OK ]
* TEST: Number of tagged IGMPv2 general query [ OK ]
* TEST: IGMPv3 QQIC linear value 60(s) [ OK ]
* TEST: MLDv2 QQIC linear value 60(s) [ OK ]
* TEST: IGMPv3 QQIC non linear value 160(s) [ OK ]
* TEST: MLDv2 QQIC non linear value 160(s) [ OK ]
* TEST: Vlan mcast_query_response_interval global option default value [ OK ]
* TEST: IGMPv3 MRC linear value of 60(x0.1s) [ OK ]
* TEST: MLDv2 MRC linear value of 24000(ms) [ OK ]
* TEST: IGMPv3 MRC non linear value of 240(x0.1s) [ OK ]
* TEST: MLDv2 MRC non linear value of 48000(ms) [ OK ]
v6:
- Added Reviewed-by: Nikolay Aleksandrov <razor@blackwall.org>
- Previous thread can be found here:
https://lore.kernel.org/all/20260501173130.3486-1-royujjal@gmail.com/
v5:
- Added 'Reviewed-by:' as requested
- Modified script to fix AND logic in classifiers and updated comments
- Passed expected value as argument rather than hard coding in script
- Added minor comments for macros
v4:
- Removed binary application and related codes for selftests
- Decided hard coded match based on extra arguments for selftests
- Ran shellcheck on modified selftests code and fixed
- Updated comments on patch[1,2,3,4] as per review
- Added 'Reviewed-by:' as requested on patch4
v3:
- Updated the series title for better clarity; old one is here
https://lore.kernel.org/all/20260326150742.50289-1-royujjal@gmail.com/
- Added key changes section for new selftests patch
- Updated netdev style comments and addressed review comments
- Fixed MLDv2 MRC conversion logic during query generation
- Mentioned intervals in units wherever applicable
- Dropped type casting and fixed indentations
v2:
- Retargeted the series to net-next as suggested
- Fixed a compilation warning in the MLD rename change
- Kept reverse xmas tree order in IGMP exponential encoding change
- Added bridge selftests to validate IGMPv3 Query MRC and QQIC handling
Ujjal Roy (5):
ipv4: igmp: get rid of IGMPV3_{QQIC,MRC} and simplify calculation
ipv6: mld: rename mldv2_mrc() and add mldv2_qqi()
ipv4: igmp: encode multicast exponential fields
ipv6: mld: encode multicast exponential fields
selftests: net: bridge: add MRC and QQIC field encoding tests
include/linux/igmp.h | 167 +++++++++++++++-
include/net/mld.h | 185 +++++++++++++++++-
net/bridge/br_multicast.c | 22 +--
net/ipv4/igmp.c | 6 +-
net/ipv6/mcast.c | 19 +-
.../net/forwarding/bridge_vlan_mcast.sh | 140 ++++++++++++-
6 files changed, 483 insertions(+), 56 deletions(-)
base-commit: 0f19519fdd22bc0f5429377da3a52327c5eee166
--
2.43.0
^ permalink raw reply
* [PATCH net-next v6 1/5] ipv4: igmp: get rid of IGMPV3_{QQIC,MRC} and simplify calculation
From: Ujjal Roy @ 2026-05-02 13:19 UTC (permalink / raw)
To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Nikolay Aleksandrov, Ido Schimmel, David Ahern,
Shuah Khan, Andy Roulin, Yong Wang, Petr Machata
Cc: Ujjal Roy, bridge, netdev, linux-kernel, linux-kselftest
In-Reply-To: <20260502131907.987-1-royujjal@gmail.com>
Get rid of the IGMPV3_MRC macro and use the igmpv3_mrt() API to
calculate the Max Resp Time from the Maximum Response Code.
Similarly, for IGMPV3_QQIC, use the igmpv3_qqi() API to calculate
the Querier's Query Interval from the QQIC field.
Reviewed-by: Nikolay Aleksandrov <razor@blackwall.org>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Ujjal Roy <royujjal@gmail.com>
---
include/linux/igmp.h | 80 +++++++++++++++++++++++++++++++++++----
net/bridge/br_multicast.c | 2 +-
net/ipv4/igmp.c | 6 +--
3 files changed, 76 insertions(+), 12 deletions(-)
diff --git a/include/linux/igmp.h b/include/linux/igmp.h
index 073b30a9b850..0e095c9f234e 100644
--- a/include/linux/igmp.h
+++ b/include/linux/igmp.h
@@ -92,15 +92,79 @@ struct ip_mc_list {
struct rcu_head rcu;
};
+/* RFC3376, relevant sections:
+ * - 4.1.1. Maximum Response Code
+ * - 4.1.7. QQIC (Querier's Query Interval Code)
+ *
+ * For both MRC and QQIC, values >= 128 use the same floating-point
+ * encoding as follows:
+ *
+ * 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+
+ * |1| exp | mant |
+ * +-+-+-+-+-+-+-+-+
+ */
+#define IGMPV3_FP_EXP(value) (((value) >> 4) & 0x07)
+#define IGMPV3_FP_MAN(value) ((value) & 0x0f)
+
+/* IGMPv3 floating-point exponential field min threshold */
+#define IGMPV3_EXP_MIN_THRESHOLD 128
+
/* V3 exponential field decoding */
-#define IGMPV3_MASK(value, nb) ((nb)>=32 ? (value) : ((1<<(nb))-1) & (value))
-#define IGMPV3_EXP(thresh, nbmant, nbexp, value) \
- ((value) < (thresh) ? (value) : \
- ((IGMPV3_MASK(value, nbmant) | (1<<(nbmant))) << \
- (IGMPV3_MASK((value) >> (nbmant), nbexp) + (nbexp))))
-
-#define IGMPV3_QQIC(value) IGMPV3_EXP(0x80, 4, 3, value)
-#define IGMPV3_MRC(value) IGMPV3_EXP(0x80, 4, 3, value)
+
+/* IGMPv3 MRC/QQIC 8-bit exponential field decode
+ *
+ * RFC3376, 4.1.1 & 4.1.7. defines the decoding formula:
+ * 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+
+ * |1| exp | mant |
+ * +-+-+-+-+-+-+-+-+
+ * Max Resp Time = (mant | 0x10) << (exp + 3)
+ * QQI = (mant | 0x10) << (exp + 3)
+ */
+static inline unsigned long igmpv3_exp_field_decode(const u8 code)
+{
+ if (code < IGMPV3_EXP_MIN_THRESHOLD) {
+ return code;
+ } else {
+ unsigned long mc_man, mc_exp;
+
+ mc_exp = IGMPV3_FP_EXP(code);
+ mc_man = IGMPV3_FP_MAN(code);
+
+ return (mc_man | 0x10) << (mc_exp + 3);
+ }
+}
+
+/* Calculate Max Resp Time from Maximum Response Code
+ *
+ * RFC3376, relevant sections:
+ * - 4.1.1. Maximum Response Code
+ * - 8.3. Query Response Interval
+ *
+ * After decode, MRC represents the Maximum Response Time (MRT) in
+ * units of 0.1 seconds (100 ms).
+ */
+static inline unsigned long igmpv3_mrt(const struct igmpv3_query *ih3)
+{
+ return igmpv3_exp_field_decode(ih3->code);
+}
+
+/* Calculate Querier's Query Interval from Querier's Query Interval Code
+ *
+ * RFC3376, relevant sections:
+ * - 4.1.7. QQIC (Querier's Query Interval Code)
+ * - 8.2. Query Interval
+ * - 8.12. Older Version Querier Present Timeout
+ * (the [Query Interval] in the last Query received)
+ *
+ * After decode, QQIC represents the Querier's Query Interval in units
+ * of seconds.
+ */
+static inline unsigned long igmpv3_qqi(const struct igmpv3_query *ih3)
+{
+ return igmpv3_exp_field_decode(ih3->qqic);
+}
static inline int ip_mc_may_pull(struct sk_buff *skb, unsigned int len)
{
diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 881d866d687a..9fec76e887bc 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -3518,7 +3518,7 @@ static void br_ip4_multicast_query(struct net_bridge_mcast *brmctx,
goto out;
max_delay = ih3->code ?
- IGMPV3_MRC(ih3->code) * (HZ / IGMP_TIMER_SCALE) : 1;
+ igmpv3_mrt(ih3) * (HZ / IGMP_TIMER_SCALE) : 1;
} else {
goto out;
}
diff --git a/net/ipv4/igmp.c b/net/ipv4/igmp.c
index a674fb44ec25..d7eff36d98c3 100644
--- a/net/ipv4/igmp.c
+++ b/net/ipv4/igmp.c
@@ -991,7 +991,7 @@ static bool igmp_heard_query(struct in_device *in_dev, struct sk_buff *skb,
* different encoding. We use the v3 encoding as more likely
* to be intended in a v3 query.
*/
- max_delay = IGMPV3_MRC(ih3->code)*(HZ/IGMP_TIMER_SCALE);
+ max_delay = igmpv3_mrt(ih3) * (HZ / IGMP_TIMER_SCALE);
if (!max_delay)
max_delay = 1; /* can't mod w/ 0 */
} else { /* v3 */
@@ -1006,7 +1006,7 @@ static bool igmp_heard_query(struct in_device *in_dev, struct sk_buff *skb,
ih3 = igmpv3_query_hdr(skb);
}
- max_delay = IGMPV3_MRC(ih3->code)*(HZ/IGMP_TIMER_SCALE);
+ max_delay = igmpv3_mrt(ih3) * (HZ / IGMP_TIMER_SCALE);
if (!max_delay)
max_delay = 1; /* can't mod w/ 0 */
WRITE_ONCE(in_dev->mr_maxdelay, max_delay);
@@ -1016,7 +1016,7 @@ static bool igmp_heard_query(struct in_device *in_dev, struct sk_buff *skb,
* configured value.
*/
in_dev->mr_qrv = ih3->qrv ?: READ_ONCE(net->ipv4.sysctl_igmp_qrv);
- in_dev->mr_qi = IGMPV3_QQIC(ih3->qqic)*HZ ?: IGMP_QUERY_INTERVAL;
+ in_dev->mr_qi = igmpv3_qqi(ih3) * HZ ? : IGMP_QUERY_INTERVAL;
/* RFC3376, 8.3. Query Response Interval:
* The number of seconds represented by the [Query Response
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v6 2/5] ipv6: mld: rename mldv2_mrc() and add mldv2_qqi()
From: Ujjal Roy @ 2026-05-02 13:19 UTC (permalink / raw)
To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Nikolay Aleksandrov, Ido Schimmel, David Ahern,
Shuah Khan, Andy Roulin, Yong Wang, Petr Machata
Cc: Ujjal Roy, bridge, netdev, linux-kernel, linux-kselftest
In-Reply-To: <20260502131907.987-1-royujjal@gmail.com>
Rename mldv2_mrc() to mldv2_mrd() as it is used to calculate
the Maximum Response Delay from the Maximum Response Code.
Introduce a new API mldv2_qqi() to define the existing
calculation logic of QQI from QQIC. This also organizes
the existing mld_update_qi() API.
Reviewed-by: Nikolay Aleksandrov <razor@blackwall.org>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Ujjal Roy <royujjal@gmail.com>
---
include/net/mld.h | 66 +++++++++++++++++++++++++++++++++------
net/bridge/br_multicast.c | 2 +-
net/ipv6/mcast.c | 19 ++---------
3 files changed, 61 insertions(+), 26 deletions(-)
diff --git a/include/net/mld.h b/include/net/mld.h
index c07359808493..10e402e0fe08 100644
--- a/include/net/mld.h
+++ b/include/net/mld.h
@@ -89,29 +89,77 @@ struct mld2_query {
#define MLDV2_QQIC_EXP(value) (((value) >> 4) & 0x07)
#define MLDV2_QQIC_MAN(value) ((value) & 0x0f)
-#define MLD_EXP_MIN_LIMIT 32768UL
-#define MLDV1_MRD_MAX_COMPAT (MLD_EXP_MIN_LIMIT - 1)
+/* MLDv2 QQIC floating-point exponential field min threshold */
+#define MLD_QQIC_MIN_THRESHOLD 128
+/* MLDv2 MRC floating-point exponential field min threshold */
+#define MLD_MRC_MIN_THRESHOLD 32768UL
+#define MLDV1_MRD_MAX_COMPAT (MLD_MRC_MIN_THRESHOLD - 1)
#define MLD_MAX_QUEUE 8
#define MLD_MAX_SKBS 32
-static inline unsigned long mldv2_mrc(const struct mld2_query *mlh2)
+/* V2 exponential field decoding */
+
+/* Calculate Maximum Response Delay from Maximum Response Code
+ *
+ * RFC3810, relevant sections:
+ * - 5.1.3. Maximum Response Code defines the decoding formula:
+ * 0 1 2 3 4 5 6 7 8 9 A B C D E F
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |1| exp | mant |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * Maximum Response Delay = (mant | 0x1000) << (exp+3)
+ * - 9.3. Query Response Interval
+ *
+ * After decode, MRC represents the Maximum Response Delay (MRD) in
+ * units of milliseconds.
+ */
+static inline unsigned long mldv2_mrd(const struct mld2_query *mlh2)
{
- /* RFC3810, 5.1.3. Maximum Response Code */
- unsigned long ret, mc_mrc = ntohs(mlh2->mld2q_mrc);
+ unsigned long mc_mrc = ntohs(mlh2->mld2q_mrc);
- if (mc_mrc < MLD_EXP_MIN_LIMIT) {
- ret = mc_mrc;
+ if (mc_mrc < MLD_MRC_MIN_THRESHOLD) {
+ return mc_mrc;
} else {
unsigned long mc_man, mc_exp;
mc_exp = MLDV2_MRC_EXP(mc_mrc);
mc_man = MLDV2_MRC_MAN(mc_mrc);
- ret = (mc_man | 0x1000) << (mc_exp + 3);
+ return (mc_man | 0x1000) << (mc_exp + 3);
}
+}
- return ret;
+/* Calculate Querier's Query Interval from Querier's Query Interval Code
+ *
+ * RFC3810, relevant sections:
+ * - 5.1.9. QQIC (Querier's Query Interval Code) defines the decoding formula:
+ * 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+
+ * |1| exp | mant |
+ * +-+-+-+-+-+-+-+-+
+ * QQI = (mant | 0x10) << (exp + 3)
+ * - 9.2. Query Interval
+ * - 9.12. Older Version Querier Present Timeout
+ * (the [Query Interval] in the last Query received)
+ *
+ * After decode, QQIC represents the Querier's Query Interval in units
+ * of seconds.
+ */
+static inline unsigned long mldv2_qqi(const struct mld2_query *mlh2)
+{
+ unsigned long qqic = mlh2->mld2q_qqic;
+
+ if (qqic < MLD_QQIC_MIN_THRESHOLD) {
+ return qqic;
+ } else {
+ unsigned long mc_man, mc_exp;
+
+ mc_exp = MLDV2_QQIC_EXP(qqic);
+ mc_man = MLDV2_QQIC_MAN(qqic);
+
+ return (mc_man | 0x10) << (mc_exp + 3);
+ }
}
#endif
diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 9fec76e887bc..1438c023db62 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -3606,7 +3606,7 @@ static int br_ip6_multicast_query(struct net_bridge_mcast *brmctx,
mld2q->mld2q_suppress)
goto out;
- max_delay = max(msecs_to_jiffies(mldv2_mrc(mld2q)), 1UL);
+ max_delay = max(msecs_to_jiffies(mldv2_mrd(mld2q)), 1UL);
}
is_general_query = group && ipv6_addr_any(group);
diff --git a/net/ipv6/mcast.c b/net/ipv6/mcast.c
index 3330adcf26db..6ddc18ac59b9 100644
--- a/net/ipv6/mcast.c
+++ b/net/ipv6/mcast.c
@@ -1315,20 +1315,7 @@ static void mld_update_qi(struct inet6_dev *idev,
* - 9.12. Older Version Querier Present Timeout
* (the [Query Interval] in the last Query received)
*/
- unsigned long mc_qqi;
-
- if (mlh2->mld2q_qqic < 128) {
- mc_qqi = mlh2->mld2q_qqic;
- } else {
- unsigned long mc_man, mc_exp;
-
- mc_exp = MLDV2_QQIC_EXP(mlh2->mld2q_qqic);
- mc_man = MLDV2_QQIC_MAN(mlh2->mld2q_qqic);
-
- mc_qqi = (mc_man | 0x10) << (mc_exp + 3);
- }
-
- idev->mc_qi = mc_qqi * HZ;
+ idev->mc_qi = mldv2_qqi(mlh2) * HZ;
}
static void mld_update_qri(struct inet6_dev *idev,
@@ -1338,7 +1325,7 @@ static void mld_update_qri(struct inet6_dev *idev,
* - 5.1.3. Maximum Response Code
* - 9.3. Query Response Interval
*/
- idev->mc_qri = msecs_to_jiffies(mldv2_mrc(mlh2));
+ idev->mc_qri = msecs_to_jiffies(mldv2_mrd(mlh2));
}
static int mld_process_v1(struct inet6_dev *idev, struct mld_msg *mld,
@@ -1390,7 +1377,7 @@ static int mld_process_v1(struct inet6_dev *idev, struct mld_msg *mld,
static void mld_process_v2(struct inet6_dev *idev, struct mld2_query *mld,
unsigned long *max_delay)
{
- *max_delay = max(msecs_to_jiffies(mldv2_mrc(mld)), 1UL);
+ *max_delay = max(msecs_to_jiffies(mldv2_mrd(mld)), 1UL);
mld_update_qrv(idev, mld);
mld_update_qi(idev, mld);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v6 3/5] ipv4: igmp: encode multicast exponential fields
From: Ujjal Roy @ 2026-05-02 13:19 UTC (permalink / raw)
To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Nikolay Aleksandrov, Ido Schimmel, David Ahern,
Shuah Khan, Andy Roulin, Yong Wang, Petr Machata
Cc: Ujjal Roy, bridge, netdev, linux-kernel, linux-kselftest
In-Reply-To: <20260502131907.987-1-royujjal@gmail.com>
In IGMP, MRC and QQIC fields are not correctly encoded
when generating query packets. Since the receiver of the
query interprets these fields using the IGMPv3 floating-
point decoding logic, any value that exceeds the linear
threshold is incorrectly parsed as an exponential value,
leading to an incorrect interval calculation.
Encode and assign the corresponding protocol fields during
query generation. Introduce the logic to dynamically
calculate the exponent and mantissa using bit-scan (fls).
This ensures MRC and QQIC fields (8-bit) are properly
encoded when transmitting query packets with intervals
that exceed their respective linear threshold value of
128 (for MRT/QQI).
RFC3376: for both MRC and QQIC, values >= 128 represent
the same floating-point encoding as follows:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|1| exp | mant |
+-+-+-+-+-+-+-+-+
Reviewed-by: Nikolay Aleksandrov <razor@blackwall.org>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Ujjal Roy <royujjal@gmail.com>
---
include/linux/igmp.h | 87 +++++++++++++++++++++++++++++++++++++++
net/bridge/br_multicast.c | 14 +++----
2 files changed, 93 insertions(+), 8 deletions(-)
diff --git a/include/linux/igmp.h b/include/linux/igmp.h
index 0e095c9f234e..3a2d35a9f307 100644
--- a/include/linux/igmp.h
+++ b/include/linux/igmp.h
@@ -109,6 +109,93 @@ struct ip_mc_list {
/* IGMPv3 floating-point exponential field min threshold */
#define IGMPV3_EXP_MIN_THRESHOLD 128
+/* IGMPv3 FP max threshold (mant = 0xF, exp = 7) -> 31744 */
+#define IGMPV3_EXP_MAX_THRESHOLD 31744
+
+/* V3 exponential field encoding */
+
+/* IGMPv3 MRC/QQIC 8-bit exponential field encode
+ *
+ * RFC3376, 4.1.1 & 4.1.7. defines only the decoding formula:
+ * MRT/QQI = (mant | 0x10) << (exp + 3)
+ *
+ * but does NOT define the encoding procedure. To derive exponent:
+ *
+ * For any value of mantissa and exponent, the decoding formula
+ * indicates that the "hidden bit" (0x10) is shifted 4 bits left
+ * to sit above the 4-bit mantissa. The RFC again shifts this
+ * entire block left by (exp + 3) to reconstruct the value.
+ * So, 'hidden bit' is the MSB which is shifted by (4 + exp + 3).
+ *
+ * Total left shift of the 'hidden bit' = 4 + (exp + 3) = exp + 7.
+ * This is the MSB at the 0-based bit position: (exp + 7).
+ * Since fls() is 1-based, fls(value) - 1 = exp + 7.
+ *
+ * Therefore:
+ * exp = fls(value) - 8
+ * mant = (value >> (exp + 3)) & 0x0F
+ *
+ * Final encoding formula:
+ * 0x80 | (exp << 4) | mant
+ *
+ * Example (value = 3200):
+ * 0 1
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0| (value = 3200)
+ * | ^-^-mant^ ^..(exp+3)..^| exp = 4, mant = 9
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Encoded:
+ * 0x80 | (4 << 4) | 9 = 0xC9
+ */
+static inline u8 igmpv3_exp_field_encode(unsigned long value)
+{
+ u8 mc_exp, mc_man;
+
+ /* MRC/QQIC < 128 is literal */
+ if (value < IGMPV3_EXP_MIN_THRESHOLD)
+ return value;
+
+ /* Saturate at max representable (mant = 0xF, exp = 7) -> 31744 */
+ if (value >= IGMPV3_EXP_MAX_THRESHOLD)
+ return 0xFF;
+
+ mc_exp = fls(value) - 8;
+ mc_man = (value >> (mc_exp + 3)) & 0x0F;
+
+ return 0x80 | (mc_exp << 4) | mc_man;
+}
+
+/* Calculate Maximum Response Code from Max Resp Time
+ *
+ * RFC3376, relevant sections:
+ * - 4.1.1. Maximum Response Code
+ * - 8.3. Query Response Interval
+ *
+ * MRC represents the encoded form of Max Resp Time (MRT); once
+ * decoded, the resulting value is in units of 0.1 seconds (100 ms).
+ */
+static inline u8 igmpv3_mrc(unsigned long mrt)
+{
+ return igmpv3_exp_field_encode(mrt);
+}
+
+/* Calculate Querier's Query Interval Code from Querier's Query Interval
+ *
+ * RFC3376, relevant sections:
+ * - 4.1.7. QQIC (Querier's Query Interval Code)
+ * - 8.2. Query Interval
+ * - 8.12. Older Version Querier Present Timeout
+ * (the [Query Interval] in the last Query received)
+ *
+ * QQIC represents the encoded form of Querier's Query Interval (QQI);
+ * once decoded, the resulting value is in units of seconds.
+ */
+static inline u8 igmpv3_qqic(unsigned long qi)
+{
+ return igmpv3_exp_field_encode(qi);
+}
/* V3 exponential field decoding */
diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 1438c023db62..27010744d7ae 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -934,12 +934,12 @@ static struct sk_buff *br_ip4_multicast_alloc_query(struct net_bridge_mcast *brm
size_t pkt_size, igmp_hdr_size;
unsigned long now = jiffies;
struct igmpv3_query *ihv3;
+ unsigned long lmqt, mrt;
void *csum_start = NULL;
__sum16 *csum = NULL;
struct sk_buff *skb;
struct igmphdr *ih;
struct ethhdr *eth;
- unsigned long lmqt;
struct iphdr *iph;
u16 lmqt_srcs = 0;
@@ -1004,15 +1004,15 @@ static struct sk_buff *br_ip4_multicast_alloc_query(struct net_bridge_mcast *brm
skb_put(skb, 24);
skb_set_transport_header(skb, skb->len);
+ mrt = group ? brmctx->multicast_last_member_interval :
+ brmctx->multicast_query_response_interval;
*igmp_type = IGMP_HOST_MEMBERSHIP_QUERY;
switch (brmctx->multicast_igmp_version) {
case 2:
ih = igmp_hdr(skb);
ih->type = IGMP_HOST_MEMBERSHIP_QUERY;
- ih->code = (group ? brmctx->multicast_last_member_interval :
- brmctx->multicast_query_response_interval) /
- (HZ / IGMP_TIMER_SCALE);
+ ih->code = mrt / (HZ / IGMP_TIMER_SCALE);
ih->group = group;
ih->csum = 0;
csum = &ih->csum;
@@ -1021,11 +1021,9 @@ static struct sk_buff *br_ip4_multicast_alloc_query(struct net_bridge_mcast *brm
case 3:
ihv3 = igmpv3_query_hdr(skb);
ihv3->type = IGMP_HOST_MEMBERSHIP_QUERY;
- ihv3->code = (group ? brmctx->multicast_last_member_interval :
- brmctx->multicast_query_response_interval) /
- (HZ / IGMP_TIMER_SCALE);
+ ihv3->code = igmpv3_mrc(mrt / (HZ / IGMP_TIMER_SCALE));
ihv3->group = group;
- ihv3->qqic = brmctx->multicast_query_interval / HZ;
+ ihv3->qqic = igmpv3_qqic(brmctx->multicast_query_interval / HZ);
ihv3->nsrcs = htons(lmqt_srcs);
ihv3->resv = 0;
ihv3->suppress = sflag;
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v6 4/5] ipv6: mld: encode multicast exponential fields
From: Ujjal Roy @ 2026-05-02 13:19 UTC (permalink / raw)
To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Nikolay Aleksandrov, Ido Schimmel, David Ahern,
Shuah Khan, Andy Roulin, Yong Wang, Petr Machata
Cc: Ujjal Roy, bridge, netdev, linux-kernel, linux-kselftest
In-Reply-To: <20260502131907.987-1-royujjal@gmail.com>
In MLD, MRC and QQIC fields are not correctly encoded when
generating query packets. Since the receiver of the query
interprets these fields using the MLDv2 floating-point
decoding logic, any value that exceeds the linear threshold
is incorrectly parsed as an exponential value, leading to
an incorrect interval calculation.
Encode and assign the corresponding protocol fields during
query generation. Introduce the logic to dynamically
calculate the exponent and mantissa using bit-scan (fls).
This ensures MRC (16-bit) and QQIC (8-bit) fields are
properly encoded when transmitting query packets with
intervals that exceed their respective linear thresholds
(32768 for MRD; 128 for QQI).
RFC3810: If Maximum Response Code >= 32768, the Maximum
Response Code field represents a floating-point value as
follows:
0 1 2 3 4 5 6 7 8 9 A B C D E F
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1| exp | mant |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
RFC3810: If QQIC >= 128, the QQIC field represents a
floating-point value as follows:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|1| exp | mant |
+-+-+-+-+-+-+-+-+
Reviewed-by: Nikolay Aleksandrov <razor@blackwall.org>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Ujjal Roy <royujjal@gmail.com>
---
include/net/mld.h | 119 ++++++++++++++++++++++++++++++++++++++
net/bridge/br_multicast.c | 4 +-
2 files changed, 121 insertions(+), 2 deletions(-)
diff --git a/include/net/mld.h b/include/net/mld.h
index 10e402e0fe08..23255cc47ced 100644
--- a/include/net/mld.h
+++ b/include/net/mld.h
@@ -91,13 +91,132 @@ struct mld2_query {
/* MLDv2 QQIC floating-point exponential field min threshold */
#define MLD_QQIC_MIN_THRESHOLD 128
+/* MLDv2 QQIC FP max threshold (mant = 0xF, exp = 7) -> 31744 */
+#define MLD_QQIC_MAX_THRESHOLD 31744
/* MLDv2 MRC floating-point exponential field min threshold */
#define MLD_MRC_MIN_THRESHOLD 32768UL
+/* MLDv2 MRC FP max threshold (mant = 0xFFF, exp = 7) -> 8387584 */
+#define MLD_MRC_MAX_THRESHOLD 8387584
#define MLDV1_MRD_MAX_COMPAT (MLD_MRC_MIN_THRESHOLD - 1)
#define MLD_MAX_QUEUE 8
#define MLD_MAX_SKBS 32
+/* V2 exponential field encoding */
+
+/*
+ * Calculate Maximum Response Code from Maximum Response Delay
+ *
+ * MRC represents the 16-bit encoded form of Maximum Response Delay (MRD);
+ * once decoded, the resulting value is in milliseconds.
+ *
+ * RFC3810, 5.1.3. defines only the decoding formula:
+ * Maximum Response Delay = (mant | 0x1000) << (exp + 3)
+ *
+ * but does NOT define the encoding procedure. To derive exponent:
+ *
+ * For the 16-bit MRC, the "hidden bit" (0x1000) is left shifted by 12 to
+ * sit above the 12-bit mantissa. The RFC then shifts this entire block
+ * left by (exp + 3) to reconstruct the value. So, 'hidden bit' is the
+ * MSB which is shifted by (12 + exp + 3).
+ *
+ * Total left shift of the hidden bit = 12 + (exp + 3) = exp + 15.
+ * This is the MSB at the 0-based bit position: (exp + 15).
+ * Since fls() is 1-based, fls(value) - 1 = exp + 15.
+ *
+ * Therefore:
+ * exp = fls(value) - 16
+ * mant = (value >> (exp + 3)) & 0x0FFF
+ *
+ * Final encoding formula:
+ * 0x8000 | (exp << 12) | mant
+ *
+ * Example (value = 1311744):
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0| 1311744
+ * | ^-^--------mant---------^ ^...(exp+3)...^| exp=5
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Encoded:
+ * 0x8000 | (5 << 12) | 0x404 = 0xD404
+ */
+static inline u16 mldv2_mrc(unsigned long mrd)
+{
+ u16 mc_man, mc_exp;
+
+ /* MRC < 32768 is literal */
+ if (mrd < MLD_MRC_MIN_THRESHOLD)
+ return mrd;
+
+ /* Saturate at max representable (mant = 0xFFF, exp = 7) -> 8387584 */
+ if (mrd >= MLD_MRC_MAX_THRESHOLD)
+ return 0xFFFF;
+
+ mc_exp = fls(mrd) - 16;
+ mc_man = (mrd >> (mc_exp + 3)) & 0x0FFF;
+
+ return 0x8000 | (mc_exp << 12) | mc_man;
+}
+
+/*
+ * Calculate Querier's Query Interval Code from Querier's Query Interval
+ *
+ * QQIC represents the 8-bit encoded form of Querier's Query Interval (QQI);
+ * once decoded, the resulting value is in seconds.
+ *
+ * RFC3810, 5.1.9. defines only the decoding formula:
+ * QQI = (mant | 0x10) << (exp + 3)
+ *
+ * but does NOT define the encoding procedure. To derive exponent:
+ *
+ * For any value of mantissa and exponent, the decoding formula indicates
+ * that the "hidden bit" (0x10) is shifted 4 bits left to sit above the
+ * 4-bit mantissa. The RFC again shifts this entire block left by (exp + 3)
+ * to reconstruct the value. So, 'hidden bit' is the MSB which is shifted
+ * by (4 + exp + 3).
+ *
+ * Total left shift of the 'hidden bit' = 4 + (exp + 3) = exp + 7.
+ * This is the MSB at the 0-based bit position: (exp + 7).
+ * Since fls() is 1-based, fls(value) - 1 = exp + 7.
+ *
+ * Therefore:
+ * exp = fls(value) - 8
+ * mant = (value >> (exp + 3)) & 0x0F
+ *
+ * Final encoding formula:
+ * 0x80 | (exp << 4) | mant
+ *
+ * Example (value = 3200):
+ * 0 1
+ * 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0| (value = 3200)
+ * | ^-^-mant^ ^..(exp+3)..^| exp = 4, mant = 9
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Encoded:
+ * 0x80 | (4 << 4) | 9 = 0xC9
+ */
+static inline u8 mldv2_qqic(unsigned long value)
+{
+ u8 mc_man, mc_exp;
+
+ /* QQIC < 128 is literal */
+ if (value < MLD_QQIC_MIN_THRESHOLD)
+ return value;
+
+ /* Saturate at max representable (mant = 0xF, exp = 7) -> 31744 */
+ if (value >= MLD_QQIC_MAX_THRESHOLD)
+ return 0xFF;
+
+ mc_exp = fls(value) - 8;
+ mc_man = (value >> (mc_exp + 3)) & 0x0F;
+
+ return 0x80 | (mc_exp << 4) | mc_man;
+}
+
/* V2 exponential field decoding */
/* Calculate Maximum Response Delay from Maximum Response Code
diff --git a/net/bridge/br_multicast.c b/net/bridge/br_multicast.c
index 27010744d7ae..49ceea3ff974 100644
--- a/net/bridge/br_multicast.c
+++ b/net/bridge/br_multicast.c
@@ -1181,7 +1181,7 @@ static struct sk_buff *br_ip6_multicast_alloc_query(struct net_bridge_mcast *brm
break;
case 2:
mld2q = (struct mld2_query *)icmp6_hdr(skb);
- mld2q->mld2q_mrc = htons((u16)jiffies_to_msecs(interval));
+ mld2q->mld2q_mrc = htons(mldv2_mrc(jiffies_to_msecs(interval)));
mld2q->mld2q_type = ICMPV6_MGM_QUERY;
mld2q->mld2q_code = 0;
mld2q->mld2q_cksum = 0;
@@ -1190,7 +1190,7 @@ static struct sk_buff *br_ip6_multicast_alloc_query(struct net_bridge_mcast *brm
mld2q->mld2q_suppress = sflag;
mld2q->mld2q_qrv = 2;
mld2q->mld2q_nsrcs = htons(llqt_srcs);
- mld2q->mld2q_qqic = brmctx->multicast_query_interval / HZ;
+ mld2q->mld2q_qqic = mldv2_qqic(brmctx->multicast_query_interval / HZ);
mld2q->mld2q_mca = *group;
csum = &mld2q->mld2q_cksum;
csum_start = (void *)mld2q;
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v6 5/5] selftests: net: bridge: add MRC and QQIC field encoding tests
From: Ujjal Roy @ 2026-05-02 13:19 UTC (permalink / raw)
To: David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Nikolay Aleksandrov, Ido Schimmel, David Ahern,
Shuah Khan, Andy Roulin, Yong Wang, Petr Machata
Cc: Ujjal Roy, bridge, netdev, linux-kernel, linux-kselftest
In-Reply-To: <20260502131907.987-1-royujjal@gmail.com>
Enhance vlmc_query_intvl_test and vlmc_query_response_intvl_test in
bridge_vlan_mcast.sh to validate IGMPv3/MLDv2 protocol compliance for
MRC and QQIC field encoding across both linear and exponential ranges.
TEST: Vlan multicast snooping enable [ OK ]
TEST: Vlan mcast_query_interval global option default value [ OK ]
TEST: Number of tagged IGMPv2 general query [ OK ]
TEST: IGMPv3 QQIC linear value 60(s) [ OK ]
TEST: MLDv2 QQIC linear value 60(s) [ OK ]
TEST: IGMPv3 QQIC non linear value 160(s) [ OK ]
TEST: MLDv2 QQIC non linear value 160(s) [ OK ]
TEST: Vlan mcast_query_response_interval global option default value [ OK ]
TEST: IGMPv3 MRC linear value of 60(x0.1s) [ OK ]
TEST: MLDv2 MRC linear value of 24000(ms) [ OK ]
TEST: IGMPv3 MRC non linear value of 240(x0.1s) [ OK ]
TEST: MLDv2 MRC non linear value of 48000(ms) [ OK ]
Reviewed-by: Nikolay Aleksandrov <razor@blackwall.org>
Reviewed-by: Ido Schimmel <idosch@nvidia.com>
Signed-off-by: Ujjal Roy <royujjal@gmail.com>
---
.../net/forwarding/bridge_vlan_mcast.sh | 140 +++++++++++++++++-
1 file changed, 132 insertions(+), 8 deletions(-)
diff --git a/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh b/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh
index e8031f68200a..07a377652a59 100755
--- a/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh
+++ b/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh
@@ -162,14 +162,27 @@ vlmc_query_cnt_setup()
{
local type=$1
local dev=$2
+ local match=($3)
if [[ $type == "igmp" ]]; then
- tc filter add dev $dev egress pref 10 prot 802.1Q \
+ # This matches: IP Protocol 2 (IGMP)
+ tc filter add dev "$dev" egress pref 10 prot 802.1Q \
flower vlan_id 10 vlan_ethtype ipv4 dst_ip 224.0.0.1 ip_proto 2 \
+ action goto chain 1
+ # AND Type 0x11 (Query) at offset 0 of IGMP header
+ # 20 bytes IPv4 header + 4 bytes Router Alert option + IGMP[offset 0]
+ tc filter add dev "$dev" egress pref 20 chain 1 prot 802.1Q u32 \
+ match u8 0x11 0xff at 24 "${match[@]}" \
action pass
else
- tc filter add dev $dev egress pref 10 prot 802.1Q \
+ # This matches: ICMPv6
+ tc filter add dev "$dev" egress pref 10 prot 802.1Q \
flower vlan_id 10 vlan_ethtype ipv6 dst_ip ff02::1 ip_proto icmpv6 \
+ action goto chain 1
+ # AND Type 0x82 (Query) at offset 0 of MLD header
+ # 40 bytes IPv6 header + 8 bytes Hop-by-hop option + MLD[offset 0]
+ tc filter add dev "$dev" egress pref 20 chain 1 prot 802.1Q u32 \
+ match u8 0x82 0xff at 48 "${match[@]}" \
action pass
fi
@@ -181,7 +194,39 @@ vlmc_query_cnt_cleanup()
local dev=$1
ip link set dev br0 type bridge mcast_stats_enabled 0
- tc filter del dev $dev egress pref 10
+ tc filter del dev "$dev" egress pref 20 chain 1
+ tc filter del dev "$dev" egress pref 10
+}
+
+vlmc_query_get_intvl_match()
+{
+ local type=$1
+ local version=$2
+ local test=$3
+ local enc_val=$4
+
+ if [ "$test" = "qqic" ]; then
+ # QQIC is 8-bit floating point encoding for IGMPv3 and MLDv2
+ if [ "${type}v${version}" = "igmpv3" ]; then
+ # QQIC is at offset 9 of IGMP header
+ # 20 bytes IPv4 header + 4 bytes Router Alert option + IGMP[offset 9]
+ echo "match u8 $enc_val 0xff at 33"
+ elif [ "${type}v${version}" = "mldv2" ]; then
+ # QQIC is at offset 25 of MLD header
+ # 40 bytes IPv6 header + 8 bytes Hop-by-hop option + MLD[offset 25]
+ echo "match u8 $enc_val 0xff at 73"
+ fi
+ elif [ "$test" = "mrc" ]; then
+ if [ "${type}v${version}" = "igmpv3" ]; then
+ # MRC is 8-bit floating point encoding at offset 1 of IGMP header
+ # 20 bytes IPv4 header + 4 bytes Router Alert option + IGMP[offset 1]
+ echo "match u8 $enc_val 0xff at 25"
+ elif [ "${type}v${version}" = "mldv2" ]; then
+ # MRC is 16-bit floating point encoding at offset 4 of MLD header
+ # 40 bytes IPv6 header + 8 bytes Hop-by-hop option + MLD[offset 4]
+ echo "match u16 $enc_val 0xffff at 52"
+ fi
+ fi
}
vlmc_check_query()
@@ -191,9 +236,13 @@ vlmc_check_query()
local dev=$3
local expect=$4
local time=$5
+ local test=$6
+ local enc_val=$7
+ local intvl_match=""
local ret=0
- vlmc_query_cnt_setup $type $dev
+ intvl_match="$(vlmc_query_get_intvl_match "$type" "$version" "$test" "$enc_val")"
+ vlmc_query_cnt_setup "$type" "$dev" "$intvl_match"
local pre_tx_xstats=$(vlmc_query_cnt_xstats $type $version $dev)
bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_querier 1
@@ -201,7 +250,7 @@ vlmc_check_query()
if [[ $ret -eq 0 ]]; then
sleep $time
- local tcstats=$(tc_rule_stats_get $dev 10 egress)
+ local tcstats=$(tc_rule_stats_get "$dev" 20 egress)
local post_tx_xstats=$(vlmc_query_cnt_xstats $type $version $dev)
if [[ $tcstats != $expect || \
@@ -448,8 +497,46 @@ vlmc_query_intvl_test()
# 1 is sent immediately, then 2 more in the next 5 seconds
vlmc_check_query igmp 2 $swp1 3 5
check_err $? "Wrong number of tagged IGMPv2 general queries sent"
- log_test "Vlan 10 mcast_query_interval option changed to 200"
+ log_test "Number of tagged IGMPv2 general query"
+
+ RET=0
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 3
+ check_err $? "Could not set mcast_igmp_version in vlan 10"
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 2
+ check_err $? "Could not set mcast_mld_version in vlan 10"
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 6000
+ check_err $? "Could not set mcast_query_interval in vlan 10"
+ # 1 is sent immediately, IGMPv3 QQIC should match with linear value 60 (0x3c)
+ # which is 8-bit encoded value of 60 [units of seconds]
+ vlmc_check_query igmp 3 $swp1 1 1 qqic 0x3c
+ check_err $? "Wrong QQIC in generated IGMPv3 general queries"
+ log_test "IGMPv3 QQIC linear value 60(s)"
+
+ RET=0
+ # 1 is sent immediately, MLDv2 QQIC should match with linear value 60 (0x3c)
+ # which is 8-bit encoded value of 60 [units of seconds]
+ vlmc_check_query mld 2 $swp1 1 1 qqic 0x3c
+ check_err $? "Wrong QQIC in generated MLDv2 general queries"
+ log_test "MLDv2 QQIC linear value 60(s)"
+
+ RET=0
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 16000
+ check_err $? "Could not set mcast_query_interval in vlan 10"
+ # 1 is sent immediately, IGMPv3 QQIC should match with non linear value 132 (0x84)
+ # which is 8-bit encoded value of 160 [units of seconds]
+ vlmc_check_query igmp 3 $swp1 1 1 qqic 0x84
+ check_err $? "Wrong QQIC in generated IGMPv3 general queries"
+ log_test "IGMPv3 QQIC non linear value 160(s)"
+ RET=0
+ # 1 is sent immediately, MLDv2 QQIC should match with non linear value 132 (0x84)
+ # which is 8-bit encoded value of 160 [units of seconds]
+ vlmc_check_query mld 2 $swp1 1 1 qqic 0x84
+ check_err $? "Wrong QQIC in generated MLDv2 general queries"
+ log_test "MLDv2 QQIC non linear value 160(s)"
+
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 2
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 1
bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 2
bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 12500
}
@@ -469,10 +556,47 @@ vlmc_query_response_intvl_test()
log_test "Vlan mcast_query_response_interval global option default value"
RET=0
- bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 200
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 0
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 3
+ check_err $? "Could not set mcast_igmp_version in vlan 10"
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 2
+ check_err $? "Could not set mcast_mld_version in vlan 10"
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 600
+ check_err $? "Could not set mcast_query_response_interval in vlan 10"
+ # 1 is sent immediately, IGMPv3 MRC should match with linear value 60 (0x3c)
+ # which is 8-bit encoded value of 60 [units of 0.1s = 6 seconds]
+ vlmc_check_query igmp 3 $swp1 1 1 mrc 0x3c
+ check_err $? "Wrong MRC in generated IGMPv3 general queries"
+ log_test "IGMPv3 MRC linear value of 60(x0.1s)"
+
+ RET=0
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 2400
+ check_err $? "Could not set mcast_query_response_interval in vlan 10"
+ # 1 is sent immediately, MLDv2 MRC should match with linear value 0x5dc0 (24000)
+ # which is 16-bit encoded value of 24000 [units of ms / 24 seconds]
+ vlmc_check_query mld 2 $swp1 1 1 mrc 0x5dc0
+ check_err $? "Wrong MRC in generated MLDv2 general queries"
+ log_test "MLDv2 MRC linear value of 24000(ms)"
+
+ RET=0
+ # 1 is sent immediately, IGMPv3 MRC should match with non linear value 142 (0x8e)
+ # which is 8-bit encoded value of 240 [units of 0.1s = 24 seconds]
+ vlmc_check_query igmp 3 $swp1 1 1 mrc 0x8e
+ check_err $? "Wrong MRC in generated IGMPv3 general queries"
+ log_test "IGMPv3 MRC non linear value of 240(x0.1s)"
+
+ RET=0
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 4800
check_err $? "Could not set mcast_query_response_interval in vlan 10"
- log_test "Vlan 10 mcast_query_response_interval option changed to 200"
+ # 1 is sent immediately, MLDv2 MRC should match with non linear value 0x8770 (34672)
+ # which is 16-bit encoded value of 48000 [units of ms / 48 seconds]
+ vlmc_check_query mld 2 $swp1 1 1 mrc 0x8770
+ check_err $? "Wrong MRC in generated MLDv2 general queries"
+ log_test "MLDv2 MRC non linear value of 48000(ms)"
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 2
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 1
+ bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 2
bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 1000
}
--
2.43.0
^ permalink raw reply related
* Re: [PATCH net v5] ipv6: flowlabel: enforce per-netns limit for unprivileged callers
From: Willem de Bruijn @ 2026-05-02 13:33 UTC (permalink / raw)
To: Maoyi Xie, netdev
Cc: willemdebruijn.kernel, willemb, edumazet, pabeni, kuba, davem,
dsahern, kuznet, linux-kernel, stable
In-Reply-To: <20260502050037.3800122-1-maoyi.xie@ntu.edu.sg>
Maoyi Xie wrote:
> fl_size, fl_ht and ip6_fl_lock in net/ipv6/ip6_flowlabel.c are file
> scope and shared across netns. mem_check() reads fl_size to decide
> whether to deny non-CAP_NET_ADMIN callers; capable() runs against
> init_user_ns, so an unprivileged user in any non-init userns can
> push fl_size past FL_MAX_SIZE - FL_MAX_SIZE/4 and starve every
> other unprivileged userns on the host.
>
> Add struct netns_ipv6::flowlabel_count, bumped and decremented next
> to fl_size in fl_intern, ip6_fl_gc and ip6_fl_purge. The new field
> is placed in the existing 4-byte hole after ipmr_seq, so struct
> netns_ipv6 stays the same size on 64-bit builds.
>
> Bump FL_MAX_SIZE from 4096 to 8192. It has been 4096 since the file
> was added; machines and connection counts have grown.
>
> mem_check() folds an extra per-netns ceiling into the existing
> non-CAP_NET_ADMIN conditional. The ceiling is half of the total
> budget that unprivileged callers have ever been able to use, i.e.
> (FL_MAX_SIZE - FL_MAX_SIZE/4) / 2 = 3072 entries. With FL_MAX_SIZE
> doubled, this preserves the original per-user reach (~3K, what an
> unprivileged caller could already obtain before this change) while
> forcing an attacker to spread allocations across at least two
> netns to exhaust the global non-CAP_NET_ADMIN budget.
>
> CAP_NET_ADMIN against init_user_ns still bypasses both caps.
>
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Suggested-by: Willem de Bruijn <willemb@google.com>
> Cc: stable@vger.kernel.org # v5.15+
> Signed-off-by: Maoyi Xie <maoyi.xie@ntu.edu.sg>
No longer applies cleanly to net. Conflict on
include/net/netns/ipv6.h. Please update your tree.
> ---
> v5 (this submission, addressing v4 review by Willem):
> - Replaced the per-netns ceiling FL_MAX_SIZE/8 with the
> computed unpriv_user_limit = (FL_MAX_SIZE - FL_MAX_SIZE/4)/2,
> which evaluates to 3072. v4's FL_MAX_SIZE/8 = 1024 would have
> reduced the per-user budget below the ~3K an unprivileged
> caller could already obtain before any of this work, defeating
> the reason FL_MAX_SIZE was doubled in the first place. The new
> ceiling preserves the original per-user reach while still
> requiring an attacker to spread across at least two netns to
> drain the global non-CAP_NET_ADMIN budget.
> - Reworded the corresponding paragraph in the commit body.
> v4: addressed Willem's v3 review on netdev. Dropped the
> flowlabel_has_excl cacheline argument in favour of "fills the
> existing 4-byte hole after ipmr_seq", and reordered
> atomic_dec(&...flowlabel_count) to sit immediately after
> atomic_dec(&fl_size) in ip6_fl_gc and ip6_fl_purge.
> v3: addressed Willem's review on the private security@ thread.
> Merged FL_MAX_SIZE doubling, dropped test data, moved
> flowlabel_count near ipmr_seq, inlined fl->fl_net in ip6_fl_gc.
> v2: per-netns counter + cap, sent to security@ as a 2-patch series.
> v1: fix-shape sketch in original disclosure.
>
> include/net/netns/ipv6.h | 1 +
> net/ipv6/ip6_flowlabel.c | 16 ++++++++++++----
> 2 files changed, 13 insertions(+), 4 deletions(-)
>
> diff --git a/include/net/netns/ipv6.h b/include/net/netns/ipv6.h
> index 34bdb1308..329482373 100644
> --- a/include/net/netns/ipv6.h
> +++ b/include/net/netns/ipv6.h
> @@ -119,6 +119,7 @@ struct netns_ipv6 {
> struct fib_notifier_ops *notifier_ops;
> struct fib_notifier_ops *ip6mr_notifier_ops;
> unsigned int ipmr_seq; /* protected by rtnl_mutex */
> + atomic_t flowlabel_count;
> struct {
> struct hlist_head head;
> spinlock_t lock;
> diff --git a/net/ipv6/ip6_flowlabel.c b/net/ipv6/ip6_flowlabel.c
> index c92f98c6f..758a2fc4d 100644
> --- a/net/ipv6/ip6_flowlabel.c
> +++ b/net/ipv6/ip6_flowlabel.c
> @@ -36,7 +36,7 @@
> /* FL hash table */
>
> #define FL_MAX_PER_SOCK 32
> -#define FL_MAX_SIZE 4096
> +#define FL_MAX_SIZE 8192
> #define FL_HASH_MASK 255
> #define FL_HASH(l) (ntohl(l)&FL_HASH_MASK)
>
> @@ -162,8 +162,9 @@ static void ip6_fl_gc(struct timer_list *unused)
> ttd = fl->expires;
> if (time_after_eq(now, ttd)) {
> *flp = fl->next;
> - fl_free(fl);
> atomic_dec(&fl_size);
> + atomic_dec(&fl->fl_net->ipv6.flowlabel_count);
> + fl_free(fl);
Do not touch fl_free (here and below)
> continue;
> }
> if (!sched || time_before(ttd, sched))
> @@ -195,8 +196,9 @@ static void __net_exit ip6_fl_purge(struct net *net)
> if (net_eq(fl->fl_net, net) &&
> atomic_read(&fl->users) == 0) {
> *flp = fl->next;
> - fl_free(fl);
> atomic_dec(&fl_size);
> + atomic_dec(&net->ipv6.flowlabel_count);
> + fl_free(fl);
> continue;
> }
> flp = &fl->next;
> @@ -245,6 +247,7 @@ static struct ip6_flowlabel *fl_intern(struct net *net,
> fl->next = fl_ht[FL_HASH(fl->label)];
> rcu_assign_pointer(fl_ht[FL_HASH(fl->label)], fl);
> atomic_inc(&fl_size);
> + atomic_inc(&net->ipv6.flowlabel_count);
> spin_unlock_bh(&ip6_fl_lock);
> rcu_read_unlock();
> return NULL;
> @@ -464,6 +467,9 @@ fl_create(struct net *net, struct sock *sk, struct in6_flowlabel_req *freq,
>
> static int mem_check(struct sock *sk)
> {
> + const int unpriv_total_limit = FL_MAX_SIZE - (FL_MAX_SIZE / 4);
> + const int unpriv_user_limit = unpriv_total_limit / 2;
> + struct net *net = sock_net(sk);
> int room = FL_MAX_SIZE - atomic_read(&fl_size);
> struct ipv6_fl_socklist *sfl;
> int count = 0;
> @@ -478,7 +484,9 @@ static int mem_check(struct sock *sk)
>
> if (room <= 0 ||
> ((count >= FL_MAX_PER_SOCK ||
> - (count > 0 && room < FL_MAX_SIZE/2) || room < FL_MAX_SIZE/4) &&
> + (count > 0 && room < FL_MAX_SIZE/2) ||
> + room < FL_MAX_SIZE/4 ||
> + atomic_read(&net->ipv6.flowlabel_count) >= unpriv_user_limit) &&
> !capable(CAP_NET_ADMIN)))
> return -ENOBUFS;
>
> --
> 2.34.1
>
^ permalink raw reply
* Re: [PATCH 00/11] mlx5 support for VFIO self test
From: Jason Gunthorpe @ 2026-05-02 13:40 UTC (permalink / raw)
To: Alex Williamson
Cc: David Matlack, kvm, Leon Romanovsky, linux-kselftest, linux-rdma,
Mark Bloch, netdev, Saeed Mahameed, Shuah Khan, Tariq Toukan,
patches
In-Reply-To: <71afd479-71b4-4ab7-bcbe-3f7b53bd11f4@app.fastmail.com>
On Fri, May 01, 2026 at 10:31:49PM -0600, Alex Williamson wrote:
> On Thu, Apr 30, 2026, at 6:08 PM, Jason Gunthorpe wrote:
> > Add an mlx5 driver to VFIO self test. This is largely a remix of the
> > existing VFIO mlx5 driver in rdma-core. It uses an RDMA loopback QP
> > to issue RDMA WRITE operations which effectively perform memory
> > copies using DMA. Since mlx5 has a stable programming ABI this
> > should work on devices from CX5 to current HW. The device FW must
> > support the QP loopback configuration.
>
> Does the PCI ID table in the series need some pruning then? It
> includes CX4.
Maybe.. I don't actually know that it doesn't work on CX4 and before,
I only was able to test as far back as CX5. I'm thinking to leave it
and if someone does try it and it doesn't work then remove it.
Jason
^ permalink raw reply
* Re: Re: [PATCH net-next v2 1/2] mv88e6xxx: Refactor 6352's serdes functions
From: Andrew Lunn @ 2026-05-02 13:55 UTC (permalink / raw)
To: Fidan Aliyeva
Cc: olteanv, davem, edumazet, kuba, pabeni, netdev, linux-kernel,
thomas.eckerman.ext
In-Reply-To: <20260502092552.2217977-1-fidan.aliyeva.ext@ericsson.com>
On Sat, May 02, 2026 at 11:25:52AM +0200, Fidan Aliyeva wrote:
> > > I wanted to make those functions generic and not introduce new
> > > functions other than 6321_serdes_get_lane. However, those functions
> > > cannot be generalised the obvious way because they run with reg_lock
> > > already taken which would cause deadlock in mv88e6352_serdes_get_lane function.
> >
> > Ah, the scratch register. None of the other serdes_get_lane() functions need to read a register. O.K.
> >
> > So we don't expect the scratch register to change at runtime do we?
> >
> > Nope, the value in it is read during reset. After that, it does not matter what happens to the pin, the value in the scratch register is fixed. So maybe read it during mv88e6xxx_setup_port() and store the value in struct mv88e6xxx_port?
>
> Hi again. Thank you for your feedback. I can make the proposed change. But
> I do not have the functional specification for 6352 to refer the change
> to, neither do I have access to a 6352 to be able to test the change on.
> Do you have the functional specification document of the switch, maybe?
I have a 6352 board i can test with. It does not have any fibre ports,
but i can test it does not deadlock, or give an error message about
the lock not being held.
Andrew
^ permalink raw reply
* Re: [PATCH net] net: eth: fbnic: Fix addr validation in pcs write
From: Andrew Lunn @ 2026-05-02 14:02 UTC (permalink / raw)
To: Mike Marciniszyn
Cc: Simon Horman, Alexander Duyck, Jakub Kicinski, kernel-team,
Andrew Lunn, David S. Miller, Eric Dumazet, Paolo Abeni, netdev,
linux-kernel, stable
In-Reply-To: <afXHpPPKhayawr9x@PF5YBGDS.localdomain>
On Sat, May 02, 2026 at 05:45:08AM -0400, Mike Marciniszyn wrote:
> On Fri, May 01, 2026 at 02:46:36PM +0100, Simon Horman wrote:
> > On Wed, Apr 29, 2026 at 11:00:49AM -0400, mike.marciniszyn@gmail.com wrote:
> > > From: "Mike Marciniszyn (Meta)" <mike.marciniszyn@gmail.com>
> > >
> > > This patch contains a fix for addr validation in fbnic_mdio_write_pcs().
> >
> > Hi Mike,
> >
> > I think this warrants a bit more explanation: Why should addr 2 be
> > accepted? What happens from a user-perspective when it is not?
> >
>
> The DW IP part has two distinct PCS address ranges cooresponding
> to the C45 PCS registers.
>
> The shim translates the PCS mmd/addr/regno into specific CSR writes
> to one of two zero-relative addr values into one of those two
> ranges.
>
> This patch fixes a one off in the test that could allow an invalid
> CSR write if an addr == 2 was called.
Stable runs say:
It must either fix a real bug that bothers people, ...
Can this bug be triggered with the current driver? Are there any
noticeable effects? How would somebody inside Meta know they need this
fix? This should be included in the commit message.
Andrew
^ permalink raw reply
* Re: [PATCH v2] net: switchdev: fix duplicate word in documentation
From: Andrew Lunn @ 2026-05-02 14:09 UTC (permalink / raw)
To: Björn Persson; +Cc: Wang Zihan, netdev, linux-doc, kuba
In-Reply-To: <20260502132446.5b2c51cf@tag.xn--rombobjrn-67a.se>
On Sat, May 02, 2026 at 01:24:46PM +0200, Björn Persson wrote:
> Wang Zihan wrote:
> > @@ -162,7 +162,7 @@ The switchdev driver can know a particular port's position in the topology by
> > monitoring NETDEV_CHANGEUPPER notifications. For example, a port moved into a
> > bond will see its upper master change. If that bond is moved into a bridge,
> > the bond's upper master will change. And so on. The driver will track such
> > -movements to know what position a port is in in the overall topology by
> > +movements to know what position a port is in the overall topology by
> > registering for netdevice events and acting on NETDEV_CHANGEUPPER.
> >
> > L2 Forwarding Offload
>
> This change claims that a port is a position. The preceding sentences,
> talking about "a particular port's position" and a port being moved,
> make it clear that a port is *in* a position *in* the topology. The
> port is not itself a position.
I assume this is a fix to make a bot happy, without actually checking
if the bot is correct.
Maybe to prevent a recurrence of the bot driver not validating the bot
output, a ',' should be added?
Andrew
---
pw-bot: cr
^ permalink raw reply
* Re: [PATCH] Documentation: Fix duplicated words
From: Andrew Lunn @ 2026-05-02 14:16 UTC (permalink / raw)
To: Wang Zihan
Cc: netdev, linux-doc, linux-media, linux-um, davem, edumazet, kuba,
pabeni, horms, corbet, skhan, mchehab, richard, anton.ivanov,
johannes, linux-kernel
In-Reply-To: <tencent_B1D6CBBF95486E31D04C2E1B92F5E605A307@qq.com>
On Sat, May 02, 2026 at 07:19:16PM +0800, Wang Zihan wrote:
> Remove duplicated words in three documentation files:
> - "in in" -> "in" (switchdev.rst)
> - "The the" -> "The" (dmx-reqbufs.rst)
> - "on on" -> "on" (user_mode_linux_howto_v2.rst)
https://www.kernel.org/doc/html/latest/process/maintainer-netdev.html
* don’t repost your patches within one 24h period
We have already told you one of the fixes is wrong, but you did not
wait 24 hours, so have not see our feedback.
Also, in general when using a tool, assume it is wrong, and convince
yourself it is correct, before making a change. "The the in this
sentence is the subject." What does the bot make of that? Can you
convince yourself the bot is correct?
Andrew
---
pw-bot: cr
^ permalink raw reply
* [PATCH net v3] psp: strip variable-length PSP header in psp_dev_rcv()
From: David Carlier @ 2026-05-02 14:19 UTC (permalink / raw)
To: daniel.zahka, kuba
Cc: willemdebruijn.kernel, davem, edumazet, pabeni, horms, raeds,
kees, cratiu, netdev, linux-kernel, David Carlier,
Willem de Bruijn, stable
psp_dev_rcv() unconditionally removes a fixed PSP_ENCAP_HLEN, even
when psph->hdrlen indicates that the PSP header carries optional
fields. A frame whose PSP header advertises a non-zero VC or any
extension would therefore be silently mis-decapsulated: option bytes
would spill into the inner packet head and downstream parsing would
fail on a corrupted skb.
Compute the full PSP header length from psph->hdrlen, pull the
optional bytes into the linear region, and strip the whole header
when decapsulating. Optional fields (VC, ...) are still ignored,
just discarded with the rest of the header instead of leaking.
crypt_offset and the VIRT flag are intentionally not validated here
- callers know their device's PSP implementation and can decide.
Both in-tree callers gate on hardware-validated PSP, so this is a
correctness fix rather than a reachable corruption path under
current configurations.
Fixes: 0eddb8023cee ("psp: provide decapsulation and receive helper for drivers")
Reviewed-by: Willem de Bruijn <willemb@google.com>
Reviewed-by: Daniel Zahka <daniel.zahka@gmail.com>
Cc: stable@vger.kernel.org
Signed-off-by: David Carlier <devnexen@gmail.com>
---
v2 -> v3 (per Daniel Zahka):
- drop Suggested-by trailer
- rename psp_hdr_len -> psp_hlen, retype to int, fold onto the
existing int declaration line to keep the reverse christmas tree
- drop the (u32) cast on psph->hdrlen
- no functional change; carry forward Reviewed-by tags from v2
net/psp/psp_main.c | 42 +++++++++++++++++++++++++++++++-----------
1 file changed, 31 insertions(+), 11 deletions(-)
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index 9508b6c38003..e45549f08eef 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -263,15 +263,16 @@ EXPORT_SYMBOL(psp_dev_encapsulate);
/* Receive handler for PSP packets.
*
- * Presently it accepts only already-authenticated packets and does not
- * support optional fields, such as virtualization cookies. The caller should
- * ensure that skb->data is pointing to the mac header, and that skb->mac_len
- * is set. This function does not currently adjust skb->csum (CHECKSUM_COMPLETE
- * is not supported).
+ * Accepts only already-authenticated packets. The full PSP header is
+ * stripped according to psph->hdrlen; any optional fields it advertises
+ * (virtualization cookies, etc.) are ignored and discarded along with the
+ * rest of the header. The caller should ensure that skb->data is pointing
+ * to the mac header, and that skb->mac_len is set. This function does not
+ * currently adjust skb->csum (CHECKSUM_COMPLETE is not supported).
*/
int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv)
{
- int l2_hlen = 0, l3_hlen, encap;
+ int l2_hlen = 0, l3_hlen, encap, psp_hlen;
struct psp_skb_ext *pse;
struct psphdr *psph;
struct ethhdr *eth;
@@ -312,18 +313,36 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv)
if (unlikely(uh->dest != htons(PSP_DEFAULT_UDP_PORT)))
return -EINVAL;
- pse = skb_ext_add(skb, SKB_EXT_PSP);
- if (!pse)
+ psph = (struct psphdr *)(skb->data + l2_hlen + l3_hlen +
+ sizeof(struct udphdr));
+
+ /* Strip the full PSP header per psph->hdrlen; VC/options are pulled
+ * into the linear region only so they can be discarded with the
+ * rest of the header.
+ */
+ psp_hlen = (psph->hdrlen + 1) * 8;
+
+ if (unlikely(psp_hlen < sizeof(struct psphdr)))
+ return -EINVAL;
+
+ if (psp_hlen > sizeof(struct psphdr) &&
+ !pskb_may_pull(skb, l2_hlen + l3_hlen +
+ sizeof(struct udphdr) + psp_hlen))
return -EINVAL;
psph = (struct psphdr *)(skb->data + l2_hlen + l3_hlen +
sizeof(struct udphdr));
+
+ pse = skb_ext_add(skb, SKB_EXT_PSP);
+ if (!pse)
+ return -EINVAL;
+
pse->spi = psph->spi;
pse->dev_id = dev_id;
pse->generation = generation;
pse->version = FIELD_GET(PSPHDR_VERFL_VERSION, psph->verfl);
- encap = PSP_ENCAP_HLEN;
+ encap = sizeof(struct udphdr) + psp_hlen;
encap += strip_icv ? PSP_TRL_SIZE : 0;
if (proto == htons(ETH_P_IP)) {
@@ -340,8 +359,9 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv)
ipv6h->payload_len = htons(ntohs(ipv6h->payload_len) - encap);
}
- memmove(skb->data + PSP_ENCAP_HLEN, skb->data, l2_hlen + l3_hlen);
- skb_pull(skb, PSP_ENCAP_HLEN);
+ memmove(skb->data + sizeof(struct udphdr) + psp_hlen,
+ skb->data, l2_hlen + l3_hlen);
+ skb_pull(skb, sizeof(struct udphdr) + psp_hlen);
if (strip_icv)
pskb_trim(skb, skb->len - PSP_TRL_SIZE);
--
2.53.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox