From 624f177d8f62032b4f3343c289120269645cec37 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Tue, 18 Mar 2025 17:14:36 +0100 Subject: landlock: Move code to ease future backports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To ease backports in setup.c, let's group changes from __lsm_ro_after_init to __ro_after_init with commit f22f9aaf6c3d ("selinux: remove the runtime disable functionality"), and the landlock_lsmid addition with commit f3b8788cde61 ("LSM: Identify modules by more than name"). That will help to backport the following errata. Cc: Günther Noack Cc: stable@vger.kernel.org Link: https://lore.kernel.org/r/20250318161443.279194-2-mic@digikod.net Fixes: f3b8788cde61 ("LSM: Identify modules by more than name") Signed-off-by: Mickaël Salaün --- security/landlock/setup.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'security') diff --git a/security/landlock/setup.c b/security/landlock/setup.c index 28519a45b11f..c71832a8e369 100644 --- a/security/landlock/setup.c +++ b/security/landlock/setup.c @@ -19,6 +19,11 @@ bool landlock_initialized __ro_after_init = false; +const struct lsm_id landlock_lsmid = { + .name = LANDLOCK_NAME, + .id = LSM_ID_LANDLOCK, +}; + struct lsm_blob_sizes landlock_blob_sizes __ro_after_init = { .lbs_cred = sizeof(struct landlock_cred_security), .lbs_file = sizeof(struct landlock_file_security), @@ -26,11 +31,6 @@ struct lsm_blob_sizes landlock_blob_sizes __ro_after_init = { .lbs_superblock = sizeof(struct landlock_superblock_security), }; -const struct lsm_id landlock_lsmid = { - .name = LANDLOCK_NAME, - .id = LSM_ID_LANDLOCK, -}; - static int __init landlock_init(void) { landlock_add_cred_hooks(); -- cgit From 15383a0d63dbcd63dc7e8d9ec1bf3a0f7ebf64ac Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Tue, 18 Mar 2025 17:14:37 +0100 Subject: landlock: Add the errata interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some fixes may require user space to check if they are applied on the running kernel before using a specific feature. For instance, this applies when a restriction was previously too restrictive and is now getting relaxed (e.g. for compatibility reasons). However, non-visible changes for legitimate use (e.g. security fixes) do not require an erratum. Because fixes are backported down to a specific Landlock ABI, we need a way to avoid cherry-pick conflicts. The solution is to only update a file related to the lower ABI impacted by this issue. All the ABI files are then used to create a bitmask of fixes. The new errata interface is similar to the one used to get the supported Landlock ABI version, but it returns a bitmask instead because the order of fixes may not match the order of versions, and not all fixes may apply to all versions. The actual errata will come with dedicated commits. The description is not actually used in the code but serves as documentation. Create the landlock_abi_version symbol and use its value to check errata consistency. Update test_base's create_ruleset_checks_ordering tests and add errata tests. This commit is backportable down to the first version of Landlock. Fixes: 3532b0b4352c ("landlock: Enable user space to infer supported features") Cc: Günther Noack Cc: stable@vger.kernel.org Link: https://lore.kernel.org/r/20250318161443.279194-3-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/errata.h | 87 ++++++++++++++++++++++++++++++++++++++++++++ security/landlock/setup.c | 30 +++++++++++++++ security/landlock/setup.h | 3 ++ security/landlock/syscalls.c | 22 +++++++++-- 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 security/landlock/errata.h (limited to 'security') diff --git a/security/landlock/errata.h b/security/landlock/errata.h new file mode 100644 index 000000000000..f26b28b9873d --- /dev/null +++ b/security/landlock/errata.h @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Errata information + * + * Copyright © 2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_ERRATA_H +#define _SECURITY_LANDLOCK_ERRATA_H + +#include + +struct landlock_erratum { + const int abi; + const u8 number; +}; + +/* clang-format off */ +#define LANDLOCK_ERRATUM(NUMBER) \ + { \ + .abi = LANDLOCK_ERRATA_ABI, \ + .number = NUMBER, \ + }, +/* clang-format on */ + +/* + * Some fixes may require user space to check if they are applied on the running + * kernel before using a specific feature. For instance, this applies when a + * restriction was previously too restrictive and is now getting relaxed (for + * compatibility or semantic reasons). However, non-visible changes for + * legitimate use (e.g. security fixes) do not require an erratum. + */ +static const struct landlock_erratum landlock_errata_init[] __initconst = { + +/* + * Only Sparse may not implement __has_include. If a compiler does not + * implement __has_include, a warning will be printed at boot time (see + * setup.c). + */ +#ifdef __has_include + +#define LANDLOCK_ERRATA_ABI 1 +#if __has_include("errata/abi-1.h") +#include "errata/abi-1.h" +#endif +#undef LANDLOCK_ERRATA_ABI + +#define LANDLOCK_ERRATA_ABI 2 +#if __has_include("errata/abi-2.h") +#include "errata/abi-2.h" +#endif +#undef LANDLOCK_ERRATA_ABI + +#define LANDLOCK_ERRATA_ABI 3 +#if __has_include("errata/abi-3.h") +#include "errata/abi-3.h" +#endif +#undef LANDLOCK_ERRATA_ABI + +#define LANDLOCK_ERRATA_ABI 4 +#if __has_include("errata/abi-4.h") +#include "errata/abi-4.h" +#endif +#undef LANDLOCK_ERRATA_ABI + +/* + * For each new erratum, we need to include all the ABI files up to the impacted + * ABI to make all potential future intermediate errata easy to backport. + * + * If such change involves more than one ABI addition, then it must be in a + * dedicated commit with the same Fixes tag as used for the actual fix. + * + * Each commit creating a new security/landlock/errata/abi-*.h file must have a + * Depends-on tag to reference the commit that previously added the line to + * include this new file, except if the original Fixes tag is enough. + * + * Each erratum must be documented in its related ABI file, and a dedicated + * commit must update Documentation/userspace-api/landlock.rst to include this + * erratum. This commit will not be backported. + */ + +#endif + + {} +}; + +#endif /* _SECURITY_LANDLOCK_ERRATA_H */ diff --git a/security/landlock/setup.c b/security/landlock/setup.c index c71832a8e369..0c85ea27e409 100644 --- a/security/landlock/setup.c +++ b/security/landlock/setup.c @@ -6,12 +6,14 @@ * Copyright © 2018-2020 ANSSI */ +#include #include #include #include #include "common.h" #include "cred.h" +#include "errata.h" #include "fs.h" #include "net.h" #include "setup.h" @@ -31,8 +33,36 @@ struct lsm_blob_sizes landlock_blob_sizes __ro_after_init = { .lbs_superblock = sizeof(struct landlock_superblock_security), }; +int landlock_errata __ro_after_init; + +static void __init compute_errata(void) +{ + size_t i; + +#ifndef __has_include + /* + * This is a safeguard to make sure the compiler implements + * __has_include (see errata.h). + */ + WARN_ON_ONCE(1); + return; +#endif + + for (i = 0; landlock_errata_init[i].number; i++) { + const int prev_errata = landlock_errata; + + if (WARN_ON_ONCE(landlock_errata_init[i].abi > + landlock_abi_version)) + continue; + + landlock_errata |= BIT(landlock_errata_init[i].number - 1); + WARN_ON_ONCE(prev_errata == landlock_errata); + } +} + static int __init landlock_init(void) { + compute_errata(); landlock_add_cred_hooks(); landlock_add_task_hooks(); landlock_add_fs_hooks(); diff --git a/security/landlock/setup.h b/security/landlock/setup.h index c4252d46d49d..fca307c35fee 100644 --- a/security/landlock/setup.h +++ b/security/landlock/setup.h @@ -11,7 +11,10 @@ #include +extern const int landlock_abi_version; + extern bool landlock_initialized; +extern int landlock_errata; extern struct lsm_blob_sizes landlock_blob_sizes; extern const struct lsm_id landlock_lsmid; diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index a9760d252fc2..cf9e0483e542 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -160,7 +160,9 @@ static const struct file_operations ruleset_fops = { * the new ruleset. * @size: Size of the pointed &struct landlock_ruleset_attr (needed for * backward and forward compatibility). - * @flags: Supported value: %LANDLOCK_CREATE_RULESET_VERSION. + * @flags: Supported value: + * - %LANDLOCK_CREATE_RULESET_VERSION + * - %LANDLOCK_CREATE_RULESET_ERRATA * * This system call enables to create a new Landlock ruleset, and returns the * related file descriptor on success. @@ -169,6 +171,10 @@ static const struct file_operations ruleset_fops = { * 0, then the returned value is the highest supported Landlock ABI version * (starting at 1). * + * If @flags is %LANDLOCK_CREATE_RULESET_ERRATA and @attr is NULL and @size is + * 0, then the returned value is a bitmask of fixed issues for the current + * Landlock ABI version. + * * Possible returned errors are: * * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; @@ -192,9 +198,15 @@ SYSCALL_DEFINE3(landlock_create_ruleset, return -EOPNOTSUPP; if (flags) { - if ((flags == LANDLOCK_CREATE_RULESET_VERSION) && !attr && - !size) - return LANDLOCK_ABI_VERSION; + if (attr || size) + return -EINVAL; + + if (flags == LANDLOCK_CREATE_RULESET_VERSION) + return landlock_abi_version; + + if (flags == LANDLOCK_CREATE_RULESET_ERRATA) + return landlock_errata; + return -EINVAL; } @@ -235,6 +247,8 @@ SYSCALL_DEFINE3(landlock_create_ruleset, return ruleset_fd; } +const int landlock_abi_version = LANDLOCK_ABI_VERSION; + /* * Returns an owned ruleset from a FD. It is thus needed to call * landlock_put_ruleset() on the return value. -- cgit From 48fce74fe209ba9e9b416d7100ccee546edc9fc6 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Tue, 18 Mar 2025 17:14:38 +0100 Subject: landlock: Add erratum for TCP fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add erratum for the TCP socket identification fixed with commit 854277e2cc8c ("landlock: Fix non-TCP sockets restriction"). Fixes: 854277e2cc8c ("landlock: Fix non-TCP sockets restriction") Cc: Günther Noack Cc: Mikhail Ivanov Cc: stable@vger.kernel.org Link: https://lore.kernel.org/r/20250318161443.279194-4-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/errata/abi-4.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 security/landlock/errata/abi-4.h (limited to 'security') diff --git a/security/landlock/errata/abi-4.h b/security/landlock/errata/abi-4.h new file mode 100644 index 000000000000..c052ee54f89f --- /dev/null +++ b/security/landlock/errata/abi-4.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +/** + * DOC: erratum_1 + * + * Erratum 1: TCP socket identification + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This fix addresses an issue where IPv4 and IPv6 stream sockets (e.g., SMC, + * MPTCP, or SCTP) were incorrectly restricted by TCP access rights during + * :manpage:`bind(2)` and :manpage:`connect(2)` operations. This change ensures + * that only TCP sockets are subject to TCP access rights, allowing other + * protocols to operate without unnecessary restrictions. + */ +LANDLOCK_ERRATUM(1) -- cgit From 6d9ac5e4d70eba3e336f9809ba91ab2c49de6d87 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Tue, 18 Mar 2025 17:14:39 +0100 Subject: landlock: Prepare to add second errata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Potentially include errata for Landlock ABI v5 (Linux 6.10) and v6 (Linux 6.12). That will be useful for the following signal scoping erratum. As explained in errata.h, this commit should be backportable without conflict down to ABI v5. It must then not include the errata/abi-6.h file. Fixes: 54a6e6bbf3be ("landlock: Add signal scoping") Cc: Günther Noack Cc: stable@vger.kernel.org Link: https://lore.kernel.org/r/20250318161443.279194-5-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/errata.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'security') diff --git a/security/landlock/errata.h b/security/landlock/errata.h index f26b28b9873d..8e626accac10 100644 --- a/security/landlock/errata.h +++ b/security/landlock/errata.h @@ -63,6 +63,18 @@ static const struct landlock_erratum landlock_errata_init[] __initconst = { #endif #undef LANDLOCK_ERRATA_ABI +#define LANDLOCK_ERRATA_ABI 5 +#if __has_include("errata/abi-5.h") +#include "errata/abi-5.h" +#endif +#undef LANDLOCK_ERRATA_ABI + +#define LANDLOCK_ERRATA_ABI 6 +#if __has_include("errata/abi-6.h") +#include "errata/abi-6.h" +#endif +#undef LANDLOCK_ERRATA_ABI + /* * For each new erratum, we need to include all the ABI files up to the impacted * ABI to make all potential future intermediate errata easy to backport. -- cgit From 18eb75f3af40be1f0fc2025d4ff821711222a2fd Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Tue, 18 Mar 2025 17:14:40 +0100 Subject: landlock: Always allow signals between threads of the same process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because Linux credentials are managed per thread, user space relies on some hack to synchronize credential update across threads from the same process. This is required by the Native POSIX Threads Library and implemented by set*id(2) wrappers and libcap(3) to use tgkill(2) to synchronize threads. See nptl(7) and libpsx(3). Furthermore, some runtimes like Go do not enable developers to have control over threads [1]. To avoid potential issues, and because threads are not security boundaries, let's relax the Landlock (optional) signal scoping to always allow signals sent between threads of the same process. This exception is similar to the __ptrace_may_access() one. hook_file_set_fowner() now checks if the target task is part of the same process as the caller. If this is the case, then the related signal triggered by the socket will always be allowed. Scoping of abstract UNIX sockets is not changed because kernel objects (e.g. sockets) should be tied to their creator's domain at creation time. Note that creating one Landlock domain per thread puts each of these threads (and their future children) in their own scope, which is probably not what users expect, especially in Go where we do not control threads. However, being able to drop permissions on all threads should not be restricted by signal scoping. We are working on a way to make it possible to atomically restrict all threads of a process with the same domain [2]. Add erratum for signal scoping. Closes: https://github.com/landlock-lsm/go-landlock/issues/36 Fixes: 54a6e6bbf3be ("landlock: Add signal scoping") Fixes: c8994965013e ("selftests/landlock: Test signal scoping for threads") Depends-on: 26f204380a3c ("fs: Fix file_set_fowner LSM hook inconsistencies") Link: https://pkg.go.dev/kernel.org/pub/linux/libs/security/libcap/psx [1] Link: https://github.com/landlock-lsm/linux/issues/2 [2] Cc: Günther Noack Cc: Paul Moore Cc: Serge Hallyn Cc: Tahera Fahimi Cc: stable@vger.kernel.org Acked-by: Christian Brauner Link: https://lore.kernel.org/r/20250318161443.279194-6-mic@digikod.net [mic: Add extra pointer check and RCU guard, and ease backport] Signed-off-by: Mickaël Salaün --- security/landlock/errata/abi-6.h | 19 +++++++++++++++++++ security/landlock/fs.c | 39 +++++++++++++++++++++++++++++++++------ security/landlock/task.c | 12 ++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 security/landlock/errata/abi-6.h (limited to 'security') diff --git a/security/landlock/errata/abi-6.h b/security/landlock/errata/abi-6.h new file mode 100644 index 000000000000..df7bc0e1fdf4 --- /dev/null +++ b/security/landlock/errata/abi-6.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +/** + * DOC: erratum_2 + * + * Erratum 2: Scoped signal handling + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This fix addresses an issue where signal scoping was overly restrictive, + * preventing sandboxed threads from signaling other threads within the same + * process if they belonged to different domains. Because threads are not + * security boundaries, user space might assume that any thread within the same + * process can send signals between themselves (see :manpage:`nptl(7)` and + * :manpage:`libpsx(3)`). Consistent with :manpage:`ptrace(2)` behavior, direct + * interaction between threads of the same process should always be allowed. + * This change ensures that any thread is allowed to send signals to any other + * thread within the same process, regardless of their domain. + */ +LANDLOCK_ERRATUM(2) diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 71b9dc331aae..c19aab87c4d2 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -27,7 +27,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -1628,21 +1630,46 @@ static int hook_file_ioctl_compat(struct file *file, unsigned int cmd, return -EACCES; } -static void hook_file_set_fowner(struct file *file) +/* + * Always allow sending signals between threads of the same process. This + * ensures consistency with hook_task_kill(). + */ +static bool control_current_fowner(struct fown_struct *const fown) { - struct landlock_ruleset *new_dom, *prev_dom; + struct task_struct *p; /* * Lock already held by __f_setown(), see commit 26f204380a3c ("fs: Fix * file_set_fowner LSM hook inconsistencies"). */ - lockdep_assert_held(&file_f_owner(file)->lock); - new_dom = landlock_get_current_domain(); - landlock_get_ruleset(new_dom); + lockdep_assert_held(&fown->lock); + + /* + * Some callers (e.g. fcntl_dirnotify) may not be in an RCU read-side + * critical section. + */ + guard(rcu)(); + p = pid_task(fown->pid, fown->pid_type); + if (!p) + return true; + + return !same_thread_group(p, current); +} + +static void hook_file_set_fowner(struct file *file) +{ + struct landlock_ruleset *prev_dom; + struct landlock_ruleset *new_dom = NULL; + + if (control_current_fowner(file_f_owner(file))) { + new_dom = landlock_get_current_domain(); + landlock_get_ruleset(new_dom); + } + prev_dom = landlock_file(file)->fown_domain; landlock_file(file)->fown_domain = new_dom; - /* Called in an RCU read-side critical section. */ + /* May be called in an RCU read-side critical section. */ landlock_put_ruleset_deferred(prev_dom); } diff --git a/security/landlock/task.c b/security/landlock/task.c index dc7dab78392e..4578ce6e319d 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -264,6 +265,17 @@ static int hook_task_kill(struct task_struct *const p, /* Dealing with USB IO. */ dom = landlock_cred(cred)->domain; } else { + /* + * Always allow sending signals between threads of the same process. + * This is required for process credential changes by the Native POSIX + * Threads Library and implemented by the set*id(2) wrappers and + * libcap(3) with tgkill(2). See nptl(7) and libpsx(3). + * + * This exception is similar to the __ptrace_may_access() one. + */ + if (same_thread_group(p, current)) + return 0; + dom = landlock_get_current_domain(); } dom = landlock_get_applicable_domain(dom, signal_scope); -- cgit From 9b08a16637eeef4d6d3a8a3b69714e8930676248 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:50 +0100 Subject: lsm: Add audit_log_lsm_data() helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract code from dump_common_audit_data() into the audit_log_lsm_data() helper. This helps reuse common LSM audit data while not abusing AUDIT_AVC records because of the common_lsm_audit() helper. Depends-on: 7ccbe076d987 ("lsm: Only build lsm_audit.c if CONFIG_SECURITY and CONFIG_AUDIT are set") Cc: Casey Schaufler Cc: James Morris Cc: Serge E. Hallyn Acked-by: Paul Moore Link: https://lore.kernel.org/r/20250320190717.2287696-2-mic@digikod.net Reviewed-by: Günther Noack Signed-off-by: Mickaël Salaün --- security/lsm_audit.c | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) (limited to 'security') diff --git a/security/lsm_audit.c b/security/lsm_audit.c index 52db886dbba8..a61c7ebdb6a7 100644 --- a/security/lsm_audit.c +++ b/security/lsm_audit.c @@ -189,16 +189,13 @@ static inline void print_ipv4_addr(struct audit_buffer *ab, __be32 addr, } /** - * dump_common_audit_data - helper to dump common audit data + * audit_log_lsm_data - helper to log common LSM audit data * @ab : the audit buffer * @a : common audit data - * */ -static void dump_common_audit_data(struct audit_buffer *ab, - struct common_audit_data *a) +void audit_log_lsm_data(struct audit_buffer *ab, + const struct common_audit_data *a) { - char comm[sizeof(current->comm)]; - /* * To keep stack sizes in check force programmers to notice if they * start making this union too large! See struct lsm_network_audit @@ -206,9 +203,6 @@ static void dump_common_audit_data(struct audit_buffer *ab, */ BUILD_BUG_ON(sizeof(a->u) > sizeof(void *)*2); - audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current)); - audit_log_untrustedstring(ab, get_task_comm(comm, current)); - switch (a->type) { case LSM_AUDIT_DATA_NONE: return; @@ -431,6 +425,21 @@ static void dump_common_audit_data(struct audit_buffer *ab, } /* switch (a->type) */ } +/** + * dump_common_audit_data - helper to dump common audit data + * @ab : the audit buffer + * @a : common audit data + */ +static void dump_common_audit_data(struct audit_buffer *ab, + const struct common_audit_data *a) +{ + char comm[sizeof(current->comm)]; + + audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current)); + audit_log_untrustedstring(ab, get_task_comm(comm, current)); + audit_log_lsm_data(ab, a); +} + /** * common_lsm_audit - generic LSM auditing function * @a: auxiliary audit data -- cgit From d9d2a68ed44bbae598a81cb95e0746fa6b13b57f Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:51 +0100 Subject: landlock: Add unique ID generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Landlock IDs can be generated to uniquely identify Landlock objects. For now, only Landlock domains get an ID at creation time. These IDs map to immutable domain hierarchies. Landlock IDs have important properties: - They are unique during the lifetime of the running system thanks to the 64-bit values: at worse, 2^60 - 2*2^32 useful IDs. - They are always greater than 2^32 and must then be stored in 64-bit integer types. - The initial ID (at boot time) is randomly picked between 2^32 and 2^33, which limits collisions in logs across different boots. - IDs are sequential, which enables users to order them. - IDs may not be consecutive but increase with a random 2^4 step, which limits side channels. Such IDs can be exposed to unprivileged processes, even if it is not the case with this audit patch series. The domain IDs will be useful for user space to identify sandboxes and get their properties. These Landlock IDs are more secure that other absolute kernel IDs such as pipe's inodes which rely on a shared global counter. For checkpoint/restore features (i.e. CRIU), we could easily implement a privileged interface (e.g. sysfs) to set the next ID counter. IDR/IDA are not used because we only need a bijection from Landlock objects to Landlock IDs, and we must not recycle IDs. This enables us to identify all Landlock objects during the lifetime of the system (e.g. in logs), but not to access an object from an ID nor know if an ID is assigned. Using a counter is simpler, it scales (i.e. avoids growing memory footprint), and it does not require locking. We'll use proper file descriptors (with IDs used as inode numbers) to access Landlock objects. Cc: Günther Noack Cc: Paul Moore Link: https://lore.kernel.org/r/20250320190717.2287696-3-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/.kunitconfig | 2 + security/landlock/Makefile | 2 + security/landlock/id.c | 251 +++++++++++++++++++++++++++++++++++++++++ security/landlock/id.h | 25 ++++ security/landlock/setup.c | 2 + 5 files changed, 282 insertions(+) create mode 100644 security/landlock/id.c create mode 100644 security/landlock/id.h (limited to 'security') diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig index 03e119466604..f9423f01ac5b 100644 --- a/security/landlock/.kunitconfig +++ b/security/landlock/.kunitconfig @@ -1,4 +1,6 @@ +CONFIG_AUDIT=y CONFIG_KUNIT=y +CONFIG_NET=y CONFIG_SECURITY=y CONFIG_SECURITY_LANDLOCK=y CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y diff --git a/security/landlock/Makefile b/security/landlock/Makefile index b4538b7cf7d2..e1777abbc413 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -4,3 +4,5 @@ landlock-y := setup.o syscalls.o object.o ruleset.o \ cred.o task.o fs.o landlock-$(CONFIG_INET) += net.o + +landlock-$(CONFIG_AUDIT) += id.o diff --git a/security/landlock/id.c b/security/landlock/id.c new file mode 100644 index 000000000000..11fab9259c15 --- /dev/null +++ b/security/landlock/id.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Unique identification number generator + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#include +#include +#include +#include + +#include "common.h" +#include "id.h" + +#define COUNTER_PRE_INIT 0 + +static atomic64_t next_id = ATOMIC64_INIT(COUNTER_PRE_INIT); + +static void __init init_id(atomic64_t *const counter, const u32 random_32bits) +{ + u64 init; + + /* + * Ensures sure 64-bit values are always used by user space (or may + * fail with -EOVERFLOW), and makes this testable. + */ + init = 1ULL << 32; + + /* + * Makes a large (2^32) boot-time value to limit ID collision in logs + * from different boots, and to limit info leak about the number of + * initially (relative to the reader) created elements (e.g. domains). + */ + init += random_32bits; + + /* Sets first or ignores. This will be the first ID. */ + atomic64_cmpxchg(counter, COUNTER_PRE_INIT, init); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void __init test_init_min(struct kunit *const test) +{ + atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT); + + init_id(&counter, 0); + KUNIT_EXPECT_EQ(test, atomic64_read(&counter), 1ULL + U32_MAX); +} + +static void __init test_init_max(struct kunit *const test) +{ + atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT); + + init_id(&counter, ~0); + KUNIT_EXPECT_EQ(test, atomic64_read(&counter), 1 + (2ULL * U32_MAX)); +} + +static void __init test_init_once(struct kunit *const test) +{ + const u64 first_init = 1ULL + U32_MAX; + atomic64_t counter = ATOMIC64_INIT(COUNTER_PRE_INIT); + + init_id(&counter, 0); + KUNIT_EXPECT_EQ(test, atomic64_read(&counter), first_init); + + init_id(&counter, ~0); + KUNIT_EXPECT_EQ_MSG( + test, atomic64_read(&counter), first_init, + "Should still have the same value after the subsequent init_id()"); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +void __init landlock_init_id(void) +{ + return init_id(&next_id, get_random_u32()); +} + +/* + * It's not worth it to try to hide the monotonic counter because it can still + * be inferred (with N counter ranges), and if we are allowed to read the inode + * number we should also be allowed to read the time creation anyway, and it + * can be handy to store and sort domain IDs for user space. + * + * Returns the value of next_id and increment it to let some space for the next + * one. + */ +static u64 get_id_range(size_t number_of_ids, atomic64_t *const counter, + u8 random_4bits) +{ + u64 id, step; + + /* + * We should return at least 1 ID, and we may need a set of consecutive + * ones (e.g. to generate a set of inodes). + */ + if (WARN_ON_ONCE(number_of_ids <= 0)) + number_of_ids = 1; + + /* + * Blurs the next ID guess with 1/16 ratio. We get 2^(64 - 4) - + * (2 * 2^32), so a bit less than 2^60 available IDs, which should be + * much more than enough considering the number of CPU cycles required + * to get a new ID (e.g. a full landlock_restrict_self() call), and the + * cost of draining all available IDs during the system's uptime. + */ + random_4bits = random_4bits % (1 << 4); + step = number_of_ids + random_4bits; + + /* It is safe to cast a signed atomic to an unsigned value. */ + id = atomic64_fetch_add(step, counter); + + /* Warns if landlock_init_id() was not called. */ + WARN_ON_ONCE(id == COUNTER_PRE_INIT); + return id; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_range1_rand0(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 0), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 1); +} + +static void test_range1_rand1(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 1), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 2); +} + +static void test_range1_rand16(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(1, &counter, 16), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 1); +} + +static void test_range2_rand0(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 0), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 2); +} + +static void test_range2_rand1(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 1), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 3); +} + +static void test_range2_rand2(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 2), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 4); +} + +static void test_range2_rand16(struct kunit *const test) +{ + atomic64_t counter; + u64 init; + + init = get_random_u32(); + atomic64_set(&counter, init); + KUNIT_EXPECT_EQ(test, get_id_range(2, &counter, 16), init); + KUNIT_EXPECT_EQ( + test, get_id_range(get_random_u8(), &counter, get_random_u8()), + init + 2); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +/** + * landlock_get_id_range - Get a range of unique IDs + * + * @number_of_ids: Number of IDs to hold. Must be greater than one. + * + * Returns: The first ID in the range. + */ +u64 landlock_get_id_range(size_t number_of_ids) +{ + return get_id_range(number_of_ids, &next_id, get_random_u8()); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static struct kunit_case __refdata test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_init_min), + KUNIT_CASE(test_init_max), + KUNIT_CASE(test_init_once), + KUNIT_CASE(test_range1_rand0), + KUNIT_CASE(test_range1_rand1), + KUNIT_CASE(test_range1_rand16), + KUNIT_CASE(test_range2_rand0), + KUNIT_CASE(test_range2_rand1), + KUNIT_CASE(test_range2_rand2), + KUNIT_CASE(test_range2_rand16), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_id", + .test_cases = test_cases, +}; + +kunit_test_init_section_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/id.h b/security/landlock/id.h new file mode 100644 index 000000000000..45dcfb9e9a8b --- /dev/null +++ b/security/landlock/id.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Unique identification number generator + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_ID_H +#define _SECURITY_LANDLOCK_ID_H + +#ifdef CONFIG_AUDIT + +void __init landlock_init_id(void); + +u64 landlock_get_id_range(size_t number_of_ids); + +#else /* CONFIG_AUDIT */ + +static inline void __init landlock_init_id(void) +{ +} + +#endif /* CONFIG_AUDIT */ + +#endif /* _SECURITY_LANDLOCK_ID_H */ diff --git a/security/landlock/setup.c b/security/landlock/setup.c index 0c85ea27e409..bd53c7a56ab9 100644 --- a/security/landlock/setup.c +++ b/security/landlock/setup.c @@ -15,6 +15,7 @@ #include "cred.h" #include "errata.h" #include "fs.h" +#include "id.h" #include "net.h" #include "setup.h" #include "task.h" @@ -67,6 +68,7 @@ static int __init landlock_init(void) landlock_add_task_hooks(); landlock_add_fs_hooks(); landlock_add_net_hooks(); + landlock_init_id(); landlock_initialized = true; pr_info("Up and running.\n"); return 0; -- cgit From 5b95b329befaf18020a0d3cd0223a90bd230eeb9 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:52 +0100 Subject: landlock: Move domain hierarchy management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create a new domain.h file containing the struct landlock_hierarchy definition and helpers. This type will grow with audit support. This also prepares for a new domain type. Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-4-mic@digikod.net Reviewed-by: Günther Noack Signed-off-by: Mickaël Salaün --- security/landlock/domain.h | 48 +++++++++++++++++++++++++++++++++++++++++++++ security/landlock/ruleset.c | 21 +++----------------- security/landlock/ruleset.h | 17 +--------------- security/landlock/task.c | 1 + 4 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 security/landlock/domain.h (limited to 'security') diff --git a/security/landlock/domain.h b/security/landlock/domain.h new file mode 100644 index 000000000000..d22712e5fb0f --- /dev/null +++ b/security/landlock/domain.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Domain management + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_DOMAIN_H +#define _SECURITY_LANDLOCK_DOMAIN_H + +#include +#include + +/** + * struct landlock_hierarchy - Node in a domain hierarchy + */ +struct landlock_hierarchy { + /** + * @parent: Pointer to the parent node, or NULL if it is a root + * Landlock domain. + */ + struct landlock_hierarchy *parent; + /** + * @usage: Number of potential children domains plus their parent + * domain. + */ + refcount_t usage; +}; + +static inline void +landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy) +{ + if (hierarchy) + refcount_inc(&hierarchy->usage); +} + +static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy) +{ + while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) { + const struct landlock_hierarchy *const freeme = hierarchy; + + hierarchy = hierarchy->parent; + kfree(freeme); + } +} + +#endif /* _SECURITY_LANDLOCK_DOMAIN_H */ diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index bff4e40a3093..adb7f87828df 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -23,6 +23,7 @@ #include #include "access.h" +#include "domain.h" #include "limits.h" #include "object.h" #include "ruleset.h" @@ -307,22 +308,6 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset, return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers)); } -static void get_hierarchy(struct landlock_hierarchy *const hierarchy) -{ - if (hierarchy) - refcount_inc(&hierarchy->usage); -} - -static void put_hierarchy(struct landlock_hierarchy *hierarchy) -{ - while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) { - const struct landlock_hierarchy *const freeme = hierarchy; - - hierarchy = hierarchy->parent; - kfree(freeme); - } -} - static int merge_tree(struct landlock_ruleset *const dst, struct landlock_ruleset *const src, const enum landlock_key_type key_type) @@ -477,7 +462,7 @@ static int inherit_ruleset(struct landlock_ruleset *const parent, err = -EINVAL; goto out_unlock; } - get_hierarchy(parent->hierarchy); + landlock_get_hierarchy(parent->hierarchy); child->hierarchy->parent = parent->hierarchy; out_unlock: @@ -501,7 +486,7 @@ static void free_ruleset(struct landlock_ruleset *const ruleset) free_rule(freeme, LANDLOCK_KEY_NET_PORT); #endif /* IS_ENABLED(CONFIG_INET) */ - put_hierarchy(ruleset->hierarchy); + landlock_put_hierarchy(ruleset->hierarchy); kfree(ruleset); } diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index 52f4f0af6ab0..bbb5996545d2 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -17,6 +17,7 @@ #include #include "access.h" +#include "domain.h" #include "limits.h" #include "object.h" @@ -108,22 +109,6 @@ struct landlock_rule { struct landlock_layer layers[] __counted_by(num_layers); }; -/** - * struct landlock_hierarchy - Node in a ruleset hierarchy - */ -struct landlock_hierarchy { - /** - * @parent: Pointer to the parent node, or NULL if it is a root - * Landlock domain. - */ - struct landlock_hierarchy *parent; - /** - * @usage: Number of potential children domains plus their parent - * domain. - */ - refcount_t usage; -}; - /** * struct landlock_ruleset - Landlock ruleset * diff --git a/security/landlock/task.c b/security/landlock/task.c index 4578ce6e319d..e04646d80e78 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -19,6 +19,7 @@ #include "common.h" #include "cred.h" +#include "domain.h" #include "fs.h" #include "ruleset.h" #include "setup.h" -- cgit From ae2483a26017d24f505caa87935fb8f17117bbfa Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:53 +0100 Subject: landlock: Prepare to use credential instead of domain for filesystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cosmetic change is needed for audit support, specifically to be able to filter according to cross-execution boundaries. Add landlock_get_applicable_subject(), mainly a copy of landlock_get_applicable_domain(), which will fully replace it in a following commit. Optimize current_check_access_path() to only handle the access request. Partially replace get_current_fs_domain() with explicit calls to landlock_get_applicable_subject(). The remaining ones will follow with more changes. Remove explicit domain->num_layers check which is now part of the landlock_get_applicable_subject() call. Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-5-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/cred.h | 53 ++++++++++++++++++++++++++++++++++++- security/landlock/fs.c | 69 ++++++++++++++++++++++++++++-------------------- 2 files changed, 92 insertions(+), 30 deletions(-) (limited to 'security') diff --git a/security/landlock/cred.h b/security/landlock/cred.h index bf755459838a..eb691130dd67 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -1,9 +1,10 @@ /* SPDX-License-Identifier: GPL-2.0-only */ /* - * Landlock LSM - Credential hooks + * Landlock - Credential hooks * * Copyright © 2019-2020 Mickaël Salaün * Copyright © 2019-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation */ #ifndef _SECURITY_LANDLOCK_CRED_H @@ -13,6 +14,7 @@ #include #include +#include "access.h" #include "ruleset.h" #include "setup.h" @@ -53,6 +55,55 @@ static inline bool landlocked(const struct task_struct *const task) return has_dom; } +/** + * landlock_get_applicable_subject - Return the subject's Landlock credential + * if its enforced domain applies to (i.e. + * handles) at least one of the access rights + * specified in @masks + * + * @cred: credential + * @masks: access masks + * @handle_layer: returned youngest layer handling a subset of @masks. Not set + * if the function returns NULL. + * + * Returns: landlock_cred(@cred) if any access rights specified in @masks is + * handled, or NULL otherwise. + */ +static inline const struct landlock_cred_security * +landlock_get_applicable_subject(const struct cred *const cred, + const struct access_masks masks, + size_t *const handle_layer) +{ + const union access_masks_all masks_all = { + .masks = masks, + }; + const struct landlock_ruleset *domain; + ssize_t layer_level; + + if (!cred) + return NULL; + + domain = landlock_cred(cred)->domain; + if (!domain) + return NULL; + + for (layer_level = domain->num_layers - 1; layer_level >= 0; + layer_level--) { + union access_masks_all layer = { + .masks = domain->access_masks[layer_level], + }; + + if (layer.all & masks_all.all) { + if (handle_layer) + *handle_layer = layer_level; + + return landlock_cred(cred); + } + } + + return NULL; +} + __init void landlock_add_cred_hooks(void); #endif /* _SECURITY_LANDLOCK_CRED_H */ diff --git a/security/landlock/fs.c b/security/landlock/fs.c index c19aab87c4d2..f59db97333f3 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Landlock LSM - Filesystem management and hooks + * Landlock - Filesystem management and hooks * * Copyright © 2016-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI - * Copyright © 2021-2022 Microsoft Corporation + * Copyright © 2021-2025 Microsoft Corporation * Copyright © 2022 Günther Noack * Copyright © 2023-2024 Google LLC */ @@ -773,11 +773,14 @@ static bool is_access_to_paths_allowed( if (!access_request_parent1 && !access_request_parent2) return true; - if (WARN_ON_ONCE(!domain || !path)) + + if (WARN_ON_ONCE(!path)) return true; + if (is_nouser_or_private(path->dentry)) return true; - if (WARN_ON_ONCE(domain->num_layers < 1 || !layer_masks_parent1)) + + if (WARN_ON_ONCE(!layer_masks_parent1)) return false; allowed_parent1 = is_layer_masks_allowed(layer_masks_parent1); @@ -928,16 +931,21 @@ jump_up: static int current_check_access_path(const struct path *const path, access_mask_t access_request) { - const struct landlock_ruleset *const dom = get_current_fs_domain(); + const struct access_masks masks = { + .fs = access_request, + }; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), masks, NULL); layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; - if (!dom) + if (!subject) return 0; - access_request = landlock_init_layer_masks( - dom, access_request, &layer_masks, LANDLOCK_KEY_INODE); - if (is_access_to_paths_allowed(dom, path, access_request, &layer_masks, - NULL, 0, NULL, NULL)) + access_request = landlock_init_layer_masks(subject->domain, + access_request, &layer_masks, + LANDLOCK_KEY_INODE); + if (is_access_to_paths_allowed(subject->domain, path, access_request, + &layer_masks, NULL, 0, NULL, NULL)) return 0; return -EACCES; @@ -1100,7 +1108,8 @@ static int current_check_refer_path(struct dentry *const old_dentry, struct dentry *const new_dentry, const bool removable, const bool exchange) { - const struct landlock_ruleset *const dom = get_current_fs_domain(); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, NULL); bool allow_parent1, allow_parent2; access_mask_t access_request_parent1, access_request_parent2; struct path mnt_dir; @@ -1108,10 +1117,9 @@ static int current_check_refer_path(struct dentry *const old_dentry, layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS] = {}, layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS] = {}; - if (!dom) + if (!subject) return 0; - if (WARN_ON_ONCE(dom->num_layers < 1)) - return -EACCES; + if (unlikely(d_is_negative(old_dentry))) return -ENOENT; if (exchange) { @@ -1136,10 +1144,11 @@ static int current_check_refer_path(struct dentry *const old_dentry, * for same-directory referer (i.e. no reparenting). */ access_request_parent1 = landlock_init_layer_masks( - dom, access_request_parent1 | access_request_parent2, + subject->domain, + access_request_parent1 | access_request_parent2, &layer_masks_parent1, LANDLOCK_KEY_INODE); if (is_access_to_paths_allowed( - dom, new_dir, access_request_parent1, + subject->domain, new_dir, access_request_parent1, &layer_masks_parent1, NULL, 0, NULL, NULL)) return 0; return -EACCES; @@ -1162,10 +1171,12 @@ static int current_check_refer_path(struct dentry *const old_dentry, old_dentry->d_parent; /* new_dir->dentry is equal to new_dentry->d_parent */ - allow_parent1 = collect_domain_accesses(dom, mnt_dir.dentry, old_parent, + allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry, + old_parent, &layer_masks_parent1); - allow_parent2 = collect_domain_accesses( - dom, mnt_dir.dentry, new_dir->dentry, &layer_masks_parent2); + allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry, + new_dir->dentry, + &layer_masks_parent2); if (allow_parent1 && allow_parent2) return 0; @@ -1177,9 +1188,9 @@ static int current_check_refer_path(struct dentry *const old_dentry, * destination parent access rights. */ if (is_access_to_paths_allowed( - dom, &mnt_dir, access_request_parent1, &layer_masks_parent1, - old_dentry, access_request_parent2, &layer_masks_parent2, - exchange ? new_dentry : NULL)) + subject->domain, &mnt_dir, access_request_parent1, + &layer_masks_parent1, old_dentry, access_request_parent2, + &layer_masks_parent2, exchange ? new_dentry : NULL)) return 0; /* @@ -1506,11 +1517,10 @@ static int hook_file_open(struct file *const file) layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; access_mask_t open_access_request, full_access_request, allowed_access, optional_access; - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain( - landlock_cred(file->f_cred)->domain, any_fs); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(file->f_cred, any_fs, NULL); - if (!dom) + if (!subject) return 0; /* @@ -1531,9 +1541,10 @@ static int hook_file_open(struct file *const file) full_access_request = open_access_request | optional_access; if (is_access_to_paths_allowed( - dom, &file->f_path, - landlock_init_layer_masks(dom, full_access_request, - &layer_masks, LANDLOCK_KEY_INODE), + subject->domain, &file->f_path, + landlock_init_layer_masks(subject->domain, + full_access_request, &layer_masks, + LANDLOCK_KEY_INODE), &layer_masks, NULL, 0, NULL, NULL)) { allowed_access = full_access_request; } else { -- cgit From 93f33f0cb2f0a4bc029912ac3e33ccf2034dc5c2 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:54 +0100 Subject: landlock: Prepare to use credential instead of domain for network MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cosmetic change that is needed for audit support, specifically to be able to filter according to cross-execution boundaries. Optimize current_check_access_socket() to only handle the access request. Remove explicit domain->num_layers check which is now part of the landlock_get_applicable_subject() call. Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-6-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/net.c | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) (limited to 'security') diff --git a/security/landlock/net.c b/security/landlock/net.c index 104b6c01fe50..c6ba42f3529c 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Landlock LSM - Network management and hooks + * Landlock - Network management and hooks * * Copyright © 2022-2023 Huawei Tech. Co., Ltd. - * Copyright © 2022-2023 Microsoft Corporation + * Copyright © 2022-2025 Microsoft Corporation */ #include @@ -39,10 +39,6 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset, return err; } -static const struct access_masks any_net = { - .net = ~0, -}; - static int current_check_access_socket(struct socket *const sock, struct sockaddr *const address, const int addrlen, @@ -54,14 +50,14 @@ static int current_check_access_socket(struct socket *const sock, struct landlock_id id = { .type = LANDLOCK_KEY_NET_PORT, }; - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain(landlock_get_current_domain(), - any_net); + const struct access_masks masks = { + .net = access_request, + }; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), masks, NULL); - if (!dom) + if (!subject) return 0; - if (WARN_ON_ONCE(dom->num_layers < 1)) - return -EACCES; if (!sk_is_tcp(sock->sk)) return 0; @@ -145,9 +141,10 @@ static int current_check_access_socket(struct socket *const sock, id.key.data = (__force uintptr_t)port; BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data)); - rule = landlock_find_rule(dom, id); - access_request = landlock_init_layer_masks( - dom, access_request, &layer_masks, LANDLOCK_KEY_NET_PORT); + rule = landlock_find_rule(subject->domain, id); + access_request = landlock_init_layer_masks(subject->domain, + access_request, &layer_masks, + LANDLOCK_KEY_NET_PORT); if (landlock_unmask_layers(rule, access_request, &layer_masks, ARRAY_SIZE(layer_masks))) return 0; -- cgit From 8d20efa9dc6cb471b71fd388923753f767ab13ce Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:55 +0100 Subject: landlock: Prepare to use credential instead of domain for scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cosmetic change that is needed for audit support, specifically to be able to filter according to cross-execution boundaries. Replace hardcoded LANDLOCK_SCOPE_SIGNAL with the signal_scope.scope variable. Use scoped guards for RCU read-side critical sections. Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-7-mic@digikod.net [mic: Update headers] Signed-off-by: Mickaël Salaün --- security/landlock/task.c | 52 ++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 24 deletions(-) (limited to 'security') diff --git a/security/landlock/task.c b/security/landlock/task.c index e04646d80e78..e3ad7403b532 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -1,12 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Landlock LSM - Ptrace hooks + * Landlock - Ptrace and scope hooks * * Copyright © 2017-2020 Mickaël Salaün * Copyright © 2019-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation */ #include +#include #include #include #include @@ -214,15 +216,15 @@ static int hook_unix_stream_connect(struct sock *const sock, struct sock *const other, struct sock *const newsk) { - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain(landlock_get_current_domain(), - unix_scope); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), unix_scope, + NULL); /* Quick return for non-landlocked tasks. */ - if (!dom) + if (!subject) return 0; - if (is_abstract_socket(other) && sock_is_scoped(other, dom)) + if (is_abstract_socket(other) && sock_is_scoped(other, subject->domain)) return -EPERM; return 0; @@ -231,11 +233,11 @@ static int hook_unix_stream_connect(struct sock *const sock, static int hook_unix_may_send(struct socket *const sock, struct socket *const other) { - const struct landlock_ruleset *const dom = - landlock_get_applicable_domain(landlock_get_current_domain(), - unix_scope); + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), unix_scope, + NULL); - if (!dom) + if (!subject) return 0; /* @@ -245,7 +247,8 @@ static int hook_unix_may_send(struct socket *const sock, if (unix_peer(sock->sk) == other->sk) return 0; - if (is_abstract_socket(other->sk) && sock_is_scoped(other->sk, dom)) + if (is_abstract_socket(other->sk) && + sock_is_scoped(other->sk, subject->domain)) return -EPERM; return 0; @@ -257,15 +260,12 @@ static const struct access_masks signal_scope = { static int hook_task_kill(struct task_struct *const p, struct kernel_siginfo *const info, const int sig, - const struct cred *const cred) + const struct cred *cred) { bool is_scoped; - const struct landlock_ruleset *dom; + const struct landlock_cred_security *subject; - if (cred) { - /* Dealing with USB IO. */ - dom = landlock_cred(cred)->domain; - } else { + if (!cred) { /* * Always allow sending signals between threads of the same process. * This is required for process credential changes by the Native POSIX @@ -277,18 +277,22 @@ static int hook_task_kill(struct task_struct *const p, if (same_thread_group(p, current)) return 0; - dom = landlock_get_current_domain(); + /* Not dealing with USB IO. */ + cred = current_cred(); } - dom = landlock_get_applicable_domain(dom, signal_scope); + + subject = landlock_get_applicable_subject(cred, signal_scope, NULL); /* Quick return for non-landlocked tasks. */ - if (!dom) + if (!subject) return 0; - rcu_read_lock(); - is_scoped = domain_is_scoped(dom, landlock_get_task_domain(p), - LANDLOCK_SCOPE_SIGNAL); - rcu_read_unlock(); + scoped_guard(rcu) + { + is_scoped = domain_is_scoped(subject->domain, + landlock_get_task_domain(p), + signal_scope.scope); + } if (is_scoped) return -EPERM; -- cgit From 79625f1b3a3df63d3289a0781fdf121bc42966f7 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:56 +0100 Subject: landlock: Prepare to use credential instead of domain for fowner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This cosmetic change is needed for audit support, specifically to be able to filter according to cross-execution boundaries. struct landlock_file_security's size stay the same for now but it will increase with struct landlock_cred_security's size. Only save Landlock domain in hook_file_set_fowner() if the current domain has LANDLOCK_SCOPE_SIGNAL, which was previously done for each hook_file_send_sigiotask() calls. This should improve a bit performance. Replace hardcoded LANDLOCK_SCOPE_SIGNAL with the signal_scope.scope variable. Use scoped guards for RCU read-side critical sections. Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-8-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/fs.c | 20 ++++++++++++++------ security/landlock/fs.h | 15 +++++++++------ security/landlock/task.c | 25 ++++++++++++++++--------- 3 files changed, 39 insertions(+), 21 deletions(-) (limited to 'security') diff --git a/security/landlock/fs.c b/security/landlock/fs.c index f59db97333f3..50e02bdab089 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1670,15 +1670,23 @@ static bool control_current_fowner(struct fown_struct *const fown) static void hook_file_set_fowner(struct file *file) { struct landlock_ruleset *prev_dom; - struct landlock_ruleset *new_dom = NULL; + struct landlock_cred_security fown_subject = {}; if (control_current_fowner(file_f_owner(file))) { - new_dom = landlock_get_current_domain(); - landlock_get_ruleset(new_dom); + static const struct access_masks signal_scope = { + .scope = LANDLOCK_SCOPE_SIGNAL, + }; + const struct landlock_cred_security *new_subject = + landlock_get_applicable_subject(current_cred(), + signal_scope, NULL); + if (new_subject) { + landlock_get_ruleset(new_subject->domain); + fown_subject = *new_subject; + } } - prev_dom = landlock_file(file)->fown_domain; - landlock_file(file)->fown_domain = new_dom; + prev_dom = landlock_file(file)->fown_subject.domain; + landlock_file(file)->fown_subject = fown_subject; /* May be called in an RCU read-side critical section. */ landlock_put_ruleset_deferred(prev_dom); @@ -1686,7 +1694,7 @@ static void hook_file_set_fowner(struct file *file) static void hook_file_free_security(struct file *file) { - landlock_put_ruleset_deferred(landlock_file(file)->fown_domain); + landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain); } static struct security_hook_list landlock_hooks[] __ro_after_init = { diff --git a/security/landlock/fs.h b/security/landlock/fs.h index d445f411c26a..b29972eb9224 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -1,9 +1,10 @@ /* SPDX-License-Identifier: GPL-2.0-only */ /* - * Landlock LSM - Filesystem management and hooks + * Landlock - Filesystem management and hooks * * Copyright © 2017-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation */ #ifndef _SECURITY_LANDLOCK_FS_H @@ -14,6 +15,7 @@ #include #include "access.h" +#include "cred.h" #include "ruleset.h" #include "setup.h" @@ -54,12 +56,13 @@ struct landlock_file_security { */ access_mask_t allowed_access; /** - * @fown_domain: Domain of the task that set the PID that may receive a - * signal e.g., SIGURG when writing MSG_OOB to the related socket. - * This pointer is protected by the related file->f_owner->lock, as for - * fown_struct's members: pid, uid, and euid. + * @fown_subject: Landlock credential of the task that set the PID that + * may receive a signal e.g., SIGURG when writing MSG_OOB to the + * related socket. This pointer is protected by the related + * file->f_owner->lock, as for fown_struct's members: pid, uid, and + * euid. */ - struct landlock_ruleset *fown_domain; + struct landlock_cred_security fown_subject; }; /** diff --git a/security/landlock/task.c b/security/landlock/task.c index e3ad7403b532..b89ab9a904eb 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -302,22 +302,29 @@ static int hook_task_kill(struct task_struct *const p, static int hook_file_send_sigiotask(struct task_struct *tsk, struct fown_struct *fown, int signum) { - const struct landlock_ruleset *dom; + const struct landlock_cred_security *subject; bool is_scoped = false; /* Lock already held by send_sigio() and send_sigurg(). */ lockdep_assert_held(&fown->lock); - dom = landlock_get_applicable_domain( - landlock_file(fown->file)->fown_domain, signal_scope); + subject = &landlock_file(fown->file)->fown_subject; - /* Quick return for unowned socket. */ - if (!dom) + /* + * Quick return for unowned socket. + * + * subject->domain has already been filtered when saved by + * hook_file_set_fowner(), so there is no need to call + * landlock_get_applicable_subject() here. + */ + if (!subject->domain) return 0; - rcu_read_lock(); - is_scoped = domain_is_scoped(dom, landlock_get_task_domain(tsk), - LANDLOCK_SCOPE_SIGNAL); - rcu_read_unlock(); + scoped_guard(rcu) + { + is_scoped = domain_is_scoped(subject->domain, + landlock_get_task_domain(tsk), + signal_scope.scope); + } if (is_scoped) return -EPERM; -- cgit From 14f6c14e9fbde4a802c10c7e770b5ba63853ebc6 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:57 +0100 Subject: landlock: Identify domain execution crossing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend struct landlock_cred_security with a domain_exec bitmask to identify which Landlock domain were created by the current task's bprm. The whole bitmask is reset on each execve(2) call. Cc: Günther Noack Cc: Paul Moore Link: https://lore.kernel.org/r/20250320190717.2287696-9-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/cred.c | 28 +++++++++++++++++++++++----- security/landlock/cred.h | 32 +++++++++++++++++++++++++++++++- security/landlock/syscalls.c | 5 +++++ 3 files changed, 59 insertions(+), 6 deletions(-) (limited to 'security') diff --git a/security/landlock/cred.c b/security/landlock/cred.c index db9fe7d906ba..0cb3edde4d18 100644 --- a/security/landlock/cred.c +++ b/security/landlock/cred.c @@ -1,11 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Landlock LSM - Credential hooks + * Landlock - Credential hooks * * Copyright © 2017-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation */ +#include #include #include @@ -17,11 +19,12 @@ static void hook_cred_transfer(struct cred *const new, const struct cred *const old) { - struct landlock_ruleset *const old_dom = landlock_cred(old)->domain; + const struct landlock_cred_security *const old_llcred = + landlock_cred(old); - if (old_dom) { - landlock_get_ruleset(old_dom); - landlock_cred(new)->domain = old_dom; + if (old_llcred->domain) { + landlock_get_ruleset(old_llcred->domain); + *landlock_cred(new) = *old_llcred; } } @@ -40,10 +43,25 @@ static void hook_cred_free(struct cred *const cred) landlock_put_ruleset_deferred(dom); } +#ifdef CONFIG_AUDIT + +static int hook_bprm_creds_for_exec(struct linux_binprm *const bprm) +{ + /* Resets for each execution. */ + landlock_cred(bprm->cred)->domain_exec = 0; + return 0; +} + +#endif /* CONFIG_AUDIT */ + static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(cred_prepare, hook_cred_prepare), LSM_HOOK_INIT(cred_transfer, hook_cred_transfer), LSM_HOOK_INIT(cred_free, hook_cred_free), + +#ifdef CONFIG_AUDIT + LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec), +#endif /* CONFIG_AUDIT */ }; __init void landlock_add_cred_hooks(void) diff --git a/security/landlock/cred.h b/security/landlock/cred.h index eb691130dd67..3bf18551d7b8 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -10,17 +10,47 @@ #ifndef _SECURITY_LANDLOCK_CRED_H #define _SECURITY_LANDLOCK_CRED_H +#include #include #include #include #include "access.h" +#include "limits.h" #include "ruleset.h" #include "setup.h" +/** + * struct landlock_cred_security - Credential security blob + * + * This structure is packed to minimize the size of struct + * landlock_file_security. However, it is always aligned in the LSM cred blob, + * see lsm_set_blob_size(). + */ struct landlock_cred_security { + /** + * @domain: Immutable ruleset enforced on a task. + */ struct landlock_ruleset *domain; -}; + +#ifdef CONFIG_AUDIT + /** + * @domain_exec: Bitmask identifying the domain layers that were enforced by + * the current task's executed file (i.e. no new execve(2) since + * landlock_restrict_self(2)). + */ + u16 domain_exec; +#endif /* CONFIG_AUDIT */ +} __packed; + +#ifdef CONFIG_AUDIT + +/* Makes sure all layer executions can be stored. */ +static_assert(BITS_PER_TYPE(typeof_member(struct landlock_cred_security, + domain_exec)) >= + LANDLOCK_MAX_NUM_LAYERS); + +#endif /* CONFIG_AUDIT */ static inline struct landlock_cred_security * landlock_cred(const struct cred *cred) diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index cf9e0483e542..b7b268f43a3b 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -510,5 +510,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, /* Replaces the old (prepared) domain. */ landlock_put_ruleset(new_llcred->domain); new_llcred->domain = new_dom; + +#ifdef CONFIG_AUDIT + new_llcred->domain_exec |= 1 << (new_dom->num_layers - 1); +#endif /* CONFIG_AUDIT */ + return commit_creds(new_cred); } -- cgit From 33e65b0d3add6bdc731e9298995cbbc979349f51 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:58 +0100 Subject: landlock: Add AUDIT_LANDLOCK_ACCESS and log ptrace denials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new AUDIT_LANDLOCK_ACCESS record type dedicated to an access request denied by a Landlock domain. AUDIT_LANDLOCK_ACCESS indicates that something unexpected happened. For now, only denied access are logged, which means that any AUDIT_LANDLOCK_ACCESS record is always followed by a SYSCALL record with "success=no". However, log parsers should check this syscall property because this is the only sign that a request was denied. Indeed, we could have "success=yes" if Landlock would support a "permissive" mode. We could also add a new field to AUDIT_LANDLOCK_DOMAIN for this mode (see following commit). By default, the only logged access requests are those coming from the same executed program that enforced the Landlock restriction on itself. In other words, no audit record are created for a task after it called execve(2). This is required to avoid log spam because programs may only be aware of their own restrictions, but not the inherited ones. Following commits will allow to conditionally generate AUDIT_LANDLOCK_ACCESS records according to dedicated landlock_restrict_self(2)'s flags. The AUDIT_LANDLOCK_ACCESS message contains: - the "domain" ID restricting the action on an object, - the "blockers" that are missing to allow the requested access, - a set of fields identifying the related object (e.g. task identified with "opid" and "ocomm"). The blockers are implicit restrictions (e.g. ptrace), or explicit access rights (e.g. filesystem), or explicit scopes (e.g. signal). This field contains a list of at least one element, each separated with a comma. The initial blocker is "ptrace", which describe all implicit Landlock restrictions related to ptrace (e.g. deny tracing of tasks outside a sandbox). Add audit support to ptrace_access_check and ptrace_traceme hooks. For the ptrace_access_check case, we log the current/parent domain and the child task. For the ptrace_traceme case, we log the parent domain and the current/child task. Indeed, the requester and the target are the current task, but the action would be performed by the parent task. Audit event sample: type=LANDLOCK_ACCESS msg=audit(1729738800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=SYSCALL msg=audit(1729738800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 A following commit adds user documentation. Add KUnit tests to check reading of domain ID relative to layer level. The quick return for non-landlocked tasks is moved from task_ptrace() to each LSM hooks. It is not useful to inline the audit_enabled check because other computation are performed by landlock_log_denial(). Use scoped guards for RCU read-side critical sections. Cc: Günther Noack Acked-by: Paul Moore Link: https://lore.kernel.org/r/20250320190717.2287696-10-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/Makefile | 5 +- security/landlock/audit.c | 151 ++++++++++++++++++++++++++++++++++++++++++++ security/landlock/audit.h | 52 +++++++++++++++ security/landlock/domain.c | 28 ++++++++ security/landlock/domain.h | 22 +++++++ security/landlock/ruleset.c | 6 ++ security/landlock/task.c | 96 +++++++++++++++++++++------- 7 files changed, 336 insertions(+), 24 deletions(-) create mode 100644 security/landlock/audit.c create mode 100644 security/landlock/audit.h create mode 100644 security/landlock/domain.c (limited to 'security') diff --git a/security/landlock/Makefile b/security/landlock/Makefile index e1777abbc413..3160c2bdac1d 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -5,4 +5,7 @@ landlock-y := setup.o syscalls.o object.o ruleset.o \ landlock-$(CONFIG_INET) += net.o -landlock-$(CONFIG_AUDIT) += id.o +landlock-$(CONFIG_AUDIT) += \ + id.o \ + audit.o \ + domain.o diff --git a/security/landlock/audit.c b/security/landlock/audit.c new file mode 100644 index 000000000000..b0752263012a --- /dev/null +++ b/security/landlock/audit.c @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Audit helpers + * + * Copyright © 2023-2025 Microsoft Corporation + */ + +#include +#include +#include + +#include "audit.h" +#include "cred.h" +#include "domain.h" +#include "limits.h" +#include "ruleset.h" + +static const char *get_blocker(const enum landlock_request_type type) +{ + switch (type) { + case LANDLOCK_REQUEST_PTRACE: + return "ptrace"; + } + + WARN_ON_ONCE(1); + return "unknown"; +} + +static void log_blockers(struct audit_buffer *const ab, + const enum landlock_request_type type) +{ + audit_log_format(ab, "%s", get_blocker(type)); +} + +static struct landlock_hierarchy * +get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer) +{ + struct landlock_hierarchy *hierarchy = domain->hierarchy; + ssize_t i; + + if (WARN_ON_ONCE(layer >= domain->num_layers)) + return hierarchy; + + for (i = domain->num_layers - 1; i > layer; i--) { + if (WARN_ON_ONCE(!hierarchy->parent)) + break; + + hierarchy = hierarchy->parent; + } + + return hierarchy; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_hierarchy(struct kunit *const test) +{ + struct landlock_hierarchy dom0_hierarchy = { + .id = 10, + }; + struct landlock_hierarchy dom1_hierarchy = { + .parent = &dom0_hierarchy, + .id = 20, + }; + struct landlock_hierarchy dom2_hierarchy = { + .parent = &dom1_hierarchy, + .id = 30, + }; + struct landlock_ruleset dom2 = { + .hierarchy = &dom2_hierarchy, + .num_layers = 3, + }; + + KUNIT_EXPECT_EQ(test, 10, get_hierarchy(&dom2, 0)->id); + KUNIT_EXPECT_EQ(test, 20, get_hierarchy(&dom2, 1)->id); + KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, 2)->id); + KUNIT_EXPECT_EQ(test, 30, get_hierarchy(&dom2, -1)->id); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +static bool is_valid_request(const struct landlock_request *const request) +{ + if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS)) + return false; + + if (WARN_ON_ONCE(!request->layer_plus_one)) + return false; + + return true; +} + +/** + * landlock_log_denial - Create audit records related to a denial + * + * @subject: The Landlock subject's credential denying an action. + * @request: Detail of the user space request. + */ +void landlock_log_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request) +{ + struct audit_buffer *ab; + struct landlock_hierarchy *youngest_denied; + size_t youngest_layer; + + if (WARN_ON_ONCE(!subject || !subject->domain || + !subject->domain->hierarchy || !request)) + return; + + if (!is_valid_request(request)) + return; + + if (!audit_enabled) + return; + + youngest_layer = request->layer_plus_one - 1; + youngest_denied = get_hierarchy(subject->domain, youngest_layer); + + /* Ignores denials after an execution. */ + if (!(subject->domain_exec & (1 << youngest_layer))) + return; + + /* Uses consistent allocation flags wrt common_lsm_audit(). */ + ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, + AUDIT_LANDLOCK_ACCESS); + if (!ab) + return; + + audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id); + log_blockers(ab, request->type); + audit_log_lsm_data(ab, &request->audit); + audit_log_end(ab); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static struct kunit_case test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_get_hierarchy), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_audit", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/audit.h b/security/landlock/audit.h new file mode 100644 index 000000000000..3a6ec7c8e7c3 --- /dev/null +++ b/security/landlock/audit.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Audit helpers + * + * Copyright © 2023-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_AUDIT_H +#define _SECURITY_LANDLOCK_AUDIT_H + +#include +#include + +#include "cred.h" + +enum landlock_request_type { + LANDLOCK_REQUEST_PTRACE = 1, +}; + +/* + * We should be careful to only use a variable of this type for + * landlock_log_denial(). This way, the compiler can remove it entirely if + * CONFIG_AUDIT is not set. + */ +struct landlock_request { + /* Mandatory fields. */ + enum landlock_request_type type; + struct common_audit_data audit; + + /** + * layer_plus_one: First layer level that denies the request + 1. The + * extra one is useful to detect uninitialized field. + */ + size_t layer_plus_one; +}; + +#ifdef CONFIG_AUDIT + +void landlock_log_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request); + +#else /* CONFIG_AUDIT */ + +static inline void +landlock_log_denial(const struct landlock_cred_security *const subject, + const struct landlock_request *const request) +{ +} + +#endif /* CONFIG_AUDIT */ + +#endif /* _SECURITY_LANDLOCK_AUDIT_H */ diff --git a/security/landlock/domain.c b/security/landlock/domain.c new file mode 100644 index 000000000000..e6367933303c --- /dev/null +++ b/security/landlock/domain.c @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Domain management + * + * Copyright © 2016-2020 Mickaël Salaün + * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + */ + +#include "domain.h" +#include "id.h" + +#ifdef CONFIG_AUDIT + +/** + * landlock_init_hierarchy_log - Partially initialize landlock_hierarchy + * + * @hierarchy: The hierarchy to initialize. + * + * @hierarchy->parent and @hierarchy->usage should already be set. + */ +int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) +{ + hierarchy->id = landlock_get_id_range(1); + return 0; +} + +#endif /* CONFIG_AUDIT */ diff --git a/security/landlock/domain.h b/security/landlock/domain.h index d22712e5fb0f..f8df7103bd3d 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -4,6 +4,7 @@ * * Copyright © 2016-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation */ #ifndef _SECURITY_LANDLOCK_DOMAIN_H @@ -26,8 +27,29 @@ struct landlock_hierarchy { * domain. */ refcount_t usage; + +#ifdef CONFIG_AUDIT + /** + * @id: Landlock domain ID, sets once at domain creation time. + */ + u64 id; +#endif /* CONFIG_AUDIT */ }; +#ifdef CONFIG_AUDIT + +int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); + +#else /* CONFIG_AUDIT */ + +static inline int +landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) +{ + return 0; +} + +#endif /* CONFIG_AUDIT */ + static inline void landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy) { diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index adb7f87828df..f273a40e9780 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -23,6 +23,7 @@ #include #include "access.h" +#include "audit.h" #include "domain.h" #include "limits.h" #include "object.h" @@ -505,6 +506,7 @@ static void free_ruleset_work(struct work_struct *const work) free_ruleset(ruleset); } +/* Only called by hook_cred_free(). */ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset) { if (ruleset && refcount_dec_and_test(&ruleset->usage)) { @@ -564,6 +566,10 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent, if (err) return ERR_PTR(err); + err = landlock_init_hierarchy_log(new_dom->hierarchy); + if (err) + return ERR_PTR(err); + return no_free_ptr(new_dom); } diff --git a/security/landlock/task.c b/security/landlock/task.c index b89ab9a904eb..30ac4340c62e 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include +#include "audit.h" #include "common.h" #include "cred.h" #include "domain.h" @@ -41,41 +43,29 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent, { const struct landlock_hierarchy *walker; + /* Quick return for non-landlocked tasks. */ if (!parent) return true; + if (!child) return false; + for (walker = child->hierarchy; walker; walker = walker->parent) { if (walker == parent->hierarchy) /* @parent is in the scoped hierarchy of @child. */ return true; } + /* There is no relationship between @parent and @child. */ return false; } -static bool task_is_scoped(const struct task_struct *const parent, - const struct task_struct *const child) -{ - bool is_scoped; - const struct landlock_ruleset *dom_parent, *dom_child; - - rcu_read_lock(); - dom_parent = landlock_get_task_domain(parent); - dom_child = landlock_get_task_domain(child); - is_scoped = domain_scope_le(dom_parent, dom_child); - rcu_read_unlock(); - return is_scoped; -} - -static int task_ptrace(const struct task_struct *const parent, - const struct task_struct *const child) +static int domain_ptrace(const struct landlock_ruleset *const parent, + const struct landlock_ruleset *const child) { - /* Quick return for non-landlocked tasks. */ - if (!landlocked(parent)) - return 0; - if (task_is_scoped(parent, child)) + if (domain_scope_le(parent, child)) return 0; + return -EPERM; } @@ -95,7 +85,39 @@ static int task_ptrace(const struct task_struct *const parent, static int hook_ptrace_access_check(struct task_struct *const child, const unsigned int mode) { - return task_ptrace(current, child); + const struct landlock_cred_security *parent_subject; + const struct landlock_ruleset *child_dom; + int err; + + /* Quick return for non-landlocked tasks. */ + parent_subject = landlock_cred(current_cred()); + if (!parent_subject) + return 0; + + scoped_guard(rcu) + { + child_dom = landlock_get_task_domain(child); + err = domain_ptrace(parent_subject->domain, child_dom); + } + + if (!err) + return 0; + + /* + * For the ptrace_access_check case, we log the current/parent domain + * and the child task. + */ + if (!(mode & PTRACE_MODE_NOAUDIT)) + landlock_log_denial(parent_subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_PTRACE, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = child, + }, + .layer_plus_one = parent_subject->domain->num_layers, + }); + + return err; } /** @@ -112,7 +134,35 @@ static int hook_ptrace_access_check(struct task_struct *const child, */ static int hook_ptrace_traceme(struct task_struct *const parent) { - return task_ptrace(parent, current); + const struct landlock_cred_security *parent_subject; + const struct landlock_ruleset *child_dom; + int err; + + child_dom = landlock_get_current_domain(); + + guard(rcu)(); + parent_subject = landlock_cred(__task_cred(parent)); + err = domain_ptrace(parent_subject->domain, child_dom); + + if (!err) + return 0; + + /* + * For the ptrace_traceme case, we log the domain which is the cause of + * the denial, which means the parent domain instead of the current + * domain. This may look unusual because the ptrace_traceme action is a + * request to be traced, but the semantic is consistent with + * hook_ptrace_access_check(). + */ + landlock_log_denial(parent_subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_PTRACE, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = current, + }, + .layer_plus_one = parent_subject->domain->num_layers, + }); + return err; } /** @@ -131,7 +181,7 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client, access_mask_t scope) { int client_layer, server_layer; - struct landlock_hierarchy *client_walker, *server_walker; + const struct landlock_hierarchy *client_walker, *server_walker; /* Quick return if client has no domain */ if (WARN_ON_ONCE(!client)) -- cgit From 1d636984e088b17e8587eb5ed9d9d7a80b656c4c Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:06:59 +0100 Subject: landlock: Add AUDIT_LANDLOCK_DOMAIN and log domain status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asynchronously log domain information when it first denies an access. This minimize the amount of generated logs, which makes it possible to always log denials for the current execution since they should not happen. These records are identified with the new AUDIT_LANDLOCK_DOMAIN type. The AUDIT_LANDLOCK_DOMAIN message contains: - the "domain" ID which is described; - the "status" which can either be "allocated" or "deallocated"; - the "mode" which is for now only "enforcing"; - for the "allocated" status, a minimal set of properties to easily identify the task that loaded the domain's policy with landlock_restrict_self(2): "pid", "uid", executable path ("exe"), and command line ("comm"); - for the "deallocated" state, the number of "denials" accounted to this domain, which is at least 1. This requires each domain to save these task properties at creation time in the new struct landlock_details. A reference to the PID is kept for the lifetime of the domain to avoid race conditions when investigating the related task. The executable path is resolved and stored to not keep a reference to the filesystem and block related actions. All these metadata are stored for the lifetime of the related domain and should then be minimal. The required memory is not accounted to the task calling landlock_restrict_self(2) contrary to most other Landlock allocations (see related comment). The AUDIT_LANDLOCK_DOMAIN record follows the first AUDIT_LANDLOCK_ACCESS record for the same domain, which is always followed by AUDIT_SYSCALL and AUDIT_PROCTITLE. This is in line with the audit logic to first record the cause of an event, and then add context with other types of record. Audit event sample for a first denial: type=LANDLOCK_ACCESS msg=audit(1732186800.349:44): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=LANDLOCK_DOMAIN msg=audit(1732186800.349:44): domain=195ba459b status=allocated mode=enforcing pid=300 uid=0 exe="/root/sandboxer" comm="sandboxer" type=SYSCALL msg=audit(1732186800.349:44): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 Audit event sample for a following denial: type=LANDLOCK_ACCESS msg=audit(1732186800.372:45): domain=195ba459b blockers=ptrace opid=1 ocomm="systemd" type=SYSCALL msg=audit(1732186800.372:45): arch=c000003e syscall=101 success=no [...] pid=300 auid=0 Log domain deletion with the "deallocated" state when a domain was previously logged. This makes it possible for log parsers to free potential resources when a domain ID will never show again. The number of denied access requests is useful to easily check how many access requests a domain blocked and potentially if some of them are missing in logs because of audit rate limiting, audit rules, or Landlock log configuration flags (see following commit). Audit event sample for a deletion of a domain that denied something: type=LANDLOCK_DOMAIN msg=audit(1732186800.393:46): domain=195ba459b status=deallocated denials=2 Cc: Günther Noack Acked-by: Paul Moore Link: https://lore.kernel.org/r/20250320190717.2287696-11-mic@digikod.net [mic: Update comment and GFP flag for landlock_log_drop_domain()] Signed-off-by: Mickaël Salaün --- security/landlock/audit.c | 89 ++++++++++++++++++++++++++++++++++++-- security/landlock/audit.h | 7 +++ security/landlock/domain.c | 101 ++++++++++++++++++++++++++++++++++++++++++++ security/landlock/domain.h | 86 +++++++++++++++++++++++++++++++++++++ security/landlock/ruleset.c | 3 ++ security/landlock/ruleset.h | 3 +- 6 files changed, 285 insertions(+), 4 deletions(-) (limited to 'security') diff --git a/security/landlock/audit.c b/security/landlock/audit.c index b0752263012a..39118f7ff990 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "audit.h" #include "cred.h" @@ -32,6 +33,38 @@ static void log_blockers(struct audit_buffer *const ab, audit_log_format(ab, "%s", get_blocker(type)); } +static void log_domain(struct landlock_hierarchy *const hierarchy) +{ + struct audit_buffer *ab; + + /* Ignores already logged domains. */ + if (READ_ONCE(hierarchy->log_status) == LANDLOCK_LOG_RECORDED) + return; + + /* Uses consistent allocation flags wrt common_lsm_audit(). */ + ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, + AUDIT_LANDLOCK_DOMAIN); + if (!ab) + return; + + WARN_ON_ONCE(hierarchy->id == 0); + audit_log_format( + ab, + "domain=%llx status=allocated mode=enforcing pid=%d uid=%u exe=", + hierarchy->id, pid_nr(hierarchy->details->pid), + hierarchy->details->uid); + audit_log_untrustedstring(ab, hierarchy->details->exe_path); + audit_log_format(ab, " comm="); + audit_log_untrustedstring(ab, hierarchy->details->comm); + audit_log_end(ab); + + /* + * There may be race condition leading to logging of the same domain + * several times but that is OK. + */ + WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED); +} + static struct landlock_hierarchy * get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer) { @@ -110,12 +143,20 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!is_valid_request(request)) return; - if (!audit_enabled) - return; - youngest_layer = request->layer_plus_one - 1; youngest_denied = get_hierarchy(subject->domain, youngest_layer); + /* + * Consistently keeps track of the number of denied access requests + * even if audit is currently disabled, or if audit rules currently + * exclude this record type, or if landlock_restrict_self(2)'s flags + * quiet logs. + */ + atomic64_inc(&youngest_denied->num_denials); + + if (!audit_enabled) + return; + /* Ignores denials after an execution. */ if (!(subject->domain_exec & (1 << youngest_layer))) return; @@ -130,6 +171,48 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, log_blockers(ab, request->type); audit_log_lsm_data(ab, &request->audit); audit_log_end(ab); + + /* Logs this domain the first time it shows in log. */ + log_domain(youngest_denied); +} + +/** + * landlock_log_drop_domain - Create an audit record on domain deallocation + * + * @hierarchy: The domain's hierarchy being deallocated. + * + * Only domains which previously appeared in the audit logs are logged again. + * This is useful to know when a domain will never show again in the audit log. + * + * Called in a work queue scheduled by landlock_put_ruleset_deferred() called + * by hook_cred_free(). + */ +void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy) +{ + struct audit_buffer *ab; + + if (WARN_ON_ONCE(!hierarchy)) + return; + + if (!audit_enabled) + return; + + /* Ignores domains that were not logged. */ + if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED) + return; + + /* + * If logging of domain allocation succeeded, warns about failure to log + * domain deallocation to highlight unbalanced domain lifetime logs. + */ + ab = audit_log_start(audit_context(), GFP_KERNEL, + AUDIT_LANDLOCK_DOMAIN); + if (!ab) + return; + + audit_log_format(ab, "domain=%llx status=deallocated denials=%llu", + hierarchy->id, atomic64_read(&hierarchy->num_denials)); + audit_log_end(ab); } #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 3a6ec7c8e7c3..40ff230316c4 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -36,11 +36,18 @@ struct landlock_request { #ifdef CONFIG_AUDIT +void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy); + void landlock_log_denial(const struct landlock_cred_security *const subject, const struct landlock_request *const request); #else /* CONFIG_AUDIT */ +static inline void +landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy) +{ +} + static inline void landlock_log_denial(const struct landlock_cred_security *const subject, const struct landlock_request *const request) diff --git a/security/landlock/domain.c b/security/landlock/domain.c index e6367933303c..3c1715e4b1c9 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -7,21 +7,122 @@ * Copyright © 2024-2025 Microsoft Corporation */ +#include +#include +#include +#include +#include +#include +#include + #include "domain.h" #include "id.h" #ifdef CONFIG_AUDIT +/** + * get_current_exe - Get the current's executable path, if any + * + * @exe_str: Returned pointer to a path string with a lifetime tied to the + * returned buffer, if any. + * @exe_size: Returned size of @exe_str (including the trailing null + * character), if any. + * + * Returns: A pointer to an allocated buffer where @exe_str point to, %NULL if + * there is no executable path, or an error otherwise. + */ +static const void *get_current_exe(const char **const exe_str, + size_t *const exe_size) +{ + const size_t buffer_size = LANDLOCK_PATH_MAX_SIZE; + struct mm_struct *mm = current->mm; + struct file *file __free(fput) = NULL; + char *buffer __free(kfree) = NULL; + const char *exe; + ssize_t size; + + if (!mm) + return NULL; + + file = get_mm_exe_file(mm); + if (!file) + return NULL; + + buffer = kmalloc(buffer_size, GFP_KERNEL); + if (!buffer) + return ERR_PTR(-ENOMEM); + + exe = d_path(&file->f_path, buffer, buffer_size); + if (WARN_ON_ONCE(IS_ERR(exe))) + /* Should never happen according to LANDLOCK_PATH_MAX_SIZE. */ + return ERR_CAST(exe); + + size = buffer + buffer_size - exe; + if (WARN_ON_ONCE(size <= 0)) + return ERR_PTR(-ENAMETOOLONG); + + *exe_size = size; + *exe_str = exe; + return no_free_ptr(buffer); +} + +/* + * Returns: A newly allocated object describing a domain, or an error + * otherwise. + */ +static struct landlock_details *get_current_details(void) +{ + /* Cf. audit_log_d_path_exe() */ + static const char null_path[] = "(null)"; + const char *path_str = null_path; + size_t path_size = sizeof(null_path); + const void *buffer __free(kfree) = NULL; + struct landlock_details *details; + + buffer = get_current_exe(&path_str, &path_size); + if (IS_ERR(buffer)) + 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. + */ + details = + kzalloc(struct_size(details, exe_path, path_size), GFP_KERNEL); + if (!details) + return ERR_PTR(-ENOMEM); + + memcpy(details->exe_path, path_str, path_size); + WARN_ON_ONCE(current_cred() != current_real_cred()); + details->pid = get_pid(task_pid(current)); + details->uid = from_kuid(&init_user_ns, current_uid()); + get_task_comm(details->comm, current); + return details; +} + /** * landlock_init_hierarchy_log - Partially initialize landlock_hierarchy * * @hierarchy: The hierarchy to initialize. * + * The current task is referenced as the domain that is enforcing the + * restriction. The subjective credentials must not be in an overridden state. + * * @hierarchy->parent and @hierarchy->usage should already be set. */ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) { + struct landlock_details *details; + + details = get_current_details(); + if (IS_ERR(details)) + return PTR_ERR(details); + + hierarchy->details = details; hierarchy->id = landlock_get_id_range(1); + hierarchy->log_status = LANDLOCK_LOG_PENDING; + atomic64_set(&hierarchy->num_denials, 0); return 0; } diff --git a/security/landlock/domain.h b/security/landlock/domain.h index f8df7103bd3d..2c6f04da3628 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -10,8 +10,62 @@ #ifndef _SECURITY_LANDLOCK_DOMAIN_H #define _SECURITY_LANDLOCK_DOMAIN_H +#include #include +#include +#include #include +#include +#include + +#include "audit.h" + +enum landlock_log_status { + LANDLOCK_LOG_PENDING = 0, + LANDLOCK_LOG_RECORDED, +}; + +/** + * struct landlock_details - Domain's creation information + * + * 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. + */ +struct landlock_details { + /** + * @pid: PID of the task that initially restricted itself. It still + * identifies the same task. Keeping a reference to this PID ensures that + * it will not be recycled. + */ + struct pid *pid; + /** + * @uid: UID of the task that initially restricted itself, at creation time. + */ + uid_t uid; + /** + * @comm: Command line of the task that initially restricted itself, at + * creation time. Always NULL terminated. + */ + char comm[TASK_COMM_LEN]; + /** + * @exe_path: Executable path of the task that initially restricted + * itself, at creation time. Always NULL terminated, and never greater + * than LANDLOCK_PATH_MAX_SIZE. + */ + char exe_path[]; +}; + +/* Adds 11 extra characters for the potential " (deleted)" suffix. */ +#define LANDLOCK_PATH_MAX_SIZE (PATH_MAX + 11) + +/* Makes sure the greatest landlock_details can be allocated. */ +static_assert(struct_size_t(struct landlock_details, exe_path, + LANDLOCK_PATH_MAX_SIZE) <= KMALLOC_MAX_SIZE); /** * struct landlock_hierarchy - Node in a domain hierarchy @@ -29,10 +83,25 @@ struct landlock_hierarchy { refcount_t usage; #ifdef CONFIG_AUDIT + /** + * @log_status: Whether this domain should be logged or not. Because + * concurrent log entries may be created at the same time, it is still + * possible to have several domain records of the same domain. + */ + enum landlock_log_status log_status; + /** + * @num_denials: Number of access requests denied by this domain. + * Masked (i.e. never logged) denials are still counted. + */ + atomic64_t num_denials; /** * @id: Landlock domain ID, sets once at domain creation time. */ u64 id; + /** + * @details: Information about the related domain. + */ + const struct landlock_details *details; #endif /* CONFIG_AUDIT */ }; @@ -40,6 +109,16 @@ struct landlock_hierarchy { int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); +static inline void +landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy) +{ + if (WARN_ON_ONCE(!hierarchy || !hierarchy->details)) + return; + + put_pid(hierarchy->details->pid); + kfree(hierarchy->details); +} + #else /* CONFIG_AUDIT */ static inline int @@ -48,6 +127,11 @@ landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) return 0; } +static inline void +landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy) +{ +} + #endif /* CONFIG_AUDIT */ static inline void @@ -62,6 +146,8 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy) while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) { const struct landlock_hierarchy *const freeme = hierarchy; + landlock_log_drop_domain(hierarchy); + landlock_free_hierarchy_details(hierarchy); hierarchy = hierarchy->parent; kfree(freeme); } diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index f273a40e9780..ce7940efea51 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -521,6 +521,9 @@ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset) * @parent: Parent domain. * @ruleset: New ruleset to be merged. * + * The current task is requesting to be restricted. The subjective credentials + * must not be in an overridden state. cf. landlock_init_hierarchy_log(). + * * Returns the intersection of @parent and @ruleset, or returns @parent if * @ruleset is empty, or returns a duplicate of @ruleset if @parent is empty. */ diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index bbb5996545d2..28af42d66261 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -17,10 +17,11 @@ #include #include "access.h" -#include "domain.h" #include "limits.h" #include "object.h" +struct landlock_hierarchy; + /** * struct landlock_layer - Access rights for a given layer */ -- cgit From c56f649646ecec3dd1a2e400e6e5ec83439d940f Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:00 +0100 Subject: landlock: Log mount-related denials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add audit support for sb_mount, move_mount, sb_umount, sb_remount, and sb_pivot_root hooks. The new related blocker is "fs.change_topology". Audit event sample: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.change_topology name="/" dev="tmpfs" ino=1 Remove landlock_get_applicable_domain() and get_current_fs_domain() which are now fully replaced with landlock_get_applicable_subject(). Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-12-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/audit.c | 3 ++ security/landlock/audit.h | 1 + security/landlock/fs.c | 81 +++++++++++++++++++++++++++++++++++++++------ security/landlock/ruleset.h | 30 ----------------- 4 files changed, 74 insertions(+), 41 deletions(-) (limited to 'security') diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 39118f7ff990..b33bc7cfa687 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -21,6 +21,9 @@ static const char *get_blocker(const enum landlock_request_type type) switch (type) { case LANDLOCK_REQUEST_PTRACE: return "ptrace"; + + case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY: + return "fs.change_topology"; } WARN_ON_ONCE(1); diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 40ff230316c4..9ebe8766bbfd 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -15,6 +15,7 @@ enum landlock_request_type { LANDLOCK_REQUEST_PTRACE = 1, + LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, }; /* diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 50e02bdab089..9b251e4d0762 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include #include "access.h" +#include "audit.h" #include "common.h" #include "cred.h" #include "fs.h" @@ -395,12 +397,6 @@ static const struct access_masks any_fs = { .fs = ~0, }; -static const struct landlock_ruleset *get_current_fs_domain(void) -{ - return landlock_get_applicable_domain(landlock_get_current_domain(), - any_fs); -} - /* * Check that a destination file hierarchy has more restrictions than a source * file hierarchy. This is only used for link and rename actions. @@ -1335,6 +1331,34 @@ static void hook_sb_delete(struct super_block *const sb) !atomic_long_read(&landlock_superblock(sb)->inode_refs)); } +static void +log_fs_change_topology_path(const struct landlock_cred_security *const subject, + size_t handle_layer, const struct path *const path) +{ + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, + .audit = { + .type = LSM_AUDIT_DATA_PATH, + .u.path = *path, + }, + .layer_plus_one = handle_layer + 1, + }); +} + +static void log_fs_change_topology_dentry( + const struct landlock_cred_security *const subject, size_t handle_layer, + struct dentry *const dentry) +{ + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, + .audit = { + .type = LSM_AUDIT_DATA_DENTRY, + .u.dentry = dentry, + }, + .layer_plus_one = handle_layer + 1, + }); +} + /* * Because a Landlock security policy is defined according to the filesystem * topology (i.e. the mount namespace), changing it may grant access to files @@ -1357,16 +1381,30 @@ static int hook_sb_mount(const char *const dev_name, const struct path *const path, const char *const type, const unsigned long flags, void *const data) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_topology_path(subject, handle_layer, path); return -EPERM; } static int hook_move_mount(const struct path *const from_path, const struct path *const to_path) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_topology_path(subject, handle_layer, to_path); return -EPERM; } @@ -1376,15 +1414,29 @@ static int hook_move_mount(const struct path *const from_path, */ static int hook_sb_umount(struct vfsmount *const mnt, const int flags) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_topology_dentry(subject, handle_layer, mnt->mnt_root); return -EPERM; } static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_topology_dentry(subject, handle_layer, sb->s_root); return -EPERM; } @@ -1399,8 +1451,15 @@ static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts) static int hook_sb_pivotroot(const struct path *const old_path, const struct path *const new_path) { - if (!get_current_fs_domain()) + size_t handle_layer; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), any_fs, + &handle_layer); + + if (!subject) return 0; + + log_fs_change_topology_path(subject, handle_layer, new_path); return -EPERM; } diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index 28af42d66261..5da9a64f5af7 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -243,36 +243,6 @@ landlock_union_access_masks(const struct landlock_ruleset *const domain) return matches.masks; } -/** - * landlock_get_applicable_domain - Return @domain if it applies to (handles) - * at least one of the access rights specified - * in @masks - * - * @domain: Landlock ruleset (used as a domain) - * @masks: access masks - * - * Returns: @domain if any access rights specified in @masks is handled, or - * NULL otherwise. - */ -static inline const struct landlock_ruleset * -landlock_get_applicable_domain(const struct landlock_ruleset *const domain, - const struct access_masks masks) -{ - const union access_masks_all masks_all = { - .masks = masks, - }; - union access_masks_all merge = {}; - - if (!domain) - return NULL; - - merge.masks = landlock_union_access_masks(domain); - if (merge.all & masks_all.all) - return domain; - - return NULL; -} - static inline void landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset, const access_mask_t fs_access_mask, -- cgit From 2fc80c69df823628f1f0f2aace99e393c57112fa Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:01 +0100 Subject: landlock: Log file-related denials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add audit support for path_mkdir, path_mknod, path_symlink, path_unlink, path_rmdir, path_truncate, path_link, path_rename, and file_open hooks. The dedicated blockers are: - fs.execute - fs.write_file - fs.read_file - fs.read_dir - fs.remove_dir - fs.remove_file - fs.make_char - fs.make_dir - fs.make_reg - fs.make_sock - fs.make_fifo - fs.make_block - fs.make_sym - fs.refer - fs.truncate - fs.ioctl_dev Audit event sample for a denied link action: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.refer path="/usr/bin" dev="vda2" ino=351 type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.make_reg,fs.refer path="/usr/local" dev="vda2" ino=365 We could pack blocker names (e.g. "fs:make_reg,refer") but that would increase complexity for the kernel and log parsers. Moreover, this could not handle blockers of different classes (e.g. fs and net). Make it simple and flexible instead. Add KUnit tests to check the identification from a layer_mask_t array of the first layer level denying such request. Cc: Günther Noack Depends-on: 058518c20920 ("landlock: Align partial refer access checks with final ones") Depends-on: d617f0d72d80 ("landlock: Optimize file path walks and prepare for audit support") Link: https://lore.kernel.org/r/20250320190717.2287696-13-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/audit.c | 178 ++++++++++++++++++++++++++++++++++++++++++++-- security/landlock/audit.h | 9 +++ security/landlock/fs.c | 62 +++++++++++++--- 3 files changed, 233 insertions(+), 16 deletions(-) (limited to 'security') diff --git a/security/landlock/audit.c b/security/landlock/audit.c index b33bc7cfa687..45e1c6ad1856 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -7,23 +7,56 @@ #include #include +#include #include #include +#include #include "audit.h" +#include "common.h" #include "cred.h" #include "domain.h" #include "limits.h" #include "ruleset.h" -static const char *get_blocker(const enum landlock_request_type type) +static const char *const fs_access_strings[] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute", + [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file", + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file", + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir", + [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir", + [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block", + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym", + [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer", + [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate", + [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev", +}; + +static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS); + +static __attribute_const__ const char * +get_blocker(const enum landlock_request_type type, + const unsigned long access_bit) { switch (type) { case LANDLOCK_REQUEST_PTRACE: + WARN_ON_ONCE(access_bit != -1); return "ptrace"; case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY: + WARN_ON_ONCE(access_bit != -1); return "fs.change_topology"; + + case LANDLOCK_REQUEST_FS_ACCESS: + if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings))) + return "unknown"; + return fs_access_strings[access_bit]; } WARN_ON_ONCE(1); @@ -31,9 +64,20 @@ static const char *get_blocker(const enum landlock_request_type type) } static void log_blockers(struct audit_buffer *const ab, - const enum landlock_request_type type) + const enum landlock_request_type type, + const access_mask_t access) { - audit_log_format(ab, "%s", get_blocker(type)); + const unsigned long access_mask = access; + unsigned long access_bit; + bool is_first = true; + + for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) { + audit_log_format(ab, "%s%s", is_first ? "" : ",", + get_blocker(type, access_bit)); + is_first = false; + } + if (is_first) + audit_log_format(ab, "%s", get_blocker(type, -1)); } static void log_domain(struct landlock_hierarchy *const hierarchy) @@ -115,12 +159,113 @@ static void test_get_hierarchy(struct kunit *const test) #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ +static size_t get_denied_layer(const struct landlock_ruleset *const domain, + access_mask_t *const access_request, + const layer_mask_t (*const layer_masks)[], + const size_t layer_masks_size) +{ + const unsigned long access_req = *access_request; + unsigned long access_bit; + access_mask_t missing = 0; + long youngest_layer = -1; + + for_each_set_bit(access_bit, &access_req, layer_masks_size) { + const access_mask_t mask = (*layer_masks)[access_bit]; + long layer; + + if (!mask) + continue; + + /* __fls(1) == 0 */ + layer = __fls(mask); + if (layer > youngest_layer) { + youngest_layer = layer; + missing = BIT(access_bit); + } else if (layer == youngest_layer) { + missing |= BIT(access_bit); + } + } + + *access_request = missing; + if (youngest_layer == -1) + return domain->num_layers - 1; + + return youngest_layer; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_denied_layer(struct kunit *const test) +{ + const struct landlock_ruleset dom = { + .num_layers = 5, + }; + const layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = BIT(1), + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = BIT(1) | BIT(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = BIT(2), + }; + access_mask_t access; + + access = LANDLOCK_ACCESS_FS_EXECUTE; + KUNIT_EXPECT_EQ(test, 0, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_EXECUTE); + + access = LANDLOCK_ACCESS_FS_READ_FILE; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_FILE); + + access = LANDLOCK_ACCESS_FS_READ_DIR; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR); + + access = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR); + + access = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_DIR; + KUNIT_EXPECT_EQ(test, 1, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_READ_DIR); + + access = LANDLOCK_ACCESS_FS_WRITE_FILE; + KUNIT_EXPECT_EQ(test, 4, + get_denied_layer(&dom, &access, &layer_masks, + sizeof(layer_masks))); + KUNIT_EXPECT_EQ(test, access, 0); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + static bool is_valid_request(const struct landlock_request *const request) { if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS)) return false; - if (WARN_ON_ONCE(!request->layer_plus_one)) + if (WARN_ON_ONCE(!(!!request->layer_plus_one ^ !!request->access))) + return false; + + if (request->access) { + if (WARN_ON_ONCE(!request->layer_masks)) + return false; + } else { + if (WARN_ON_ONCE(request->layer_masks)) + return false; + } + + if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size)) return false; return true; @@ -138,6 +283,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, struct audit_buffer *ab; struct landlock_hierarchy *youngest_denied; size_t youngest_layer; + access_mask_t missing; if (WARN_ON_ONCE(!subject || !subject->domain || !subject->domain->hierarchy || !request)) @@ -146,8 +292,25 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!is_valid_request(request)) return; - youngest_layer = request->layer_plus_one - 1; - youngest_denied = get_hierarchy(subject->domain, youngest_layer); + missing = request->access; + if (missing) { + /* Gets the nearest domain that denies the request. */ + if (request->layer_masks) { + youngest_layer = get_denied_layer( + subject->domain, &missing, request->layer_masks, + request->layer_masks_size); + } else { + /* This will change with the next commit. */ + WARN_ON_ONCE(1); + youngest_layer = subject->domain->num_layers; + } + youngest_denied = + get_hierarchy(subject->domain, youngest_layer); + } else { + youngest_layer = request->layer_plus_one - 1; + youngest_denied = + get_hierarchy(subject->domain, youngest_layer); + } /* * Consistently keeps track of the number of denied access requests @@ -171,7 +334,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, return; audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id); - log_blockers(ab, request->type); + log_blockers(ab, request->type, missing); audit_log_lsm_data(ab, &request->audit); audit_log_end(ab); @@ -223,6 +386,7 @@ void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy) static struct kunit_case test_cases[] = { /* clang-format off */ KUNIT_CASE(test_get_hierarchy), + KUNIT_CASE(test_get_denied_layer), {} /* clang-format on */ }; diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 9ebe8766bbfd..2a154116134e 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -11,11 +11,13 @@ #include #include +#include "access.h" #include "cred.h" enum landlock_request_type { LANDLOCK_REQUEST_PTRACE = 1, LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, + LANDLOCK_REQUEST_FS_ACCESS, }; /* @@ -33,6 +35,13 @@ struct landlock_request { * extra one is useful to detect uninitialized field. */ size_t layer_plus_one; + + /* Required field for configurable access control. */ + access_mask_t access; + + /* Required fields for requests with layer masks. */ + const layer_mask_t (*layer_masks)[]; + size_t layer_masks_size; }; #ifdef CONFIG_AUDIT diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 9b251e4d0762..12be90929ec2 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -726,6 +726,7 @@ static void test_is_eacces_with_write(struct kunit *const test) * those identified by @access_request_parent1). This matrix can * initially refer to domain layer masks and, when the accesses for the * destination and source are the same, to requested layer masks. + * @log_request_parent1: Audit request to fill if the related access is denied. * @dentry_child1: Dentry to the initial child of the parent1 path. This * pointer must be NULL for non-refer actions (i.e. not link nor rename). * @access_request_parent2: Similar to @access_request_parent1 but for a @@ -734,6 +735,7 @@ static void test_is_eacces_with_write(struct kunit *const test) * the source. Must be set to 0 when using a simple path request. * @layer_masks_parent2: Similar to @layer_masks_parent1 but for a refer * action. This must be NULL otherwise. + * @log_request_parent2: Audit request to fill if the related access is denied. * @dentry_child2: Dentry to the initial child of the parent2 path. This * pointer is only set for RENAME_EXCHANGE actions and must be NULL * otherwise. @@ -753,10 +755,12 @@ static bool is_access_to_paths_allowed( const struct path *const path, const access_mask_t access_request_parent1, layer_mask_t (*const layer_masks_parent1)[LANDLOCK_NUM_ACCESS_FS], - const struct dentry *const dentry_child1, + struct landlock_request *const log_request_parent1, + struct dentry *const dentry_child1, const access_mask_t access_request_parent2, layer_mask_t (*const layer_masks_parent2)[LANDLOCK_NUM_ACCESS_FS], - const struct dentry *const dentry_child2) + struct landlock_request *const log_request_parent2, + struct dentry *const dentry_child2) { bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check, child1_is_directory = true, child2_is_directory = true; @@ -921,6 +925,25 @@ jump_up: } path_put(&walker_path); + if (!allowed_parent1) { + log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS; + log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH; + log_request_parent1->audit.u.path = *path; + log_request_parent1->access = access_masked_parent1; + log_request_parent1->layer_masks = layer_masks_parent1; + log_request_parent1->layer_masks_size = + ARRAY_SIZE(*layer_masks_parent1); + } + + if (!allowed_parent2) { + log_request_parent2->type = LANDLOCK_REQUEST_FS_ACCESS; + log_request_parent2->audit.type = LSM_AUDIT_DATA_PATH; + log_request_parent2->audit.u.path = *path; + log_request_parent2->access = access_masked_parent2; + log_request_parent2->layer_masks = layer_masks_parent2; + log_request_parent2->layer_masks_size = + ARRAY_SIZE(*layer_masks_parent2); + } return allowed_parent1 && allowed_parent2; } @@ -933,6 +956,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); layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; + struct landlock_request request = {}; if (!subject) return 0; @@ -941,9 +965,11 @@ static int current_check_access_path(const struct path *const path, access_request, &layer_masks, LANDLOCK_KEY_INODE); if (is_access_to_paths_allowed(subject->domain, path, access_request, - &layer_masks, NULL, 0, NULL, NULL)) + &layer_masks, &request, NULL, 0, NULL, + NULL, NULL)) return 0; + landlock_log_denial(subject, &request); return -EACCES; } @@ -1112,6 +1138,7 @@ static int current_check_refer_path(struct dentry *const old_dentry, struct dentry *old_parent; layer_mask_t layer_masks_parent1[LANDLOCK_NUM_ACCESS_FS] = {}, layer_masks_parent2[LANDLOCK_NUM_ACCESS_FS] = {}; + struct landlock_request request1 = {}, request2 = {}; if (!subject) return 0; @@ -1143,10 +1170,13 @@ static int current_check_refer_path(struct dentry *const old_dentry, subject->domain, access_request_parent1 | access_request_parent2, &layer_masks_parent1, LANDLOCK_KEY_INODE); - if (is_access_to_paths_allowed( - subject->domain, new_dir, access_request_parent1, - &layer_masks_parent1, NULL, 0, NULL, NULL)) + if (is_access_to_paths_allowed(subject->domain, new_dir, + access_request_parent1, + &layer_masks_parent1, &request1, + NULL, 0, NULL, NULL, NULL)) return 0; + + landlock_log_denial(subject, &request1); return -EACCES; } @@ -1185,10 +1215,20 @@ static int current_check_refer_path(struct dentry *const old_dentry, */ if (is_access_to_paths_allowed( subject->domain, &mnt_dir, access_request_parent1, - &layer_masks_parent1, old_dentry, access_request_parent2, - &layer_masks_parent2, exchange ? new_dentry : NULL)) + &layer_masks_parent1, &request1, old_dentry, + access_request_parent2, &layer_masks_parent2, &request2, + exchange ? new_dentry : NULL)) return 0; + if (request1.access) { + request1.audit.u.path.dentry = old_parent; + landlock_log_denial(subject, &request1); + } + if (request2.access) { + request2.audit.u.path.dentry = new_dir->dentry; + landlock_log_denial(subject, &request2); + } + /* * This prioritizes EACCES over EXDEV for all actions, including * renames with RENAME_EXCHANGE. @@ -1578,6 +1618,7 @@ static int hook_file_open(struct file *const file) optional_access; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(file->f_cred, any_fs, NULL); + struct landlock_request request = {}; if (!subject) return 0; @@ -1604,7 +1645,7 @@ static int hook_file_open(struct file *const file) landlock_init_layer_masks(subject->domain, full_access_request, &layer_masks, LANDLOCK_KEY_INODE), - &layer_masks, NULL, 0, NULL, NULL)) { + &layer_masks, &request, NULL, 0, NULL, NULL, NULL)) { allowed_access = full_access_request; } else { unsigned long access_bit; @@ -1634,6 +1675,9 @@ static int hook_file_open(struct file *const file) if ((open_access_request & allowed_access) == open_access_request) return 0; + /* Sets access to reflect the actual request. */ + request.access = open_access_request; + landlock_log_denial(subject, &request); return -EACCES; } -- cgit From e120b3c29302532aba2f46c0544ad728c56c11f5 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:02 +0100 Subject: landlock: Factor out IOCTL hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compat and non-compat IOCTL hooks are almost the same, except to compare the IOCTL command. Factor out these two IOCTL hooks to highlight the difference and minimize audit changes (see next commit). Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-14-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/fs.c | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) (limited to 'security') diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 12be90929ec2..c24fcc70262d 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1698,8 +1698,8 @@ static int hook_file_truncate(struct file *const file) return -EACCES; } -static int hook_file_ioctl(struct file *file, unsigned int cmd, - unsigned long arg) +static int hook_file_ioctl_common(const struct file *const file, + const unsigned int cmd, const bool is_compat) { access_mask_t allowed_access = landlock_file(file)->allowed_access; @@ -1715,33 +1715,23 @@ static int hook_file_ioctl(struct file *file, unsigned int cmd, if (!is_device(file)) return 0; - if (is_masked_device_ioctl(cmd)) + if (unlikely(is_compat) ? is_masked_device_ioctl_compat(cmd) : + is_masked_device_ioctl(cmd)) return 0; return -EACCES; } +static int hook_file_ioctl(struct file *file, unsigned int cmd, + unsigned long arg) +{ + return hook_file_ioctl_common(file, cmd, false); +} + static int hook_file_ioctl_compat(struct file *file, unsigned int cmd, unsigned long arg) { - access_mask_t allowed_access = landlock_file(file)->allowed_access; - - /* - * It is the access rights at the time of opening the file which - * determine whether IOCTL can be used on the opened file later. - * - * The access right is attached to the opened file in hook_file_open(). - */ - if (allowed_access & LANDLOCK_ACCESS_FS_IOCTL_DEV) - return 0; - - if (!is_device(file)) - return 0; - - if (is_masked_device_ioctl_compat(cmd)) - return 0; - - return -EACCES; + return hook_file_ioctl_common(file, cmd, true); } /* -- cgit From 20fd2954945458c1b04060d1ce6320f897b3a701 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:03 +0100 Subject: landlock: Log truncate and IOCTL denials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add audit support to the file_truncate and file_ioctl hooks. Add a deny_masks_t type and related helpers to store the domain's layer level per optional access rights (i.e. LANDLOCK_ACCESS_FS_TRUNCATE and LANDLOCK_ACCESS_FS_IOCTL_DEV) when opening a file, which cannot be inferred later. In practice, the landlock_file_security aligned blob size is still 16 bytes because this new one-byte deny_masks field follows the existing two-bytes allowed_access field and precede the packed fown_subject. Implementing deny_masks_t with a bitfield instead of a struct enables a generic implementation to store and extract layer levels. Add KUnit tests to check the identification of a layer level from a deny_masks_t, and the computation of a deny_masks_t from an access right with its layer level or a layer_mask_t array. Audit event sample: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=fs.ioctl_dev path="/dev/tty" dev="devtmpfs" ino=9 ioctlcmd=0x5401 Cc: Günther Noack Link: https://lore.kernel.org/r/20250320190717.2287696-15-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/access.h | 25 ++++++++- security/landlock/audit.c | 101 ++++++++++++++++++++++++++++++++-- security/landlock/audit.h | 4 ++ security/landlock/domain.c | 133 +++++++++++++++++++++++++++++++++++++++++++++ security/landlock/domain.h | 7 +++ security/landlock/fs.c | 34 ++++++++++++ security/landlock/fs.h | 9 +++ 7 files changed, 307 insertions(+), 6 deletions(-) (limited to 'security') diff --git a/security/landlock/access.h b/security/landlock/access.h index 74fd8f399fbd..7961c6630a2d 100644 --- a/security/landlock/access.h +++ b/security/landlock/access.h @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: GPL-2.0-only */ /* - * Landlock LSM - Access types and helpers + * Landlock - Access types and helpers * * Copyright © 2016-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI @@ -28,6 +28,12 @@ LANDLOCK_ACCESS_FS_REFER) /* clang-format on */ +/* clang-format off */ +#define _LANDLOCK_ACCESS_FS_OPTIONAL ( \ + LANDLOCK_ACCESS_FS_TRUNCATE | \ + LANDLOCK_ACCESS_FS_IOCTL_DEV) +/* clang-format on */ + typedef u16 access_mask_t; /* Makes sure all filesystem access rights can be stored. */ @@ -60,6 +66,23 @@ typedef u16 layer_mask_t; /* Makes sure all layers can be checked. */ static_assert(BITS_PER_TYPE(layer_mask_t) >= LANDLOCK_MAX_NUM_LAYERS); +/* + * Tracks domains responsible of a denied access. This is required to avoid + * storing in each object the full layer_masks[] required by update_request(). + */ +typedef u8 deny_masks_t; + +/* + * Makes sure all optional access rights can be tied to a layer index (cf. + * get_deny_mask). + */ +static_assert(BITS_PER_TYPE(deny_masks_t) >= + (HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) * + HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL))); + +/* LANDLOCK_MAX_NUM_LAYERS must be a power of two (cf. deny_masks_t assert). */ +static_assert(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS) == 1); + /* Upgrades with all initially denied by default access rights. */ static inline struct access_masks landlock_upgrade_handled_access_masks(struct access_masks access_masks) diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 45e1c6ad1856..0127646abe79 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -12,6 +12,7 @@ #include #include +#include "access.h" #include "audit.h" #include "common.h" #include "cred.h" @@ -249,6 +250,88 @@ static void test_get_denied_layer(struct kunit *const test) #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_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 unsigned long access_opt = all_existing_optional_access; + const unsigned long access_req = *access_request; + access_mask_t missing = 0; + size_t youngest_layer = 0; + size_t access_index = 0; + unsigned long access_bit; + + /* 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)) { + if (access_req & BIT(access_bit)) { + const size_t layer = + (deny_masks >> (access_index * 4)) & + (LANDLOCK_MAX_NUM_LAYERS - 1); + + if (layer > youngest_layer) { + youngest_layer = layer; + missing = BIT(access_bit); + } else if (layer == youngest_layer) { + missing |= BIT(access_bit); + } + } + access_index++; + } + + *access_request = missing; + return youngest_layer; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_layer_from_deny_masks(struct kunit *const test) +{ + deny_masks_t deny_mask; + access_mask_t access; + + /* truncate:0 ioctl_dev:2 */ + deny_mask = 0x20; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks(&access, + _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + + 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)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + + /* truncate:15 ioctl_dev:15 */ + deny_mask = 0xff; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks(&access, + _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + + 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)); + KUNIT_EXPECT_EQ(test, access, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + static bool is_valid_request(const struct landlock_request *const request) { if (WARN_ON_ONCE(request->layer_plus_one > LANDLOCK_MAX_NUM_LAYERS)) @@ -258,16 +341,23 @@ static bool is_valid_request(const struct landlock_request *const request) return false; if (request->access) { - if (WARN_ON_ONCE(!request->layer_masks)) + if (WARN_ON_ONCE(!(!!request->layer_masks ^ + !!request->all_existing_optional_access))) return false; } else { - if (WARN_ON_ONCE(request->layer_masks)) + if (WARN_ON_ONCE(request->layer_masks || + request->all_existing_optional_access)) return false; } if (WARN_ON_ONCE(!!request->layer_masks ^ !!request->layer_masks_size)) return false; + if (request->deny_masks) { + if (WARN_ON_ONCE(!request->all_existing_optional_access)) + return false; + } + return true; } @@ -300,9 +390,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, subject->domain, &missing, request->layer_masks, request->layer_masks_size); } else { - /* This will change with the next commit. */ - WARN_ON_ONCE(1); - youngest_layer = subject->domain->num_layers; + youngest_layer = get_layer_from_deny_masks( + &missing, request->all_existing_optional_access, + request->deny_masks); } youngest_denied = get_hierarchy(subject->domain, youngest_layer); @@ -387,6 +477,7 @@ static struct kunit_case test_cases[] = { /* clang-format off */ KUNIT_CASE(test_get_hierarchy), KUNIT_CASE(test_get_denied_layer), + KUNIT_CASE(test_get_layer_from_deny_masks), {} /* clang-format on */ }; diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 2a154116134e..8130a6fcad07 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -42,6 +42,10 @@ struct landlock_request { /* Required fields for requests with layer masks. */ const layer_mask_t (*layer_masks)[]; size_t layer_masks_size; + + /* Required fields for requests with deny masks. */ + const access_mask_t all_existing_optional_access; + deny_masks_t deny_masks; }; #ifdef CONFIG_AUDIT diff --git a/security/landlock/domain.c b/security/landlock/domain.c index 3c1715e4b1c9..f61fa5ae8e56 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -7,6 +7,9 @@ * Copyright © 2024-2025 Microsoft Corporation */ +#include +#include +#include #include #include #include @@ -15,6 +18,8 @@ #include #include +#include "access.h" +#include "common.h" #include "domain.h" #include "id.h" @@ -126,4 +131,132 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) return 0; } +static deny_masks_t +get_layer_deny_mask(const access_mask_t all_existing_optional_access, + const unsigned long access_bit, const size_t layer) +{ + unsigned long access_weight; + + /* This may require change with new object types. */ + WARN_ON_ONCE(all_existing_optional_access != + _LANDLOCK_ACCESS_FS_OPTIONAL); + + if (WARN_ON_ONCE(layer >= LANDLOCK_MAX_NUM_LAYERS)) + return 0; + + access_weight = hweight_long(all_existing_optional_access & + GENMASK(access_bit, 0)); + if (WARN_ON_ONCE(access_weight < 1)) + return 0; + + return layer + << ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1)); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_layer_deny_mask(struct kunit *const test) +{ + const unsigned long truncate = BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE); + const unsigned long ioctl_dev = BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV); + + KUNIT_EXPECT_EQ(test, 0, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + truncate, 0)); + KUNIT_EXPECT_EQ(test, 0x3, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + truncate, 3)); + + KUNIT_EXPECT_EQ(test, 0, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + ioctl_dev, 0)); + KUNIT_EXPECT_EQ(test, 0xf0, + get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, + ioctl_dev, 15)); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +deny_masks_t +landlock_get_deny_masks(const access_mask_t all_existing_optional_access, + const access_mask_t optional_access, + const layer_mask_t (*const layer_masks)[], + const size_t layer_masks_size) +{ + const unsigned long access_opt = optional_access; + unsigned long access_bit; + deny_masks_t deny_masks = 0; + + /* This may require change with new object types. */ + WARN_ON_ONCE(access_opt != + (optional_access & all_existing_optional_access)); + + if (WARN_ON_ONCE(!layer_masks)) + return 0; + + if (WARN_ON_ONCE(!access_opt)) + return 0; + + for_each_set_bit(access_bit, &access_opt, layer_masks_size) { + const layer_mask_t mask = (*layer_masks)[access_bit]; + + if (!mask) + continue; + + /* __fls(1) == 0 */ + deny_masks |= get_layer_deny_mask(all_existing_optional_access, + access_bit, __fls(mask)); + } + return deny_masks; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_landlock_get_deny_masks(struct kunit *const test) +{ + const layer_mask_t layers1[BITS_PER_TYPE(access_mask_t)] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0) | + BIT_ULL(9), + [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = BIT_ULL(1), + [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = BIT_ULL(2) | + BIT_ULL(0), + }; + + KUNIT_EXPECT_EQ(test, 0x1, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_TRUNCATE, + &layers1, ARRAY_SIZE(layers1))); + KUNIT_EXPECT_EQ(test, 0x20, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_IOCTL_DEV, + &layers1, ARRAY_SIZE(layers1))); + KUNIT_EXPECT_EQ( + test, 0x21, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV, + &layers1, ARRAY_SIZE(layers1))); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static struct kunit_case test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_get_layer_deny_mask), + KUNIT_CASE(test_landlock_get_deny_masks), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_domain", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + #endif /* CONFIG_AUDIT */ diff --git a/security/landlock/domain.h b/security/landlock/domain.h index 2c6f04da3628..96642dd87b33 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -18,6 +18,7 @@ #include #include +#include "access.h" #include "audit.h" enum landlock_log_status { @@ -107,6 +108,12 @@ struct landlock_hierarchy { #ifdef CONFIG_AUDIT +deny_masks_t +landlock_get_deny_masks(const access_mask_t all_existing_optional_access, + const access_mask_t optional_access, + const layer_mask_t (*const layer_masks)[], + size_t layer_masks_size); + 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 c24fcc70262d..0a58962cf61d 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -43,6 +43,7 @@ #include "audit.h" #include "common.h" #include "cred.h" +#include "domain.h" #include "fs.h" #include "limits.h" #include "object.h" @@ -1671,6 +1672,11 @@ static int hook_file_open(struct file *const file) * file access rights in the opened struct file. */ landlock_file(file)->allowed_access = allowed_access; +#ifdef CONFIG_AUDIT + landlock_file(file)->deny_masks = landlock_get_deny_masks( + _LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks, + ARRAY_SIZE(layer_masks)); +#endif /* CONFIG_AUDIT */ if ((open_access_request & allowed_access) == open_access_request) return 0; @@ -1695,6 +1701,19 @@ static int hook_file_truncate(struct file *const file) */ if (landlock_file(file)->allowed_access & LANDLOCK_ACCESS_FS_TRUNCATE) return 0; + + landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_ACCESS, + .audit = { + .type = LSM_AUDIT_DATA_FILE, + .u.file = file, + }, + .all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL, + .access = LANDLOCK_ACCESS_FS_TRUNCATE, +#ifdef CONFIG_AUDIT + .deny_masks = landlock_file(file)->deny_masks, +#endif /* CONFIG_AUDIT */ + }); return -EACCES; } @@ -1719,6 +1738,21 @@ static int hook_file_ioctl_common(const struct file *const file, is_masked_device_ioctl(cmd)) return 0; + landlock_log_denial(landlock_cred(file->f_cred), &(struct landlock_request) { + .type = LANDLOCK_REQUEST_FS_ACCESS, + .audit = { + .type = LSM_AUDIT_DATA_IOCTL_OP, + .u.op = &(struct lsm_ioctlop_audit) { + .path = file->f_path, + .cmd = cmd, + }, + }, + .all_existing_optional_access = _LANDLOCK_ACCESS_FS_OPTIONAL, + .access = LANDLOCK_ACCESS_FS_IOCTL_DEV, +#ifdef CONFIG_AUDIT + .deny_masks = landlock_file(file)->deny_masks, +#endif /* CONFIG_AUDIT */ + }); return -EACCES; } diff --git a/security/landlock/fs.h b/security/landlock/fs.h index b29972eb9224..8c48fad4e123 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -55,6 +55,15 @@ struct landlock_file_security { * needed to authorize later operations on the open file. */ access_mask_t allowed_access; + +#ifdef CONFIG_AUDIT + /** + * @deny_masks: Domain layer levels that deny an optional access (see + * _LANDLOCK_ACCESS_FS_OPTIONAL). + */ + deny_masks_t deny_masks; +#endif /* CONFIG_AUDIT */ + /** * @fown_subject: Landlock credential of the task that set the PID that * may receive a signal e.g., SIGURG when writing MSG_OOB to the -- cgit From 9f74411a40cecc6faca2a3e3bbb7c1834276d4a2 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:04 +0100 Subject: landlock: Log TCP bind and connect denials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add audit support to socket_bind and socket_connect hooks. The related blockers are: - net.bind_tcp - net.connect_tcp Audit event sample: type=LANDLOCK_DENY msg=audit(1729738800.349:44): domain=195ba459b blockers=net.connect_tcp daddr=127.0.0.1 dest=80 Cc: Günther Noack Cc: Konstantin Meskhidze Cc: Mikhail Ivanov Link: https://lore.kernel.org/r/20250320190717.2287696-16-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/audit.c | 12 +++++++++++ security/landlock/audit.h | 1 + security/landlock/net.c | 51 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 60 insertions(+), 4 deletions(-) (limited to 'security') diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 0127646abe79..ed8fa129178d 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -41,6 +41,13 @@ static const char *const fs_access_strings[] = { 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", +}; + +static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET); + static __attribute_const__ const char * get_blocker(const enum landlock_request_type type, const unsigned long access_bit) @@ -58,6 +65,11 @@ get_blocker(const enum landlock_request_type type, if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings))) return "unknown"; return fs_access_strings[access_bit]; + + case LANDLOCK_REQUEST_NET_ACCESS: + if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(net_access_strings))) + return "unknown"; + return net_access_strings[access_bit]; } WARN_ON_ONCE(1); diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 8130a6fcad07..486b4e7050d3 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -18,6 +18,7 @@ enum landlock_request_type { LANDLOCK_REQUEST_PTRACE = 1, LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, LANDLOCK_REQUEST_FS_ACCESS, + LANDLOCK_REQUEST_NET_ACCESS, }; /* diff --git a/security/landlock/net.c b/security/landlock/net.c index c6ba42f3529c..1f3915a90a80 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -7,10 +7,12 @@ */ #include +#include #include #include #include +#include "audit.h" #include "common.h" #include "cred.h" #include "limits.h" @@ -55,6 +57,7 @@ static int current_check_access_socket(struct socket *const sock, }; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), masks, NULL); + struct lsm_network_audit audit_net = {}; if (!subject) return 0; @@ -68,18 +71,48 @@ static int current_check_access_socket(struct socket *const sock, switch (address->sa_family) { case AF_UNSPEC: - case AF_INET: + case AF_INET: { + const struct sockaddr_in *addr4; + if (addrlen < sizeof(struct sockaddr_in)) return -EINVAL; - port = ((struct sockaddr_in *)address)->sin_port; + + addr4 = (struct sockaddr_in *)address; + port = addr4->sin_port; + + 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) { + audit_net.sport = port; + audit_net.v4info.saddr = addr4->sin_addr.s_addr; + } else { + WARN_ON_ONCE(1); + } break; + } #if IS_ENABLED(CONFIG_IPV6) - case AF_INET6: + case AF_INET6: { + const struct sockaddr_in6 *addr6; + if (addrlen < SIN6_LEN_RFC2133) return -EINVAL; - port = ((struct sockaddr_in6 *)address)->sin6_port; + + addr6 = (struct sockaddr_in6 *)address; + port = addr6->sin6_port; + + 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) { + audit_net.sport = port; + audit_net.v6info.saddr = addr6->sin6_addr; + } else { + WARN_ON_ONCE(1); + } break; + } #endif /* IS_ENABLED(CONFIG_IPV6) */ default: @@ -149,6 +182,16 @@ static int current_check_access_socket(struct socket *const sock, ARRAY_SIZE(layer_masks))) return 0; + audit_net.family = address->sa_family; + landlock_log_denial(subject, + &(struct landlock_request){ + .type = LANDLOCK_REQUEST_NET_ACCESS, + .audit.type = LSM_AUDIT_DATA_NET, + .audit.u.net = &audit_net, + .access = access_request, + .layer_masks = &layer_masks, + .layer_masks_size = ARRAY_SIZE(layer_masks), + }); return -EACCES; } -- cgit From 1176a15b5ec02925ea89bae05b5c860ddcce1e2e Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:05 +0100 Subject: landlock: Log scoped denials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add audit support for unix_stream_connect, unix_may_send, task_kill, and file_send_sigiotask hooks. The related blockers are: - scope.abstract_unix_socket - scope.signal Audit event sample for abstract unix socket: type=LANDLOCK_DENY msg=audit(1729738800.268:30): domain=195ba459b blockers=scope.abstract_unix_socket path=00666F6F Audit event sample for signal: type=LANDLOCK_DENY msg=audit(1729738800.291:31): domain=195ba459b blockers=scope.signal opid=1 ocomm="systemd" Refactor and simplify error handling in LSM hooks. Extend struct landlock_file_security with fown_layer and use it to log the blocking domain. The struct aligned size is still 16 bytes. Cc: Günther Noack Cc: Tahera Fahimi Link: https://lore.kernel.org/r/20250320190717.2287696-17-mic@digikod.net Signed-off-by: Mickaël Salaün --- security/landlock/audit.c | 8 +++++ security/landlock/audit.h | 2 ++ security/landlock/fs.c | 8 +++-- security/landlock/fs.h | 16 ++++++++++ security/landlock/task.c | 81 +++++++++++++++++++++++++++++++++++++---------- 5 files changed, 97 insertions(+), 18 deletions(-) (limited to 'security') diff --git a/security/landlock/audit.c b/security/landlock/audit.c index ed8fa129178d..77d11355f6ed 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -70,6 +70,14 @@ get_blocker(const enum landlock_request_type type, if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(net_access_strings))) return "unknown"; return net_access_strings[access_bit]; + + case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: + WARN_ON_ONCE(access_bit != -1); + return "scope.abstract_unix_socket"; + + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + WARN_ON_ONCE(access_bit != -1); + return "scope.signal"; } WARN_ON_ONCE(1); diff --git a/security/landlock/audit.h b/security/landlock/audit.h index 486b4e7050d3..92428b7fc4d8 100644 --- a/security/landlock/audit.h +++ b/security/landlock/audit.h @@ -19,6 +19,8 @@ enum landlock_request_type { LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, LANDLOCK_REQUEST_FS_ACCESS, LANDLOCK_REQUEST_NET_ACCESS, + LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET, + LANDLOCK_REQUEST_SCOPE_SIGNAL, }; /* diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 0a58962cf61d..3e98b748033c 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -1798,14 +1798,15 @@ static void hook_file_set_fowner(struct file *file) { struct landlock_ruleset *prev_dom; struct landlock_cred_security fown_subject = {}; + size_t fown_layer = 0; if (control_current_fowner(file_f_owner(file))) { static const struct access_masks signal_scope = { .scope = LANDLOCK_SCOPE_SIGNAL, }; const struct landlock_cred_security *new_subject = - landlock_get_applicable_subject(current_cred(), - signal_scope, NULL); + landlock_get_applicable_subject( + current_cred(), signal_scope, &fown_layer); if (new_subject) { landlock_get_ruleset(new_subject->domain); fown_subject = *new_subject; @@ -1814,6 +1815,9 @@ static void hook_file_set_fowner(struct file *file) prev_dom = landlock_file(file)->fown_subject.domain; landlock_file(file)->fown_subject = fown_subject; +#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); diff --git a/security/landlock/fs.h b/security/landlock/fs.h index 8c48fad4e123..bf9948941f2f 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -10,6 +10,7 @@ #ifndef _SECURITY_LANDLOCK_FS_H #define _SECURITY_LANDLOCK_FS_H +#include #include #include #include @@ -62,6 +63,11 @@ struct landlock_file_security { * _LANDLOCK_ACCESS_FS_OPTIONAL). */ deny_masks_t deny_masks; + /** + * @fown_layer: Layer level of @fown_subject->domain with + * LANDLOCK_SCOPE_SIGNAL. + */ + u8 fown_layer; #endif /* CONFIG_AUDIT */ /** @@ -74,6 +80,16 @@ struct landlock_file_security { struct landlock_cred_security fown_subject; }; +#ifdef CONFIG_AUDIT + +/* Makes sure all layers can be identified. */ +/* clang-format off */ +static_assert((typeof_member(struct landlock_file_security, fown_layer))~0 >= + LANDLOCK_MAX_NUM_LAYERS); +/* clang-format off */ + +#endif /* CONFIG_AUDIT */ + /** * struct landlock_superblock_security - Superblock security blob * diff --git a/security/landlock/task.c b/security/landlock/task.c index 30ac4340c62e..2385017418ca 100644 --- a/security/landlock/task.c +++ b/security/landlock/task.c @@ -266,26 +266,41 @@ static int hook_unix_stream_connect(struct sock *const sock, struct sock *const other, struct sock *const newsk) { + size_t handle_layer; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), unix_scope, - NULL); + &handle_layer); /* Quick return for non-landlocked tasks. */ if (!subject) return 0; - if (is_abstract_socket(other) && sock_is_scoped(other, subject->domain)) - return -EPERM; + if (!is_abstract_socket(other)) + return 0; + + if (!sock_is_scoped(other, subject->domain)) + return 0; - return 0; + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET, + .audit = { + .type = LSM_AUDIT_DATA_NET, + .u.net = &(struct lsm_network_audit) { + .sk = other, + }, + }, + .layer_plus_one = handle_layer + 1, + }); + return -EPERM; } static int hook_unix_may_send(struct socket *const sock, struct socket *const other) { + size_t handle_layer; const struct landlock_cred_security *const subject = landlock_get_applicable_subject(current_cred(), unix_scope, - NULL); + &handle_layer); if (!subject) return 0; @@ -297,11 +312,23 @@ static int hook_unix_may_send(struct socket *const sock, if (unix_peer(sock->sk) == other->sk) return 0; - if (is_abstract_socket(other->sk) && - sock_is_scoped(other->sk, subject->domain)) - return -EPERM; + if (!is_abstract_socket(other->sk)) + return 0; + + if (!sock_is_scoped(other->sk, subject->domain)) + return 0; - return 0; + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET, + .audit = { + .type = LSM_AUDIT_DATA_NET, + .u.net = &(struct lsm_network_audit) { + .sk = other->sk, + }, + }, + .layer_plus_one = handle_layer + 1, + }); + return -EPERM; } static const struct access_masks signal_scope = { @@ -313,6 +340,7 @@ static int hook_task_kill(struct task_struct *const p, const struct cred *cred) { bool is_scoped; + size_t handle_layer; const struct landlock_cred_security *subject; if (!cred) { @@ -331,7 +359,8 @@ static int hook_task_kill(struct task_struct *const p, cred = current_cred(); } - subject = landlock_get_applicable_subject(cred, signal_scope, NULL); + subject = landlock_get_applicable_subject(cred, signal_scope, + &handle_layer); /* Quick return for non-landlocked tasks. */ if (!subject) @@ -343,10 +372,19 @@ static int hook_task_kill(struct task_struct *const p, landlock_get_task_domain(p), signal_scope.scope); } - if (is_scoped) - return -EPERM; - return 0; + if (!is_scoped) + return 0; + + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_SIGNAL, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = p, + }, + .layer_plus_one = handle_layer + 1, + }); + return -EPERM; } static int hook_file_send_sigiotask(struct task_struct *tsk, @@ -375,10 +413,21 @@ static int hook_file_send_sigiotask(struct task_struct *tsk, landlock_get_task_domain(tsk), signal_scope.scope); } - if (is_scoped) - return -EPERM; - return 0; + if (!is_scoped) + return 0; + + landlock_log_denial(subject, &(struct landlock_request) { + .type = LANDLOCK_REQUEST_SCOPE_SIGNAL, + .audit = { + .type = LSM_AUDIT_DATA_TASK, + .u.tsk = tsk, + }, +#ifdef CONFIG_AUDIT + .layer_plus_one = landlock_file(fown->file)->fown_layer + 1, +#endif /* CONFIG_AUDIT */ + }); + return -EPERM; } static struct security_hook_list landlock_hooks[] __ro_after_init = { -- cgit From 12bfcda73ac2cf3083c9d6d05724af92da3a4b4b Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:06 +0100 Subject: landlock: Add LANDLOCK_RESTRICT_SELF_LOG_*_EXEC_* flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most of the time we want to log denied access because they should not happen and such information helps diagnose issues. However, when sandboxing processes that we know will try to access denied resources (e.g. unknown, bogus, or malicious binary), we might want to not log related access requests that might fill up logs. By default, denied requests are logged until the task call execve(2). If the LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF flag is set, denied requests will not be logged for the same executed file. If the LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON flag is set, denied requests from after an execve(2) call will be logged. The rationale is that a program should know its own behavior, but not necessarily the behavior of other programs. Because LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF is set for a specific Landlock domain, it makes it possible to selectively mask some access requests that would be logged by a parent domain, which might be handy for unprivileged processes to limit logs. However, system administrators should still use the audit filtering mechanism. There is intentionally no audit nor sysctl configuration to re-enable these logs. This is delegated to the user space program. Increment the Landlock ABI version to reflect this interface change. Cc: Günther Noack Cc: Paul Moore Link: https://lore.kernel.org/r/20250320190717.2287696-18-mic@digikod.net [mic: Rename variables and fix __maybe_unused] Signed-off-by: Mickaël Salaün --- security/landlock/audit.c | 16 +++++++++++++--- security/landlock/domain.c | 2 ++ security/landlock/domain.h | 11 +++++++++++ security/landlock/limits.h | 7 ++++++- security/landlock/syscalls.c | 39 +++++++++++++++++++++++++++++++-------- 5 files changed, 63 insertions(+), 12 deletions(-) (limited to 'security') diff --git a/security/landlock/audit.c b/security/landlock/audit.c index 77d11355f6ed..7e5e0ed0e4e5 100644 --- a/security/landlock/audit.c +++ b/security/landlock/audit.c @@ -422,6 +422,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, get_hierarchy(subject->domain, youngest_layer); } + if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED) + return; + /* * Consistently keeps track of the number of denied access requests * even if audit is currently disabled, or if audit rules currently @@ -433,9 +436,16 @@ void landlock_log_denial(const struct landlock_cred_security *const subject, if (!audit_enabled) return; - /* Ignores denials after an execution. */ - if (!(subject->domain_exec & (1 << youngest_layer))) - return; + /* Checks if the current exec was restricting itself. */ + if (subject->domain_exec & (1 << youngest_layer)) { + /* Ignores denials for the same execution. */ + if (!youngest_denied->log_same_exec) + return; + } else { + /* Ignores denials after a new execution. */ + if (!youngest_denied->log_new_exec) + return; + } /* Uses consistent allocation flags wrt common_lsm_audit(). */ ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN, diff --git a/security/landlock/domain.c b/security/landlock/domain.c index f61fa5ae8e56..bae2e9909013 100644 --- a/security/landlock/domain.c +++ b/security/landlock/domain.c @@ -127,6 +127,8 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) hierarchy->details = details; hierarchy->id = landlock_get_id_range(1); hierarchy->log_status = LANDLOCK_LOG_PENDING; + hierarchy->log_same_exec = true; + hierarchy->log_new_exec = false; atomic64_set(&hierarchy->num_denials, 0); return 0; } diff --git a/security/landlock/domain.h b/security/landlock/domain.h index 96642dd87b33..ed0d348e214c 100644 --- a/security/landlock/domain.h +++ b/security/landlock/domain.h @@ -24,6 +24,7 @@ enum landlock_log_status { LANDLOCK_LOG_PENDING = 0, LANDLOCK_LOG_RECORDED, + LANDLOCK_LOG_DISABLED, }; /** @@ -103,6 +104,16 @@ struct landlock_hierarchy { * @details: Information about the related domain. */ const struct landlock_details *details; + /** + * @log_same_exec: Set if the domain is *not* configured with + * %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF. Set to true by default. + */ + u32 log_same_exec : 1, + /** + * @log_new_exec: Set if the domain is configured with + * %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Set to false by default. + */ + log_new_exec : 1; #endif /* CONFIG_AUDIT */ }; diff --git a/security/landlock/limits.h b/security/landlock/limits.h index 15f7606066c8..404e880cccf9 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -1,9 +1,10 @@ /* SPDX-License-Identifier: GPL-2.0-only */ /* - * Landlock LSM - Limits for different components + * Landlock - Limits for different components * * Copyright © 2016-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation */ #ifndef _SECURITY_LANDLOCK_LIMITS_H @@ -29,6 +30,10 @@ #define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL #define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1) #define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE) + +#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON +#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1) + /* clang-format on */ #endif /* _SECURITY_LANDLOCK_LIMITS_H */ diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index b7b268f43a3b..75bc9fcd0a8f 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -1,9 +1,10 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Landlock LSM - System call implementations and user space interfaces + * Landlock - System call implementations and user space interfaces * * Copyright © 2016-2020 Mickaël Salaün * Copyright © 2018-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation */ #include @@ -28,6 +29,7 @@ #include #include "cred.h" +#include "domain.h" #include "fs.h" #include "limits.h" #include "net.h" @@ -151,7 +153,14 @@ static const struct file_operations ruleset_fops = { .write = fop_dummy_write, }; -#define LANDLOCK_ABI_VERSION 6 +/* + * The Landlock ABI version should be incremented for each new Landlock-related + * user space visible change (e.g. Landlock syscalls). This version should + * only be incremented once per Linux release, and the date in + * Documentation/userspace-api/landlock.rst should be updated to reflect the + * UAPI change. + */ +const int landlock_abi_version = 7; /** * sys_landlock_create_ruleset - Create a new ruleset @@ -247,8 +256,6 @@ SYSCALL_DEFINE3(landlock_create_ruleset, return ruleset_fd; } -const int landlock_abi_version = LANDLOCK_ABI_VERSION; - /* * Returns an owned ruleset from a FD. It is thus needed to call * landlock_put_ruleset() on the return value. @@ -443,7 +450,10 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, * sys_landlock_restrict_self - Enforce a ruleset on the calling thread * * @ruleset_fd: File descriptor tied to the ruleset to merge with the target. - * @flags: Must be 0. + * @flags: Supported values: + * + * - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF + * - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON * * This system call enables to enforce a Landlock ruleset on the current * thread. Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its @@ -453,7 +463,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, * Possible returned errors are: * * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; - * - %EINVAL: @flags is not 0. + * - %EINVAL: @flags contains an unknown bit. * - %EBADF: @ruleset_fd is not a file descriptor for the current thread; * - %EBADFD: @ruleset_fd is not a ruleset file descriptor; * - %EPERM: @ruleset_fd has no read access to the underlying ruleset, or the @@ -469,6 +479,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, *ruleset __free(landlock_put_ruleset) = NULL; struct cred *new_cred; struct landlock_cred_security *new_llcred; + bool __maybe_unused log_same_exec, log_new_exec; if (!is_initialized()) return -EOPNOTSUPP; @@ -481,10 +492,15 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) return -EPERM; - /* No flag for now. */ - if (flags) + if ((flags | LANDLOCK_MASK_RESTRICT_SELF) != + LANDLOCK_MASK_RESTRICT_SELF) return -EINVAL; + /* Translates "off" flag to boolean. */ + log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF); + /* Translates "on" flag to boolean. */ + log_new_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON); + /* Gets and checks the ruleset. */ ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ); if (IS_ERR(ruleset)) @@ -507,6 +523,13 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, return PTR_ERR(new_dom); } +#ifdef CONFIG_AUDIT + new_dom->hierarchy->log_same_exec = log_same_exec; + new_dom->hierarchy->log_new_exec = log_new_exec; + if (!log_same_exec && !log_new_exec) + new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED; +#endif /* CONFIG_AUDIT */ + /* Replaces the old (prepared) domain. */ landlock_put_ruleset(new_llcred->domain); new_llcred->domain = new_dom; -- cgit From ead9079f75696a028aea8860787770c80eddb8f9 Mon Sep 17 00:00:00 2001 From: Mickaël Salaün Date: Thu, 20 Mar 2025 20:07:07 +0100 Subject: landlock: Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF for the case of sandboxer tools, init systems, or runtime containers launching programs sandboxing themselves in an inconsistent way. Setting this flag should only depends on runtime configuration (i.e. not hardcoded). We don't create a new ruleset's option because this should not be part of the security policy: only the task that enforces the policy (not the one that create it) knows if itself or its children may request denied actions. This is the first and only flag that can be set without actually restricting the caller (i.e. without providing a ruleset). Extend struct landlock_cred_security with a u8 log_subdomains_off. struct landlock_file_security is still 16 bytes. Cc: Günther Noack Cc: Paul Moore Closes: https://github.com/landlock-lsm/linux/issues/3 Link: https://lore.kernel.org/r/20250320190717.2287696-19-mic@digikod.net [mic: Fix comment] Signed-off-by: Mickaël Salaün --- security/landlock/cred.h | 7 +++++++ security/landlock/limits.h | 2 +- security/landlock/syscalls.c | 41 +++++++++++++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 7 deletions(-) (limited to 'security') diff --git a/security/landlock/cred.h b/security/landlock/cred.h index 3bf18551d7b8..c82fe63ec598 100644 --- a/security/landlock/cred.h +++ b/security/landlock/cred.h @@ -40,6 +40,13 @@ struct landlock_cred_security { * landlock_restrict_self(2)). */ u16 domain_exec; + /** + * @log_subdomains_off: Set if the domain descendants's log_status should be + * set to %LANDLOCK_LOG_DISABLED. This is not a landlock_hierarchy + * configuration because it applies to future descendant domains and it does + * not require a current domain. + */ + u8 log_subdomains_off : 1; #endif /* CONFIG_AUDIT */ } __packed; diff --git a/security/landlock/limits.h b/security/landlock/limits.h index 404e880cccf9..65b5ff051674 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -31,7 +31,7 @@ #define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1) #define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE) -#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON +#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF #define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1) /* clang-format on */ diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index 75bc9fcd0a8f..54a9f29e6ebb 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -454,12 +454,16 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, * * - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF * - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON + * - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF * * This system call enables to enforce a Landlock ruleset on the current * thread. Enforcing a ruleset requires that the task has %CAP_SYS_ADMIN in its * namespace or is running with no_new_privs. This avoids scenarios where * unprivileged tasks can affect the behavior of privileged children. * + * It is allowed to only pass the %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF + * flag with a @ruleset_fd value of -1. + * * Possible returned errors are: * * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; @@ -479,7 +483,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, *ruleset __free(landlock_put_ruleset) = NULL; struct cred *new_cred; struct landlock_cred_security *new_llcred; - bool __maybe_unused log_same_exec, log_new_exec; + bool __maybe_unused log_same_exec, log_new_exec, log_subdomains, + prev_log_subdomains; if (!is_initialized()) return -EOPNOTSUPP; @@ -500,11 +505,20 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, log_same_exec = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF); /* Translates "on" flag to boolean. */ log_new_exec = !!(flags & LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON); + /* Translates "off" flag to boolean. */ + log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF); - /* Gets and checks the ruleset. */ - ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ); - if (IS_ERR(ruleset)) - return PTR_ERR(ruleset); + /* + * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with + * -1 as ruleset_fd, but no other flag must be set. + */ + if (!(ruleset_fd == -1 && + flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) { + /* Gets and checks the ruleset. */ + ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ); + if (IS_ERR(ruleset)) + return PTR_ERR(ruleset); + } /* Prepares new credentials. */ new_cred = prepare_creds(); @@ -513,6 +527,21 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, new_llcred = landlock_cred(new_cred); +#ifdef CONFIG_AUDIT + prev_log_subdomains = !new_llcred->log_subdomains_off; + new_llcred->log_subdomains_off = !prev_log_subdomains || + !log_subdomains; +#endif /* CONFIG_AUDIT */ + + /* + * The only case when a ruleset may not be set is if + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1. + * We could optimize this case by not calling commit_creds() if this flag + * was already set, but it is not worth the complexity. + */ + if (!ruleset) + return commit_creds(new_cred); + /* * There is no possible race condition while copying and manipulating * the current credentials because they are dedicated per thread. @@ -526,7 +555,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, #ifdef CONFIG_AUDIT new_dom->hierarchy->log_same_exec = log_same_exec; new_dom->hierarchy->log_new_exec = log_new_exec; - if (!log_same_exec && !log_new_exec) + if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains) new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED; #endif /* CONFIG_AUDIT */ -- cgit