From d936e1a9170f9cadaa5f37586b1dfe6f20f98799 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Fri, 12 Jun 2026 19:27:55 +0200 Subject: landlock: Set audit_net.sk for socket access checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set audit_net.sk in current_check_access_socket() to provide the socket object to audit_log_lsm_data(). This makes Landlock consistent with AppArmor, which always sets .sk for socket operations, and with SELinux's generic socket permission checks. The socket's local and foreign address information (laddr, lport, faddr, fport) is logged by the shared lsm_audit.c infrastructure when the socket has bound or connected state. Fields with zero values are suppressed by print_ipv4_addr()/print_ipv6_addr(), so the audit output is unchanged for the common case of bind denials on unbound sockets. For connect denials after a prior bind, the bound local address (laddr, lport) appears before the existing sockaddr fields (daddr, dest). No existing fields are removed or reordered, and the new field names (laddr, lport, faddr, fport) are standard audit fields already emitted by other LSMs through the same lsm_audit.c code path. Add a connect_tcp_bound audit test that binds to an allowed port and then connects to a denied one, verifying that the denial record reports laddr/lport from the bound socket in addition to the connect destination. Cc: Günther Noack Cc: Tingmao Wang Cc: stable@vger.kernel.org Fixes: 9f74411a40ce ("landlock: Log TCP bind and connect denials") Link: https://patch.msgid.link/20260612172757.1003481-1-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/net.c | 1 + 1 file changed, 1 insertion(+) (limited to 'security') diff --git a/security/landlock/net.c b/security/landlock/net.c index c368649985c5..a38bdfcffc22 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -198,6 +198,7 @@ static int current_check_access_socket(struct socket *const sock, return 0; audit_net.family = address->sa_family; + audit_net.sk = sock->sk; landlock_log_denial(subject, &(struct landlock_request){ .type = LANDLOCK_REQUEST_NET_ACCESS, -- cgit v1.2.3 From b232bd12789fa57405b5092f28788be97aae9999 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Wed, 13 May 2026 20:03:08 +0200 Subject: landlock: Account all audit data allocations to user space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark the kzalloc_flex() of struct landlock_details with GFP_KERNEL_ACCOUNT so the allocation is charged to the calling task, like the other Landlock per-domain allocations which have used GFP_KERNEL_ACCOUNT forever. Every property of landlock_details is caller-attributable: allocated by landlock_restrict_self(2), owned by the caller's landlock_hierarchy, contents are the caller's pid, uid, comm, and exe_path, lifetime bounded by the caller's domain. While the caller may not know nor control the size of this allocation (i.e. exe_path), this data should still be accounted for it. The deciding factor is whether userspace can trigger the allocation, not whether the size of the data is known nor controlled by the caller. This aligns with the kmemcg accounting policy established by commit 5d097056c9a0 ("kmemcg: account certain kmem allocations to memcg"). No new failure modes: the hierarchy and ruleset are allocated before details and are already accounted, so landlock_restrict_self(2) already returns -ENOMEM under memcg pressure. This change widens that existing failure window slightly; it does not introduce a new error code. Cc: Günther Noack Cc: Paul Moore Cc: stable@vger.kernel.org Fixes: 1d636984e088 ("landlock: Add AUDIT_LANDLOCK_DOMAIN and log domain status") Link: https://patch.msgid.link/20260513180309.165840-1-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/domain.c | 9 +++++---- security/landlock/domain.h | 5 +---- 2 files changed, 6 insertions(+), 8 deletions(-) (limited to 'security') diff --git a/security/landlock/domain.c b/security/landlock/domain.c index 06b6bd845060..04b69b43fbf5 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -90,11 +90,12 @@ static struct landlock_details *get_current_details(void) return ERR_CAST(buffer); /* - * Create the new details according to the path's length. Do not - * allocate with GFP_KERNEL_ACCOUNT because it is independent from the - * caller. + * Create the new details according to the path's length. Account to + * the calling task's memcg, like the other Landlock per-domain + * allocations, even if it may not control the related size. */ - details = kzalloc_flex(*details, exe_path, path_size); + details = + kzalloc_flex(*details, exe_path, path_size, GFP_KERNEL_ACCOUNT); if (!details) return ERR_PTR(-ENOMEM); diff --git a/security/landlock/domain.h b/security/landlock/domain.h index a9d57db0120d..35cac8f6daee 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -33,10 +33,7 @@ enum landlock_log_status { * Rarely accessed, mainly when logging the first domain's denial. * * The contained pointers are initialized at the domain creation time and never - * changed again. Contrary to most other Landlock object types, this one is - * not allocated with GFP_KERNEL_ACCOUNT because its size may not be under the - * caller's control (e.g. unknown exe_path) and the data is not explicitly - * requested nor used by tasks. + * changed again. */ struct landlock_details { /** -- cgit v1.2.3 From 4b80320ca7ed03d6e683f95b6066565dc97b9f92 Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Thu, 4 Jun 2026 23:16:56 +0000 Subject: landlock: Fix LANDLOCK_SCOPE_SIGNAL bypass on the SIGIO path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LANDLOCK_SCOPE_SIGNAL must prevent a sandboxed process from signaling processes outside its Landlock domain. It can be bypassed through the asynchronous SIGIO delivery path. A sandboxed process that owns any file or socket can arm it with fcntl(fd, F_SETOWN, -pgid), fcntl(fd, F_SETSIG, SIGKILL) and O_ASYNC, so that an I/O event makes the kernel deliver the chosen signal to the whole process group. As the head of its process group's task list (the default position right after fork()) that group can also hold the non-sandboxed process that launched it, e.g. a supervisor or a security monitor. The sandbox can thus kill or signal the processes LANDLOCK_SCOPE_SIGNAL is meant to protect from it. The scope is enforced in hook_file_send_sigiotask() against the Landlock domain recorded at F_SETOWN time, not the live domain of the sender. control_current_fowner() decides whether to record that domain and skips recording it when the fowner target is in the caller's thread group, which is safe only for a single-task target (PIDTYPE_PID, PIDTYPE_TGID). For a process group (PIDTYPE_PGID) pid_task() returns only one member; recording is skipped whenever that member shares the caller's thread group, and hook_file_send_sigiotask() then lets the signal fan out to the whole group unchecked. Record the domain for every non single-process target so the scope is enforced against each group member at delivery time. That recording is necessary but not sufficient on its own: the kernel signals a process group through its members' thread-group leaders, and the leader of the registrant's own process can carry a different Landlock domain than the sibling thread that armed the owner. domain_is_scoped() would then deny that leader, even though commit 18eb75f3af40 ("landlock: Always allow signals between threads of the same process") requires same-process delivery to be allowed. hook_task_kill() avoids this by evaluating same_thread_group() live, per recipient; the SIGIO path instead delegates the whole decision to a single registration-time check, which a process-group fan-out cannot honor. So also record the registrant's thread group next to its domain and exempt it at delivery: hook_file_send_sigiotask() allows the signal whenever the recipient belongs to the registrant's own process, restoring the same-process guarantee while keeping out-of-domain group members blocked. The direct kill() path (hook_task_kill) already evaluates the live domain and is unaffected. Fixes: 18eb75f3af40 ("landlock: Always allow signals between threads of the same process") Cc: stable@vger.kernel.org Signed-off-by: Bryam Vargas Reviewed-by: Günther Noack Link: https://patch.msgid.link/56bffc24f3d0d08b45a686a48e99766b0a0821fa.1780614610.git.hexlabsecurity@proton.me [mic: Check pid_type earlier and improve comment, fix commit message, fix comment formatting] Signed-off-by: Mickaël Salaün --- security/landlock/fs.c | 14 ++++++++++++++ security/landlock/fs.h | 10 ++++++++++ security/landlock/task.c | 11 +++++++++++ 3 files changed, 35 insertions(+) (limited to 'security') diff --git a/security/landlock/fs.c b/security/landlock/fs.c index c1ecfe239032..664962a416d7 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1900,6 +1900,14 @@ static bool control_current_fowner(struct fown_struct *const fown) */ lockdep_assert_held(&fown->lock); + /* + * A process-group or session owner (PIDTYPE_PGID/PIDTYPE_SID) fans the + * signal out to every member at delivery time, so record the domain and + * let hook_file_send_sigiotask() check the live scope per recipient. + */ + if (fown->pid_type != PIDTYPE_PID && fown->pid_type != PIDTYPE_TGID) + return true; + /* * Some callers (e.g. fcntl_dirnotify) may not be in an RCU read-side * critical section. @@ -1916,6 +1924,7 @@ static void hook_file_set_fowner(struct file *file) { struct landlock_ruleset *prev_dom; struct landlock_cred_security fown_subject = {}; + struct pid *prev_tg, *fown_tg = NULL; size_t fown_layer = 0; if (control_current_fowner(file_f_owner(file))) { @@ -1928,21 +1937,26 @@ static void hook_file_set_fowner(struct file *file) if (new_subject) { landlock_get_ruleset(new_subject->domain); fown_subject = *new_subject; + fown_tg = get_pid(task_tgid(current)); } } prev_dom = landlock_file(file)->fown_subject.domain; + prev_tg = landlock_file(file)->fown_tg; landlock_file(file)->fown_subject = fown_subject; + landlock_file(file)->fown_tg = fown_tg; #ifdef CONFIG_AUDIT landlock_file(file)->fown_layer = fown_layer; #endif /* CONFIG_AUDIT*/ /* May be called in an RCU read-side critical section. */ landlock_put_ruleset_deferred(prev_dom); + put_pid(prev_tg); } static void hook_file_free_security(struct file *file) { + put_pid(landlock_file(file)->fown_tg); landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain); } diff --git a/security/landlock/fs.h b/security/landlock/fs.h index bf9948941f2f..911b83669e20 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -78,6 +78,16 @@ struct landlock_file_security { * euid. */ struct landlock_cred_security fown_subject; + /** + * @fown_tg: Thread group of the task that set the file owner, pinned + * while @fown_subject holds a domain. It lets + * hook_file_send_sigiotask() always allow a SIGIO delivered to the + * owner's own process -- e.g. the thread-group leader reached through a + * process-group owner -- matching the same-process exemption of + * hook_task_kill(). NULL when no domain is recorded. Protected by + * file->f_owner->lock, like @fown_subject. + */ + struct pid *fown_tg; }; #ifdef CONFIG_AUDIT diff --git a/security/landlock/task.c b/security/landlock/task.c index 6d46042132ce..7ddf211f75c3 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -411,6 +411,17 @@ static int hook_file_send_sigiotask(struct task_struct *tsk, if (!subject->domain) return 0; + /* + * Always allow delivery to the file owner's own process, including a + * thread-group leader reached through a process-group owner. This + * mirrors hook_task_kill()'s same-process exemption and preserves the + * guarantee of commit 18eb75f3af40 ("landlock: Always allow signals + * between threads of the same process"), which the registration-time + * check cannot honor for a process-group target. + */ + if (task_tgid(tsk) == landlock_file(fown->file)->fown_tg) + return 0; + scoped_guard(rcu) { is_scoped = domain_is_scoped(subject->domain, -- cgit v1.2.3 From 0ce4243509d1580349dd0d50624036d6b097e958 Mon Sep 17 00:00:00 2001 From: Matthieu Buffet Date: Tue, 9 Jun 2026 23:15:10 +0200 Subject: landlock: Fix unmarked concurrent access to socket family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Socket family is read (twice) in a context where the socket is not locked, so another thread can setsockopt(IPV6_ADDRFORM) to write it concurrently. Add needed READ_ONCE() annotation. Use the proper macro to access __sk_common.skc_family like everywhere else. Fixes: fff69fb03dde ("landlock: Support network rules with TCP bind and connect") Signed-off-by: Matthieu Buffet Link: https://patch.msgid.link/20260609211511.85630-1-matthieu@buffet.re Link: https://patch.msgid.link/20260609211511.85630-2-matthieu@buffet.re [mic: Squash two patches, move variable to ease backport, fix comment formatting] Signed-off-by: Mickaël Salaün --- security/landlock/net.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'security') diff --git a/security/landlock/net.c b/security/landlock/net.c index a38bdfcffc22..4ee4002a8f56 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -46,6 +46,7 @@ static int current_check_access_socket(struct socket *const sock, const int addrlen, access_mask_t access_request) { + unsigned short sock_family; __be16 port; struct layer_access_masks layer_masks = {}; const struct landlock_rule *rule; @@ -66,6 +67,12 @@ static int current_check_access_socket(struct socket *const sock, if (addrlen < offsetofend(typeof(*address), sa_family)) return -EINVAL; + /* + * The socket is not locked, so sk_family can change concurrently due to + * e.g. setsockopt(IPV6_ADDRFORM). + */ + sock_family = READ_ONCE(sock->sk->sk_family); + switch (address->sa_family) { case AF_UNSPEC: if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) { @@ -102,7 +109,7 @@ static int current_check_access_socket(struct socket *const sock, * these checks, but it is safer to return a proper * error and test consistency thanks to kselftest. */ - if (sock->sk->__sk_common.skc_family == AF_INET) { + if (sock_family == AF_INET) { const struct sockaddr_in *const sockaddr = (struct sockaddr_in *)address; @@ -180,7 +187,7 @@ static int current_check_access_socket(struct socket *const sock, * check, but it is safer to return a proper error and test * consistency thanks to kselftest. */ - if (address->sa_family != sock->sk->__sk_common.skc_family && + if (address->sa_family != sock_family && address->sa_family != AF_UNSPEC) return -EINVAL; -- cgit v1.2.3 From 9a8ed15ce22472fe0363e33738b4317d06b13c3a Mon Sep 17 00:00:00 2001 From: Matthieu Buffet Date: Thu, 11 Jun 2026 18:21:01 +0200 Subject: landlock: Add UDP bind() access control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Link: https://patch.msgid.link/20260611162107.49278-2-matthieu@buffet.re [mic: Fix comment formatting] Signed-off-by: Mickaël Salaün --- include/uapi/linux/landlock.h | 14 ++++++++++---- 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, 30 insertions(+), 16 deletions(-) (limited to 'security') diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index 10a346e55e95..f2927681e92d 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -200,10 +200,10 @@ struct landlock_net_port_attr { * (also used for IPv6), and within that range, on a per-socket basis * 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. + * A Landlock rule with port 0 and the %LANDLOCK_ACCESS_NET_BIND_TCP 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 4ee4002a8f56..f57fe2a44f0d 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -88,15 +88,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 @@ -142,7 +144,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 { @@ -164,7 +167,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 { @@ -224,6 +228,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 0c256e7c8675..135b09fd1880 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 */ -- cgit v1.2.3 From e61247a2e694d17236149135b2d22f0f7d19578c Mon Sep 17 00:00:00 2001 From: Matthieu Buffet Date: Thu, 11 Jun 2026 18:21:02 +0200 Subject: landlock: Add UDP send+connect access control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for a second fine-grained UDP access right. LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP controls the ability to set the remote port of a socket (via connect()) and to specify an explicit destination when sending a datagram, to override any remote peer set on a UDP socket (e.g. in sendto() or sendmsg()). 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. Add detection of when autobind is about to be required, and deny the operation if the process would not be allowed to call bind(0) explicitly. Autobind can only be performed in udp_lib_get_port() from code paths already controlled by LSM hooks: when connect()ing, sending a first datagram, and in some splice() EOF edge case which, afaiu, can only happen after a remote peer has been set. This invariant needs to be preserved to keep bind policies actually enforced. Signed-off-by: Matthieu Buffet Link: https://patch.msgid.link/20260611162107.49278-3-matthieu@buffet.re [mic: Add quick return for non-sandboxed tasks, fix sa_family dereferencing, fix comment formatting] Signed-off-by: Mickaël Salaün --- include/uapi/linux/landlock.h | 23 +++++ security/landlock/audit.c | 2 + security/landlock/limits.h | 2 +- security/landlock/net.c | 148 ++++++++++++++++++++++++---- tools/testing/selftests/landlock/net_test.c | 5 +- 5 files changed, 160 insertions(+), 20 deletions(-) (limited to 'security') diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index f2927681e92d..811ec77f9105 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -378,11 +378,34 @@ 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. + * + * .. 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) #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 f57fe2a44f0d..942f856433c9 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) { unsigned short sock_family; __be16 port; @@ -75,19 +76,50 @@ 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)) { /* - * 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 - * inconsistencies and return -EINVAL if needed. + * Connecting to an address with AF_UNSPEC dissolves 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; + } else if (access_request == + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) { + if (sock_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; + audit_net.sk = sock->sk; + 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) { /* @@ -130,7 +162,11 @@ 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. */ + /* + * AF_UNSPEC is treated as AF_INET only in + * bind(AF_UNSPEC+INADDR_ANY) on IPv4 sockets and when sending + * to AF_UNSPEC addresses on IPv4 sockets. + */ fallthrough; case AF_INET: { const struct sockaddr_in *addr4; @@ -141,7 +177,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 || @@ -164,7 +201,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 || @@ -221,6 +259,44 @@ static int current_check_access_socket(struct socket *const sock, return -EACCES; } +static int current_check_autobind_udp_socket(struct socket *const sock) +{ + const struct access_masks bind_udp = { + .net = LANDLOCK_ACCESS_NET_BIND_UDP, + }; + struct sockaddr_storage port0 = {}; + unsigned short num; + bool slow; + + /* Quick return for non-Landlocked tasks. */ + if (!landlock_get_applicable_subject(current_cred(), bind_udp, NULL)) + return 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. Hold the socket lock + * around the inet_num read to exclude udp_lib_get_port()'s transient + * inet_num = snum write that is reverted to 0 on a failing reuseport + * bind. + */ + slow = lock_sock_fast(sock->sk); + num = inet_sk(sock->sk)->inet_num; + unlock_sock_fast(sock->sk, slow); + if (num != 0) + return 0; + + /* + * Construct a struct sockaddr* with port 0 to pretend the process tried + * to bind() on that address. + */ + port0.ss_family = READ_ONCE(sock->sk->sk_family); + + return current_check_access_socket(sock, (struct sockaddr *)&port0, + sizeof(port0), bind_udp.net, false); +} + static int hook_socket_bind(struct socket *const sock, struct sockaddr *const address, const int addrlen) { @@ -234,7 +310,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, @@ -242,19 +318,57 @@ 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, true); + + /* + * connect()ing to an AF_UNSPEC address does not trigger an autobind and + * should never be restricted. + */ + if (ret == 0 && sk_is_udp(sock->sk) && + addrlen >= offsetofend(typeof(*address), sa_family) && + address->sa_family != AF_UNSPEC) + ret = current_check_autobind_udp_socket(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) diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c index 135b09fd1880..23d860e76372 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 */ -- cgit v1.2.3 From a260c0055665fc38804400b3dbdca165d5e0aa15 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Fri, 12 Jun 2026 02:48:47 +0100 Subject: landlock: Add a place for flags to layer rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To avoid unnecessarily increasing the size of struct landlock_layer, we make the layer level a u8 and use the space to store the flags struct. struct layer_access_masks is renamed to struct layer_masks, and a new field is added to track whether a quiet flag rule is seen for each layer. Through use of bitfields, this does not increase the size of the struct. Cc: Justin Suess Assisted-by: GitHub-Copilot:claude-opus-4.8 copilot-review Signed-off-by: Tingmao Wang Co-developed-by: Justin Suess Signed-off-by: Justin Suess Tested-by: Justin Suess Link: https://patch.msgid.link/be3fec3927bc9faaacd4ce0e7f0d1ff5474e2210.1781228815.git.m@maowtm.org [mic: Fix comment formatting] Signed-off-by: Mickaël Salaün --- security/landlock/access.h | 39 +++++++++--- security/landlock/audit.c | 20 +++--- security/landlock/audit.h | 2 +- security/landlock/domain.c | 19 +++--- security/landlock/domain.h | 2 +- security/landlock/fs.c | 147 ++++++++++++++++++++++++-------------------- security/landlock/limits.h | 3 + security/landlock/net.c | 2 +- security/landlock/ruleset.c | 37 +++++++---- security/landlock/ruleset.h | 17 ++++- 10 files changed, 176 insertions(+), 112 deletions(-) (limited to 'security') diff --git a/security/landlock/access.h b/security/landlock/access.h index c19d5bc13944..ce374a65f865 100644 --- a/security/landlock/access.h +++ b/security/landlock/access.h @@ -62,18 +62,41 @@ static_assert(sizeof(typeof_member(union access_masks_all, masks)) == sizeof(typeof_member(union access_masks_all, all))); /** - * struct layer_access_masks - A boolean matrix of layers and access rights + * struct layer_mask - The access rights and rule flags for a layer. * - * This has a bit for each combination of layer numbers and access rights. - * During access checks, it is used to represent the access rights for each - * layer which still need to be fulfilled. When all bits are 0, the access - * request is considered to be fulfilled. + * This has a bit for each access rights and rule flags. During access checks, + * it is used to represent the access rights for each layer which still need to + * be fulfilled. When all bits are 0, the access request is considered to be + * fulfilled. */ -struct layer_access_masks { +struct layer_mask { /** - * @access: The unfulfilled access rights for each layer. + * @access: The unfulfilled access rights for this layer. */ - access_mask_t access[LANDLOCK_MAX_NUM_LAYERS]; + access_mask_t access : LANDLOCK_NUM_ACCESS_MAX; +#ifdef CONFIG_AUDIT + /** + * @quiet: Whether we have encountered a rule with the quiet flag for + * this layer. Used to control logging. + */ + access_mask_t quiet : 1; +#endif /* CONFIG_AUDIT */ +} __packed __aligned(sizeof(access_mask_t)); + +/* + * Make sure that we don't increase the size of struct layer_mask when storing + * rule flags. + */ +static_assert(sizeof(struct layer_mask) == sizeof(access_mask_t)); + +/** + * struct layer_masks - An array of struct layer_mask, one per layer. + */ +struct layer_masks { + /** + * @layers: The unfulfilled access rights for each layer. + */ + struct layer_mask layers[LANDLOCK_MAX_NUM_LAYERS]; }; /* diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 851647197a01..8c56f7f6467a 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -187,11 +187,11 @@ static void test_get_hierarchy(struct kunit *const test) /* Get the youngest layer that denied the access_request. */ static size_t get_denied_layer(const struct landlock_ruleset *const domain, access_mask_t *const access_request, - const struct layer_access_masks *masks) + const struct layer_masks *masks) { - for (ssize_t i = ARRAY_SIZE(masks->access) - 1; i >= 0; i--) { - if (masks->access[i] & *access_request) { - *access_request &= masks->access[i]; + for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) { + if (masks->layers[i].access & *access_request) { + *access_request &= masks->layers[i].access; return i; } } @@ -208,12 +208,12 @@ static void test_get_denied_layer(struct kunit *const test) const struct landlock_ruleset dom = { .num_layers = 5, }; - const struct layer_access_masks masks = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE | - LANDLOCK_ACCESS_FS_READ_DIR, - .access[1] = LANDLOCK_ACCESS_FS_READ_FILE | - LANDLOCK_ACCESS_FS_READ_DIR, - .access[2] = LANDLOCK_ACCESS_FS_REMOVE_DIR, + const struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_READ_DIR, + .layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + .layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR, }; access_mask_t access; diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 56778331b58c..b85d752273ac 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -43,7 +43,7 @@ struct landlock_request { access_mask_t access; /* Required fields for requests with layer masks. */ - const struct layer_access_masks *layer_masks; + const struct layer_masks *layer_masks; /* Required fields for requests with deny masks. */ const access_mask_t all_existing_optional_access; diff --git a/security/landlock/domain.c b/security/landlock/domain.c index 04b69b43fbf5..4ae45b300071 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -184,7 +184,7 @@ static void test_get_layer_deny_mask(struct kunit *const test) deny_masks_t landlock_get_deny_masks(const access_mask_t all_existing_optional_access, const access_mask_t optional_access, - const struct layer_access_masks *const masks) + const struct layer_masks *const masks) { const unsigned long access_opt = optional_access; unsigned long access_bit; @@ -201,8 +201,9 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access, if (WARN_ON_ONCE(!access_opt)) return 0; - for (ssize_t i = ARRAY_SIZE(masks->access) - 1; i >= 0; i--) { - const access_mask_t denied = masks->access[i] & optional_access; + for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) { + const access_mask_t denied = masks->layers[i].access & + optional_access; const unsigned long newly_denied = denied & ~all_denied; if (!newly_denied) @@ -222,12 +223,12 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access, static void test_landlock_get_deny_masks(struct kunit *const test) { - const struct layer_access_masks layers1 = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE | - LANDLOCK_ACCESS_FS_IOCTL_DEV, - .access[1] = LANDLOCK_ACCESS_FS_TRUNCATE, - .access[2] = LANDLOCK_ACCESS_FS_IOCTL_DEV, - .access[9] = LANDLOCK_ACCESS_FS_EXECUTE, + const struct layer_masks layers1 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_IOCTL_DEV, + .layers[1].access = LANDLOCK_ACCESS_FS_TRUNCATE, + .layers[2].access = LANDLOCK_ACCESS_FS_IOCTL_DEV, + .layers[9].access = LANDLOCK_ACCESS_FS_EXECUTE, }; KUNIT_EXPECT_EQ(test, 0x1, diff --git a/security/landlock/domain.h b/security/landlock/domain.h index 35cac8f6daee..af100a8cd939 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -119,7 +119,7 @@ struct landlock_hierarchy { deny_masks_t landlock_get_deny_masks(const access_mask_t all_existing_optional_access, const access_mask_t optional_access, - const struct layer_access_masks *const masks); + const struct layer_masks *const masks); int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 664962a416d7..d7cd2d5c9057 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -406,15 +406,15 @@ static const struct access_masks any_fs = { * src_parent would result in having the same or fewer access rights if it were * moved under new_parent. */ -static bool may_refer(const struct layer_access_masks *const src_parent, - const struct layer_access_masks *const src_child, - const struct layer_access_masks *const new_parent, +static bool may_refer(const struct layer_masks *const src_parent, + const struct layer_masks *const src_child, + const struct layer_masks *const new_parent, const bool child_is_dir) { - for (size_t i = 0; i < ARRAY_SIZE(new_parent->access); i++) { - access_mask_t child_access = src_parent->access[i] & - src_child->access[i]; - access_mask_t parent_access = new_parent->access[i]; + for (size_t i = 0; i < ARRAY_SIZE(new_parent->layers); i++) { + access_mask_t child_access = src_parent->layers[i].access & + src_child->layers[i].access; + access_mask_t parent_access = new_parent->layers[i].access; if (!child_is_dir) { child_access &= ACCESS_FILE; @@ -436,11 +436,11 @@ static bool may_refer(const struct layer_access_masks *const src_parent, * that child2 may be used from parent2 to parent1 without increasing its access * rights), false otherwise. */ -static bool no_more_access(const struct layer_access_masks *const parent1, - const struct layer_access_masks *const child1, +static bool no_more_access(const struct layer_masks *const parent1, + const struct layer_masks *const child1, const bool child1_is_dir, - const struct layer_access_masks *const parent2, - const struct layer_access_masks *const child2, + const struct layer_masks *const parent2, + const struct layer_masks *const child2, const bool child2_is_dir) { if (!may_refer(parent1, child1, parent2, child1_is_dir)) @@ -459,25 +459,25 @@ static bool no_more_access(const struct layer_access_masks *const parent1, static void test_no_more_access(struct kunit *const test) { - const struct layer_access_masks rx0 = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE | - LANDLOCK_ACCESS_FS_READ_FILE, + const struct layer_masks rx0 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_READ_FILE, }; - const struct layer_access_masks mx0 = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE | - LANDLOCK_ACCESS_FS_MAKE_REG, + const struct layer_masks mx0 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, }; - const struct layer_access_masks x0 = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE, + const struct layer_masks x0 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, }; - const struct layer_access_masks x1 = { - .access[1] = LANDLOCK_ACCESS_FS_EXECUTE, + const struct layer_masks x1 = { + .layers[1].access = LANDLOCK_ACCESS_FS_EXECUTE, }; - const struct layer_access_masks x01 = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE, - .access[1] = LANDLOCK_ACCESS_FS_EXECUTE, + const struct layer_masks x01 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, + .layers[1].access = LANDLOCK_ACCESS_FS_EXECUTE, }; - const struct layer_access_masks allows_all = {}; + const struct layer_masks allows_all = {}; /* Checks without restriction. */ NMA_TRUE(&x0, &allows_all, false, &allows_all, NULL, false); @@ -565,9 +565,13 @@ static void test_no_more_access(struct kunit *const test) #undef NMA_TRUE #undef NMA_FALSE -static bool is_layer_masks_allowed(const struct layer_access_masks *masks) +static bool is_layer_masks_allowed(const struct layer_masks *masks) { - return mem_is_zero(&masks->access, sizeof(masks->access)); + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { + if (masks->layers[i].access) + return false; + } + return true; } /* @@ -576,16 +580,16 @@ static bool is_layer_masks_allowed(const struct layer_access_masks *masks) * Returns true if the request is allowed, false otherwise. */ static bool scope_to_request(const access_mask_t access_request, - struct layer_access_masks *masks) + struct layer_masks *masks) { bool saw_unfulfilled_access = false; if (WARN_ON_ONCE(!masks)) return true; - for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) { - masks->access[i] &= access_request; - if (masks->access[i]) + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { + masks->layers[i].access &= access_request; + if (masks->layers[i].access) saw_unfulfilled_access = true; } return !saw_unfulfilled_access; @@ -596,41 +600,46 @@ static bool scope_to_request(const access_mask_t access_request, static void test_scope_to_request_with_exec_none(struct kunit *const test) { /* Allows everything. */ - struct layer_access_masks masks = {}; + struct layer_masks masks = {}; /* Checks and scopes with execute. */ KUNIT_EXPECT_TRUE(test, scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE, &masks)); - KUNIT_EXPECT_EQ(test, 0, masks.access[0]); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[0].access); } static void test_scope_to_request_with_exec_some(struct kunit *const test) { /* Denies execute and write. */ - struct layer_access_masks masks = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE, - .access[1] = LANDLOCK_ACCESS_FS_WRITE_FILE, + struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, + .layers[1].access = LANDLOCK_ACCESS_FS_WRITE_FILE, }; /* Checks and scopes with execute. */ KUNIT_EXPECT_FALSE(test, scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE, &masks)); - KUNIT_EXPECT_EQ(test, LANDLOCK_ACCESS_FS_EXECUTE, masks.access[0]); - KUNIT_EXPECT_EQ(test, 0, masks.access[1]); + /* + * These casts to access_mask_t are needed because typeof(), used in + * KUNIT_EXPECT_EQ(), does not work on bitfields. + */ + KUNIT_EXPECT_EQ(test, LANDLOCK_ACCESS_FS_EXECUTE, + (access_mask_t)masks.layers[0].access); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[1].access); } static void test_scope_to_request_without_access(struct kunit *const test) { /* Denies execute and write. */ - struct layer_access_masks masks = { - .access[0] = LANDLOCK_ACCESS_FS_EXECUTE, - .access[1] = LANDLOCK_ACCESS_FS_WRITE_FILE, + struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, + .layers[1].access = LANDLOCK_ACCESS_FS_WRITE_FILE, }; /* Checks and scopes without access request. */ KUNIT_EXPECT_TRUE(test, scope_to_request(0, &masks)); - KUNIT_EXPECT_EQ(test, 0, masks.access[0]); - KUNIT_EXPECT_EQ(test, 0, masks.access[1]); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[0].access); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[1].access); } #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ @@ -639,15 +648,15 @@ static void test_scope_to_request_without_access(struct kunit *const test) * Returns true if there is at least one access right different than * LANDLOCK_ACCESS_FS_REFER. */ -static bool is_eacces(const struct layer_access_masks *masks, +static bool is_eacces(const struct layer_masks *masks, const access_mask_t access_request) { if (!masks) return false; - for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) { + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { /* LANDLOCK_ACCESS_FS_REFER alone must return -EXDEV. */ - if (masks->access[i] & access_request & + if (masks->layers[i].access & access_request & ~LANDLOCK_ACCESS_FS_REFER) return true; } @@ -661,7 +670,7 @@ static bool is_eacces(const struct layer_access_masks *masks, static void test_is_eacces_with_none(struct kunit *const test) { - const struct layer_access_masks masks = {}; + const struct layer_masks masks = {}; IE_FALSE(&masks, 0); IE_FALSE(&masks, LANDLOCK_ACCESS_FS_REFER); @@ -671,8 +680,8 @@ static void test_is_eacces_with_none(struct kunit *const test) static void test_is_eacces_with_refer(struct kunit *const test) { - const struct layer_access_masks masks = { - .access[0] = LANDLOCK_ACCESS_FS_REFER, + const struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_REFER, }; IE_FALSE(&masks, 0); @@ -683,8 +692,8 @@ static void test_is_eacces_with_refer(struct kunit *const test) static void test_is_eacces_with_write(struct kunit *const test) { - const struct layer_access_masks masks = { - .access[0] = LANDLOCK_ACCESS_FS_WRITE_FILE, + const struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_WRITE_FILE, }; IE_FALSE(&masks, 0); @@ -743,11 +752,11 @@ static bool is_access_to_paths_allowed(const struct landlock_ruleset *const domain, const struct path *const path, const access_mask_t access_request_parent1, - struct layer_access_masks *layer_masks_parent1, + struct layer_masks *layer_masks_parent1, struct landlock_request *const log_request_parent1, struct dentry *const dentry_child1, const access_mask_t access_request_parent2, - struct layer_access_masks *layer_masks_parent2, + struct layer_masks *layer_masks_parent2, struct landlock_request *const log_request_parent2, struct dentry *const dentry_child2) { @@ -755,9 +764,9 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain, child1_is_directory = true, child2_is_directory = true; struct path walker_path; access_mask_t access_masked_parent1, access_masked_parent2; - struct layer_access_masks _layer_masks_child1, _layer_masks_child2; - struct layer_access_masks *layer_masks_child1 = NULL, - *layer_masks_child2 = NULL; + struct layer_masks _layer_masks_child1, _layer_masks_child2; + struct layer_masks *layer_masks_child1 = NULL, + *layer_masks_child2 = NULL; if (!access_request_parent1 && !access_request_parent2) return true; @@ -797,6 +806,10 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain, } if (unlikely(dentry_child1)) { + /* + * Get the layer masks for the child dentries for use by domain + * check later. + */ if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS, &_layer_masks_child1, LANDLOCK_KEY_INODE)) @@ -952,7 +965,7 @@ static int current_check_access_path(const struct path *const path, }; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), masks, NULL); - struct layer_access_masks layer_masks; + struct layer_masks layer_masks; struct landlock_request request = {}; if (!subject) @@ -1029,7 +1042,7 @@ static access_mask_t maybe_remove(const struct dentry *const dentry) static bool collect_domain_accesses(const struct landlock_ruleset *const domain, const struct dentry *const mnt_root, struct dentry *dir, - struct layer_access_masks *layer_masks_dom) + struct layer_masks *layer_masks_dom) { bool ret = false; @@ -1135,8 +1148,7 @@ static int current_check_refer_path(struct dentry *const old_dentry, access_mask_t access_request_parent1, access_request_parent2; struct path mnt_dir; struct dentry *old_parent; - struct layer_access_masks layer_masks_parent1 = {}, - layer_masks_parent2 = {}; + struct layer_masks layer_masks_parent1 = {}, layer_masks_parent2 = {}; struct landlock_request request1 = {}, request2 = {}; if (!subject) @@ -1202,7 +1214,6 @@ static int current_check_refer_path(struct dentry *const old_dentry, allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry, new_dir->dentry, &layer_masks_parent2); - if (allow_parent1 && allow_parent2) return 0; @@ -1580,7 +1591,7 @@ static int hook_path_truncate(const struct path *const path) */ static void unmask_scoped_access(const struct landlock_ruleset *const client, const struct landlock_ruleset *const server, - struct layer_access_masks *const masks, + struct layer_masks *const masks, const access_mask_t access) { int client_layer, server_layer; @@ -1621,9 +1632,9 @@ static void unmask_scoped_access(const struct landlock_ruleset *const client, server_walker = server_walker->parent; for (; client_layer >= 0; client_layer--) { - if (masks->access[client_layer] & access && + if (masks->layers[client_layer].access & access && client_walker == server_walker) - masks->access[client_layer] &= ~access; + masks->layers[client_layer].access &= ~access; client_walker = client_walker->parent; server_walker = server_walker->parent; @@ -1635,7 +1646,7 @@ static int hook_unix_find(const struct path *const path, struct sock *other, { const struct landlock_ruleset *dom_other; const struct landlock_cred_security *subject; - struct layer_access_masks layer_masks; + struct layer_masks layer_masks; struct landlock_request request = {}; static const struct access_masks fs_resolve_unix = { .fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX, @@ -1739,7 +1750,7 @@ static bool is_device(const struct file *const file) static int hook_file_open(struct file *const file) { - struct layer_access_masks layer_masks = {}; + struct layer_masks layer_masks = {}; access_mask_t open_access_request, full_access_request, allowed_access, optional_access; const struct landlock_cred_security *const subject = @@ -1780,8 +1791,8 @@ static int hook_file_open(struct file *const file) * are still unfulfilled in any of the layers. */ allowed_access = full_access_request; - for (size_t i = 0; i < ARRAY_SIZE(layer_masks.access); i++) - allowed_access &= ~layer_masks.access[i]; + for (size_t i = 0; i < ARRAY_SIZE(layer_masks.layers); i++) + allowed_access &= ~layer_masks.layers[i].access; } /* diff --git a/security/landlock/limits.h b/security/landlock/limits.h index a4d908b240a2..08d5f2f6d321 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -31,6 +31,9 @@ #define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1) #define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE) +#define LANDLOCK_NUM_ACCESS_MAX \ + MAX(MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET), LANDLOCK_NUM_SCOPE) + #define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_TSYNC #define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1) diff --git a/security/landlock/net.c b/security/landlock/net.c index 942f856433c9..d7a4d116f7ee 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -49,7 +49,7 @@ static int current_check_access_socket(struct socket *const sock, { unsigned short sock_family; __be16 port; - struct layer_access_masks layer_masks = {}; + struct layer_masks layer_masks = {}; const struct landlock_rule *rule; struct landlock_id id = { .type = LANDLOCK_KEY_NET_PORT, diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index 181df7736bb9..23779e473563 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -628,7 +628,7 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset, * remaining unfulfilled access rights and masks has no leftover set bits). */ bool landlock_unmask_layers(const struct landlock_rule *const rule, - struct layer_access_masks *masks) + struct layer_masks *masks) { if (!masks) return true; @@ -649,11 +649,17 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule, const struct landlock_layer *const layer = &rule->layers[i]; /* Clear the bits where the layer in the rule grants access. */ - masks->access[layer->level - 1] &= ~layer->access; + masks->layers[layer->level - 1].access &= ~layer->access; + +#ifdef CONFIG_AUDIT + /* Collect rule flags for each layer. */ + if (layer->flags.quiet) + masks->layers[layer->level - 1].quiet = true; +#endif /* CONFIG_AUDIT */ } - for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) { - if (masks->access[i]) + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { + if (masks->layers[i].access) return false; } return true; @@ -666,8 +672,9 @@ get_access_mask_t(const struct landlock_ruleset *const ruleset, /** * landlock_init_layer_masks - Initialize layer masks from an access request * - * Populates @masks such that for each access right in @access_request, - * the bits for all the layers are set where this access right is handled. + * Populates @masks such that for each access right in @access_request, the bits + * for all the layers are set where this access right is handled. Rule flags + * are also zeroed. * * @domain: The domain that defines the current restrictions. * @access_request: The requested access rights to check. @@ -680,7 +687,7 @@ get_access_mask_t(const struct landlock_ruleset *const ruleset, access_mask_t landlock_init_layer_masks(const struct landlock_ruleset *const domain, const access_mask_t access_request, - struct layer_access_masks *const masks, + struct layer_masks *const masks, const enum landlock_key_type key_type) { access_mask_t handled_accesses = 0; @@ -709,11 +716,19 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain, for (size_t i = 0; i < domain->num_layers; i++) { const access_mask_t handled = get_access_mask(domain, i); - masks->access[i] = access_request & handled; - handled_accesses |= masks->access[i]; + masks->layers[i].access = access_request & handled; + handled_accesses |= masks->layers[i].access; +#ifdef CONFIG_AUDIT + masks->layers[i].quiet = false; +#endif /* CONFIG_AUDIT */ + } + for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers); + i++) { + masks->layers[i].access = 0; +#ifdef CONFIG_AUDIT + masks->layers[i].quiet = false; +#endif /* CONFIG_AUDIT */ } - for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->access); i++) - masks->access[i] = 0; return handled_accesses; } diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index 889f4b30301a..ffcb29a1c437 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -29,7 +29,18 @@ struct landlock_layer { /** * @level: Position of this layer in the layer stack. Starts from 1. */ - u16 level; + u8 level; + /** + * @flags: Bitfield for special flags attached to this rule. + */ + struct { + /** + * @quiet: Suppresses denial logs for the object covered by this + * rule in this domain. For filesystem rules, this inherits + * down the file hierarchy. + */ + u8 quiet : 1; + } flags; /** * @access: Bitfield of allowed actions on the kernel object. They are * relative to the object type (e.g. %LANDLOCK_ACTION_FS_READ). @@ -302,12 +313,12 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset, } bool landlock_unmask_layers(const struct landlock_rule *const rule, - struct layer_access_masks *masks); + struct layer_masks *masks); access_mask_t landlock_init_layer_masks(const struct landlock_ruleset *const domain, const access_mask_t access_request, - struct layer_access_masks *masks, + struct layer_masks *masks, const enum landlock_key_type key_type); #endif /* _SECURITY_LANDLOCK_RULESET_H */ -- cgit v1.2.3 From 29752205db5ff1793437b352c9e343b8e41fb184 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Fri, 12 Jun 2026 02:48:48 +0100 Subject: landlock: Add API support and docs for the quiet flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the UAPI for the quiet flags feature (but not the implementation yet). Even though currently LANDLOCK_ADD_RULE_QUIET only affects audit logging, in the future this can also be used as part of a supervisor mechanism, where it will also suppress denial notifications on a per-object basis. Thus the name is deliberately generic, as opposed to e.g. LANDLOCK_ADD_RULE_LOG_QUIET. According to pahole, even after adding the struct access_masks quiet_masks in struct landlock_hierarchy, the u32 log_* bitfield still only has a size of 2 bytes, so there's minimal wasted space. Assisted-by: GitHub-Copilot:claude-opus-4.8 Signed-off-by: Tingmao Wang [mic: Update date, fix comment formatting] Link: https://patch.msgid.link/031184748a8e74c0bb02f1fa13d7a3f10918c627.1781228815.git.m@maowtm.org Signed-off-by: Mickaël Salaün --- Documentation/admin-guide/LSM/landlock.rst | 9 ++-- Documentation/userspace-api/landlock.rst | 14 ++++++ include/uapi/linux/landlock.h | 60 +++++++++++++++++++++++ security/landlock/domain.h | 5 ++ security/landlock/fs.c | 4 +- security/landlock/fs.h | 2 +- security/landlock/net.c | 5 +- security/landlock/net.h | 5 +- security/landlock/ruleset.c | 12 ++++- security/landlock/ruleset.h | 12 +++-- security/landlock/syscalls.c | 71 +++++++++++++++++++++------- tools/testing/selftests/landlock/base_test.c | 2 +- 12 files changed, 170 insertions(+), 31 deletions(-) (limited to 'security') diff --git a/Documentation/admin-guide/LSM/landlock.rst b/Documentation/admin-guide/LSM/landlock.rst index 2dacb381c1a9..314052bbeb0a 100644 --- a/Documentation/admin-guide/LSM/landlock.rst +++ b/Documentation/admin-guide/LSM/landlock.rst @@ -19,8 +19,10 @@ Audit Denied access requests are logged by default for a sandboxed program if `audit` is enabled. This default behavior can be changed with the sys_landlock_restrict_self() flags (cf. -Documentation/userspace-api/landlock.rst). Landlock logs can also be masked -thanks to audit rules. Landlock can generate 2 audit record types. +Documentation/userspace-api/landlock.rst), or suppressed on a per-object +basis by using ``LANDLOCK_ADD_RULE_QUIET`` (ABI 10+). Landlock logs can +also be masked thanks to audit rules. Landlock can generate 2 audit +record types. Record types ------------ @@ -174,7 +176,8 @@ If you get spammed with audit logs related to Landlock, this is either an attack attempt or a bug in the security policy. We can put in place some filters to limit noise with two complementary ways: -- with sys_landlock_restrict_self()'s flags if we can fix the sandboxed +- with sys_landlock_restrict_self()'s flags, or + ``LANDLOCK_ADD_RULE_QUIET`` (ABI 10+) if we can fix the sandboxed programs, - or with audit rules (see :manpage:`auditctl(8)`). diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst index b5a2ab6f4766..5a63d4476c1c 100644 --- a/Documentation/userspace-api/landlock.rst +++ b/Documentation/userspace-api/landlock.rst @@ -775,6 +775,20 @@ remote port of UDP sockets (via :manpage:`connect(2)`), and sending datagrams to an explicit remote port (ignoring any destination set on UDP sockets, via e.g. :manpage:`sendto(2)`). +Quiet rule flag (ABI < 10) +-------------------------- + +Starting with the Landlock ABI version 10, it is possible to selectively +suppress logs for specific denied accesses on a per-object basis with +the ``LANDLOCK_ADD_RULE_QUIET`` flag of sys_landlock_add_rule(), in +combination with the ``quiet_access_fs`` and ``quiet_access_net`` fields +of struct landlock_ruleset_attr. It is also now possible to suppress +logs for scope accesses via the ``quiet_scoped`` field of struct +landlock_ruleset_attr. The object is marked as quiet within a ruleset +when at least one sys_landlock_add_rule() call is made for it with the +``LANDLOCK_ADD_RULE_QUIET`` flag, additional add-rule calls for the same +object without this flag do not clear it. + .. _kernel_support: Kernel support diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index 811ec77f9105..7ffe2ef127ee 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -32,6 +32,19 @@ * *handle* a wide range or all access rights that they know about at build time * (and that they have tested with a kernel that supported them all). * + * @quiet_access_fs and @quiet_access_net are bitmasks of actions for which a + * denial by this layer will not trigger a log if the corresponding object (or + * its children, for filesystem rules) is marked with the "quiet" bit via + * %LANDLOCK_ADD_RULE_QUIET, even if logging would normally take place per + * landlock_restrict_self() flags. @quiet_scoped is similar, except that it + * does not require marking any objects as quiet - if the ruleset is created + * with any bits set in @quiet_scoped, then denial of such scoped resources will + * not trigger any log. These 3 fields are available since Landlock ABI version + * 10. + * + * @quiet_access_fs, @quiet_access_net and @quiet_scoped must be a subset of + * @handled_access_fs, @handled_access_net and @scoped respectively. + * * This structure can grow in future Landlock versions. */ struct landlock_ruleset_attr { @@ -51,6 +64,20 @@ struct landlock_ruleset_attr { * resources (e.g. IPCs). */ __u64 scoped; + /** + * @quiet_access_fs: Bitmask of filesystem actions which should not be + * logged if per-object quiet flag is set. + */ + __u64 quiet_access_fs; + /** + * @quiet_access_net: Bitmask of network actions which should not be + * logged if per-object quiet flag is set. + */ + __u64 quiet_access_net; + /** + * @quiet_scoped: Bitmask of scoped actions which should not be logged. + */ + __u64 quiet_scoped; }; /** @@ -69,6 +96,39 @@ struct landlock_ruleset_attr { #define LANDLOCK_CREATE_RULESET_ERRATA (1U << 1) /* clang-format on */ +/** + * DOC: landlock_add_rule_flags + * + * **Flags** + * + * %LANDLOCK_ADD_RULE_QUIET + * Together with the quiet_* fields in struct landlock_ruleset_attr, + * this flag controls whether Landlock will log audit messages when + * access to the objects covered by this rule is denied by this layer. + * + * If logging is enabled, when Landlock denies an access, it will + * suppress the log if all of the following are true: + * + * - this layer is the innermost layer that denied the access; + * - all accesses denied by this layer are part of the quiet_* fields + * in the related struct landlock_ruleset_attr; + * - the object (or one of its parents, for filesystem rules) is + * marked as "quiet" via %LANDLOCK_ADD_RULE_QUIET. + * + * Because logging is only suppressed by a layer if the layer denies + * access, a sandboxed program cannot use this flag to "hide" access + * denials, without denying itself the access in the first place. + * + * The effect of this flag does not depend on the value of + * allowed_access in the passed in rule_attr. When this flag is + * present, the caller is also allowed to pass in an empty + * allowed_access. + */ + +/* clang-format off */ +#define LANDLOCK_ADD_RULE_QUIET (1U << 0) +/* clang-format on */ + /** * DOC: landlock_restrict_self_flags * diff --git a/security/landlock/domain.h b/security/landlock/domain.h index af100a8cd939..9f560f3c3bd1 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -111,6 +111,11 @@ struct landlock_hierarchy { * %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Set to false by default. */ log_new_exec : 1; + /** + * @quiet_masks: Bitmasks of access that should be quieted (i.e. not + * logged) if the related object is marked as quiet. + */ + struct access_masks quiet_masks; #endif /* CONFIG_AUDIT */ }; diff --git a/security/landlock/fs.c b/security/landlock/fs.c index d7cd2d5c9057..bd68a752abbf 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -325,7 +325,7 @@ retry: */ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, const struct path *const path, - access_mask_t access_rights) + access_mask_t access_rights, const u32 flags) { int err; struct landlock_id id = { @@ -346,7 +346,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, if (IS_ERR(id.key.object)) return PTR_ERR(id.key.object); mutex_lock(&ruleset->lock); - err = landlock_insert_rule(ruleset, id, access_rights); + err = landlock_insert_rule(ruleset, id, access_rights, flags); mutex_unlock(&ruleset->lock); /* * No need to check for an error because landlock_insert_rule() diff --git a/security/landlock/fs.h b/security/landlock/fs.h index 911b83669e20..e4c530511360 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -136,6 +136,6 @@ __init void landlock_add_fs_hooks(void); int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, const struct path *const path, - access_mask_t access_hierarchy); + access_mask_t access_hierarchy, const u32 flags); #endif /* _SECURITY_LANDLOCK_FS_H */ diff --git a/security/landlock/net.c b/security/landlock/net.c index d7a4d116f7ee..cbff59ec3aba 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -20,7 +20,8 @@ #include "ruleset.h" int landlock_append_net_rule(struct landlock_ruleset *const ruleset, - const u16 port, access_mask_t access_rights) + const u16 port, access_mask_t access_rights, + const u32 flags) { int err; const struct landlock_id id = { @@ -35,7 +36,7 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset, ~landlock_get_net_access_mask(ruleset, 0); mutex_lock(&ruleset->lock); - err = landlock_insert_rule(ruleset, id, access_rights); + err = landlock_insert_rule(ruleset, id, access_rights, flags); mutex_unlock(&ruleset->lock); return err; diff --git a/security/landlock/net.h b/security/landlock/net.h index 09960c237a13..5c0e3b4090cb 100644 --- a/security/landlock/net.h +++ b/security/landlock/net.h @@ -16,7 +16,8 @@ __init void landlock_add_net_hooks(void); int landlock_append_net_rule(struct landlock_ruleset *const ruleset, - const u16 port, access_mask_t access_rights); + const u16 port, access_mask_t access_rights, + const u32 flags); #else /* IS_ENABLED(CONFIG_INET) */ static inline void landlock_add_net_hooks(void) { @@ -24,7 +25,7 @@ static inline void landlock_add_net_hooks(void) static inline int landlock_append_net_rule(struct landlock_ruleset *const ruleset, const u16 port, - access_mask_t access_rights) + access_mask_t access_rights, const u32 flags) { return -EAFNOSUPPORT; } diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index 23779e473563..4dd09ea22c84 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -21,6 +21,7 @@ #include #include #include +#include #include "access.h" #include "domain.h" @@ -255,6 +256,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset, if (WARN_ON_ONCE(this->layers[0].level != 0)) return -EINVAL; this->layers[0].access |= (*layers)[0].access; + this->layers[0].flags.quiet |= (*layers)[0].flags.quiet; return 0; } @@ -305,12 +307,15 @@ static void build_check_layer(void) /* @ruleset must be locked by the caller. */ int landlock_insert_rule(struct landlock_ruleset *const ruleset, const struct landlock_id id, - const access_mask_t access) + const access_mask_t access, const u32 flags) { struct landlock_layer layers[] = { { .access = access, /* When @level is zero, insert_rule() extends @ruleset. */ .level = 0, + .flags = { + .quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET), + }, } }; build_check_layer(); @@ -351,6 +356,7 @@ static int merge_tree(struct landlock_ruleset *const dst, return -EINVAL; layers[0].access = walker_rule->layers[0].access; + layers[0].flags = walker_rule->layers[0].flags; err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers)); if (err) @@ -581,6 +587,10 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent, if (err) return ERR_PTR(err); +#ifdef CONFIG_AUDIT + new_dom->hierarchy->quiet_masks = ruleset->quiet_masks; +#endif /* CONFIG_AUDIT */ + return no_free_ptr(new_dom); } diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index ffcb29a1c437..61f3c253d5c9 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -156,8 +156,8 @@ struct landlock_ruleset { * @work_free: Enables to free a ruleset within a lockless * section. This is only used by * landlock_put_ruleset_deferred() when @usage reaches zero. - * The fields @lock, @usage, @num_rules, @num_layers and - * @access_masks are then unused. + * The fields @lock, @usage, @num_rules, @num_layers, + * @quiet_masks and @access_masks are then unused. */ struct work_struct work_free; struct { @@ -183,6 +183,12 @@ struct landlock_ruleset { * non-merged ruleset (i.e. not a domain). */ u32 num_layers; + /** + * @quiet_masks: Stores the quiet flags for an unmerged + * ruleset. For a merged domain, this is stored in each + * layer's struct landlock_hierarchy instead. + */ + struct access_masks quiet_masks; /** * @access_masks: Contains the subset of filesystem and * network actions that are restricted by a ruleset. @@ -213,7 +219,7 @@ DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *, int landlock_insert_rule(struct landlock_ruleset *const ruleset, const struct landlock_id id, - const access_mask_t access); + const access_mask_t access, const u32 flags); struct landlock_ruleset * landlock_merge_ruleset(struct landlock_ruleset *const parent, diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index d45469d5d464..36b02892c62f 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -105,8 +105,11 @@ static void build_check_abi(void) ruleset_size = sizeof(ruleset_attr.handled_access_fs); ruleset_size += sizeof(ruleset_attr.handled_access_net); ruleset_size += sizeof(ruleset_attr.scoped); + ruleset_size += sizeof(ruleset_attr.quiet_access_fs); + ruleset_size += sizeof(ruleset_attr.quiet_access_net); + ruleset_size += sizeof(ruleset_attr.quiet_scoped); BUILD_BUG_ON(sizeof(ruleset_attr) != ruleset_size); - BUILD_BUG_ON(sizeof(ruleset_attr) != 24); + BUILD_BUG_ON(sizeof(ruleset_attr) != 48); path_beneath_size = sizeof(path_beneath_attr.allowed_access); path_beneath_size += sizeof(path_beneath_attr.parent_fd); @@ -193,6 +196,9 @@ const int landlock_abi_version = 10; * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; * - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small * @size; + * - %EINVAL: quiet_access_fs, quiet_access_net, or quiet_scoped is not a + * subset of the corresponding handled_access_fs, handled_access_net, or + * scoped; * - %E2BIG: @attr or @size inconsistencies; * - %EFAULT: @attr or @size inconsistencies; * - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs. @@ -249,6 +255,21 @@ SYSCALL_DEFINE3(landlock_create_ruleset, if ((ruleset_attr.scoped | LANDLOCK_MASK_SCOPE) != LANDLOCK_MASK_SCOPE) return -EINVAL; + /* + * Check that quiet masks are subsets of the respective handled masks. + * Because of the checks above this is sufficient to also ensure that + * the quiet masks are valid access masks. + */ + if ((ruleset_attr.quiet_access_fs | ruleset_attr.handled_access_fs) != + ruleset_attr.handled_access_fs) + return -EINVAL; + if ((ruleset_attr.quiet_access_net | ruleset_attr.handled_access_net) != + ruleset_attr.handled_access_net) + return -EINVAL; + if ((ruleset_attr.quiet_scoped | ruleset_attr.scoped) != + ruleset_attr.scoped) + return -EINVAL; + /* Checks arguments and transforms to kernel struct. */ ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs, ruleset_attr.handled_access_net, @@ -256,6 +277,10 @@ SYSCALL_DEFINE3(landlock_create_ruleset, if (IS_ERR(ruleset)) return PTR_ERR(ruleset); + ruleset->quiet_masks.fs = ruleset_attr.quiet_access_fs; + ruleset->quiet_masks.net = ruleset_attr.quiet_access_net; + ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped; + /* Creates anonymous FD referring to the ruleset. */ ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops, ruleset, O_RDWR | O_CLOEXEC); @@ -320,7 +345,7 @@ static int get_path_from_fd(const s32 fd, struct path *const path) } static int add_rule_path_beneath(struct landlock_ruleset *const ruleset, - const void __user *const rule_attr) + const void __user *const rule_attr, u32 flags) { struct landlock_path_beneath_attr path_beneath_attr; struct path path; @@ -335,9 +360,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset, /* * Informs about useless rule: empty allowed_access (i.e. deny rules) - * are ignored in path walks. + * are ignored in path walks. However, the rule is not useless if it is + * there to hold a quiet flag. */ - if (!path_beneath_attr.allowed_access) + if (!flags && !path_beneath_attr.allowed_access) return -ENOMSG; /* Checks that allowed_access matches the @ruleset constraints. */ @@ -345,6 +371,10 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset, if ((path_beneath_attr.allowed_access | mask) != mask) return -EINVAL; + /* Checks for useless quiet flag. */ + if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.fs) + return -EINVAL; + /* Gets and checks the new rule. */ err = get_path_from_fd(path_beneath_attr.parent_fd, &path); if (err) @@ -352,13 +382,13 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset, /* Imports the new rule. */ err = landlock_append_fs_rule(ruleset, &path, - path_beneath_attr.allowed_access); + path_beneath_attr.allowed_access, flags); path_put(&path); return err; } static int add_rule_net_port(struct landlock_ruleset *ruleset, - const void __user *const rule_attr) + const void __user *const rule_attr, u32 flags) { struct landlock_net_port_attr net_port_attr; int res; @@ -371,9 +401,10 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset, /* * Informs about useless rule: empty allowed_access (i.e. deny rules) - * are ignored by network actions. + * are ignored by network actions. However, the rule is not useless if + * it is there to hold a quiet flag. */ - if (!net_port_attr.allowed_access) + if (!flags && !net_port_attr.allowed_access) return -ENOMSG; /* Checks that allowed_access matches the @ruleset constraints. */ @@ -381,13 +412,17 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset, if ((net_port_attr.allowed_access | mask) != mask) return -EINVAL; + /* Checks for useless quiet flag. */ + if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.net) + return -EINVAL; + /* Denies inserting a rule with port greater than 65535. */ if (net_port_attr.port > U16_MAX) return -EINVAL; /* Imports the new rule. */ return landlock_append_net_rule(ruleset, net_port_attr.port, - net_port_attr.allowed_access); + net_port_attr.allowed_access, flags); } /** @@ -398,7 +433,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset, * @rule_type: Identify the structure type pointed to by @rule_attr: * %LANDLOCK_RULE_PATH_BENEATH or %LANDLOCK_RULE_NET_PORT. * @rule_attr: Pointer to a rule (matching the @rule_type). - * @flags: Must be 0. + * @flags: Must be 0 or %LANDLOCK_ADD_RULE_QUIET. * * This system call enables to define a new rule and add it to an existing * ruleset. @@ -408,20 +443,25 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset, * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; * - %EAFNOSUPPORT: @rule_type is %LANDLOCK_RULE_NET_PORT but TCP/IP is not * supported by the running kernel; - * - %EINVAL: @flags is not 0; + * - %EINVAL: @flags is not valid; * - %EINVAL: The rule accesses are inconsistent (i.e. * &landlock_path_beneath_attr.allowed_access or * &landlock_net_port_attr.allowed_access is not a subset of the ruleset * handled accesses) * - %EINVAL: &landlock_net_port_attr.port is greater than 65535; + * - %EINVAL: LANDLOCK_ADD_RULE_QUIET is passed but the ruleset has no + * quiet access bits set for the corresponding rule type. * - %ENOMSG: Empty accesses (e.g. &landlock_path_beneath_attr.allowed_access is - * 0); + * 0) and no flags; * - %EBADF: @ruleset_fd is not a file descriptor for the current thread, or a * member of @rule_attr is not a file descriptor as expected; * - %EBADFD: @ruleset_fd is not a ruleset file descriptor, or a member of * @rule_attr is not the expected file descriptor type; * - %EPERM: @ruleset_fd has no write access to the underlying ruleset; * - %EFAULT: @rule_attr was not a valid address. + * + * .. kernel-doc:: include/uapi/linux/landlock.h + * :identifiers: landlock_add_rule_flags */ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, const enum landlock_rule_type, rule_type, @@ -432,8 +472,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, if (!is_initialized()) return -EOPNOTSUPP; - /* No flag for now. */ - if (flags) + if (flags && flags != LANDLOCK_ADD_RULE_QUIET) return -EINVAL; /* Gets and checks the ruleset. */ @@ -443,9 +482,9 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, switch (rule_type) { case LANDLOCK_RULE_PATH_BENEATH: - return add_rule_path_beneath(ruleset, rule_attr); + return add_rule_path_beneath(ruleset, rule_attr, flags); case LANDLOCK_RULE_NET_PORT: - return add_rule_net_port(ruleset, rule_attr); + return add_rule_net_port(ruleset, rule_attr, flags); default: return -EINVAL; } diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index 6c8113c2ded1..84e91fcaa1b2 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -201,7 +201,7 @@ TEST(add_rule_checks_ordering) ASSERT_LE(0, ruleset_fd); /* Checks invalid flags. */ - ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 1)); + ASSERT_EQ(-1, landlock_add_rule(-1, 0, NULL, 100)); ASSERT_EQ(EINVAL, errno); /* Checks invalid ruleset FD. */ -- cgit v1.2.3 From 5f12f8effb5acb38a8b554ea39bd30d43d54f9f0 Mon Sep 17 00:00:00 2001 From: Tingmao Wang Date: Fri, 12 Jun 2026 02:48:49 +0100 Subject: landlock: Suppress logging when quiet flag is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quietness behaviour is as documented in the previous patch. For optional accesses, since the existing deny_masks can only store 2x4bit of layer index, with no way to represent "no layer", we need to either expand it or have another field to correctly handle quieting of those. This commit uses the latter approach - we add another field to store which optional access (of the 2) are covered by quiet rules in their respective layers as stored in deny_masks. Assisted-by: GitHub-Copilot:claude-opus-4.8 copilot-review Signed-off-by: Tingmao Wang Link: https://patch.msgid.link/2510a357a94183683eefc49917dcb2240d67be96.1781228815.git.m@maowtm.org [mic: Cosmetic fixes] Signed-off-by: Mickaël Salaün --- security/landlock/access.h | 5 + security/landlock/audit.c | 269 ++++++++++++++++++++++++++++++++++++++++++--- security/landlock/audit.h | 1 + security/landlock/domain.c | 38 +++++++ security/landlock/domain.h | 4 + security/landlock/fs.c | 6 + security/landlock/fs.h | 17 ++- 7 files changed, 324 insertions(+), 16 deletions(-) (limited to 'security') diff --git a/security/landlock/access.h b/security/landlock/access.h index ce374a65f865..d926078bf0a5 100644 --- a/security/landlock/access.h +++ b/security/landlock/access.h @@ -143,4 +143,9 @@ static inline bool access_mask_subset(access_mask_t subset, return (subset | superset) == superset; } +/* A bitmask that is large enough to hold set of optional accesses. */ +typedef u8 optional_access_t; +static_assert(BITS_PER_TYPE(optional_access_t) >= + HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)); + #endif /* _SECURITY_LANDLOCK_ACCESS_H */ diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 8c56f7f6467a..50536c568526 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -249,7 +249,9 @@ static void test_get_denied_layer(struct kunit *const test) static size_t get_layer_from_deny_masks(access_mask_t *const access_request, const access_mask_t all_existing_optional_access, - const deny_masks_t deny_masks) + const deny_masks_t deny_masks, + optional_access_t quiet_optional_accesses, + bool *quiet) { const unsigned long access_opt = all_existing_optional_access; const unsigned long access_req = *access_request; @@ -257,6 +259,7 @@ get_layer_from_deny_masks(access_mask_t *const access_request, size_t youngest_layer = 0; size_t access_index = 0; unsigned long access_bit; + bool should_quiet = false; /* This will require change with new object types. */ WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL); @@ -265,20 +268,34 @@ get_layer_from_deny_masks(access_mask_t *const access_request, BITS_PER_TYPE(access_mask_t)) { if (access_req & BIT(access_bit)) { const size_t layer = - (deny_masks >> (access_index * 4)) & + (deny_masks >> + (access_index * + HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) & (LANDLOCK_MAX_NUM_LAYERS - 1); + const bool layer_has_quiet = + !!(quiet_optional_accesses & BIT(access_index)); if (layer > youngest_layer) { youngest_layer = layer; missing = BIT(access_bit); + should_quiet = layer_has_quiet; } else if (layer == youngest_layer) { missing |= BIT(access_bit); + /* + * Whether the layer has rules with quiet flag + * covering the file accessed does not depend on + * the access, and so the following + * WARN_ON_ONCE() should not fail. + */ + WARN_ON_ONCE(should_quiet && !layer_has_quiet); + should_quiet = layer_has_quiet; } } access_index++; } *access_request = missing; + *quiet = should_quiet; return youngest_layer; } @@ -288,42 +305,188 @@ static void test_get_layer_from_deny_masks(struct kunit *const test) { deny_masks_t deny_mask; access_mask_t access; + optional_access_t quiet_optional_accesses; + bool quiet; /* truncate:0 ioctl_dev:2 */ deny_mask = 0x20; + quiet_optional_accesses = 0; access = LANDLOCK_ACCESS_FS_TRUNCATE; KUNIT_EXPECT_EQ(test, 0, - get_layer_from_deny_masks(&access, - _LANDLOCK_ACCESS_FS_OPTIONAL, - deny_mask)); + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; KUNIT_EXPECT_EQ(test, 2, - get_layer_from_deny_masks(&access, - _LANDLOCK_ACCESS_FS_OPTIONAL, - deny_mask)); + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* layer denying truncate: quiet, ioctl: not quiet */ + quiet_optional_accesses = 0b01; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* Reverse order - truncate:2 ioctl_dev:0 */ + deny_mask = 0x02; + quiet_optional_accesses = 0; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* layer denying truncate: quiet, ioctl: not quiet */ + quiet_optional_accesses = 0b01; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); + + /* layer denying truncate: not quiet, ioctl: quiet */ + quiet_optional_accesses = 0b10; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, true); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); /* truncate:15 ioctl_dev:15 */ deny_mask = 0xff; + quiet_optional_accesses = 0; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* Both quiet (same layer so quietness must be the same) */ + quiet_optional_accesses = 0b11; access = LANDLOCK_ACCESS_FS_TRUNCATE; KUNIT_EXPECT_EQ(test, 15, - get_layer_from_deny_masks(&access, - _LANDLOCK_ACCESS_FS_OPTIONAL, - deny_mask)); + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; KUNIT_EXPECT_EQ(test, 15, - get_layer_from_deny_masks(&access, - _LANDLOCK_ACCESS_FS_OPTIONAL, - deny_mask)); + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, true); } #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ @@ -349,11 +512,34 @@ static bool is_valid_request(const struct landlock_request *const request) if (request->deny_masks) { if (WARN_ON_ONCE(!request->all_existing_optional_access)) return false; + static_assert(sizeof(request->all_existing_optional_access) == + sizeof(u32)); + if (WARN_ON_ONCE( + request->quiet_optional_accesses >= + BIT(hweight32( + request->all_existing_optional_access)))) + return false; } return true; } +static access_mask_t +pick_access_mask_for_request_type(const enum landlock_request_type type, + const struct access_masks access_masks) +{ + switch (type) { + case LANDLOCK_REQUEST_FS_ACCESS: + return access_masks.fs; + case LANDLOCK_REQUEST_NET_ACCESS: + return access_masks.net; + default: + WARN_ONCE(1, "Invalid request type %d passed to %s", type, + __func__); + return 0; + } +} + /** * landlock_log_denial - Create audit records related to a denial * @@ -367,6 +553,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, struct landlock_hierarchy *youngest_denied; size_t youngest_layer; access_mask_t missing; + bool object_quiet_flag = false, quiet_applicable_to_access = false; if (WARN_ON_ONCE(!subject || !subject->domain || !subject->domain->hierarchy || !request)) @@ -382,10 +569,15 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, youngest_layer = get_denied_layer(subject->domain, &missing, request->layer_masks); + object_quiet_flag = + request->layer_masks->layers[youngest_layer] + .quiet; } else { youngest_layer = get_layer_from_deny_masks( &missing, _LANDLOCK_ACCESS_FS_OPTIONAL, - request->deny_masks); + request->deny_masks, + request->quiet_optional_accesses, + &object_quiet_flag); } youngest_denied = get_hierarchy(subject->domain, youngest_layer); @@ -420,6 +612,53 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, return; } + /* + * Checks if the object is marked quiet by the layer that denied the + * request. If it's a different layer that marked it as quiet, but that + * layer is not the one that denied the request, we should still audit + * log the denial. + */ + if (object_quiet_flag) { + /* + * We now check if the denied requests are all covered by the + * layer's quiet access bits. + */ + const access_mask_t quiet_mask = + pick_access_mask_for_request_type( + request->type, youngest_denied->quiet_masks); + + quiet_applicable_to_access = (quiet_mask & missing) == missing; + } else { + /* + * Either the object is not quiet, or this is a scope request. + * We check request->type to distinguish between the two cases. + */ + const access_mask_t quiet_mask = + youngest_denied->quiet_masks.scope; + + switch (request->type) { + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + quiet_applicable_to_access = + !!(quiet_mask & LANDLOCK_SCOPE_SIGNAL); + break; + case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: + quiet_applicable_to_access = + !!(quiet_mask & + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + break; + /* + * Leave LANDLOCK_REQUEST_PTRACE and + * LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY unhandled for now - they + * are never quiet. + */ + default: + break; + } + } + + if (quiet_applicable_to_access) + return; + /* Uses consistent allocation flags wrt common_lsm_audit(). */ ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, AUDIT_LANDLOCK_ACCESS); diff --git a/security/landlock/audit.h b/security/landlock/audit.h index b85d752273ac..620f8a24291d 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -48,6 +48,7 @@ struct landlock_request { /* Required fields for requests with deny masks. */ const access_mask_t all_existing_optional_access; deny_masks_t deny_masks; + optional_access_t quiet_optional_accesses; }; #ifdef CONFIG_AUDIT diff --git a/security/landlock/domain.c b/security/landlock/domain.c index 4ae45b300071..9a8355fccd26 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -157,6 +157,44 @@ get_layer_deny_mask(const access_mask_t all_existing_optional_access, << ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1)); } +/** + * landlock_get_quiet_optional_accesses - Get optional accesses which are + * covered by quiet rule flags. + * + * @all_existing_optional_access: Bitmask of valid optional accesses. + * @deny_masks: Domain layer levels that denied each optional access (the + * deny_masks field on struct landlock_file_security). + * @masks: The struct layer_masks collected during the path walk. + * + * Return: a bitmask of which optional accesses are denied by layers for which + * the quiet flag was collected during the path walk. + */ +optional_access_t landlock_get_quiet_optional_accesses( + const access_mask_t all_existing_optional_access, + const deny_masks_t deny_masks, const struct layer_masks *const masks) +{ + const unsigned long access_opt = all_existing_optional_access; + size_t access_index = 0; + unsigned long access_bit; + optional_access_t quiet_optional_accesses = 0; + + /* This will require change with new object types. */ + WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL); + + for_each_set_bit(access_bit, &access_opt, + BITS_PER_TYPE(access_mask_t)) { + const u8 layer = + (deny_masks >> (access_index * + HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) & + (LANDLOCK_MAX_NUM_LAYERS - 1); + + if (masks->layers[layer].quiet) + quiet_optional_accesses |= BIT(access_index); + access_index++; + } + return quiet_optional_accesses; +} + #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST static void test_get_layer_deny_mask(struct kunit *const test) diff --git a/security/landlock/domain.h b/security/landlock/domain.h index 9f560f3c3bd1..2a1660e3dea7 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -126,6 +126,10 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access, const access_mask_t optional_access, const struct layer_masks *const masks); +optional_access_t landlock_get_quiet_optional_accesses( + const access_mask_t all_existing_optional_access, + const deny_masks_t deny_masks, const struct layer_masks *const masks); + int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); static inline void diff --git a/security/landlock/fs.c b/security/landlock/fs.c index bd68a752abbf..6292887e6cef 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1805,6 +1805,10 @@ static int hook_file_open(struct file *const file) #ifdef CONFIG_AUDIT landlock_file(file)->deny_masks = landlock_get_deny_masks( _LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks); + landlock_file(file)->quiet_optional_accesses = + landlock_get_quiet_optional_accesses( + _LANDLOCK_ACCESS_FS_OPTIONAL, + landlock_file(file)->deny_masks, &layer_masks); #endif /* CONFIG_AUDIT */ if (access_mask_subset(open_access_request, allowed_access)) @@ -1841,6 +1845,7 @@ static int hook_file_truncate(struct file *const file) .access = LANDLOCK_ACCESS_FS_TRUNCATE, #ifdef CONFIG_AUDIT .deny_masks = landlock_file(file)->deny_masks, + .quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses, #endif /* CONFIG_AUDIT */ }); return -EACCES; @@ -1880,6 +1885,7 @@ static int hook_file_ioctl_common(const struct file *const file, .access = LANDLOCK_ACCESS_FS_IOCTL_DEV, #ifdef CONFIG_AUDIT .deny_masks = landlock_file(file)->deny_masks, + .quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses, #endif /* CONFIG_AUDIT */ }); return -EACCES; diff --git a/security/landlock/fs.h b/security/landlock/fs.h index e4c530511360..b4421d9df68f 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -63,6 +63,13 @@ struct landlock_file_security { * _LANDLOCK_ACCESS_FS_OPTIONAL). */ deny_masks_t deny_masks; + /** + * @quiet_optional_accesses: Stores which optional accesses are covered + * by quiet rules within the layer referred to in deny_masks, one access + * per bit. Does not take into account whether the quiet access bits + * are actually set in the layer's corresponding landlock_hierarchy. + */ + optional_access_t quiet_optional_accesses; /** * @fown_layer: Layer level of @fown_subject->domain with * LANDLOCK_SCOPE_SIGNAL. @@ -96,7 +103,15 @@ struct landlock_file_security { /* clang-format off */ static_assert((typeof_member(struct landlock_file_security, fown_layer))~0 >= LANDLOCK_MAX_NUM_LAYERS); -/* clang-format off */ +/* clang-format on */ + +/* + * Make sure quiet_optional_accesses has enough bits to cover all optional + * accesses. + */ +static_assert(BITS_PER_TYPE(typeof_member(struct landlock_file_security, + quiet_optional_accesses)) >= + HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)); #endif /* CONFIG_AUDIT */ -- cgit v1.2.3