diff options
| author | Sean Christopherson <seanjc@google.com> | 2026-09-23 09:37:21 -0700 |
|---|---|---|
| committer | Paolo Bonzini <pbonzini@redhat.com> | 2026-09-26 00:39:55 -0400 |
| commit | 93de2a6a4b91b72607136dd656edf03fb399d27f (patch) | |
| tree | 9220c76eb23cb39a146fac9692ee69afa4aa3135 /security/landlock | |
| download | linux-stable-93de2a6a4b91b72607136dd656edf03fb399d27f.tar.gz linux-stable-93de2a6a4b91b72607136dd656edf03fb399d27f.zip | |
KVM: SEV: Do cache maintenance on the source VM during intra-host migrationgrafted
Manually perform cache maintenance on the source VM during intra-host
migration to ensure no stale data is left in CPU caches after the VM is
destroyed. Because the source VM is "converted" to a non-SEV VM, KVM's
memory reclaim flows won't trigger cache maintenance, e.g. when all guest
memory is reclaimed in response to detaching from the mmu_notifier.
Note, relying on the destination VM to do cache maintenance isn't an option
as KVM doesn't require identical guest memory configurations, i.e. the
source VM may have access to memory that the destination VM does not.
Enforcing equivalent memory configurations is infeasible, as it would
require a *deep* comparison of memslots, e.g. to verify that not only are
the memslot identical, but what the memslots point at is also identical.
Fixes: b56639318bb2 ("KVM: SEV: Add support for SEV intra host migration")
Cc: stable@vger.kernel.org
Reported-by: Stefan Teodorescu <fane@google.com>
Signed-off-by: Sean Christopherson <seanjc@google.com>
Message-ID: <20260923163721.1584779-3-seanjc@google.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Diffstat (limited to 'security/landlock')
37 files changed, 9024 insertions, 0 deletions
diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig new file mode 100644 index 000000000..fe36228d3 --- /dev/null +++ b/security/landlock/.kunitconfig @@ -0,0 +1,8 @@ +CONFIG_AUDIT=y +CONFIG_FTRACE=y +CONFIG_KUNIT=y +CONFIG_NET=y +CONFIG_SCHED_TRACER=y +CONFIG_SECURITY=y +CONFIG_SECURITY_LANDLOCK=y +CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig new file mode 100644 index 000000000..7aeac2916 --- /dev/null +++ b/security/landlock/Kconfig @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-2.0-only + +config SECURITY_LANDLOCK + bool "Landlock support" + depends on SECURITY + select SECURITY_NETWORK + select SECURITY_PATH + help + Landlock is a sandboxing mechanism that enables processes to restrict + themselves (and their future children) by gradually enforcing + tailored access control policies. A Landlock security policy is a + set of access rights (e.g. open a file in read-only, make a + directory, etc.) tied to a file hierarchy. Such policy can be + configured and enforced by any processes for themselves using the + dedicated system calls: landlock_create_ruleset(), + landlock_add_rule(), and landlock_restrict_self(). + + See Documentation/userspace-api/landlock.rst for further information. + + If you are unsure how to answer this question, answer N. Otherwise, + you should also prepend "landlock," to the content of CONFIG_LSM to + enable Landlock at boot time. + +config SECURITY_LANDLOCK_LOG + bool + depends on SECURITY_LANDLOCK + default y if AUDIT || TRACEPOINTS + +config SECURITY_LANDLOCK_KUNIT_TEST + bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS + depends on KUNIT=y + depends on SECURITY_LANDLOCK + default KUNIT_ALL_TESTS + help + Build KUnit tests for Landlock. + + See the KUnit documentation in Documentation/dev-tools/kunit + + Run all KUnit tests for Landlock with: + ./tools/testing/kunit/kunit.py run --kunitconfig security/landlock + + If you are unsure how to answer this question, answer N. diff --git a/security/landlock/Makefile b/security/landlock/Makefile new file mode 100644 index 000000000..2711f4876 --- /dev/null +++ b/security/landlock/Makefile @@ -0,0 +1,22 @@ +obj-$(CONFIG_SECURITY_LANDLOCK) := landlock.o + +landlock-y := \ + setup.o \ + syscalls.o \ + object.o \ + ruleset.o \ + cred.o \ + task.o \ + fs.o \ + tsync.o \ + domain.o + +landlock-$(CONFIG_INET) += net.o + +landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \ + id.o \ + log.o + +landlock-$(CONFIG_AUDIT) += audit.o + +landlock-$(CONFIG_TRACEPOINTS) += trace.o diff --git a/security/landlock/access.h b/security/landlock/access.h new file mode 100644 index 000000000..bbbb41f41 --- /dev/null +++ b/security/landlock/access.h @@ -0,0 +1,151 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Access types and helpers + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_ACCESS_H +#define _SECURITY_LANDLOCK_ACCESS_H + +#include <linux/bitops.h> +#include <linux/build_bug.h> +#include <linux/kernel.h> +#include <uapi/linux/landlock.h> + +#include "limits.h" + +/* + * All access rights that are denied by default whether they are handled or not + * by a ruleset/layer. This must be ORed with all domain->handled_masks[] + * entries when we need to get the absolute handled access masks, see + * landlock_upgrade_handled_access_masks(). + */ +/* clang-format off */ +#define _LANDLOCK_ACCESS_FS_INITIALLY_DENIED ( \ + 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 u32 access_mask_t; + +/* Makes sure all filesystem access rights can be stored. */ +static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_FS); +/* Makes sure all network access rights can be stored. */ +static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_ACCESS_NET); +/* Makes sure all scoped rights can be stored. */ +static_assert(BITS_PER_TYPE(access_mask_t) >= LANDLOCK_NUM_SCOPE); +/* Makes sure for_each_set_bit() and for_each_clear_bit() calls are OK. */ +static_assert(sizeof(unsigned long) >= sizeof(access_mask_t)); + +/* Ruleset access masks. */ +struct access_masks { + access_mask_t fs : LANDLOCK_NUM_ACCESS_FS; + access_mask_t net : LANDLOCK_NUM_ACCESS_NET; + access_mask_t scope : LANDLOCK_NUM_SCOPE; +} __packed __aligned(sizeof(u32)); + +union access_masks_all { + struct access_masks masks; + u32 all; +}; + +/* Makes sure all fields are covered. */ +static_assert(sizeof(typeof_member(union access_masks_all, masks)) == + sizeof(typeof_member(union access_masks_all, all))); + +/** + * struct layer_mask - The access rights and rule flags for a layer. + * + * This has a bit for each access rights and rule flags. During access checks, + * it is used to represent the access rights for each layer which still need to + * be fulfilled. When all bits are 0, the access request is considered to be + * fulfilled. + */ +struct layer_mask { + /** + * @access: The unfulfilled access rights for this layer. + */ + access_mask_t access : LANDLOCK_NUM_ACCESS_MAX; +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + /** + * @quiet: Whether we have encountered a rule with the quiet flag for + * this layer. Used to control logging. + */ + access_mask_t quiet : 1; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ +} __packed __aligned(sizeof(access_mask_t)); + +/* + * Make sure that we don't increase the size of struct layer_mask when storing + * rule flags. + */ +static_assert(sizeof(struct layer_mask) == sizeof(access_mask_t)); + +/** + * struct layer_masks - An array of struct layer_mask, one per layer. + */ +struct layer_masks { + /** + * @layers: The unfulfilled access rights for each layer. + */ + struct layer_mask layers[LANDLOCK_MAX_NUM_LAYERS]; +}; + +/* + * Tracks domains responsible of a denied access. This avoids storing in each + * object the full matrix of per-layer unfulfilled access rights, which is + * required by update_request(). + * + * Each nibble represents the layer index of the newest layer which denied a + * certain access right. For file system access rights, the upper four bits are + * the index of the layer which denies LANDLOCK_ACCESS_FS_IOCTL_DEV and the + * lower nibble represents LANDLOCK_ACCESS_FS_TRUNCATE. + */ +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) +{ + /* + * All access rights that are denied by default whether they are + * explicitly handled or not. + */ + if (access_masks.fs) + access_masks.fs |= _LANDLOCK_ACCESS_FS_INITIALLY_DENIED; + + return access_masks; +} + +/* Checks the subset relation between access masks. */ +static inline bool access_mask_subset(access_mask_t subset, + access_mask_t superset) +{ + return (subset | superset) == superset; +} + +/* A bitmask that is large enough to hold set of optional accesses. */ +typedef u8 optional_access_t; +static_assert(BITS_PER_TYPE(optional_access_t) >= + HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)); + +#endif /* _SECURITY_LANDLOCK_ACCESS_H */ diff --git a/security/landlock/audit.c b/security/landlock/audit.c new file mode 100644 index 000000000..e02963834 --- /dev/null +++ b/security/landlock/audit.c @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Audit helpers + * + * Copyright © 2023-2025 Microsoft Corporation + */ + +#include <linux/audit.h> +#include <linux/bitops.h> +#include <linux/landlock.h> +#include <linux/lsm_audit.h> +#include <linux/pid.h> +#include <uapi/linux/landlock.h> + +#include "access.h" +#include "audit.h" +#include "common.h" +#include "cred.h" +#include "domain.h" +#include "limits.h" +#include "log.h" + +/* + * Access-right and scope names are built from the lists shared with the trace + * events (see <linux/landlock.h>). The designated initializer places each name + * at its bit index, so the lookup stays O(1) and does not depend on the entry + * order. log_blockers() adds the "fs."/"net."/"scope." category prefix. + */ +#define _LANDLOCK_NAME_ENTRY(mask, name) [BIT_INDEX(mask)] = name + +static const char *const fs_access_strings[] = { _LANDLOCK_ACCESS_FS_NAMES }; + +static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS); + +static const char *const net_access_strings[] = { _LANDLOCK_ACCESS_NET_NAMES }; + +static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET); + +static const char *const scope_strings[] = { _LANDLOCK_SCOPE_NAMES }; + +static_assert(ARRAY_SIZE(scope_strings) == LANDLOCK_NUM_SCOPE); + +#undef _LANDLOCK_NAME_ENTRY + +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 "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]; + + case LANDLOCK_REQUEST_NET_ACCESS: + 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_strings[BIT_INDEX( + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)]; + + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + WARN_ON_ONCE(access_bit != -1); + return scope_strings[BIT_INDEX(LANDLOCK_SCOPE_SIGNAL)]; + } + + WARN_ON_ONCE(1); + return "unknown"; +} + +/* + * Returns the audit category prefix prepended to the unprefixed blocker name + * returned by get_blocker() (filesystem and network access rights, + * change_topology, and scopes). The ptrace blocker is standalone and carries + * its full name in get_blocker(), so it uses no prefix. + */ +static __attribute_const__ const char * +blocker_prefix(const enum landlock_request_type type) +{ + switch (type) { + case LANDLOCK_REQUEST_PTRACE: + return ""; + + case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY: + case LANDLOCK_REQUEST_FS_ACCESS: + return "fs."; + + case LANDLOCK_REQUEST_NET_ACCESS: + return "net."; + + case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + return "scope."; + } + + WARN_ON_ONCE(1); + return ""; +} + +static void log_blockers(struct audit_buffer *const ab, + const enum landlock_request_type type, + const access_mask_t access) +{ + const unsigned long access_mask = access; + const char *const prefix = blocker_prefix(type); + 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%s", is_first ? "" : ",", prefix, + get_blocker(type, access_bit)); + is_first = false; + } + if (is_first) + audit_log_format(ab, "%s%s", prefix, get_blocker(type, -1)); +} + +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); +} + +/** + * landlock_audit_denial - Create an audit record for a denied access request + * + * @request: Detail of the user space request. + * @youngest_denied: The youngest hierarchy node that denied the access. + * @missing: The set of denied access rights. + * @logged: Whether the denial is selected for logging, as computed by + * landlock_log_denial() (domain policy and quiet rules). + * + * Emits the record when audit is enabled and the denial is selected for + * logging. + */ +void landlock_audit_denial(const struct landlock_request *const request, + struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool logged) +{ + struct audit_buffer *ab; + + if (!audit_enabled) + return; + + /* + * Skips denials the domain's policy or a quiet rule excludes from + * logging (folded into @logged by landlock_log_denial()). + */ + if (!logged) + 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, missing); + 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_audit_free_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 from landlock_log_free_domain(). + */ +void landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy) +{ + struct audit_buffer *ab; + + 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); +} diff --git a/security/landlock/audit.h b/security/landlock/audit.h new file mode 100644 index 000000000..14a514065 --- /dev/null +++ b/security/landlock/audit.h @@ -0,0 +1,43 @@ +/* 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 <linux/types.h> + +#include "access.h" + +struct landlock_hierarchy; +struct landlock_request; + +#ifdef CONFIG_AUDIT + +void landlock_audit_denial(const struct landlock_request *const request, + struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool logged); + +void landlock_audit_free_domain( + const struct landlock_hierarchy *const hierarchy); + +#else /* CONFIG_AUDIT */ + +static inline void +landlock_audit_denial(const struct landlock_request *const request, + struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool logged) +{ +} + +static inline void +landlock_audit_free_domain(const struct landlock_hierarchy *const hierarchy) +{ +} + +#endif /* CONFIG_AUDIT */ + +#endif /* _SECURITY_LANDLOCK_AUDIT_H */ diff --git a/security/landlock/common.h b/security/landlock/common.h new file mode 100644 index 000000000..0eb1d34c2 --- /dev/null +++ b/security/landlock/common.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Common constants and helpers + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_COMMON_H +#define _SECURITY_LANDLOCK_COMMON_H + +#define LANDLOCK_NAME "landlock" + +#ifdef pr_fmt +#undef pr_fmt +#endif + +#define pr_fmt(fmt) LANDLOCK_NAME ": " fmt + +#define BIT_INDEX(bit) HWEIGHT(bit - 1) + +#endif /* _SECURITY_LANDLOCK_COMMON_H */ diff --git a/security/landlock/cred.c b/security/landlock/cred.c new file mode 100644 index 000000000..03449c262 --- /dev/null +++ b/security/landlock/cred.c @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Credential hooks + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + */ + +#include <linux/binfmts.h> +#include <linux/cred.h> +#include <linux/lsm_hooks.h> + +#include "common.h" +#include "cred.h" +#include "ruleset.h" +#include "setup.h" + +static void hook_cred_transfer(struct cred *const new, + const struct cred *const old) +{ + const struct landlock_cred_security *const old_llcred = + landlock_cred(old); + + landlock_get_domain(old_llcred->domain); + *landlock_cred(new) = *old_llcred; +} + +static int hook_cred_prepare(struct cred *const new, + const struct cred *const old, const gfp_t gfp) +{ + hook_cred_transfer(new, old); + return 0; +} + +static void hook_cred_free(struct cred *const cred) +{ + struct landlock_domain *const dom = landlock_cred(cred)->domain; + + if (dom) + landlock_put_domain_deferred(dom); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + +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_SECURITY_LANDLOCK_LOG */ + +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_SECURITY_LANDLOCK_LOG + LSM_HOOK_INIT(bprm_creds_for_exec, hook_bprm_creds_for_exec), +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ +}; + +__init void landlock_add_cred_hooks(void) +{ + security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), + &landlock_lsmid); +} diff --git a/security/landlock/cred.h b/security/landlock/cred.h new file mode 100644 index 000000000..a5ff99579 --- /dev/null +++ b/security/landlock/cred.h @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Credential hooks + * + * Copyright © 2019-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_CRED_H +#define _SECURITY_LANDLOCK_CRED_H + +#include <linux/container_of.h> +#include <linux/cred.h> +#include <linux/init.h> +#include <linux/rcupdate.h> + +#include "access.h" +#include "domain.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(). + * + * When updating this, also update landlock_cred_copy() if needed. + */ +struct landlock_cred_security { + /** + * @domain: Immutable domain enforced on a task. + */ + struct landlock_domain *domain; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + /** + * @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; + /** + * @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_SECURITY_LANDLOCK_LOG */ +} __packed; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + +/* 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_SECURITY_LANDLOCK_LOG */ + +static inline struct landlock_cred_security * +landlock_cred(const struct cred *cred) +{ + return cred->security + landlock_blob_sizes.lbs_cred; +} + +static inline void landlock_cred_copy(struct landlock_cred_security *dst, + const struct landlock_cred_security *src) +{ + landlock_put_domain(dst->domain); + + *dst = *src; + + landlock_get_domain(src->domain); +} + +static inline struct landlock_domain *landlock_get_current_domain(void) +{ + return landlock_cred(current_cred())->domain; +} + +/* The call needs to come from an RCU read-side critical section. */ +static inline const struct landlock_domain * +landlock_get_task_domain(const struct task_struct *const task) +{ + return landlock_cred(__task_cred(task))->domain; +} + +static inline bool landlocked(const struct task_struct *const task) +{ + bool has_dom; + + if (task == current) + return !!landlock_get_current_domain(); + + rcu_read_lock(); + has_dom = !!landlock_get_task_domain(task); + rcu_read_unlock(); + 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. + * + * Return: 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_domain *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->handled_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/domain.c b/security/landlock/domain.c new file mode 100644 index 000000000..4031b581b --- /dev/null +++ b/security/landlock/domain.c @@ -0,0 +1,776 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Domain management + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + * Copyright © 2026 Cloudflare, Inc. + */ + +#include <kunit/test.h> +#include <linux/bitops.h> +#include <linux/bits.h> +#include <linux/cleanup.h> +#include <linux/cred.h> +#include <linux/err.h> +#include <linux/file.h> +#include <linux/lockdep.h> +#include <linux/mm.h> +#include <linux/mutex.h> +#include <linux/overflow.h> +#include <linux/path.h> +#include <linux/pid.h> +#include <linux/rbtree.h> +#include <linux/refcount.h> +#include <linux/sched.h> +#include <linux/signal.h> +#include <linux/slab.h> +#include <linux/uidgid.h> +#include <linux/workqueue.h> + +#include "access.h" +#include "common.h" +#include "domain.h" +#include "id.h" +#include "limits.h" +#include "ruleset.h" + +static void build_check_domain(void) +{ + const struct landlock_domain domain = { + .num_layers = ~0, + }; + + BUILD_BUG_ON(domain.num_layers < LANDLOCK_MAX_NUM_LAYERS); +} + +static struct landlock_domain *create_domain(const u32 num_layers) +{ + struct landlock_domain *new_domain; + + build_check_domain(); + new_domain = kzalloc_flex(*new_domain, handled_masks, num_layers, + GFP_KERNEL_ACCOUNT); + if (!new_domain) + return ERR_PTR(-ENOMEM); + + refcount_set(&new_domain->usage, 1); + new_domain->rules.root_inode = RB_ROOT; + +#if IS_ENABLED(CONFIG_INET) + new_domain->rules.root_net_port = RB_ROOT; +#endif /* IS_ENABLED(CONFIG_INET) */ + + new_domain->num_layers = num_layers; + return new_domain; +} + +static void free_domain(struct landlock_domain *const domain) +{ + might_sleep(); + landlock_free_rules(&domain->rules); + landlock_put_hierarchy(domain->hierarchy); + kfree(domain); +} + +void landlock_put_domain(struct landlock_domain *const domain) +{ + might_sleep(); + if (domain && refcount_dec_and_test(&domain->usage)) + free_domain(domain); +} + +static void free_domain_work(struct work_struct *const work) +{ + struct landlock_domain *domain; + + domain = container_of(work, struct landlock_domain, work_free); + free_domain(domain); +} + +void landlock_put_domain_deferred(struct landlock_domain *const domain) +{ + if (domain && refcount_dec_and_test(&domain->usage)) { + INIT_WORK(&domain->work_free, free_domain_work); + schedule_work(&domain->work_free); + } +} + +/* The returned access has the same lifetime as the domain. */ +static const struct landlock_rule * +find_rule(const struct landlock_domain *const domain, + const struct landlock_id id) +{ + const struct rb_root *root; + const struct rb_node *node; + + root = landlock_get_rule_root((struct landlock_rules *)&domain->rules, + id.type); + if (IS_ERR(root)) + return NULL; + node = root->rb_node; + + while (node) { + struct landlock_rule *this = + rb_entry(node, struct landlock_rule, node); + + if (this->key.data == id.key.data) + return this; + if (this->key.data < id.key.data) + node = node->rb_right; + else + node = node->rb_left; + } + return NULL; +} + +/** + * landlock_unmask_layers - Remove the access rights in @masks which are + * granted by a matching rule + * + * Looks up the rule matching @id in @domain, then updates the set of + * (per-layer) unfulfilled access rights @masks so that all the access rights + * granted by that rule are removed (because they are now fulfilled). + * + * @domain: The Landlock domain to search for a matching rule. + * @id: Identifier for the rule target (e.g. inode, port). + * @masks: A matrix of unfulfilled access rights for each layer. + * @matched_rule: Optional output for the matched rule (for tracing); set to + * the matching rule when non-NULL, unchanged otherwise. + * + * Return: True if the request is allowed (i.e. the access rights granted all + * remaining unfulfilled access rights and masks has no leftover set bits). + */ +bool landlock_unmask_layers(const struct landlock_domain *const domain, + const struct landlock_id id, + struct layer_masks *masks, + const struct landlock_rule **matched_rule) +{ + const struct landlock_rule *rule; + + if (!masks) + return true; + + rule = find_rule(domain, id); + if (!rule) + return false; + + if (matched_rule) + *matched_rule = rule; + + /* + * An access is granted if, for each policy layer, at least one rule + * encountered on the pathwalk grants the requested access, regardless + * of its position in the layer stack. We must then check the remaining + * layers for each inode, from the first added layer to the last one. + * When there are multiple requested accesses, for each policy layer, + * the full set of requested accesses may not be granted by only one + * rule, but by the union (binary OR) of multiple rules. For example, + * /a/b <execute> + /a <read> grants /a/b <execute + read>. + * + * This function is called once per matching rule during the pathwalk, + * progressively clearing bits in @masks. The overall access decision + * is per-layer: access is granted iff masks->layers[l].access == 0 for + * all layers l. When two independent mechanisms can each grant access + * within a layer (e.g. a path rule OR a scope exception), the + * composition must evaluate per-layer: FOR-ALL l (A(l) OR B(l)), not + * (FOR-ALL l A(l)) OR (FOR-ALL l B(l)), to prevent bypass when + * different layers grant via different mechanisms. + */ + for (size_t i = 0; i < rule->num_layers; i++) { + const struct landlock_layer *const layer = &rule->layers[i]; + + /* Clear the bits where the layer in the rule grants access. */ + masks->layers[layer->level - 1].access &= ~layer->access; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + /* Collect rule flags for each layer. */ + if (layer->flags.quiet) + masks->layers[layer->level - 1].quiet = true; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + } + + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { + if (masks->layers[i].access) + return false; + } + return true; +} + +typedef access_mask_t +get_access_mask_t(const struct landlock_domain *const domain, + const u16 layer_level); + +/** + * landlock_init_layer_masks - Initialize layer masks from an access request + * + * Populates @masks such that for each access right in @access_request, the bits + * for all the layers are set where this access right is handled. Rule flags + * are also zeroed. + * + * @domain: The domain that defines the current restrictions. + * @access_request: The requested access rights to check. + * @masks: Layer access masks to populate. + * @key_type: The key type to switch between access masks of different types. + * + * Return: An access mask where each access right bit is set which is handled in + * any of the active layers in @domain. + */ +access_mask_t +landlock_init_layer_masks(const struct landlock_domain *const domain, + const access_mask_t access_request, + struct layer_masks *const masks, + const enum landlock_key_type key_type) +{ + access_mask_t handled_accesses = 0; + get_access_mask_t *get_access_mask; + + switch (key_type) { + case LANDLOCK_KEY_INODE: + get_access_mask = landlock_get_fs_access_mask; + break; + +#if IS_ENABLED(CONFIG_INET) + case LANDLOCK_KEY_NET_PORT: + get_access_mask = landlock_get_net_access_mask; + break; +#endif /* IS_ENABLED(CONFIG_INET) */ + + default: + WARN_ON_ONCE(1); + return 0; + } + + /* An empty access request can happen because of O_WRONLY | O_RDWR. */ + if (!access_request) + return 0; + + for (size_t i = 0; i < domain->num_layers; i++) { + const access_mask_t handled = get_access_mask(domain, i); + + masks->layers[i].access = access_request & handled; + handled_accesses |= masks->layers[i].access; +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + masks->layers[i].quiet = false; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + } + for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers); + i++) { + masks->layers[i].access = 0; +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + masks->layers[i].quiet = false; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + } + + return handled_accesses; +} + +static int merge_tree(struct landlock_domain *const dst, + struct landlock_ruleset *const src, + const enum landlock_key_type key_type) +{ + struct landlock_rule *walker_rule, *next_rule; + struct rb_root *src_root; + int err = 0; + + might_sleep(); + lockdep_assert_held(&src->lock); + + src_root = landlock_get_rule_root(&src->rules, key_type); + if (IS_ERR(src_root)) + return PTR_ERR(src_root); + + /* Merges the @src tree. */ + rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root, + node) { + struct landlock_layer layers[] = { { + .level = dst->num_layers, + } }; + const struct landlock_id id = { + .key = walker_rule->key, + .type = key_type, + }; + + if (WARN_ON_ONCE(walker_rule->num_layers != 1)) + return -EINVAL; + + if (WARN_ON_ONCE(walker_rule->layers[0].level != 0)) + return -EINVAL; + + layers[0].access = walker_rule->layers[0].access; + layers[0].flags = walker_rule->layers[0].flags; + + err = landlock_store_rule(&dst->rules, id, &layers, + ARRAY_SIZE(layers)); + if (err) + return err; + } + return err; +} + +static int merge_ruleset(struct landlock_domain *const dst, + struct landlock_ruleset *const src) +{ + int err = 0; + + might_sleep(); + /* Should already be checked by landlock_merge_ruleset() */ + if (WARN_ON_ONCE(!src)) + return 0; + /* Only merge into a domain. */ + if (WARN_ON_ONCE(!dst || !dst->hierarchy)) + return -EINVAL; + + lockdep_assert_held(&src->lock); + + /* Stacks the new layer. */ + if (WARN_ON_ONCE(dst->num_layers < 1)) + return -EINVAL; + + dst->handled_masks[dst->num_layers - 1] = + landlock_upgrade_handled_access_masks(src->handled_masks); + + /* Merges the @src inode tree. */ + err = merge_tree(dst, src, LANDLOCK_KEY_INODE); + if (err) + return err; + +#if IS_ENABLED(CONFIG_INET) + /* Merges the @src network port tree. */ + err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT); + if (err) + return err; +#endif /* IS_ENABLED(CONFIG_INET) */ + + return 0; +} + +static int inherit_tree(struct landlock_domain *const parent, + struct landlock_domain *const child, + const enum landlock_key_type key_type) +{ + struct landlock_rule *walker_rule, *next_rule; + struct rb_root *parent_root; + int err = 0; + + might_sleep(); + + parent_root = landlock_get_rule_root(&parent->rules, key_type); + if (IS_ERR(parent_root)) + return PTR_ERR(parent_root); + + /* Copies the @parent inode or network tree. */ + rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, + parent_root, node) { + const struct landlock_id id = { + .key = walker_rule->key, + .type = key_type, + }; + + err = landlock_store_rule(&child->rules, id, + &walker_rule->layers, + walker_rule->num_layers); + if (err) + return err; + } + return err; +} + +static int inherit_ruleset(struct landlock_domain *const parent, + struct landlock_domain *const child) +{ + int err = 0; + + might_sleep(); + if (!parent) + return 0; + + /* Copies the @parent inode tree. */ + err = inherit_tree(parent, child, LANDLOCK_KEY_INODE); + if (err) + return err; + +#if IS_ENABLED(CONFIG_INET) + /* Copies the @parent network port tree. */ + err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT); + if (err) + return err; +#endif /* IS_ENABLED(CONFIG_INET) */ + + if (WARN_ON_ONCE(child->num_layers <= parent->num_layers)) + return -EINVAL; + + /* + * Copies the parent layer stack and leaves a space for the new layer. + */ + memcpy(child->handled_masks, parent->handled_masks, + flex_array_size(parent, handled_masks, parent->num_layers)); + + if (WARN_ON_ONCE(!parent->hierarchy)) + return -EINVAL; + + landlock_get_hierarchy(parent->hierarchy); + child->hierarchy->parent = parent->hierarchy; + + return 0; +} + +/** + * landlock_merge_ruleset - Merge a ruleset with a domain + * + * @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(). + * + * The caller must hold @ruleset->lock. + * + * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on + * failure. If @parent is NULL, the new domain duplicates @ruleset. + */ +struct landlock_domain * +landlock_merge_ruleset(struct landlock_domain *const parent, + struct landlock_ruleset *const ruleset) +{ + struct landlock_domain *new_dom __free(landlock_put_domain) = NULL; + u32 num_layers; + int err; + + might_sleep(); + if (WARN_ON_ONCE(!ruleset)) + return ERR_PTR(-EINVAL); + + lockdep_assert_held(&ruleset->lock); + + if (parent) { + if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS) + return ERR_PTR(-E2BIG); + num_layers = parent->num_layers + 1; + } else { + num_layers = 1; + } + + /* Creates a new domain... */ + new_dom = create_domain(num_layers); + if (IS_ERR(new_dom)) + return new_dom; + + new_dom->hierarchy = + kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT); + if (!new_dom->hierarchy) + return ERR_PTR(-ENOMEM); + + refcount_set(&new_dom->hierarchy->usage, 1); + + /* ...as a child of @parent... */ + err = inherit_ruleset(parent, new_dom); + if (err) + return ERR_PTR(err); + + /* ...and including @ruleset. */ + err = merge_ruleset(new_dom, ruleset); + if (err) + return ERR_PTR(err); + + err = landlock_init_hierarchy_log(new_dom->hierarchy); + if (err) + return ERR_PTR(err); + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + new_dom->hierarchy->quiet_masks = ruleset->quiet_masks; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + return no_free_ptr(new_dom); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + +/** + * 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. + * + * Return: 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); +} + +/* + * Return: 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. Account to + * the calling task's memcg, like the other Landlock per-domain + * allocations, even if it may not control the related size. + */ + details = + kzalloc_flex(*details, exe_path, path_size, GFP_KERNEL_ACCOUNT); + if (!details) + return ERR_PTR(-ENOMEM); + + memcpy(details->exe_path, path_str, path_size); + details->pid = get_pid(task_tgid(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. + * + * Return: 0 on success, -errno on failure. + */ +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); + /* + * The hierarchy is born unobservable: landlock_restrict_self() moves it + * out of LANDLOCK_LOG_UNCOMMITTED once it has emitted the creation + * event, so the matching free_domain event fires for it and not for a + * hierarchy whose creation was never observed. + */ + hierarchy->log_status = LANDLOCK_LOG_UNCOMMITTED; + hierarchy->log_same_exec = true; + hierarchy->log_new_exec = false; + atomic64_set(&hierarchy->num_denials, 0); + 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)); +} + +/** + * landlock_get_quiet_optional_accesses - Get optional accesses which are + * covered by quiet rule flags. + * + * @all_existing_optional_access: Bitmask of valid optional accesses. + * @deny_masks: Domain layer levels that denied each optional access (the + * deny_masks field on struct landlock_file_security). + * @masks: The struct layer_masks collected during the path walk. + * + * Return: a bitmask of which optional accesses are denied by layers for which + * the quiet flag was collected during the path walk. + */ +optional_access_t landlock_get_quiet_optional_accesses( + const access_mask_t all_existing_optional_access, + const deny_masks_t deny_masks, const struct layer_masks *const masks) +{ + const unsigned long access_opt = all_existing_optional_access; + size_t access_index = 0; + unsigned long access_bit; + optional_access_t quiet_optional_accesses = 0; + + /* This will require change with new object types. */ + WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL); + + for_each_set_bit(access_bit, &access_opt, + BITS_PER_TYPE(access_mask_t)) { + const u8 layer = + (deny_masks >> (access_index * + HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) & + (LANDLOCK_MAX_NUM_LAYERS - 1); + + if (masks->layers[layer].quiet) + quiet_optional_accesses |= BIT(access_index); + access_index++; + } + return quiet_optional_accesses; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_layer_deny_mask(struct kunit *const test) +{ + 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 struct layer_masks *const masks) +{ + const unsigned long access_opt = optional_access; + unsigned long access_bit; + deny_masks_t deny_masks = 0; + access_mask_t all_denied = 0; + + /* This may require change with new object types. */ + WARN_ON_ONCE(!access_mask_subset(optional_access, + all_existing_optional_access)); + + if (WARN_ON_ONCE(!masks)) + return 0; + + if (WARN_ON_ONCE(!access_opt)) + return 0; + + for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) { + const access_mask_t denied = masks->layers[i].access & + optional_access; + const unsigned long newly_denied = denied & ~all_denied; + + if (!newly_denied) + continue; + + for_each_set_bit(access_bit, &newly_denied, + 8 * sizeof(access_mask_t)) { + deny_masks |= get_layer_deny_mask( + all_existing_optional_access, access_bit, i); + } + all_denied |= denied; + } + return deny_masks; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_landlock_get_deny_masks(struct kunit *const test) +{ + const struct layer_masks layers1 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_IOCTL_DEV, + .layers[1].access = LANDLOCK_ACCESS_FS_TRUNCATE, + .layers[2].access = LANDLOCK_ACCESS_FS_IOCTL_DEV, + .layers[9].access = LANDLOCK_ACCESS_FS_EXECUTE, + }; + + KUNIT_EXPECT_EQ(test, 0x1, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_TRUNCATE, + &layers1)); + KUNIT_EXPECT_EQ(test, 0x20, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_IOCTL_DEV, + &layers1)); + KUNIT_EXPECT_EQ( + test, 0x21, + landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV, + &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_SECURITY_LANDLOCK_LOG */ diff --git a/security/landlock/domain.h b/security/landlock/domain.h new file mode 100644 index 000000000..5ce2f9148 --- /dev/null +++ b/security/landlock/domain.h @@ -0,0 +1,326 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Domain management + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + * Copyright © 2026 Cloudflare, Inc. + */ + +#ifndef _SECURITY_LANDLOCK_DOMAIN_H +#define _SECURITY_LANDLOCK_DOMAIN_H + +#include <linux/cleanup.h> +#include <linux/limits.h> +#include <linux/mm.h> +#include <linux/path.h> +#include <linux/pid.h> +#include <linux/refcount.h> +#include <linux/sched.h> +#include <linux/slab.h> +#include <linux/workqueue.h> + +#include "access.h" +#include "log.h" +#include "ruleset.h" + +enum landlock_log_status { + /* + * Hierarchy whose creation event has not been emitted, so it is not yet + * observable from user space. A hierarchy is born in this state (the + * zero value, so a partially initialized hierarchy defaults to "not + * observable") and leaves it when landlock_restrict_self() emits its + * creation event, right after the merge and before the thread-sync + * wait. No trace free_domain event (and no audit deallocation record) + * fires while a hierarchy is in this state, so a hierarchy that never + * became observable (e.g. its initialization failed) is freed silently. + * A domain aborted by a thread-sync failure already emitted its + * creation event, so it is no longer UNCOMMITTED and does fire + * free_domain. + */ + LANDLOCK_LOG_UNCOMMITTED = 0, + LANDLOCK_LOG_PENDING, + LANDLOCK_LOG_RECORDED, + LANDLOCK_LOG_DISABLED, +}; + +/** + * 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. + */ +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 + */ +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; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + /** + * @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, set once at domain creation time. + */ + u64 id; + /** + * @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; + /** + * @quiet_masks: Bitmasks of access that should be quieted (i.e. not + * logged) if the related object is marked as quiet. + */ + struct access_masks quiet_masks; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ +}; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + +deny_masks_t +landlock_get_deny_masks(const access_mask_t all_existing_optional_access, + const access_mask_t optional_access, + const struct layer_masks *const masks); + +optional_access_t landlock_get_quiet_optional_accesses( + const access_mask_t all_existing_optional_access, + const deny_masks_t deny_masks, const struct layer_masks *const masks); + +int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy); + +static inline void +landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy) +{ + if (!hierarchy || !hierarchy->details) + return; + + put_pid(hierarchy->details->pid); + kfree(hierarchy->details); +} + +#else /* CONFIG_SECURITY_LANDLOCK_LOG */ + +static inline int +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_SECURITY_LANDLOCK_LOG */ + +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; + + landlock_log_free_domain(hierarchy); + landlock_free_hierarchy_details(hierarchy); + hierarchy = hierarchy->parent; + kfree(freeme); + } +} + +/** + * struct landlock_domain - Immutable Landlock domain + * + * A domain is created from a ruleset by landlock_merge_ruleset() and enforced + * on a task. Once created, its rules and access masks are immutable. Unlike + * &struct landlock_ruleset, a domain has no lock field. + */ +struct landlock_domain { + /** + * @rules: Red-black tree storage for rules. + */ + struct landlock_rules rules; + /** + * @hierarchy: Enables hierarchy identification even when a parent + * domain vanishes. This is needed for the ptrace and scope + * restrictions. + */ + struct landlock_hierarchy *hierarchy; + union { + /** + * @work_free: Enables to free a domain within a lockless + * section. This is only used by landlock_put_domain_deferred() + * when @usage reaches zero. The fields @usage, @num_layers and + * @handled_masks are then unused. + */ + struct work_struct work_free; + struct { + /** + * @usage: Number of credentials referencing this + * domain. + */ + refcount_t usage; + /** + * @num_layers: Number of layers that are used in this + * domain. This enables to check that all the layers + * allow an access request. + */ + u32 num_layers; + /** + * @handled_masks: Contains the subset of filesystem and + * network actions that are restricted by a domain. A + * domain saves all layers of merged rulesets in a stack + * (FAM), starting from the first layer to the last one. + * These layers are used when merging rulesets, for user + * space backward compatibility (i.e. future-proof), and + * to properly handle merged rulesets without + * overlapping access rights. These layers are set once + * and never changed for the lifetime of the domain. + */ + struct access_masks handled_masks[]; + }; + }; +}; + +static inline access_mask_t +landlock_get_fs_access_mask(const struct landlock_domain *const domain, + const u16 layer_level) +{ + /* Handles all initially denied by default access rights. */ + return domain->handled_masks[layer_level].fs | + _LANDLOCK_ACCESS_FS_INITIALLY_DENIED; +} + +static inline access_mask_t +landlock_get_net_access_mask(const struct landlock_domain *const domain, + const u16 layer_level) +{ + return domain->handled_masks[layer_level].net; +} + +static inline access_mask_t +landlock_get_scope_mask(const struct landlock_domain *const domain, + const u16 layer_level) +{ + return domain->handled_masks[layer_level].scope; +} + +/** + * landlock_union_access_masks - Return all access rights handled in the + * domain + * + * @domain: Landlock domain + * + * Return: An access_masks result of the OR of all the domain's access masks. + */ +static inline struct access_masks +landlock_union_access_masks(const struct landlock_domain *const domain) +{ + union access_masks_all matches = {}; + size_t layer_level; + + for (layer_level = 0; layer_level < domain->num_layers; layer_level++) { + union access_masks_all layer = { + .masks = domain->handled_masks[layer_level], + }; + + matches.all |= layer.all; + } + + return matches.masks; +} + +void landlock_put_domain(struct landlock_domain *const domain); +void landlock_put_domain_deferred(struct landlock_domain *const domain); + +DEFINE_FREE(landlock_put_domain, struct landlock_domain *, + if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T)) + +struct landlock_domain * +landlock_merge_ruleset(struct landlock_domain *const parent, + struct landlock_ruleset *const ruleset); + +bool landlock_unmask_layers(const struct landlock_domain *const domain, + const struct landlock_id id, + struct layer_masks *masks, + const struct landlock_rule **matched_rule); + +access_mask_t +landlock_init_layer_masks(const struct landlock_domain *const domain, + const access_mask_t access_request, + struct layer_masks *masks, + const enum landlock_key_type key_type); + +static inline void landlock_get_domain(struct landlock_domain *const domain) +{ + if (domain) + refcount_inc(&domain->usage); +} + +#endif /* _SECURITY_LANDLOCK_DOMAIN_H */ diff --git a/security/landlock/errata.h b/security/landlock/errata.h new file mode 100644 index 000000000..8e626acca --- /dev/null +++ b/security/landlock/errata.h @@ -0,0 +1,99 @@ +/* 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 <linux/init.h> + +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 + +#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. + * + * 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/errata/abi-1.h b/security/landlock/errata/abi-1.h new file mode 100644 index 000000000..315ea7e0f --- /dev/null +++ b/security/landlock/errata/abi-1.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +/** + * DOC: erratum_3 + * + * Erratum 3: Disconnected directory handling + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This fix addresses an issue with disconnected directories that occur when a + * directory is moved outside the scope of a bind mount. The change ensures + * that evaluated access rights include both those from the disconnected file + * hierarchy down to its filesystem root and those from the related mount point + * hierarchy. This prevents access right widening through rename or link + * actions. + * + * Impact: + * + * Without this fix, it was possible to widen access rights through rename or + * link actions involving disconnected directories, potentially bypassing + * ``LANDLOCK_ACCESS_FS_REFER`` restrictions. This could allow privilege + * escalation in complex mount scenarios where directories become disconnected + * from their original mount points. + */ +LANDLOCK_ERRATUM(3) + +/** + * DOC: erratum_4 + * + * Erratum 4: Creation of whiteout objects + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * This fix changes the access rights required for the creation of whiteout + * objects through :manpage:`mknod(2)`, :manpage:`renameat2(2)`, or + * :manpage:`link(2)`. Creating whiteout objects is now guarded by + * ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of ``LANDLOCK_ACCESS_FS_MAKE_CHAR``. + * + * Whiteout objects are used in OverlayFS to mark the absence of a file in an + * upper file system. Despite being created with ``S_IFCHR``, whiteout objects + * do not count as character devices. + * + * Impact: + * + * Sandboxed programs that create OverlayFS whiteouts (such as fuse-overlayfs) + * now require ``LANDLOCK_ACCESS_FS_MAKE_REG`` instead of + * ``LANDLOCK_ACCESS_FS_MAKE_CHAR``. + */ +LANDLOCK_ERRATUM(4) diff --git a/security/landlock/errata/abi-4.h b/security/landlock/errata/abi-4.h new file mode 100644 index 000000000..fe11ec7d7 --- /dev/null +++ b/security/landlock/errata/abi-4.h @@ -0,0 +1,22 @@ +/* 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. + * + * Impact: + * + * In kernels without this fix, using ``LANDLOCK_ACCESS_NET_BIND_TCP`` or + * ``LANDLOCK_ACCESS_NET_CONNECT_TCP`` would incorrectly restrict non-TCP + * stream protocols (SMC, MPTCP, SCTP), potentially breaking applications + * that rely on these protocols while using Landlock network restrictions. + */ +LANDLOCK_ERRATUM(1) diff --git a/security/landlock/errata/abi-6.h b/security/landlock/errata/abi-6.h new file mode 100644 index 000000000..5cb1475c7 --- /dev/null +++ b/security/landlock/errata/abi-6.h @@ -0,0 +1,29 @@ +/* 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 all threads 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. + * + * Impact: + * + * This problem only manifests when the userspace process is itself using + * :manpage:`libpsx(3)` or an equivalent mechanism to enforce a Landlock policy + * on multiple already-running threads at once. Programs which enforce a + * Landlock policy at startup time and only then become multithreaded are not + * affected. Without this fix, signal scoping could break multi-threaded + * applications that expect threads within the same process to freely signal + * each other. + */ +LANDLOCK_ERRATUM(2) diff --git a/security/landlock/fs.c b/security/landlock/fs.c new file mode 100644 index 000000000..330a1871b --- /dev/null +++ b/security/landlock/fs.c @@ -0,0 +1,2142 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Filesystem management and hooks + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation + * Copyright © 2022 Günther Noack <gnoack3000@gmail.com> + * Copyright © 2023-2024 Google LLC + */ + +#include <asm/ioctls.h> +#include <kunit/test.h> +#include <linux/atomic.h> +#include <linux/bitops.h> +#include <linux/bits.h> +#include <linux/compiler_types.h> +#include <linux/dcache.h> +#include <linux/err.h> +#include <linux/falloc.h> +#include <linux/fs.h> +#include <linux/init.h> +#include <linux/kdev_t.h> +#include <linux/kernel.h> +#include <linux/limits.h> +#include <linux/list.h> +#include <linux/lsm_audit.h> +#include <linux/lsm_hooks.h> +#include <linux/mount.h> +#include <linux/namei.h> +#include <linux/net.h> +#include <linux/path.h> +#include <linux/pid.h> +#include <linux/rcupdate.h> +#include <linux/sched/signal.h> +#include <linux/spinlock.h> +#include <linux/stat.h> +#include <linux/types.h> +#include <linux/wait_bit.h> +#include <linux/workqueue.h> +#include <net/af_unix.h> +#include <uapi/linux/fiemap.h> +#include <uapi/linux/landlock.h> + +#include "access.h" +#include "common.h" +#include "cred.h" +#include "domain.h" +#include "fs.h" +#include "limits.h" +#include "log.h" +#include "object.h" +#include "ruleset.h" +#include "setup.h" + +#include <trace/events/landlock.h> + +/* Underlying object management */ + +static void release_inode(struct landlock_object *const object) + __releases(object->lock) +{ + struct inode *const inode = object->underobj; + struct super_block *sb; + + if (!inode) { + spin_unlock(&object->lock); + return; + } + + /* + * Protects against concurrent use by hook_sb_delete() of the reference + * to the underlying inode. + */ + object->underobj = NULL; + /* + * Makes sure that if the filesystem is concurrently unmounted, + * hook_sb_delete() will wait for us to finish iput(). + */ + sb = inode->i_sb; + atomic_long_inc(&landlock_superblock(sb)->inode_refs); + spin_unlock(&object->lock); + /* + * Because object->underobj was not NULL, hook_sb_delete() and + * get_inode_object() guarantee that it is safe to reset + * landlock_inode(inode)->object while it is not NULL. It is therefore + * not necessary to lock inode->i_lock. + */ + rcu_assign_pointer(landlock_inode(inode)->object, NULL); + /* + * Now, new rules can safely be tied to @inode with get_inode_object(). + */ + + iput(inode); + if (atomic_long_dec_and_test(&landlock_superblock(sb)->inode_refs)) + wake_up_var(&landlock_superblock(sb)->inode_refs); +} + +static const struct landlock_object_underops landlock_fs_underops = { + .release = release_inode +}; + +/* IOCTL helpers */ + +/** + * is_masked_device_ioctl - Determine whether an IOCTL command is always + * permitted with Landlock for device files. These commands can not be + * restricted on device files by enforcing a Landlock policy. + * + * @cmd: The IOCTL command that is supposed to be run. + * + * By default, any IOCTL on a device file requires the + * LANDLOCK_ACCESS_FS_IOCTL_DEV right. However, we blanket-permit some + * commands, if: + * + * 1. The command is implemented in fs/ioctl.c's do_vfs_ioctl(), + * not in f_ops->unlocked_ioctl() or f_ops->compat_ioctl(). + * + * 2. The command is harmless when invoked on devices. + * + * We also permit commands that do not make sense for devices, but where the + * do_vfs_ioctl() implementation returns a more conventional error code. + * + * Any new IOCTL commands that are implemented in fs/ioctl.c's do_vfs_ioctl() + * should be considered for inclusion here. + * + * Return: True if the IOCTL @cmd can not be restricted with Landlock for + * device files, false otherwise. + */ +static __attribute_const__ bool is_masked_device_ioctl(const unsigned int cmd) +{ + switch (cmd) { + /* + * FIOCLEX, FIONCLEX, FIONBIO and FIOASYNC manipulate the FD's + * close-on-exec and the file's buffered-IO and async flags. These + * operations are also available through fcntl(2), and are + * unconditionally permitted in Landlock. + */ + case FIOCLEX: + case FIONCLEX: + case FIONBIO: + case FIOASYNC: + /* + * FIOQSIZE queries the size of a regular file, directory, or link. + * + * We still permit it, because it always returns -ENOTTY for + * other file types. + */ + case FIOQSIZE: + /* + * FIFREEZE and FITHAW freeze and thaw the file system which the + * given file belongs to. Requires CAP_SYS_ADMIN. + * + * These commands operate on the file system's superblock rather + * than on the file itself. The same operations can also be + * done through any other file or directory on the same file + * system, so it is safe to permit these. + */ + case FIFREEZE: + case FITHAW: + /* + * FS_IOC_FIEMAP queries information about the allocation of + * blocks within a file. + * + * This IOCTL command only makes sense for regular files and is + * not implemented by devices. It is harmless to permit. + */ + case FS_IOC_FIEMAP: + /* + * FIGETBSZ queries the file system's block size for a file or + * directory. + * + * This command operates on the file system's superblock rather + * than on the file itself. The same operation can also be done + * through any other file or directory on the same file system, + * so it is safe to permit it. + */ + case FIGETBSZ: + /* + * FICLONE, FICLONERANGE and FIDEDUPERANGE make files share + * their underlying storage ("reflink") between source and + * destination FDs, on file systems which support that. + * + * These IOCTL commands only apply to regular files + * and are harmless to permit for device files. + */ + case FICLONE: + case FICLONERANGE: + case FIDEDUPERANGE: + /* + * FS_IOC_GETFSUUID and FS_IOC_GETFSSYSFSPATH both operate on + * the file system superblock, not on the specific file, so + * these operations are available through any other file on the + * same file system as well. + */ + case FS_IOC_GETFSUUID: + case FS_IOC_GETFSSYSFSPATH: + return true; + + /* + * FIONREAD, FS_IOC_GETFLAGS, FS_IOC_SETFLAGS, FS_IOC_FSGETXATTR and + * FS_IOC_FSSETXATTR are forwarded to device implementations. + */ + + /* + * file_ioctl() commands (FIBMAP, FS_IOC_RESVSP, FS_IOC_RESVSP64, + * FS_IOC_UNRESVSP, FS_IOC_UNRESVSP64 and FS_IOC_ZERO_RANGE) are + * forwarded to device implementations, so not permitted. + */ + + /* Other commands are guarded by the access right. */ + default: + return false; + } +} + +/* + * is_masked_device_ioctl_compat - same as the helper above, but checking the + * "compat" IOCTL commands. + * + * The IOCTL commands with special handling in compat-mode should behave the + * same as their non-compat counterparts. + */ +static __attribute_const__ bool +is_masked_device_ioctl_compat(const unsigned int cmd) +{ + switch (cmd) { + /* FICLONE is permitted, same as in the non-compat variant. */ + case FICLONE: + return true; + +#if defined(CONFIG_X86_64) + /* + * FS_IOC_RESVSP_32, FS_IOC_RESVSP64_32, FS_IOC_UNRESVSP_32, + * FS_IOC_UNRESVSP64_32, FS_IOC_ZERO_RANGE_32: not blanket-permitted, + * for consistency with their non-compat variants. + */ + case FS_IOC_RESVSP_32: + case FS_IOC_RESVSP64_32: + case FS_IOC_UNRESVSP_32: + case FS_IOC_UNRESVSP64_32: + case FS_IOC_ZERO_RANGE_32: +#endif + + /* + * FS_IOC32_GETFLAGS, FS_IOC32_SETFLAGS are forwarded to their device + * implementations. + */ + case FS_IOC32_GETFLAGS: + case FS_IOC32_SETFLAGS: + return false; + default: + return is_masked_device_ioctl(cmd); + } +} + +/* Ruleset management */ + +static struct landlock_object *get_inode_object(struct inode *const inode) +{ + struct landlock_object *object, *new_object; + struct landlock_inode_security *inode_sec = landlock_inode(inode); + + rcu_read_lock(); +retry: + object = rcu_dereference(inode_sec->object); + if (object) { + if (likely(refcount_inc_not_zero(&object->usage))) { + rcu_read_unlock(); + return object; + } + /* + * We are racing with release_inode(), the object is going + * away. Wait for release_inode(), then retry. + */ + spin_lock(&object->lock); + spin_unlock(&object->lock); + goto retry; + } + rcu_read_unlock(); + + /* + * If there is no object tied to @inode, then create a new one (without + * holding any locks). + */ + new_object = landlock_create_object(&landlock_fs_underops, inode); + if (IS_ERR(new_object)) + return new_object; + + /* + * Protects against concurrent calls to get_inode_object() or + * hook_sb_delete(). + */ + spin_lock(&inode->i_lock); + if (unlikely(rcu_access_pointer(inode_sec->object))) { + /* Someone else just created the object, bail out and retry. */ + spin_unlock(&inode->i_lock); + kfree(new_object); + + rcu_read_lock(); + goto retry; + } + + /* + * @inode will be released by hook_sb_delete() on its superblock + * shutdown, or by release_inode() when no more ruleset references the + * related object. + */ + ihold(inode); + rcu_assign_pointer(inode_sec->object, new_object); + spin_unlock(&inode->i_lock); + return new_object; +} + +/* All access rights that can be tied to files. */ +/* clang-format off */ +#define ACCESS_FILE ( \ + LANDLOCK_ACCESS_FS_EXECUTE | \ + LANDLOCK_ACCESS_FS_WRITE_FILE | \ + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_TRUNCATE | \ + LANDLOCK_ACCESS_FS_IOCTL_DEV | \ + LANDLOCK_ACCESS_FS_RESOLVE_UNIX) +/* clang-format on */ + +/* + * @path: Should have been checked by get_path_from_fd(). + */ +int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, + const struct path *const path, + access_mask_t access_rights, const u32 flags) +{ + int err; + struct landlock_id id = { + .type = LANDLOCK_KEY_INODE, + }; + + /* Files only get access rights that make sense. */ + if (!d_is_dir(path->dentry) && + !access_mask_subset(access_rights, ACCESS_FILE)) + return -EINVAL; + + /* Transforms relative access rights to absolute ones. */ + access_rights |= LANDLOCK_MASK_ACCESS_FS & + ~(ruleset->handled_masks.fs | + _LANDLOCK_ACCESS_FS_INITIALLY_DENIED); + id.key.object = get_inode_object(d_backing_inode(path->dentry)); + if (IS_ERR(id.key.object)) + return PTR_ERR(id.key.object); + mutex_lock(&ruleset->lock); + err = landlock_insert_rule(ruleset, id, access_rights, flags); + + /* + * Emit after the rule insertion succeeds, so every event corresponds to + * a rule that is actually in the ruleset. The ruleset lock is still + * held for BTF consistency (enforced by lockdep_assert_held in + * TP_fast_assign). + */ + if (!err && trace_landlock_add_rule_fs_enabled()) { + char *buffer __free(__putname) = __getname(); + const char *pathname = + buffer ? resolve_path_for_trace(path, buffer) : + "<no_mem>"; + + trace_landlock_add_rule_fs(ruleset, access_rights, path, + pathname); + } + mutex_unlock(&ruleset->lock); + + /* + * No need to check for an error because landlock_insert_rule() + * increments the refcount for the new object if needed. + */ + landlock_put_object(id.key.object); + return err; +} + +/* Access-control management */ + +/** + * get_inode_id - Look up the Landlock object for a dentry + * @dentry: The dentry to look up. + * @id: Filled with the inode's Landlock object pointer on success. + * + * Extracts the Landlock object pointer from @dentry's inode security blob and + * stores it in @id for use as a rule-tree lookup key. + * + * When this returns false (negative dentry or no Landlock object), no rule can + * match this inode, so landlock_unmask_layers() need not be called. Callers + * that gate landlock_unmask_layers() on this function must handle the NULL + * masks case independently, since the !masks-returns-true early-return in + * landlock_unmask_layers() will not be reached. See the allowed_parent2 + * initialization in is_access_to_paths_allowed(). + * + * Return: True if a Landlock object exists for @dentry, false otherwise. + */ +static bool get_inode_id(const struct dentry *const dentry, + struct landlock_id *id) +{ + /* Ignores nonexistent leafs. */ + if (d_is_negative(dentry)) + return false; + + /* + * rcu_access_pointer() is sufficient: the pointer is used only as a + * numeric comparison key for rule lookup, not dereferenced. The object + * cannot be freed while the domain exists because the domain's rule + * tree holds its own reference to it. + */ + id->key.object = rcu_access_pointer( + landlock_inode(d_backing_inode(dentry))->object); + return !!id->key.object; +} + +static bool unmask_layers_fs(const struct landlock_domain *const domain, + const struct landlock_id id, + const access_mask_t access_request, + struct layer_masks *masks, + const struct dentry *const dentry) +{ + const struct landlock_rule *rule = NULL; + bool ret; + + ret = landlock_unmask_layers(domain, id, masks, &rule); + if (rule) + trace_landlock_check_rule_fs(domain, rule, access_request, + dentry); + return ret; +} + +/* + * Allows access to pseudo filesystems that will never be mountable (e.g. + * sockfs, pipefs), but can still be reachable through + * /proc/<pid>/fd/<file-descriptor> + */ +static bool is_nouser_or_private(const struct dentry *dentry) +{ + return (dentry->d_sb->s_flags & SB_NOUSER) || + (d_is_positive(dentry) && + unlikely(IS_PRIVATE(d_backing_inode(dentry)))); +} + +static const struct access_masks any_fs = { + .fs = ~0, +}; + +/* + * Returns true iff the child file with the given src_child access rights under + * src_parent would result in having the same or fewer access rights if it were + * moved under new_parent. + */ +static bool may_refer(const struct layer_masks *const src_parent, + const struct layer_masks *const src_child, + const struct layer_masks *const new_parent, + const bool child_is_dir) +{ + for (size_t i = 0; i < ARRAY_SIZE(new_parent->layers); i++) { + access_mask_t child_access = src_parent->layers[i].access & + src_child->layers[i].access; + access_mask_t parent_access = new_parent->layers[i].access; + + if (!child_is_dir) { + child_access &= ACCESS_FILE; + parent_access &= ACCESS_FILE; + } + + if (!access_mask_subset(child_access, parent_access)) + return false; + } + return true; +} + +/* + * Check that a destination file hierarchy has more restrictions than a source + * file hierarchy. This is only used for link and rename actions. + * + * Return: True if child1 may be moved from parent1 to parent2 without + * increasing its access rights (if child2 is set, an additional condition is + * that child2 may be used from parent2 to parent1 without increasing its access + * rights), false otherwise. + */ +static bool no_more_access(const struct layer_masks *const parent1, + const struct layer_masks *const child1, + const bool child1_is_dir, + const struct layer_masks *const parent2, + const struct layer_masks *const child2, + const bool child2_is_dir) +{ + if (!may_refer(parent1, child1, parent2, child1_is_dir)) + return false; + + if (!child2) + return true; + + return may_refer(parent2, child2, parent1, child2_is_dir); +} + +#define NMA_TRUE(...) KUNIT_EXPECT_TRUE(test, no_more_access(__VA_ARGS__)) +#define NMA_FALSE(...) KUNIT_EXPECT_FALSE(test, no_more_access(__VA_ARGS__)) + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_no_more_access(struct kunit *const test) +{ + const struct layer_masks rx0 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_READ_FILE, + }; + const struct layer_masks mx0 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_MAKE_REG, + }; + const struct layer_masks x0 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, + }; + const struct layer_masks x1 = { + .layers[1].access = LANDLOCK_ACCESS_FS_EXECUTE, + }; + const struct layer_masks x01 = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, + .layers[1].access = LANDLOCK_ACCESS_FS_EXECUTE, + }; + const struct layer_masks allows_all = {}; + + /* Checks without restriction. */ + NMA_TRUE(&x0, &allows_all, false, &allows_all, NULL, false); + NMA_TRUE(&allows_all, &x0, false, &allows_all, NULL, false); + NMA_FALSE(&x0, &x0, false, &allows_all, NULL, false); + + /* + * Checks that we can only refer a file if no more access could be + * inherited. + */ + NMA_TRUE(&x0, &x0, false, &rx0, NULL, false); + NMA_TRUE(&rx0, &rx0, false, &rx0, NULL, false); + NMA_FALSE(&rx0, &rx0, false, &x0, NULL, false); + NMA_FALSE(&rx0, &rx0, false, &x1, NULL, false); + + /* Checks allowed referring with different nested domains. */ + NMA_TRUE(&x0, &x1, false, &x0, NULL, false); + NMA_TRUE(&x1, &x0, false, &x0, NULL, false); + NMA_TRUE(&x0, &x01, false, &x0, NULL, false); + NMA_TRUE(&x0, &x01, false, &rx0, NULL, false); + NMA_TRUE(&x01, &x0, false, &x0, NULL, false); + NMA_TRUE(&x01, &x0, false, &rx0, NULL, false); + NMA_FALSE(&x01, &x01, false, &x0, NULL, false); + + /* Checks that file access rights are also enforced for a directory. */ + NMA_FALSE(&rx0, &rx0, true, &x0, NULL, false); + + /* Checks that directory access rights don't impact file referring... */ + NMA_TRUE(&mx0, &mx0, false, &x0, NULL, false); + /* ...but only directory referring. */ + NMA_FALSE(&mx0, &mx0, true, &x0, NULL, false); + + /* Checks directory exchange. */ + NMA_TRUE(&mx0, &mx0, true, &mx0, &mx0, true); + NMA_TRUE(&mx0, &mx0, true, &mx0, &x0, true); + NMA_FALSE(&mx0, &mx0, true, &x0, &mx0, true); + NMA_FALSE(&mx0, &mx0, true, &x0, &x0, true); + NMA_FALSE(&mx0, &mx0, true, &x1, &x1, true); + + /* Checks file exchange with directory access rights... */ + NMA_TRUE(&mx0, &mx0, false, &mx0, &mx0, false); + NMA_TRUE(&mx0, &mx0, false, &mx0, &x0, false); + NMA_TRUE(&mx0, &mx0, false, &x0, &mx0, false); + NMA_TRUE(&mx0, &mx0, false, &x0, &x0, false); + /* ...and with file access rights. */ + NMA_TRUE(&rx0, &rx0, false, &rx0, &rx0, false); + NMA_TRUE(&rx0, &rx0, false, &rx0, &x0, false); + NMA_FALSE(&rx0, &rx0, false, &x0, &rx0, false); + NMA_FALSE(&rx0, &rx0, false, &x0, &x0, false); + NMA_FALSE(&rx0, &rx0, false, &x1, &x1, false); + + /* + * Allowing the following requests should not be a security risk + * because domain 0 denies execute access, and domain 1 is always + * nested with domain 0. However, adding an exception for this case + * would mean to check all nested domains to make sure none can get + * more privileges (e.g. processes only sandboxed by domain 0). + * Moreover, this behavior (i.e. composition of N domains) could then + * be inconsistent compared to domain 1's ruleset alone (e.g. it might + * be denied to link/rename with domain 1's ruleset, whereas it would + * be allowed if nested on top of domain 0). Another drawback would be + * to create a cover channel that could enable sandboxed processes to + * infer most of the filesystem restrictions from their domain. To + * make it simple, efficient, safe, and more consistent, this case is + * always denied. + */ + NMA_FALSE(&x1, &x1, false, &x0, NULL, false); + NMA_FALSE(&x1, &x1, false, &rx0, NULL, false); + NMA_FALSE(&x1, &x1, true, &x0, NULL, false); + NMA_FALSE(&x1, &x1, true, &rx0, NULL, false); + + /* Checks the same case of exclusive domains with a file... */ + NMA_TRUE(&x1, &x1, false, &x01, NULL, false); + NMA_FALSE(&x1, &x1, false, &x01, &x0, false); + NMA_FALSE(&x1, &x1, false, &x01, &x01, false); + NMA_FALSE(&x1, &x1, false, &x0, &x0, false); + /* ...and with a directory. */ + NMA_FALSE(&x1, &x1, false, &x0, &x0, true); + NMA_FALSE(&x1, &x1, true, &x0, &x0, false); + NMA_FALSE(&x1, &x1, true, &x0, &x0, true); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +#undef NMA_TRUE +#undef NMA_FALSE + +static bool is_layer_masks_allowed(const struct layer_masks *masks) +{ + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { + if (masks->layers[i].access) + return false; + } + return true; +} + +/* + * Removes @masks accesses that are not requested. + * + * Returns true if the request is allowed, false otherwise. + */ +static bool scope_to_request(const access_mask_t access_request, + struct layer_masks *masks) +{ + bool saw_unfulfilled_access = false; + + if (WARN_ON_ONCE(!masks)) + return true; + + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { + masks->layers[i].access &= access_request; + if (masks->layers[i].access) + saw_unfulfilled_access = true; + } + return !saw_unfulfilled_access; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_scope_to_request_with_exec_none(struct kunit *const test) +{ + /* Allows everything. */ + struct layer_masks masks = {}; + + /* Checks and scopes with execute. */ + KUNIT_EXPECT_TRUE(test, + scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE, &masks)); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[0].access); +} + +static void test_scope_to_request_with_exec_some(struct kunit *const test) +{ + /* Denies execute and write. */ + struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, + .layers[1].access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }; + + /* Checks and scopes with execute. */ + KUNIT_EXPECT_FALSE(test, scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE, + &masks)); + /* + * These casts to access_mask_t are needed because typeof(), used in + * KUNIT_EXPECT_EQ(), does not work on bitfields. + */ + KUNIT_EXPECT_EQ(test, LANDLOCK_ACCESS_FS_EXECUTE, + (access_mask_t)masks.layers[0].access); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[1].access); +} + +static void test_scope_to_request_without_access(struct kunit *const test) +{ + /* Denies execute and write. */ + struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE, + .layers[1].access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }; + + /* Checks and scopes without access request. */ + KUNIT_EXPECT_TRUE(test, scope_to_request(0, &masks)); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[0].access); + KUNIT_EXPECT_EQ(test, 0, (access_mask_t)masks.layers[1].access); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +/* + * Returns true if there is at least one access right different than + * LANDLOCK_ACCESS_FS_REFER. + */ +static bool is_eacces(const struct layer_masks *masks, + const access_mask_t access_request) +{ + if (!masks) + return false; + + for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { + /* LANDLOCK_ACCESS_FS_REFER alone must return -EXDEV. */ + if (masks->layers[i].access & access_request & + ~LANDLOCK_ACCESS_FS_REFER) + return true; + } + return false; +} + +#define IE_TRUE(...) KUNIT_EXPECT_TRUE(test, is_eacces(__VA_ARGS__)) +#define IE_FALSE(...) KUNIT_EXPECT_FALSE(test, is_eacces(__VA_ARGS__)) + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_is_eacces_with_none(struct kunit *const test) +{ + const struct layer_masks masks = {}; + + IE_FALSE(&masks, 0); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_REFER); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_EXECUTE); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_WRITE_FILE); +} + +static void test_is_eacces_with_refer(struct kunit *const test) +{ + const struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_REFER, + }; + + IE_FALSE(&masks, 0); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_REFER); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_EXECUTE); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_WRITE_FILE); +} + +static void test_is_eacces_with_write(struct kunit *const test) +{ + const struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_WRITE_FILE, + }; + + IE_FALSE(&masks, 0); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_REFER); + IE_FALSE(&masks, LANDLOCK_ACCESS_FS_EXECUTE); + + IE_TRUE(&masks, LANDLOCK_ACCESS_FS_WRITE_FILE); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +#undef IE_TRUE +#undef IE_FALSE + +/** + * is_access_to_paths_allowed - Check accesses for requests with a common path + * + * @domain: Domain to check against. + * @path: File hierarchy to walk through. For refer checks, this would be + * the common mountpoint. + * @access_request_parent1: Accesses to check, once @layer_masks_parent1 is + * equal to @layer_masks_parent2 (if any). This is tied to the unique + * requested path for most actions, or the source in case of a refer action + * (i.e. rename or link), or the source and destination in case of + * RENAME_EXCHANGE. + * @layer_masks_parent1: Pointer to a matrix of layer masks per access + * masks, identifying the layers that forbid a specific access. Bits from + * this matrix can be unset according to the @path walk. An empty matrix + * means that @domain allows all possible Landlock accesses (i.e. not only + * 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 + * request involving a source and a destination. This refers to the + * destination, except in case of RENAME_EXCHANGE where it also refers to + * 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. + * + * This helper first checks that the destination has a superset of restrictions + * compared to the source (if any) for a common path. Because of + * RENAME_EXCHANGE actions, source and destinations may be swapped. It then + * checks that the collected accesses and the remaining ones are enough to + * allow the request. + * + * Return: True if the access request is granted, false otherwise. + */ +static bool +is_access_to_paths_allowed(const struct landlock_domain *const domain, + const struct path *const path, + const access_mask_t access_request_parent1, + struct layer_masks *layer_masks_parent1, + struct landlock_request *const log_request_parent1, + struct dentry *const dentry_child1, + const access_mask_t access_request_parent2, + struct layer_masks *layer_masks_parent2, + 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; + struct path walker_path; + struct landlock_id id = { + .type = LANDLOCK_KEY_INODE, + }; + access_mask_t access_masked_parent1, access_masked_parent2; + struct layer_masks _layer_masks_child1, _layer_masks_child2; + struct layer_masks *layer_masks_child1 = NULL, + *layer_masks_child2 = NULL; + + if (!access_request_parent1 && !access_request_parent2) + return true; + + if (WARN_ON_ONCE(!path)) + return true; + + if (is_nouser_or_private(path->dentry)) + return true; + + if (WARN_ON_ONCE(!layer_masks_parent1)) + return false; + + allowed_parent1 = is_layer_masks_allowed(layer_masks_parent1); + + if (unlikely(layer_masks_parent2)) { + if (WARN_ON_ONCE(!dentry_child1)) + return false; + + allowed_parent2 = is_layer_masks_allowed(layer_masks_parent2); + + /* + * For a double request, first check for potential privilege + * escalation by looking at domain handled accesses (which are + * a superset of the meaningful requested accesses). + */ + access_masked_parent1 = access_masked_parent2 = + landlock_union_access_masks(domain).fs; + is_dom_check = true; + } else { + if (WARN_ON_ONCE(dentry_child1 || dentry_child2)) + return false; + /* For a simple request, only check for requested accesses. */ + access_masked_parent1 = access_request_parent1; + access_masked_parent2 = access_request_parent2; + /* + * Simple requests have no parent2 to check, so parent2 is + * trivially allowed. This must be set explicitly because the + * get_inode_id() gate in the pathwalk loop may prevent + * landlock_unmask_layers() from being called (which would + * otherwise return true for NULL masks as a side effect). + */ + allowed_parent2 = true; + is_dom_check = false; + } + + if (unlikely(dentry_child1)) { + struct landlock_id id = { + .type = LANDLOCK_KEY_INODE, + }; + access_mask_t handled; + + handled = landlock_init_layer_masks(domain, + LANDLOCK_MASK_ACCESS_FS, + &_layer_masks_child1, + LANDLOCK_KEY_INODE); + if (handled && get_inode_id(dentry_child1, &id)) + unmask_layers_fs(domain, id, handled, + &_layer_masks_child1, dentry_child1); + layer_masks_child1 = &_layer_masks_child1; + child1_is_directory = d_is_dir(dentry_child1); + } + if (unlikely(dentry_child2)) { + struct landlock_id id = { + .type = LANDLOCK_KEY_INODE, + }; + access_mask_t handled; + + handled = landlock_init_layer_masks(domain, + LANDLOCK_MASK_ACCESS_FS, + &_layer_masks_child2, + LANDLOCK_KEY_INODE); + if (handled && get_inode_id(dentry_child2, &id)) + unmask_layers_fs(domain, id, handled, + &_layer_masks_child2, dentry_child2); + layer_masks_child2 = &_layer_masks_child2; + child2_is_directory = d_is_dir(dentry_child2); + } + + walker_path = *path; + path_get(&walker_path); + /* + * We need to walk through all the hierarchy to not miss any relevant + * restriction. + */ + while (true) { + /* + * If at least all accesses allowed on the destination are + * already allowed on the source, respectively if there is at + * least as much as restrictions on the destination than on the + * source, then we can safely refer files from the source to + * the destination without risking a privilege escalation. + * This also applies in the case of RENAME_EXCHANGE, which + * implies checks on both direction. This is crucial for + * standalone multilayered security policies. Furthermore, + * this helps avoid policy writers to shoot themselves in the + * foot. + */ + if (unlikely(is_dom_check && + no_more_access( + layer_masks_parent1, layer_masks_child1, + child1_is_directory, layer_masks_parent2, + layer_masks_child2, + child2_is_directory))) { + /* + * Now, downgrades the remaining checks from domain + * handled accesses to requested accesses. + */ + is_dom_check = false; + access_masked_parent1 = access_request_parent1; + access_masked_parent2 = access_request_parent2; + + allowed_parent1 = + allowed_parent1 || + scope_to_request(access_masked_parent1, + layer_masks_parent1); + allowed_parent2 = + allowed_parent2 || + scope_to_request(access_masked_parent2, + layer_masks_parent2); + + /* Stops when all accesses are granted. */ + if (allowed_parent1 && allowed_parent2) + break; + } + + if (get_inode_id(walker_path.dentry, &id)) { + allowed_parent1 = + allowed_parent1 || + unmask_layers_fs(domain, id, + access_masked_parent1, + layer_masks_parent1, + walker_path.dentry); + allowed_parent2 = + allowed_parent2 || + unmask_layers_fs(domain, id, + access_masked_parent2, + layer_masks_parent2, + walker_path.dentry); + } + + /* Stops when a rule from each layer grants access. */ + if (allowed_parent1 && allowed_parent2) + break; + +jump_up: + if (walker_path.dentry == walker_path.mnt->mnt_root) { + if (follow_up(&walker_path)) { + /* Ignores hidden mount points. */ + goto jump_up; + } else { + /* + * Stops at the real root. Denies access + * because not all layers have granted access. + */ + break; + } + } + + if (unlikely(IS_ROOT(walker_path.dentry))) { + if (likely(walker_path.mnt->mnt_flags & MNT_INTERNAL)) { + /* + * Stops and allows access when reaching disconnected root + * directories that are part of internal filesystems (e.g. nsfs, + * which is reachable through /proc/<pid>/ns/<namespace>). + */ + allowed_parent1 = true; + allowed_parent2 = true; + break; + } + + /* + * We reached a disconnected root directory from a bind mount. + * Let's continue the walk with the mount point we missed. + */ + dput(walker_path.dentry); + walker_path.dentry = walker_path.mnt->mnt_root; + dget(walker_path.dentry); + } else { + struct dentry *const parent_dentry = + dget_parent(walker_path.dentry); + + dput(walker_path.dentry); + walker_path.dentry = parent_dentry; + } + } + path_put(&walker_path); + + /* + * Check CONFIG_SECURITY_LANDLOCK_LOG to enable elision of + * log_request_parent* and associated caller's stack variables thanks to + * dead code elimination. + */ +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + if (!allowed_parent1 && log_request_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; + } + + if (!allowed_parent2 && log_request_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; + } +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + return allowed_parent1 && allowed_parent2; +} + +static int current_check_access_path(const struct path *const path, + access_mask_t access_request) +{ + const struct access_masks masks = { + .fs = access_request, + }; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), masks, NULL); + struct layer_masks layer_masks; + struct landlock_request request = {}; + + if (!subject) + return 0; + + 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, &request, NULL, 0, NULL, + NULL, NULL)) + return 0; + + landlock_log_denial(subject, &request); + return -EACCES; +} + +static __attribute_const__ access_mask_t get_mode_access(const umode_t mode, + const dev_t dev) +{ + switch (mode & S_IFMT) { + case S_IFLNK: + return LANDLOCK_ACCESS_FS_MAKE_SYM; + case S_IFDIR: + return LANDLOCK_ACCESS_FS_MAKE_DIR; + case S_IFCHR: + /* Whiteout objects are guarded with MAKE_REG. */ + if (dev == WHITEOUT_DEV) + return LANDLOCK_ACCESS_FS_MAKE_REG; + return LANDLOCK_ACCESS_FS_MAKE_CHAR; + case S_IFBLK: + return LANDLOCK_ACCESS_FS_MAKE_BLOCK; + case S_IFIFO: + return LANDLOCK_ACCESS_FS_MAKE_FIFO; + case S_IFSOCK: + return LANDLOCK_ACCESS_FS_MAKE_SOCK; + case S_IFREG: + case 0: + /* A zero mode translates to S_IFREG. */ + default: + /* Treats weird files as regular files. */ + return LANDLOCK_ACCESS_FS_MAKE_REG; + } +} + +static access_mask_t get_dentry_access(const struct dentry *const dentry) +{ + const struct inode *const inode = d_backing_inode(dentry); + + return get_mode_access(inode->i_mode, inode->i_rdev); +} + +static access_mask_t maybe_remove(const struct dentry *const dentry) +{ + if (d_is_negative(dentry)) + return 0; + return d_is_dir(dentry) ? LANDLOCK_ACCESS_FS_REMOVE_DIR : + LANDLOCK_ACCESS_FS_REMOVE_FILE; +} + +/** + * collect_domain_accesses - Walk through a file path and collect accesses + * + * @domain: Domain to check against. + * @mnt_root: Last directory to check. + * @dir: Directory to start the walk from. + * @layer_masks_dom: Where to store the collected accesses. + * + * This helper is useful to begin a path walk from the @dir directory to a + * @mnt_root directory used as a mount point. This mount point is the common + * ancestor between the source and the destination of a renamed and linked + * file. While walking from @dir to @mnt_root, we record all the domain's + * allowed accesses in @layer_masks_dom. + * + * Because of disconnected directories, this walk may not reach @mnt_dir. In + * this case, the walk will continue to @mnt_dir after this call. + * + * This is similar to is_access_to_paths_allowed() but much simpler because it + * only handles walking on the same mount point and only checks one set of + * accesses. + * + * Return: True if all the domain access rights are allowed for @dir, false if + * the walk reached @mnt_root. + */ +static bool collect_domain_accesses(const struct landlock_domain *const domain, + const struct dentry *const mnt_root, + struct dentry *dir, + struct layer_masks *layer_masks_dom) +{ + bool ret = false; + access_mask_t access_masked_dom; + + if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom)) + return true; + if (is_nouser_or_private(dir)) + return true; + + access_masked_dom = + landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS, + layer_masks_dom, LANDLOCK_KEY_INODE); + if (!access_masked_dom) + return true; + + dget(dir); + while (true) { + struct dentry *parent_dentry; + struct landlock_id id = { + .type = LANDLOCK_KEY_INODE, + }; + + /* Gets all layers allowing all domain accesses. */ + if (get_inode_id(dir, &id) && + unmask_layers_fs(domain, id, access_masked_dom, + layer_masks_dom, dir)) { + /* + * Stops when all handled accesses are allowed by at + * least one rule in each layer. + */ + ret = true; + break; + } + + /* + * Stops at the mount point or the filesystem root for a disconnected + * directory. + */ + if (dir == mnt_root || unlikely(IS_ROOT(dir))) + break; + + parent_dentry = dget_parent(dir); + dput(dir); + dir = parent_dentry; + } + dput(dir); + return ret; +} + +/** + * current_check_refer_path - Check if a rename or link action is allowed + * + * @old_dentry: File or directory requested to be moved or linked. + * @new_dir: Destination parent directory. + * @new_dentry: Destination file or directory. + * @removable: Sets to true if it is a rename operation. + * @exchange: Sets to true if it is a rename operation with RENAME_EXCHANGE. + * @whiteout: Sets to true if it is a rename operation with RENAME_WHITEOUT. + * + * Because of its unprivileged constraints, Landlock relies on file hierarchies + * (and not only inodes) to tie access rights to files. Being able to link or + * rename a file hierarchy brings some challenges. Indeed, moving or linking a + * file (i.e. creating a new reference to an inode) can have an impact on the + * actions allowed for a set of files if it would change its parent directory + * (i.e. reparenting). + * + * To avoid trivial access right bypasses, Landlock first checks if the file or + * directory requested to be moved would gain new access rights inherited from + * its new hierarchy. Before returning any error, Landlock then checks that + * the parent source hierarchy and the destination hierarchy would allow the + * link or rename action. If it is not the case, an error with EACCES is + * returned to inform user space that there is no way to remove or create the + * requested source file type. If it should be allowed but the new inherited + * access rights would be greater than the source access rights, then the + * kernel returns an error with EXDEV. Prioritizing EACCES over EXDEV enables + * user space to abort the whole operation if there is no way to do it, or to + * manually copy the source to the destination if this remains allowed, e.g. + * because file creation is allowed on the destination directory but not direct + * linking. + * + * To achieve this goal, the kernel needs to compare two file hierarchies: the + * one identifying the source file or directory (including itself), and the + * destination one. This can be seen as a multilayer partial ordering problem. + * The kernel walks through these paths and collects in a matrix the access + * rights that are denied per layer. These matrices are then compared to see + * if the destination one has more (or the same) restrictions as the source + * one. If this is the case, the requested action will not return EXDEV, which + * doesn't mean the action is allowed. The parent hierarchy of the source + * (i.e. parent directory), and the destination hierarchy must also be checked + * to verify that they explicitly allow such action (i.e. referencing, + * creation and potentially removal rights). The kernel implementation is then + * required to rely on potentially four matrices of access rights: one for the + * source file or directory (i.e. the child), a potentially other one for the + * other source/destination (in case of RENAME_EXCHANGE), one for the source + * parent hierarchy and a last one for the destination hierarchy. These + * ephemeral matrices take some space on the stack, which limits the number of + * layers to a deemed reasonable number: 16. + * + * Return: 0 if access is allowed, -EXDEV if @old_dentry would inherit new + * access rights from @new_dir, or -EACCES if file removal or creation is + * denied. + */ +static int current_check_refer_path(struct dentry *const old_dentry, + const struct path *const new_dir, + struct dentry *const new_dentry, + const bool removable, const bool exchange, + const bool whiteout) +{ + 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; + struct dentry *old_parent; + struct layer_masks layer_masks_parent1 = {}, layer_masks_parent2 = {}; + struct landlock_request request1 = {}, request2 = {}; + + if (!subject) + return 0; + + if (unlikely(d_is_negative(old_dentry))) + return -ENOENT; + if (exchange) { + if (unlikely(d_is_negative(new_dentry))) + return -ENOENT; + access_request_parent1 = get_dentry_access(new_dentry); + } else { + access_request_parent1 = 0; + } + access_request_parent2 = get_dentry_access(old_dentry); + if (removable) { + access_request_parent1 |= maybe_remove(old_dentry); + access_request_parent2 |= maybe_remove(new_dentry); + } + + /* + * In case of renameat2(2) with RENAME_WHITEOUT, a whiteout object is + * created in the source location, so we require an additional access + * right there. + */ + if (whiteout) + access_request_parent1 |= + get_mode_access(S_IFCHR | WHITEOUT_MODE, WHITEOUT_DEV); + + /* The mount points are the same for old and new paths, cf. EXDEV. */ + if (old_dentry->d_parent == new_dir->dentry) { + /* + * The LANDLOCK_ACCESS_FS_REFER access right is not required + * for same-directory referer (i.e. no reparenting). + */ + access_request_parent1 = landlock_init_layer_masks( + 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, &request1, + NULL, 0, NULL, NULL, NULL)) + return 0; + + landlock_log_denial(subject, &request1); + return -EACCES; + } + + access_request_parent1 |= LANDLOCK_ACCESS_FS_REFER; + access_request_parent2 |= LANDLOCK_ACCESS_FS_REFER; + + /* Saves the common mount point. */ + mnt_dir.mnt = new_dir->mnt; + mnt_dir.dentry = new_dir->mnt->mnt_root; + + /* + * old_dentry may be the root of the common mount point and + * !IS_ROOT(old_dentry) at the same time (e.g. with open_tree() and + * OPEN_TREE_CLONE). Pin the dentry used as old_parent in either case. + * Otherwise, dget_parent() safely fetches and pins the current parent + * against a concurrent rename(2). + */ + old_parent = (old_dentry == mnt_dir.dentry) ? dget(old_dentry) : + dget_parent(old_dentry); + + /* new_dir->dentry is equal to new_dentry->d_parent */ + allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry, + old_parent, + &layer_masks_parent1); + allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry, + new_dir->dentry, + &layer_masks_parent2); + if (allow_parent1 && allow_parent2) { + dput(old_parent); + return 0; + } + + /* + * To be able to compare source and destination domain access rights, + * take into account the @old_dentry access rights aggregated with its + * parent access rights. This will be useful to compare with the + * destination parent access rights. + */ + if (is_access_to_paths_allowed( + subject->domain, &mnt_dir, access_request_parent1, + &layer_masks_parent1, &request1, old_dentry, + access_request_parent2, &layer_masks_parent2, &request2, + exchange ? new_dentry : NULL)) { + dput(old_parent); + 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); + } + dput(old_parent); + + /* + * This prioritizes EACCES over EXDEV for all actions, including + * renames with RENAME_EXCHANGE. + */ + if (likely(is_eacces(&layer_masks_parent1, access_request_parent1) || + is_eacces(&layer_masks_parent2, access_request_parent2))) + return -EACCES; + + /* + * Gracefully forbids reparenting if the destination directory + * hierarchy is not a superset of restrictions of the source directory + * hierarchy, or if LANDLOCK_ACCESS_FS_REFER is not allowed by the + * source or the destination. + */ + return -EXDEV; +} + +/* Inode hooks */ + +static void hook_inode_free_security_rcu(void *inode_security) +{ + struct landlock_inode_security *inode_sec; + + /* + * All inodes must already have been untied from their object by + * release_inode() or hook_sb_delete(). + */ + inode_sec = inode_security + landlock_blob_sizes.lbs_inode; + WARN_ON_ONCE(inode_sec->object); +} + +/* Super-block hooks */ + +/* + * Release the inodes used in a security policy. + * + * Cf. fsnotify_unmount_inodes() and evict_inodes() + */ +static void hook_sb_delete(struct super_block *const sb) +{ + struct inode *inode, *prev_inode = NULL; + + if (!landlock_initialized) + return; + + spin_lock(&sb->s_inode_list_lock); + list_for_each_entry(inode, &sb->s_inodes, i_sb_list) { + struct landlock_object *object; + + /* Only handles referenced inodes. */ + if (!icount_read_once(inode)) + continue; + + /* + * Protects against concurrent modification of inode (e.g. + * from get_inode_object()). + */ + spin_lock(&inode->i_lock); + /* + * Checks I_FREEING and I_WILL_FREE to protect against a race + * condition when release_inode() just called iput(), which + * could lead to a NULL dereference of inode->security or a + * second call to iput() for the same Landlock object. Also + * checks I_NEW because such inode cannot be tied to an object. + */ + if (inode_state_read(inode) & + (I_FREEING | I_WILL_FREE | I_NEW)) { + spin_unlock(&inode->i_lock); + continue; + } + + rcu_read_lock(); + object = rcu_dereference(landlock_inode(inode)->object); + if (!object) { + rcu_read_unlock(); + spin_unlock(&inode->i_lock); + continue; + } + /* Keeps a reference to this inode until the next loop walk. */ + __iget(inode); + spin_unlock(&inode->i_lock); + + /* + * If there is no concurrent release_inode() ongoing, then we + * are in charge of calling iput() on this inode, otherwise we + * will just wait for it to finish. + */ + spin_lock(&object->lock); + if (object->underobj == inode) { + object->underobj = NULL; + spin_unlock(&object->lock); + rcu_read_unlock(); + + /* + * Because object->underobj was not NULL, + * release_inode() and get_inode_object() guarantee + * that it is safe to reset + * landlock_inode(inode)->object while it is not NULL. + * It is therefore not necessary to lock inode->i_lock. + */ + rcu_assign_pointer(landlock_inode(inode)->object, NULL); + /* + * At this point, we own the ihold() reference that was + * originally set up by get_inode_object() and the + * __iget() reference that we just set in this loop + * walk. Therefore there are at least two references + * on the inode. + */ + iput_not_last(inode); + } else { + spin_unlock(&object->lock); + rcu_read_unlock(); + } + + if (prev_inode) { + /* + * At this point, we still own the __iget() reference + * that we just set in this loop walk. Therefore we + * can drop the list lock and know that the inode won't + * disappear from under us until the next loop walk. + */ + spin_unlock(&sb->s_inode_list_lock); + /* + * We can now actually put the inode reference from the + * previous loop walk, which is not needed anymore. + */ + iput(prev_inode); + cond_resched(); + spin_lock(&sb->s_inode_list_lock); + } + prev_inode = inode; + } + spin_unlock(&sb->s_inode_list_lock); + + /* Puts the inode reference from the last loop walk, if any. */ + if (prev_inode) + iput(prev_inode); + /* Waits for pending iput() in release_inode(). */ + wait_var_event(&landlock_superblock(sb)->inode_refs, + !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 + * not previously allowed. + * + * To make it simple, deny any filesystem topology modification by landlocked + * processes. Non-landlocked processes may still change the namespace of a + * landlocked process, but this kind of threat must be handled by a system-wide + * access-control security policy. + * + * This could be lifted in the future if Landlock can safely handle mount + * namespace updates requested by a landlocked process. Indeed, we could + * update the current domain (which is currently read-only) by taking into + * account the accesses of the source and the destination of a new mount point. + * However, it would also require to make all the child domains dynamically + * inherit these new constraints. Anyway, for backward compatibility reasons, + * a dedicated user space option would be required (e.g. as a ruleset flag). + */ +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) +{ + 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) +{ + 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; +} + +/* + * Removing a mount point may reveal a previously hidden file hierarchy, which + * may then grant access to files, which may have previously been forbidden. + */ +static int hook_sb_umount(struct vfsmount *const mnt, const int flags) +{ + 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) +{ + 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; +} + +/* + * pivot_root(2), like mount(2), changes the current mount namespace. It must + * then be forbidden for a landlocked process. + * + * However, chroot(2) may be allowed because it only changes the relative root + * directory of the current process. Moreover, it can be used to restrict the + * view of the filesystem. + */ +static int hook_sb_pivotroot(const struct path *const old_path, + const struct path *const new_path) +{ + 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; +} + +/* Path hooks */ + +static int hook_path_link(struct dentry *const old_dentry, + const struct path *const new_dir, + struct dentry *const new_dentry) +{ + return current_check_refer_path(old_dentry, new_dir, new_dentry, false, + false, false); +} + +static int hook_path_rename(const struct path *const old_dir, + struct dentry *const old_dentry, + const struct path *const new_dir, + struct dentry *const new_dentry, + const unsigned int flags) +{ + /* old_dir refers to old_dentry->d_parent and new_dir->mnt */ + return current_check_refer_path(old_dentry, new_dir, new_dentry, true, + !!(flags & RENAME_EXCHANGE), + !!(flags & RENAME_WHITEOUT)); +} + +static int hook_path_mkdir(const struct path *const dir, + struct dentry *const dentry, const umode_t mode) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_DIR); +} + +static int hook_path_mknod(const struct path *const dir, + struct dentry *const dentry, const umode_t mode, + const unsigned int dev) +{ + return current_check_access_path( + dir, get_mode_access(mode, new_decode_dev(dev))); +} + +static int hook_path_symlink(const struct path *const dir, + struct dentry *const dentry, + const char *const old_name) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_SYM); +} + +static int hook_path_unlink(const struct path *const dir, + struct dentry *const dentry) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_FILE); +} + +static int hook_path_rmdir(const struct path *const dir, + struct dentry *const dentry) +{ + return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR); +} + +static int hook_path_truncate(const struct path *const path) +{ + return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE); +} + +/** + * unmask_scoped_access - Remove access right bits in @masks in all layers + * where @client and @server have the same domain + * + * This does the same as domain_is_scoped(), but unmasks bits in @masks. + * It can not return early as domain_is_scoped() does. + * + * A scoped access for a given access right bit is allowed iff, for all layer + * depths where the access bit is set, the client and server domain are the + * same. This function clears the access rights @access in @masks at all layer + * depths where the client and server domain are the same, so that, when they + * are all cleared, the access is allowed. + * + * @client: Client domain + * @server: Server domain + * @masks: Layer access masks to unmask + * @access: Access bits that control scoping + */ +static void unmask_scoped_access(const struct landlock_domain *const client, + const struct landlock_domain *const server, + struct layer_masks *const masks, + const access_mask_t access) +{ + int client_layer, server_layer; + const struct landlock_hierarchy *client_walker, *server_walker; + + /* This should not happen. */ + if (WARN_ON_ONCE(!client)) + return; + + /* Server has no Landlock domain; nothing to clear. */ + if (!server) + return; + + /* + * client_layer must be able to represent all numbers from + * LANDLOCK_MAX_NUM_LAYERS - 1 to -1 for the loop below to terminate. + * (It must be large enough, and it must be signed.) + */ + BUILD_BUG_ON(!is_signed_type(typeof(client_layer))); + BUILD_BUG_ON(LANDLOCK_MAX_NUM_LAYERS - 1 > + type_max(typeof(client_layer))); + + client_layer = client->num_layers - 1; + client_walker = client->hierarchy; + server_layer = server->num_layers - 1; + server_walker = server->hierarchy; + + /* + * Clears the access bits at all layers where the client domain is the + * same as the server domain. We start the walk at min(client_layer, + * server_layer). The layer bits until there can not be cleared because + * either the client or the server domain is missing. + */ + for (; client_layer > server_layer; client_layer--) + client_walker = client_walker->parent; + + for (; server_layer > client_layer; server_layer--) + server_walker = server_walker->parent; + + for (; client_layer >= 0; client_layer--) { + if (masks->layers[client_layer].access & access && + client_walker == server_walker) + masks->layers[client_layer].access &= ~access; + + client_walker = client_walker->parent; + server_walker = server_walker->parent; + } +} + +static int hook_unix_find(const struct path *const path, struct sock *other, + int flags) +{ + const struct landlock_domain *dom_other; + const struct landlock_cred_security *subject; + struct layer_masks layer_masks; + struct landlock_request request = {}; + static const struct access_masks fs_resolve_unix = { + .fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX, + }; + + /* Lookup for the purpose of saving coredumps is OK. */ + if (unlikely(flags & SOCK_COREDUMP)) + return 0; + + subject = landlock_get_applicable_subject(current_cred(), + fs_resolve_unix, NULL); + + if (!subject) + return 0; + + /* + * Ignoring return value: that the domains apply was already checked in + * landlock_get_applicable_subject() above. + */ + landlock_init_layer_masks(subject->domain, fs_resolve_unix.fs, + &layer_masks, LANDLOCK_KEY_INODE); + + /* Checks the layers in which we are connecting within the same domain. */ + unix_state_lock(other); + if (unlikely(sock_flag(other, SOCK_DEAD) || !other->sk_socket || + !other->sk_socket->file)) { + unix_state_unlock(other); + /* + * We rely on the caller to catch the (non-reversible) SOCK_DEAD + * condition and retry the lookup. If we returned an error + * here, the lookup would not get retried. + */ + return 0; + } + dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; + + /* Access to the same (or a lower) domain is always allowed. */ + unmask_scoped_access(subject->domain, dom_other, &layer_masks, + fs_resolve_unix.fs); + unix_state_unlock(other); + + /* Checks the connections to allow-listed paths. */ + if (is_access_to_paths_allowed(subject->domain, path, + fs_resolve_unix.fs, &layer_masks, + &request, NULL, 0, NULL, NULL, NULL)) + return 0; + + landlock_log_denial(subject, &request); + return -EACCES; +} + +/* File hooks */ + +/** + * get_required_file_open_access - Get access needed to open a file + * + * @file: File being opened. + * + * Return: The access rights that are required for opening the given file, + * depending on the file type and open mode. + */ +static access_mask_t +get_required_file_open_access(const struct file *const file) +{ + access_mask_t access = 0; + + if (file->f_mode & FMODE_READ) { + /* A directory can only be opened in read mode. */ + if (S_ISDIR(file_inode(file)->i_mode)) + return LANDLOCK_ACCESS_FS_READ_DIR; + access = LANDLOCK_ACCESS_FS_READ_FILE; + } + if (file->f_mode & FMODE_WRITE) + access |= LANDLOCK_ACCESS_FS_WRITE_FILE; + /* __FMODE_EXEC is indeed part of f_flags, not f_mode. */ + if (file->f_flags & __FMODE_EXEC) + access |= LANDLOCK_ACCESS_FS_EXECUTE; + return access; +} + +static int hook_file_alloc_security(struct file *const file) +{ + /* + * Grants all access rights, even if most of them are not checked later + * on. It is more consistent. + * + * Notably, file descriptors for regular files can also be acquired + * without going through the file_open hook, for example when using + * memfd_create(2). + */ + landlock_file(file)->allowed_access = LANDLOCK_MASK_ACCESS_FS; + return 0; +} + +static bool is_device(const struct file *const file) +{ + const struct inode *inode = file_inode(file); + + return S_ISBLK(inode->i_mode) || S_ISCHR(inode->i_mode); +} + +static int hook_file_open(struct file *const file) +{ + struct layer_masks layer_masks = {}; + access_mask_t open_access_request, full_access_request, allowed_access, + optional_access; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(file->f_cred, any_fs, NULL); + struct landlock_request request = {}; + + if (!subject) + return 0; + + /* + * Because a file may be opened with O_PATH, get_required_file_open_access() + * may return 0. This case will be handled with a future Landlock + * evolution. + */ + open_access_request = get_required_file_open_access(file); + + /* + * We look up more access than what we immediately need for open(), so + * that we can later authorize operations on opened files. + */ + optional_access = LANDLOCK_ACCESS_FS_TRUNCATE; + if (is_device(file)) + optional_access |= LANDLOCK_ACCESS_FS_IOCTL_DEV; + + full_access_request = open_access_request | optional_access; + + if (is_access_to_paths_allowed( + subject->domain, &file->f_path, + landlock_init_layer_masks(subject->domain, + full_access_request, &layer_masks, + LANDLOCK_KEY_INODE), + &layer_masks, &request, NULL, 0, NULL, NULL, NULL)) { + allowed_access = full_access_request; + } else { + /* + * Calculate the actual allowed access rights from layer_masks. + * Remove the access rights from the full access request which + * are still unfulfilled in any of the layers. + */ + allowed_access = full_access_request; + for (size_t i = 0; i < ARRAY_SIZE(layer_masks.layers); i++) + allowed_access &= ~layer_masks.layers[i].access; + } + + /* + * For operations on already opened files (i.e. ftruncate()), it is the + * access rights at the time of open() which decide whether the + * operation is permitted. Therefore, we record the relevant subset of + * file access rights in the opened struct file. + */ + landlock_file(file)->allowed_access = allowed_access; +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + landlock_file(file)->deny_masks = landlock_get_deny_masks( + _LANDLOCK_ACCESS_FS_OPTIONAL, optional_access, &layer_masks); + landlock_file(file)->quiet_optional_accesses = + landlock_get_quiet_optional_accesses( + _LANDLOCK_ACCESS_FS_OPTIONAL, + landlock_file(file)->deny_masks, &layer_masks); +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + if (access_mask_subset(open_access_request, allowed_access)) + return 0; + + /* Sets access to reflect the actual request. */ + request.access = open_access_request; + landlock_log_denial(subject, &request); + return -EACCES; +} + +static int hook_file_truncate(struct file *const file) +{ + /* + * Allows truncation if the truncate right was available at the time of + * opening the file, to get a consistent access check as for read, write + * and execute operations. + * + * Note: For checks done based on the file's Landlock allowed access, we + * enforce them independently of whether the current thread is in a + * Landlock domain, so that open files passed between independent + * processes retain their behaviour. + */ + 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_SECURITY_LANDLOCK_LOG + .deny_masks = landlock_file(file)->deny_masks, + .quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses, +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + }); + return -EACCES; +} + +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; + + /* + * 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 (unlikely(is_compat) ? is_masked_device_ioctl_compat(cmd) : + 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_SECURITY_LANDLOCK_LOG + .deny_masks = landlock_file(file)->deny_masks, + .quiet_optional_accesses = landlock_file(file)->quiet_optional_accesses, +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + }); + 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) +{ + return hook_file_ioctl_common(file, cmd, true); +} + +/* + * 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 task_struct *p; + + /* + * Lock already held by __f_setown(), see commit 26f204380a3c ("fs: Fix + * file_set_fowner LSM hook inconsistencies"). + */ + lockdep_assert_held(&fown->lock); + + /* + * A process-group or session owner (PIDTYPE_PGID/PIDTYPE_SID) fans the + * signal out to every member at delivery time, so record the domain and + * let hook_file_send_sigiotask() check the live scope per recipient. + */ + if (fown->pid_type != PIDTYPE_PID && fown->pid_type != PIDTYPE_TGID) + return true; + + /* + * Some callers (e.g. fcntl_dirnotify) may not be in an RCU read-side + * critical section. + */ + 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_domain *prev_dom; + struct landlock_cred_security fown_subject = {}; + struct pid *prev_tg, *fown_tg = NULL; + size_t fown_layer = 0; + + if (control_current_fowner(file_f_owner(file))) { + 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, &fown_layer); + if (new_subject) { + landlock_get_domain(new_subject->domain); + fown_subject = *new_subject; + fown_tg = get_pid(task_tgid(current)); + } + } + + prev_dom = landlock_file(file)->fown_subject.domain; + prev_tg = landlock_file(file)->fown_tg; + landlock_file(file)->fown_subject = fown_subject; + landlock_file(file)->fown_tg = fown_tg; +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + landlock_file(file)->fown_layer = fown_layer; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + /* May be called in an RCU read-side critical section. */ + landlock_put_domain_deferred(prev_dom); + put_pid(prev_tg); +} + +static void hook_file_free_security(struct file *file) +{ + put_pid(landlock_file(file)->fown_tg); + landlock_put_domain_deferred(landlock_file(file)->fown_subject.domain); +} + +static struct security_hook_list landlock_hooks[] __ro_after_init = { + LSM_HOOK_INIT(inode_free_security_rcu, hook_inode_free_security_rcu), + + LSM_HOOK_INIT(sb_delete, hook_sb_delete), + LSM_HOOK_INIT(sb_mount, hook_sb_mount), + LSM_HOOK_INIT(move_mount, hook_move_mount), + LSM_HOOK_INIT(sb_umount, hook_sb_umount), + LSM_HOOK_INIT(sb_remount, hook_sb_remount), + LSM_HOOK_INIT(sb_pivotroot, hook_sb_pivotroot), + + LSM_HOOK_INIT(path_link, hook_path_link), + LSM_HOOK_INIT(path_rename, hook_path_rename), + LSM_HOOK_INIT(path_mkdir, hook_path_mkdir), + LSM_HOOK_INIT(path_mknod, hook_path_mknod), + LSM_HOOK_INIT(path_symlink, hook_path_symlink), + LSM_HOOK_INIT(path_unlink, hook_path_unlink), + LSM_HOOK_INIT(path_rmdir, hook_path_rmdir), + LSM_HOOK_INIT(path_truncate, hook_path_truncate), + LSM_HOOK_INIT(unix_find, hook_unix_find), + + LSM_HOOK_INIT(file_alloc_security, hook_file_alloc_security), + LSM_HOOK_INIT(file_open, hook_file_open), + LSM_HOOK_INIT(file_truncate, hook_file_truncate), + LSM_HOOK_INIT(file_ioctl, hook_file_ioctl), + LSM_HOOK_INIT(file_ioctl_compat, hook_file_ioctl_compat), + LSM_HOOK_INIT(file_set_fowner, hook_file_set_fowner), + LSM_HOOK_INIT(file_free_security, hook_file_free_security), +}; + +__init void landlock_add_fs_hooks(void) +{ + security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), + &landlock_lsmid); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +/* clang-format off */ +static struct kunit_case test_cases[] = { + KUNIT_CASE(test_no_more_access), + KUNIT_CASE(test_scope_to_request_with_exec_none), + KUNIT_CASE(test_scope_to_request_with_exec_some), + KUNIT_CASE(test_scope_to_request_without_access), + KUNIT_CASE(test_is_eacces_with_none), + KUNIT_CASE(test_is_eacces_with_refer), + KUNIT_CASE(test_is_eacces_with_write), + {} +}; +/* clang-format on */ + +static struct kunit_suite test_suite = { + .name = "landlock_fs", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/fs.h b/security/landlock/fs.h new file mode 100644 index 000000000..50b3ddb8a --- /dev/null +++ b/security/landlock/fs.h @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Filesystem management and hooks + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_FS_H +#define _SECURITY_LANDLOCK_FS_H + +#include <linux/build_bug.h> +#include <linux/cleanup.h> +#include <linux/fs.h> +#include <linux/init.h> +#include <linux/rcupdate.h> + +#include "access.h" +#include "cred.h" +#include "ruleset.h" +#include "setup.h" + +DEFINE_FREE(__putname, char *, if (_T) __putname(_T)) + +/** + * struct landlock_inode_security - Inode security blob + * + * Enable to reference a &struct landlock_object tied to an inode (i.e. + * underlying object). + */ +struct landlock_inode_security { + /** + * @object: Weak pointer to an allocated object. All assignments of a + * new object are protected by the underlying inode->i_lock. However, + * atomically disassociating @object from the inode is only protected + * by @object->lock, from the time @object's usage refcount drops to + * zero to the time this pointer is nulled out (cf. release_inode() and + * hook_sb_delete()). Indeed, such disassociation doesn't require + * inode->i_lock thanks to the careful rcu_access_pointer() check + * performed by get_inode_object(). + */ + struct landlock_object __rcu *object; +}; + +/** + * struct landlock_file_security - File security blob + * + * This information is populated when opening a file in hook_file_open, and + * tracks the relevant Landlock access rights that were available at the time + * of opening the file. Other LSM hooks use these rights in order to authorize + * operations on already opened files. + */ +struct landlock_file_security { + /** + * @allowed_access: Access rights that were available at the time of + * opening the file. This is not necessarily the full set of access + * rights available at that time, but it's the necessary subset as + * needed to authorize later operations on the open file. + */ + access_mask_t allowed_access; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + /** + * @deny_masks: Domain layer levels that deny an optional access (see + * _LANDLOCK_ACCESS_FS_OPTIONAL). + */ + deny_masks_t deny_masks; + /** + * @quiet_optional_accesses: Stores which optional accesses are covered + * by quiet rules within the layer referred to in deny_masks, one access + * per bit. Does not take into account whether the quiet access bits + * are actually set in the layer's corresponding landlock_hierarchy. + */ + optional_access_t quiet_optional_accesses; + /** + * @fown_layer: Layer level of @fown_subject->domain with + * LANDLOCK_SCOPE_SIGNAL. + */ + u8 fown_layer; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + /** + * @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_cred_security fown_subject; + /** + * @fown_tg: Thread group of the task that set the file owner, pinned + * while @fown_subject holds a domain. It lets + * hook_file_send_sigiotask() always allow a SIGIO delivered to the + * owner's own process -- e.g. the thread-group leader reached through a + * process-group owner -- matching the same-process exemption of + * hook_task_kill(). NULL when no domain is recorded. Protected by + * file->f_owner->lock, like @fown_subject. + */ + struct pid *fown_tg; +}; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + +/* 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 on */ + +/* + * Make sure quiet_optional_accesses has enough bits to cover all optional + * accesses. + */ +static_assert(BITS_PER_TYPE(typeof_member(struct landlock_file_security, + quiet_optional_accesses)) >= + HWEIGHT(_LANDLOCK_ACCESS_FS_OPTIONAL)); + +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + +/** + * struct landlock_superblock_security - Superblock security blob + * + * Enable hook_sb_delete() to wait for concurrent calls to release_inode(). + */ +struct landlock_superblock_security { + /** + * @inode_refs: Number of pending inodes (from this superblock) that + * are being released by release_inode(). + * Cf. struct super_block->s_fsnotify_inode_refs . + */ + atomic_long_t inode_refs; +}; + +static inline struct landlock_file_security * +landlock_file(const struct file *const file) +{ + return file->f_security + landlock_blob_sizes.lbs_file; +} + +static inline struct landlock_inode_security * +landlock_inode(const struct inode *const inode) +{ + return inode->i_security + landlock_blob_sizes.lbs_inode; +} + +static inline struct landlock_superblock_security * +landlock_superblock(const struct super_block *const superblock) +{ + return superblock->s_security + landlock_blob_sizes.lbs_superblock; +} + +__init void landlock_add_fs_hooks(void); + +int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, + const struct path *const path, + access_mask_t access_hierarchy, const u32 flags); + +/** + * resolve_path_for_trace - Resolve a path for tracepoint display + * + * @path: The path to resolve. + * @buf: A buffer of at least PATH_MAX bytes for the resolved path. + * + * Uses d_absolute_path() to produce a namespace-independent absolute path, + * unlike d_path() which resolves relative to the process's chroot. This + * ensures trace output is deterministic regardless of the tracer's mount + * namespace. + * + * Return: A pointer into @buf with the resolved path, or an error string + * ("<too_long>", "<unreachable>"). + */ +static inline const char *resolve_path_for_trace(const struct path *path, + char *buf) +{ + const char *p; + + p = d_absolute_path(path, buf, PATH_MAX); + if (!IS_ERR_OR_NULL(p)) + return p; + + if (PTR_ERR(p) == -ENAMETOOLONG) + return "<too_long>"; + + return "<unreachable>"; +} + +#endif /* _SECURITY_LANDLOCK_FS_H */ diff --git a/security/landlock/id.c b/security/landlock/id.c new file mode 100644 index 000000000..6c8769777 --- /dev/null +++ b/security/landlock/id.c @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Unique identification number generator + * + * Copyright © 2024-2025 Microsoft Corporation + */ + +#include <kunit/test.h> +#include <linux/atomic.h> +#include <linux/bitops.h> +#include <linux/random.h> +#include <linux/spinlock.h> + +#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 = BIT_ULL(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 &= 0b1111; + 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 u8 get_random_u8_positive(void) +{ + /* max() evaluates its arguments once. */ + return max(1, get_random_u8()); +} + +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_positive(), &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_positive(), &counter, + get_random_u8()), + init + 2); +} + +static void test_range1_rand15(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, 15), init); + KUNIT_EXPECT_EQ(test, + get_id_range(get_random_u8_positive(), &counter, + get_random_u8()), + init + 16); +} + +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_positive(), &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_positive(), &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_positive(), &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_positive(), &counter, + get_random_u8()), + init + 4); +} + +static void test_range2_rand15(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, 15), init); + KUNIT_EXPECT_EQ(test, + get_id_range(get_random_u8_positive(), &counter, + get_random_u8()), + init + 17); +} + +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_positive(), &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. + * + * Return: 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_rand15), + KUNIT_CASE(test_range1_rand16), + KUNIT_CASE(test_range2_rand0), + KUNIT_CASE(test_range2_rand1), + KUNIT_CASE(test_range2_rand2), + KUNIT_CASE(test_range2_rand15), + 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 000000000..2a43c2b52 --- /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_SECURITY_LANDLOCK_LOG + +void __init landlock_init_id(void); + +u64 landlock_get_id_range(size_t number_of_ids); + +#else /* CONFIG_SECURITY_LANDLOCK_LOG */ + +static inline void __init landlock_init_id(void) +{ +} + +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + +#endif /* _SECURITY_LANDLOCK_ID_H */ diff --git a/security/landlock/limits.h b/security/landlock/limits.h new file mode 100644 index 000000000..1a7c5fb8f --- /dev/null +++ b/security/landlock/limits.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Limits for different components + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation + */ + +#ifndef _SECURITY_LANDLOCK_LIMITS_H +#define _SECURITY_LANDLOCK_LIMITS_H + +#include <linux/bitops.h> +#include <linux/limits.h> +#include <uapi/linux/landlock.h> + +/* clang-format off */ + +#define LANDLOCK_MAX_NUM_LAYERS 16 +#define LANDLOCK_MAX_NUM_RULES U32_MAX + +#define LANDLOCK_LAST_ACCESS_FS LANDLOCK_ACCESS_FS_RESOLVE_UNIX +#define LANDLOCK_MASK_ACCESS_FS ((LANDLOCK_LAST_ACCESS_FS << 1) - 1) +#define LANDLOCK_NUM_ACCESS_FS __const_hweight64(LANDLOCK_MASK_ACCESS_FS) + +#define LANDLOCK_LAST_ACCESS_NET LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP +#define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1) +#define LANDLOCK_NUM_ACCESS_NET __const_hweight64(LANDLOCK_MASK_ACCESS_NET) + +#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_NUM_ACCESS_MAX \ + MAX(MAX(LANDLOCK_NUM_ACCESS_FS, LANDLOCK_NUM_ACCESS_NET), LANDLOCK_NUM_SCOPE) + +#define LANDLOCK_LAST_RESTRICT_SELF LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS +#define LANDLOCK_MASK_RESTRICT_SELF ((LANDLOCK_LAST_RESTRICT_SELF << 1) - 1) + +/* clang-format on */ + +#endif /* _SECURITY_LANDLOCK_LIMITS_H */ diff --git a/security/landlock/log.c b/security/landlock/log.c new file mode 100644 index 000000000..a8578a6f2 --- /dev/null +++ b/security/landlock/log.c @@ -0,0 +1,587 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Log helpers + * + * Copyright © 2023-2025 Microsoft Corporation + * Copyright © 2026 Cloudflare, Inc. + */ + +#include <kunit/test.h> +#include <linux/bitops.h> +#include <uapi/linux/landlock.h> + +#include "access.h" +#include "audit.h" +#include "common.h" +#include "cred.h" +#include "domain.h" +#include "limits.h" +#include "log.h" +#include "ruleset.h" +#include "trace.h" + +static struct landlock_hierarchy * +get_hierarchy(const struct landlock_domain *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_domain 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 */ + +/* Get the youngest layer that denied the access_request. */ +static size_t get_denied_layer(const struct landlock_domain *const domain, + access_mask_t *const access_request, + const struct layer_masks *masks) +{ + for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) { + if (masks->layers[i].access & *access_request) { + *access_request &= masks->layers[i].access; + return i; + } + } + + /* Not found - fall back to default values */ + *access_request = 0; + return domain->num_layers - 1; +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_get_denied_layer(struct kunit *const test) +{ + const struct landlock_domain dom = { + .num_layers = 5, + }; + const struct layer_masks masks = { + .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | + LANDLOCK_ACCESS_FS_READ_DIR, + .layers[1].access = LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_READ_DIR, + .layers[2].access = LANDLOCK_ACCESS_FS_REMOVE_DIR, + }; + access_mask_t access; + + access = LANDLOCK_ACCESS_FS_EXECUTE; + KUNIT_EXPECT_EQ(test, 0, get_denied_layer(&dom, &access, &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, &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, &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, &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, &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, &masks)); + KUNIT_EXPECT_EQ(test, access, 0); +} + +#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, + optional_access_t quiet_optional_accesses, + bool *quiet) +{ + 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; + bool should_quiet = false; + + /* 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 * + HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) & + (LANDLOCK_MAX_NUM_LAYERS - 1); + const bool layer_has_quiet = + !!(quiet_optional_accesses & BIT(access_index)); + + if (layer > youngest_layer) { + youngest_layer = layer; + missing = BIT(access_bit); + should_quiet = layer_has_quiet; + } else if (layer == youngest_layer) { + missing |= BIT(access_bit); + /* + * Whether the layer has rules with quiet flag + * covering the file accessed does not depend on + * the access, and so the following + * WARN_ON_ONCE() should not fail. + */ + WARN_ON_ONCE(should_quiet && !layer_has_quiet); + should_quiet = layer_has_quiet; + } + } + access_index++; + } + + *access_request = missing; + *quiet = should_quiet; + return youngest_layer; +} + +#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; + optional_access_t quiet_optional_accesses; + bool quiet; + + /* truncate:0 ioctl_dev:2 */ + deny_mask = 0x20; + quiet_optional_accesses = 0; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* layer denying truncate: quiet, ioctl: not quiet */ + quiet_optional_accesses = 0b01; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* Reverse order - truncate:2 ioctl_dev:0 */ + deny_mask = 0x02; + quiet_optional_accesses = 0; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* layer denying truncate: quiet, ioctl: not quiet */ + quiet_optional_accesses = 0b01; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); + + /* layer denying truncate: not quiet, ioctl: quiet */ + quiet_optional_accesses = 0b10; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 0, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, true); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 2, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* truncate:15 ioctl_dev:15 */ + deny_mask = 0xff; + quiet_optional_accesses = 0; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, false); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, false); + + /* Both quiet (same layer so quietness must be the same) */ + quiet_optional_accesses = 0b11; + + access = LANDLOCK_ACCESS_FS_TRUNCATE; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, LANDLOCK_ACCESS_FS_TRUNCATE); + KUNIT_EXPECT_EQ(test, quiet, true); + + access = LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_DEV; + KUNIT_EXPECT_EQ(test, 15, + get_layer_from_deny_masks( + &access, _LANDLOCK_ACCESS_FS_OPTIONAL, + deny_mask, quiet_optional_accesses, &quiet)); + KUNIT_EXPECT_EQ(test, access, + LANDLOCK_ACCESS_FS_TRUNCATE | + LANDLOCK_ACCESS_FS_IOCTL_DEV); + KUNIT_EXPECT_EQ(test, quiet, true); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +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 ^ !!request->access))) + return false; + + if (request->access) { + if (WARN_ON_ONCE(!(!!request->layer_masks ^ + !!request->all_existing_optional_access))) + return false; + } else { + if (WARN_ON_ONCE(request->layer_masks || + request->all_existing_optional_access)) + return false; + } + + if (request->deny_masks) { + if (WARN_ON_ONCE(!request->all_existing_optional_access)) + return false; + static_assert(sizeof(request->all_existing_optional_access) == + sizeof(u32)); + if (WARN_ON_ONCE( + request->quiet_optional_accesses >= + BIT(hweight32( + request->all_existing_optional_access)))) + return false; + } + + return true; +} + +static access_mask_t +pick_access_mask_for_request_type(const enum landlock_request_type type, + const struct access_masks access_masks) +{ + switch (type) { + case LANDLOCK_REQUEST_FS_ACCESS: + return access_masks.fs; + case LANDLOCK_REQUEST_NET_ACCESS: + return access_masks.net; + default: + WARN_ONCE(1, "Invalid request type %d passed to %s", type, + __func__); + return 0; + } +} + +/* + * Whether a quiet rule silences the denial: the rule must cover the whole + * denied access in the layer that denied it (a quiet rule in a non-denying + * layer does not suppress the denial). + */ +static bool +is_denial_quieted(const struct landlock_request *const request, + const struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool object_quiet_flag) +{ + if (object_quiet_flag) { + const access_mask_t quiet_mask = + pick_access_mask_for_request_type( + request->type, youngest_denied->quiet_masks); + + return (quiet_mask & missing) == missing; + } + + /* + * Either the object is not quiet, or this is a scope request. We check + * request->type to distinguish between the two cases. + */ + switch (request->type) { + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + return !!(youngest_denied->quiet_masks.scope & + LANDLOCK_SCOPE_SIGNAL); + case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: + return !!(youngest_denied->quiet_masks.scope & + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); + /* + * Leave LANDLOCK_REQUEST_PTRACE and LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY + * unhandled for now - they are never quiet. + */ + default: + return false; + } +} + +/* + * Computes whether a denial from youngest_denied is selected for logging by the + * domain's policy: its logging must not be disabled (by both per-execution + * flags being off, or by an ancestor's + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF), the per-execution flag matching + * same_exec must be set, and no quiet rule may cover the denied access. + * landlock_log_denial() computes this once and passes it to + * landlock_audit_denial(), which additionally requires audit_enabled. + */ +static bool +is_denial_logged(const struct landlock_request *const request, + const struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool same_exec, + const bool object_quiet_flag) +{ + if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED) + return false; + + if (!(same_exec ? youngest_denied->log_same_exec : + youngest_denied->log_new_exec)) + return false; + + return !is_denial_quieted(request, youngest_denied, missing, + object_quiet_flag); +} + +/** + * landlock_log_denial - Log a denied access + * + * @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 landlock_hierarchy *youngest_denied; + size_t youngest_layer; + access_mask_t missing; + bool object_quiet_flag = false; + + if (WARN_ON_ONCE(!subject || !subject->domain || + !subject->domain->hierarchy || !request)) + return; + + if (!is_valid_request(request)) + return; + + 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); + object_quiet_flag = + request->layer_masks->layers[youngest_layer] + .quiet; + } else { + youngest_layer = get_layer_from_deny_masks( + &missing, _LANDLOCK_ACCESS_FS_OPTIONAL, + request->deny_masks, + request->quiet_optional_accesses, + &object_quiet_flag); + } + youngest_denied = + get_hierarchy(subject->domain, youngest_layer); + } else { + youngest_layer = request->layer_plus_one - 1; + youngest_denied = + get_hierarchy(subject->domain, youngest_layer); + } + + const bool same_exec = !!(subject->domain_exec & BIT(youngest_layer)); + const bool logged = is_denial_logged(request, youngest_denied, missing, + same_exec, object_quiet_flag); + + /* + * 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); + + landlock_trace_denial(request, youngest_denied, missing, same_exec, + logged); + landlock_audit_denial(request, youngest_denied, missing, logged); +} + +/** + * landlock_log_free_domain - Log domain deallocation + * + * @hierarchy: The domain's hierarchy being deallocated. + * + * Called from landlock_put_domain_deferred() (via a work queue scheduled by + * hook_cred_free()) or directly from landlock_put_domain(). + */ +void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy) +{ + if (WARN_ON_ONCE(!hierarchy)) + return; + + landlock_trace_free_domain(hierarchy); + landlock_audit_free_domain(hierarchy); +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +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 */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_log", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/log.h b/security/landlock/log.h new file mode 100644 index 000000000..e0a6e44f3 --- /dev/null +++ b/security/landlock/log.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Log helpers + * + * Copyright © 2023-2025 Microsoft Corporation + * Copyright © 2026 Cloudflare, Inc. + */ + +#ifndef _SECURITY_LANDLOCK_LOG_H +#define _SECURITY_LANDLOCK_LOG_H + +#include <linux/lsm_audit.h> + +#include "access.h" + +struct landlock_cred_security; +struct landlock_hierarchy; + +enum landlock_request_type { + LANDLOCK_REQUEST_PTRACE = 1, + LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY, + LANDLOCK_REQUEST_FS_ACCESS, + LANDLOCK_REQUEST_NET_ACCESS, + LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET, + LANDLOCK_REQUEST_SCOPE_SIGNAL, +}; + +/* + * 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_SECURITY_LANDLOCK_LOG 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; + + /* Required field for configurable access control. */ + access_mask_t access; + + /* Required fields for requests with layer masks. */ + const struct layer_masks *layer_masks; + + /* Required fields for requests with deny masks. */ + const access_mask_t all_existing_optional_access; + deny_masks_t deny_masks; + optional_access_t quiet_optional_accesses; + + /* + * Other-party domain ID for a relational (scope/ptrace) denial, or 0 if + * that party is unsandboxed. An ID, not a pointer: the other task can + * replace its credential and free the domain it referenced. Trace path + * only; audit ignores it. + */ + u64 other_domain_id; +}; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + +void landlock_log_free_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_SECURITY_LANDLOCK_LOG */ + +static inline void +landlock_log_free_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) +{ +} + +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + +#endif /* _SECURITY_LANDLOCK_LOG_H */ diff --git a/security/landlock/net.c b/security/landlock/net.c new file mode 100644 index 000000000..8f2aaac54 --- /dev/null +++ b/security/landlock/net.c @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Network management and hooks + * + * Copyright © 2022-2023 Huawei Tech. Co., Ltd. + * Copyright © 2022-2025 Microsoft Corporation + */ + +#include <linux/in.h> +#include <linux/lsm_audit.h> +#include <linux/net.h> +#include <linux/socket.h> +#include <net/ipv6.h> + +#include "common.h" +#include "cred.h" +#include "domain.h" +#include "limits.h" +#include "log.h" +#include "net.h" +#include "ruleset.h" + +#include <trace/events/landlock.h> + +int landlock_append_net_rule(struct landlock_ruleset *const ruleset, + const u16 port, access_mask_t access_rights, + const u32 flags) +{ + int err; + const struct landlock_id id = { + .key.data = (__force uintptr_t)htons(port), + .type = LANDLOCK_KEY_NET_PORT, + }; + + BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data)); + + /* Transforms relative access rights to absolute ones. */ + access_rights |= LANDLOCK_MASK_ACCESS_NET & ~ruleset->handled_masks.net; + + mutex_lock(&ruleset->lock); + err = landlock_insert_rule(ruleset, id, access_rights, flags); + + /* + * Emit after the rule insertion succeeds, so every event corresponds to + * a rule that is actually in the ruleset. The ruleset lock is still + * held for BTF consistency (enforced by lockdep_assert_held in + * TP_fast_assign). + */ + if (!err) + trace_landlock_add_rule_net(ruleset, access_rights, port); + mutex_unlock(&ruleset->lock); + + return err; +} + +static bool unmask_layers_net(const struct landlock_domain *const domain, + const struct landlock_id id, + struct layer_masks *masks, + access_mask_t access_request) +{ + const struct landlock_rule *rule = NULL; + bool ret; + + ret = landlock_unmask_layers(domain, id, masks, &rule); + if (rule) + trace_landlock_check_rule_net( + domain, rule, access_request, + ntohs((__force __be16)id.key.data)); + return ret; +} + +static int current_check_access_socket(struct socket *const sock, + struct sockaddr *const address, + const int addrlen, + access_mask_t access_request, + bool connecting) +{ + unsigned short sock_family; + __be16 port; + struct layer_masks layer_masks = {}; + struct landlock_id id = { + .type = LANDLOCK_KEY_NET_PORT, + }; + const struct access_masks masks = { + .net = access_request, + }; + 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; + + /* Checks for minimal header length to safely read sa_family. */ + if (addrlen < offsetofend(typeof(*address), sa_family)) + return -EINVAL; + + /* + * The socket is not locked, so sk_family can change concurrently due to + * e.g. setsockopt(IPV6_ADDRFORM). + */ + sock_family = READ_ONCE(sock->sk->sk_family); + + switch (address->sa_family) { + case AF_UNSPEC: + if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP || + (access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP && + connecting)) { + /* + * Connecting to an address with AF_UNSPEC dissolves the + * remote association while retaining the socket object + * (i.e., the file descriptor). For TCP, it has the same + * effect as closing the connection. For UDP, it removes + * any preset remote address. As for dropping + * privileges, these actions are always allowed. Let + * the network stack handle potential inconsistencies + * and return -EINVAL if needed. + */ + return 0; + } else if (access_request == + LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) { + if (sock_family == AF_INET6) { + /* + * We cannot allow sending UDP datagrams to an + * explicit AF_UNSPEC address on IPv6 sockets, + * even if AF_UNSPEC is treated as "no address" + * on such sockets (so it should always be + * allowed). That's because the socket's family + * can change under our feet (if another thread + * calls setsockopt(IPV6_ADDRFORM)) to IPv4, + * which would then treat AF_UNSPEC as AF_INET. + */ + audit_net.family = AF_UNSPEC; + audit_net.sk = sock->sk; + landlock_init_layer_masks( + subject->domain, access_request, + &layer_masks, LANDLOCK_KEY_NET_PORT); + landlock_log_denial( + subject, + &(struct landlock_request){ + .type = LANDLOCK_REQUEST_NET_ACCESS, + .audit.type = + LSM_AUDIT_DATA_NET, + .audit.u.net = &audit_net, + .access = access_request, + .layer_masks = &layer_masks, + }); + return -EACCES; + } + } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP || + access_request == LANDLOCK_ACCESS_NET_BIND_UDP) { + /* + * Binding to an AF_UNSPEC address is treated + * differently by IPv4 and IPv6 sockets. The socket's + * family may change under our feet due to + * setsockopt(IPV6_ADDRFORM), but that's ok: we either + * reject entirely for IPv6 or require + * %LANDLOCK_ACCESS_NET_BIND_TCP or + * %LANDLOCK_ACCESS_NET_BIND_UDP for IPv4, so it cannot + * be used to bypass the policy. + * + * IPv4 sockets map AF_UNSPEC to AF_INET for + * retrocompatibility for bind accesses, only if the + * address is INADDR_ANY (cf. __inet_bind). IPv6 + * sockets always reject it. + * + * Checking the address is required to not wrongfully + * return -EACCES instead of -EAFNOSUPPORT or -EINVAL. + * We could return 0 and let the network stack handle + * these checks, but it is safer to return a proper + * error and test consistency thanks to kselftest. + */ + if (sock_family == AF_INET) { + const struct sockaddr_in *const sockaddr = + (struct sockaddr_in *)address; + + if (addrlen < sizeof(struct sockaddr_in)) + return -EINVAL; + + if (sockaddr->sin_addr.s_addr != + htonl(INADDR_ANY)) + return -EAFNOSUPPORT; + } else { + if (addrlen < SIN6_LEN_RFC2133) + return -EINVAL; + else + return -EAFNOSUPPORT; + } + } else { + WARN_ON_ONCE(1); + } + /* + * AF_UNSPEC is treated as AF_INET only in + * bind(AF_UNSPEC+INADDR_ANY) on IPv4 sockets and when sending + * to AF_UNSPEC addresses on IPv4 sockets. + */ + fallthrough; + case AF_INET: { + const struct sockaddr_in *addr4; + + if (addrlen < sizeof(struct sockaddr_in)) + return -EINVAL; + + addr4 = (struct sockaddr_in *)address; + port = addr4->sin_port; + + if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP || + access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) { + audit_net.dport = port; + audit_net.v4info.daddr = addr4->sin_addr.s_addr; + } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP || + access_request == LANDLOCK_ACCESS_NET_BIND_UDP) { + 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: { + const struct sockaddr_in6 *addr6; + + if (addrlen < SIN6_LEN_RFC2133) + return -EINVAL; + + addr6 = (struct sockaddr_in6 *)address; + port = addr6->sin6_port; + + if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP || + access_request == LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP) { + audit_net.dport = port; + audit_net.v6info.daddr = addr6->sin6_addr; + } else if (access_request == LANDLOCK_ACCESS_NET_BIND_TCP || + access_request == LANDLOCK_ACCESS_NET_BIND_UDP) { + audit_net.sport = port; + audit_net.v6info.saddr = addr6->sin6_addr; + } else { + WARN_ON_ONCE(1); + } + break; + } +#endif /* IS_ENABLED(CONFIG_IPV6) */ + + default: + return 0; + } + + /* + * Checks sa_family consistency to not wrongfully return + * -EACCES instead of -EINVAL. Valid sa_family changes are + * only (from AF_INET or AF_INET6) to AF_UNSPEC. + * + * We could return 0 and let the network stack handle this + * check, but it is safer to return a proper error and test + * consistency thanks to kselftest. + */ + if (address->sa_family != sock_family && + address->sa_family != AF_UNSPEC) + return -EINVAL; + + id.key.data = (__force uintptr_t)port; + BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data)); + + access_request = landlock_init_layer_masks(subject->domain, + access_request, &layer_masks, + LANDLOCK_KEY_NET_PORT); + if (!access_request) + return 0; + + if (unmask_layers_net(subject->domain, id, &layer_masks, + access_request)) + return 0; + + audit_net.family = address->sa_family; + audit_net.sk = sock->sk; + landlock_log_denial(subject, + &(struct landlock_request){ + .type = LANDLOCK_REQUEST_NET_ACCESS, + .audit.type = LSM_AUDIT_DATA_NET, + .audit.u.net = &audit_net, + .access = access_request, + .layer_masks = &layer_masks, + }); + return -EACCES; +} + +static int current_check_autobind_udp_socket(struct socket *const sock) +{ + const struct access_masks bind_udp = { + .net = LANDLOCK_ACCESS_NET_BIND_UDP, + }; + struct sockaddr_storage port0 = {}; + unsigned short num; + bool slow; + + /* Quick return for non-Landlocked tasks. */ + if (!landlock_get_applicable_subject(current_cred(), bind_udp, NULL)) + return 0; + + /* + * On UDP sockets, if a local port has not already been bound, calling + * connect() or sending a first datagram has the side effect of + * autobinding an ephemeral port: we also have to check that the process + * would have had the right to bind(0) explicitly. Hold the socket lock + * around the inet_num read to exclude udp_lib_get_port()'s transient + * inet_num = snum write that is reverted to 0 on a failing reuseport + * bind. + */ + slow = lock_sock_fast(sock->sk); + num = inet_sk(sock->sk)->inet_num; + unlock_sock_fast(sock->sk, slow); + if (num != 0) + return 0; + + /* + * Construct a struct sockaddr* with port 0 to pretend the process tried + * to bind() on that address. + */ + port0.ss_family = READ_ONCE(sock->sk->sk_family); + + return current_check_access_socket(sock, (struct sockaddr *)&port0, + sizeof(port0), bind_udp.net, false); +} + +static int hook_socket_bind(struct socket *const sock, + struct sockaddr *const address, const int addrlen) +{ + access_mask_t access_request; + + if (sk_is_tcp(sock->sk)) + access_request = LANDLOCK_ACCESS_NET_BIND_TCP; + else if (sk_is_udp(sock->sk)) + access_request = LANDLOCK_ACCESS_NET_BIND_UDP; + else + return 0; + + return current_check_access_socket(sock, address, addrlen, + access_request, false); +} + +static int hook_socket_connect(struct socket *const sock, + struct sockaddr *const address, + const int addrlen) +{ + access_mask_t access_request; + int ret = 0; + + if (sk_is_tcp(sock->sk)) + access_request = LANDLOCK_ACCESS_NET_CONNECT_TCP; + else if (sk_is_udp(sock->sk)) + access_request = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP; + else + return 0; + + ret = current_check_access_socket(sock, address, addrlen, + access_request, true); + + /* + * connect()ing to an AF_UNSPEC address does not trigger an autobind and + * should never be restricted. + */ + if (ret == 0 && sk_is_udp(sock->sk) && + addrlen >= offsetofend(typeof(*address), sa_family) && + address->sa_family != AF_UNSPEC) + ret = current_check_autobind_udp_socket(sock); + + return ret; +} + +static int hook_socket_sendmsg(struct socket *const sock, + struct msghdr *const msg, const int size) +{ + struct sockaddr *const address = msg->msg_name; + const int addrlen = msg->msg_namelen; + access_mask_t access_request; + int ret = 0; + + if ((msg->msg_flags & MSG_FASTOPEN) && address && sk_is_tcp(sock->sk)) { + ret = current_check_access_socket( + sock, address, addrlen, LANDLOCK_ACCESS_NET_CONNECT_TCP, + true); + if (ret != 0) + return ret; + } + + if (sk_is_udp(sock->sk)) + access_request = LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP; + else + return 0; + + if (address != NULL) + ret = current_check_access_socket(sock, address, addrlen, + access_request, false); + + if (ret == 0) + ret = current_check_autobind_udp_socket(sock); + + return ret; +} + +static struct security_hook_list landlock_hooks[] __ro_after_init = { + LSM_HOOK_INIT(socket_bind, hook_socket_bind), + LSM_HOOK_INIT(socket_connect, hook_socket_connect), + LSM_HOOK_INIT(socket_sendmsg, hook_socket_sendmsg), +}; + +__init void landlock_add_net_hooks(void) +{ + security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), + &landlock_lsmid); +} diff --git a/security/landlock/net.h b/security/landlock/net.h new file mode 100644 index 000000000..5c0e3b409 --- /dev/null +++ b/security/landlock/net.h @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Network management and hooks + * + * Copyright © 2022-2023 Huawei Tech. Co., Ltd. + */ + +#ifndef _SECURITY_LANDLOCK_NET_H +#define _SECURITY_LANDLOCK_NET_H + +#include "common.h" +#include "ruleset.h" +#include "setup.h" + +#if IS_ENABLED(CONFIG_INET) +__init void landlock_add_net_hooks(void); + +int landlock_append_net_rule(struct landlock_ruleset *const ruleset, + const u16 port, access_mask_t access_rights, + const u32 flags); +#else /* IS_ENABLED(CONFIG_INET) */ +static inline void landlock_add_net_hooks(void) +{ +} + +static inline int +landlock_append_net_rule(struct landlock_ruleset *const ruleset, const u16 port, + access_mask_t access_rights, const u32 flags) +{ + return -EAFNOSUPPORT; +} +#endif /* IS_ENABLED(CONFIG_INET) */ + +#endif /* _SECURITY_LANDLOCK_NET_H */ diff --git a/security/landlock/object.c b/security/landlock/object.c new file mode 100644 index 000000000..0d6e159ef --- /dev/null +++ b/security/landlock/object.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Object management + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + */ + +#include <linux/bug.h> +#include <linux/compiler_types.h> +#include <linux/err.h> +#include <linux/kernel.h> +#include <linux/rcupdate.h> +#include <linux/refcount.h> +#include <linux/slab.h> +#include <linux/spinlock.h> + +#include "object.h" + +struct landlock_object * +landlock_create_object(const struct landlock_object_underops *const underops, + void *const underobj) +{ + struct landlock_object *new_object; + + if (WARN_ON_ONCE(!underops || !underobj)) + return ERR_PTR(-ENOENT); + new_object = kzalloc_obj(*new_object, GFP_KERNEL_ACCOUNT); + if (!new_object) + return ERR_PTR(-ENOMEM); + refcount_set(&new_object->usage, 1); + spin_lock_init(&new_object->lock); + new_object->underops = underops; + new_object->underobj = underobj; + return new_object; +} + +/* + * The caller must own the object (i.e. thanks to object->usage) to safely put + * it. + */ +void landlock_put_object(struct landlock_object *const object) +{ + /* + * The call to @object->underops->release(object) might sleep, e.g. + * because of iput(). + */ + might_sleep(); + if (!object) + return; + + /* + * If the @object's refcount cannot drop to zero, we can just decrement + * the refcount without holding a lock. Otherwise, the decrement must + * happen under @object->lock for synchronization with things like + * get_inode_object(). + */ + if (refcount_dec_and_lock(&object->usage, &object->lock)) { + __acquire(&object->lock); + /* + * With @object->lock initially held, remove the reference from + * @object->underobj to @object (if it still exists). + */ + object->underops->release(object); + kfree_rcu(object, rcu_free); + } +} diff --git a/security/landlock/object.h b/security/landlock/object.h new file mode 100644 index 000000000..5f28c35e8 --- /dev/null +++ b/security/landlock/object.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Object management + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_OBJECT_H +#define _SECURITY_LANDLOCK_OBJECT_H + +#include <linux/compiler_types.h> +#include <linux/refcount.h> +#include <linux/spinlock.h> + +struct landlock_object; + +/** + * struct landlock_object_underops - Operations on an underlying object + */ +struct landlock_object_underops { + /** + * @release: Releases the underlying object (e.g. iput() for an inode). + */ + void (*release)(struct landlock_object *const object) + __releases(object->lock); +}; + +/** + * struct landlock_object - Security blob tied to a kernel object + * + * The goal of this structure is to enable to tie a set of ephemeral access + * rights (pertaining to different domains) to a kernel object (e.g an inode) + * in a safe way. This implies to handle concurrent use and modification. + * + * The lifetime of a &struct landlock_object depends on the rules referring to + * it. + */ +struct landlock_object { + /** + * @usage: This counter is used to tie an object to the rules matching + * it or to keep it alive while adding a new rule. If this counter + * reaches zero, this struct must not be modified, but this counter can + * still be read from within an RCU read-side critical section. When + * adding a new rule to an object with a usage counter of zero, we must + * wait until the pointer to this object is set to NULL (or recycled). + */ + refcount_t usage; + /** + * @lock: Protects against concurrent modifications. This lock must be + * held from the time @usage drops to zero until any weak references + * from @underobj to this object have been cleaned up. + * + * Lock ordering: inode->i_lock nests inside this. + */ + spinlock_t lock; + /** + * @underobj: Used when cleaning up an object and to mark an object as + * tied to its underlying kernel structure. This pointer is protected + * by @lock. Cf. landlock_release_inodes() and release_inode(). + */ + void *underobj; + union { + /** + * @rcu_free: Enables lockless use of @usage, @lock and + * @underobj from within an RCU read-side critical section. + * @rcu_free and @underops are only used by + * landlock_put_object(). + */ + struct rcu_head rcu_free; + /** + * @underops: Enables landlock_put_object() to release the + * underlying object (e.g. inode). + */ + const struct landlock_object_underops *underops; + }; +}; + +struct landlock_object * +landlock_create_object(const struct landlock_object_underops *const underops, + void *const underobj); + +void landlock_put_object(struct landlock_object *const object); + +static inline void landlock_get_object(struct landlock_object *const object) +{ + if (object) + refcount_inc(&object->usage); +} + +#endif /* _SECURITY_LANDLOCK_OBJECT_H */ diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c new file mode 100644 index 000000000..a5d135d08 --- /dev/null +++ b/security/landlock/ruleset.c @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Ruleset management + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2026 Cloudflare, Inc. + */ + +#include <linux/bits.h> +#include <linux/bug.h> +#include <linux/cleanup.h> +#include <linux/compiler_types.h> +#include <linux/err.h> +#include <linux/errno.h> +#include <linux/kernel.h> +#include <linux/lockdep.h> +#include <linux/mutex.h> +#include <linux/overflow.h> +#include <linux/rbtree.h> +#include <linux/refcount.h> +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <uapi/linux/landlock.h> + +#include "access.h" +#include "id.h" +#include "limits.h" +#include "object.h" +#include "ruleset.h" + +#include <trace/events/landlock.h> + +struct landlock_ruleset * +landlock_create_ruleset(const access_mask_t fs_access_mask, + const access_mask_t net_access_mask, + const access_mask_t scope_mask) +{ + struct landlock_ruleset *new_ruleset; + + /* Informs about useless ruleset. */ + if (!fs_access_mask && !net_access_mask && !scope_mask) + return ERR_PTR(-ENOMSG); + + new_ruleset = kzalloc_obj(*new_ruleset, GFP_KERNEL_ACCOUNT); + if (!new_ruleset) + return ERR_PTR(-ENOMEM); + + refcount_set(&new_ruleset->usage, 1); + mutex_init(&new_ruleset->lock); + new_ruleset->rules.root_inode = RB_ROOT; + +#if IS_ENABLED(CONFIG_INET) + new_ruleset->rules.root_net_port = RB_ROOT; +#endif /* IS_ENABLED(CONFIG_INET) */ + +#ifdef CONFIG_TRACEPOINTS + new_ruleset->id = landlock_get_id_range(1); +#endif /* CONFIG_TRACEPOINTS */ + + /* The caller must only pass supported access rights and scopes. */ + if (fs_access_mask) { + const access_mask_t mask = fs_access_mask & + LANDLOCK_MASK_ACCESS_FS; + + WARN_ON_ONCE(fs_access_mask != mask); + new_ruleset->handled_masks.fs |= mask; + } + if (net_access_mask) { + const access_mask_t mask = net_access_mask & + LANDLOCK_MASK_ACCESS_NET; + + WARN_ON_ONCE(net_access_mask != mask); + new_ruleset->handled_masks.net |= mask; + } + if (scope_mask) { + const access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE; + + WARN_ON_ONCE(scope_mask != mask); + new_ruleset->handled_masks.scope |= mask; + } + return new_ruleset; +} + +static void build_check_rule(void) +{ + const struct landlock_rule rule = { + .num_layers = ~0, + }; + + /* + * Checks that .num_layers is large enough for at least + * LANDLOCK_MAX_NUM_LAYERS layers. + */ + BUILD_BUG_ON(rule.num_layers < LANDLOCK_MAX_NUM_LAYERS); +} + +static bool is_object_pointer(const enum landlock_key_type key_type) +{ + switch (key_type) { + case LANDLOCK_KEY_INODE: + return true; + +#if IS_ENABLED(CONFIG_INET) + case LANDLOCK_KEY_NET_PORT: + return false; +#endif /* IS_ENABLED(CONFIG_INET) */ + + default: + WARN_ON_ONCE(1); + return false; + } +} + +static struct landlock_rule * +create_rule(const struct landlock_id id, + const struct landlock_layer (*layers)[], const u32 num_layers, + const struct landlock_layer *const new_layer) +{ + struct landlock_rule *new_rule; + u32 new_num_layers; + + build_check_rule(); + if (new_layer) { + /* Should already be checked by landlock_merge_ruleset(). */ + if (WARN_ON_ONCE(num_layers >= LANDLOCK_MAX_NUM_LAYERS)) + return ERR_PTR(-E2BIG); + new_num_layers = num_layers + 1; + } else { + new_num_layers = num_layers; + } + new_rule = kzalloc_flex(*new_rule, layers, new_num_layers, + GFP_KERNEL_ACCOUNT); + if (!new_rule) + return ERR_PTR(-ENOMEM); + RB_CLEAR_NODE(&new_rule->node); + if (is_object_pointer(id.type)) { + /* This should have been caught by landlock_store_rule(). */ + WARN_ON_ONCE(!id.key.object); + landlock_get_object(id.key.object); + } + + new_rule->key = id.key; + new_rule->num_layers = new_num_layers; + /* Copies the original layer stack. */ + memcpy(new_rule->layers, layers, + flex_array_size(new_rule, layers, num_layers)); + if (new_layer) + /* Adds a copy of @new_layer on the layer stack. */ + new_rule->layers[new_rule->num_layers - 1] = *new_layer; + return new_rule; +} + +static void free_rule(struct landlock_rule *const rule, + const enum landlock_key_type key_type) +{ + might_sleep(); + if (!rule) + return; + if (is_object_pointer(key_type)) + landlock_put_object(rule->key.object); + kfree(rule); +} + +static void build_check_ruleset(void) +{ + const struct landlock_rules rules = { + .num_rules = ~0, + }; + + BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES); +} + +/** + * landlock_store_rule - Create and insert a rule into the rule storage + * + * @rules: The rule storage to be updated. The caller is responsible for + * any required locking. For rulesets, this means holding + * &landlock_ruleset.lock. For domains under construction, no lock is + * needed because the domain is not yet visible to other tasks. + * @id: The ID to build the new rule with. The underlying kernel object, if + * any, must be held by the caller. + * @layers: One or multiple layers to be copied into the new rule. + * @num_layers: The number of @layers entries. + * + * When user space requests to add a new rule to a ruleset, @layers only + * contains one entry and this entry is not assigned to any level. In this + * case, the new rule will extend @rules, similarly to a boolean OR between + * access rights. + * + * When merging a ruleset in a domain, or copying a domain, @layers will be + * added to @rules as new constraints, similarly to a boolean AND between access + * rights. + * + * Return: 0 on success, -errno on failure. + */ +int landlock_store_rule(struct landlock_rules *const rules, + const struct landlock_id id, + const struct landlock_layer (*layers)[], + const size_t num_layers) +{ + struct rb_node **walker_node; + struct rb_node *parent_node = NULL; + struct landlock_rule *new_rule; + struct rb_root *root; + + might_sleep(); + if (WARN_ON_ONCE(!layers)) + return -ENOENT; + + if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object)) + return -ENOENT; + + root = landlock_get_rule_root(rules, id.type); + if (IS_ERR(root)) + return PTR_ERR(root); + + walker_node = &root->rb_node; + while (*walker_node) { + struct landlock_rule *const this = + rb_entry(*walker_node, struct landlock_rule, node); + + if (this->key.data != id.key.data) { + parent_node = *walker_node; + if (this->key.data < id.key.data) + walker_node = &((*walker_node)->rb_right); + else + walker_node = &((*walker_node)->rb_left); + continue; + } + + /* Only a single-level layer should match an existing rule. */ + if (WARN_ON_ONCE(num_layers != 1)) + return -EINVAL; + + /* If there is a matching rule, updates it. */ + if ((*layers)[0].level == 0) { + /* + * Extends access rights when the request comes from + * landlock_add_rule(2), i.e. @rules is not a domain. + */ + if (WARN_ON_ONCE(this->num_layers != 1)) + return -EINVAL; + if (WARN_ON_ONCE(this->layers[0].level != 0)) + return -EINVAL; + this->layers[0].access |= (*layers)[0].access; + this->layers[0].flags.quiet |= (*layers)[0].flags.quiet; + return 0; + } + + if (WARN_ON_ONCE(this->layers[0].level == 0)) + return -EINVAL; + + /* + * Intersects access rights when it is a merge between a + * ruleset and a domain. + */ + new_rule = create_rule(id, &this->layers, this->num_layers, + &(*layers)[0]); + if (IS_ERR(new_rule)) + return PTR_ERR(new_rule); + rb_replace_node(&this->node, &new_rule->node, root); + free_rule(this, id.type); + return 0; + } + + /* There is no match for @id. */ + build_check_ruleset(); + if (rules->num_rules >= LANDLOCK_MAX_NUM_RULES) + return -E2BIG; + new_rule = create_rule(id, layers, num_layers, NULL); + if (IS_ERR(new_rule)) + return PTR_ERR(new_rule); + rb_link_node(&new_rule->node, parent_node, walker_node); + rb_insert_color(&new_rule->node, root); + rules->num_rules++; + return 0; +} + +static void build_check_layer(void) +{ + const struct landlock_layer layer = { + .level = ~0, + .access = ~0, + }; + + /* + * Checks that .level and .access are large enough to contain their expected + * maximum values. + */ + BUILD_BUG_ON(layer.level < LANDLOCK_MAX_NUM_LAYERS); + BUILD_BUG_ON(layer.access < LANDLOCK_MASK_ACCESS_FS); +} + +/* @ruleset must be locked by the caller. */ +int landlock_insert_rule(struct landlock_ruleset *const ruleset, + const struct landlock_id id, + const access_mask_t access, const u32 flags) +{ + struct landlock_layer layers[] = { { + .access = access, + /* + * When @level is zero, landlock_store_rule() extends @ruleset. + */ + .level = 0, + .flags = { + .quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET), + }, + } }; + int err; + + build_check_layer(); + lockdep_assert_held(&ruleset->lock); + err = landlock_store_rule(&ruleset->rules, id, &layers, + ARRAY_SIZE(layers)); + +#ifdef CONFIG_TRACEPOINTS + if (!err) + ruleset->version++; +#endif /* CONFIG_TRACEPOINTS */ + + return err; +} + +void landlock_free_rules(struct landlock_rules *const rules) +{ + struct landlock_rule *freeme, *next; + + might_sleep(); + rbtree_postorder_for_each_entry_safe(freeme, next, &rules->root_inode, + node) + free_rule(freeme, LANDLOCK_KEY_INODE); + +#if IS_ENABLED(CONFIG_INET) + rbtree_postorder_for_each_entry_safe(freeme, next, + &rules->root_net_port, node) + free_rule(freeme, LANDLOCK_KEY_NET_PORT); +#endif /* IS_ENABLED(CONFIG_INET) */ +} + +static void free_ruleset(struct landlock_ruleset *const ruleset) +{ + might_sleep(); + trace_landlock_free_ruleset(ruleset); + landlock_free_rules(&ruleset->rules); + kfree(ruleset); +} + +void landlock_put_ruleset(struct landlock_ruleset *const ruleset) +{ + might_sleep(); + if (ruleset && refcount_dec_and_test(&ruleset->usage)) + free_ruleset(ruleset); +} diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h new file mode 100644 index 000000000..b536fa042 --- /dev/null +++ b/security/landlock/ruleset.h @@ -0,0 +1,250 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Ruleset management + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2026 Cloudflare, Inc. + */ + +#ifndef _SECURITY_LANDLOCK_RULESET_H +#define _SECURITY_LANDLOCK_RULESET_H + +#include <linux/cleanup.h> +#include <linux/err.h> +#include <linux/mutex.h> +#include <linux/rbtree.h> +#include <linux/refcount.h> + +#include "access.h" +#include "limits.h" +#include "object.h" + +/** + * struct landlock_layer - Access rights for a given layer + */ +struct landlock_layer { + /** + * @level: Position of this layer in the layer stack. Starts from 1. + */ + u8 level; + /** + * @flags: Bitfield for special flags attached to this rule. + */ + struct { + /** + * @flags.quiet: Suppresses denial logs for the object covered by + * this rule in this domain. For filesystem rules, this inherits + * down the file hierarchy. + */ + u8 quiet : 1; + } flags; + /** + * @access: Bitfield of allowed actions on the kernel object. They are + * relative to the object type (e.g. %LANDLOCK_ACTION_FS_READ). + */ + access_mask_t access; +}; + +/** + * union landlock_key - Key of a ruleset's red-black tree + */ +union landlock_key { + /** + * @object: Pointer to identify a kernel object (e.g. an inode). + */ + struct landlock_object *object; + /** + * @data: Raw data to identify an arbitrary 32-bit value + * (e.g. a TCP port). + */ + uintptr_t data; +}; + +/** + * enum landlock_key_type - Type of &union landlock_key + */ +enum landlock_key_type { + /** + * @LANDLOCK_KEY_INODE: Type of &landlock_rules.root_inode's node keys. + */ + LANDLOCK_KEY_INODE = 1, + /** + * @LANDLOCK_KEY_NET_PORT: Type of &landlock_rules.root_net_port's node + * keys. + */ + LANDLOCK_KEY_NET_PORT, +}; + +/** + * struct landlock_id - Unique rule identifier for a ruleset + */ +struct landlock_id { + /** + * @key: Identifies either a kernel object (e.g. an inode) or + * a raw value (e.g. a TCP port). + */ + union landlock_key key; + /** + * @type: Type of a landlock_ruleset's root tree. + */ + const enum landlock_key_type type; +}; + +/** + * struct landlock_rule - Access rights tied to an object + */ +struct landlock_rule { + /** + * @node: Node in the ruleset's red-black tree. + */ + struct rb_node node; + /** + * @key: A union to identify either a kernel object (e.g. an inode) or + * a raw data value (e.g. a network socket port). This is used as a key + * for this ruleset element. The pointer is set once and never + * modified. It always points to an allocated object because each rule + * increments the refcount of its object. + */ + union landlock_key key; + /** + * @num_layers: Number of entries in @layers. + */ + u32 num_layers; + /** + * @layers: Stack of layers, from the latest to the newest, implemented + * as a flexible array member (FAM). + */ + struct landlock_layer layers[] __counted_by(num_layers); +}; + +/** + * struct landlock_rules - Red-black tree storage for Landlock rules + * + * This structure holds the rule trees shared by both rulesets and domains. + */ +struct landlock_rules { + /** + * @root_inode: Root of a red-black tree containing &struct + * landlock_rule nodes with inode object. Immutable for domains. + */ + struct rb_root root_inode; + +#if IS_ENABLED(CONFIG_INET) + /** + * @root_net_port: Root of a red-black tree containing &struct + * landlock_rule nodes with network port. Immutable for domains. + */ + struct rb_root root_net_port; +#endif /* IS_ENABLED(CONFIG_INET) */ + + /** + * @num_rules: Number of non-overlapping (i.e. not for the same object) + * rules in this tree storage. + */ + u32 num_rules; +}; + +/** + * struct landlock_ruleset - Landlock ruleset + * + * This data structure must contain unique entries, be updatable, and quick to + * match an object. + */ +struct landlock_ruleset { + /** + * @rules: Red-black tree storage for rules. + */ + struct landlock_rules rules; + /** + * @lock: Protects against concurrent modifications of @rules, if @usage + * is greater than zero. + */ + struct mutex lock; + /** + * @usage: Number of file descriptors referencing this ruleset. + */ + refcount_t usage; + +#ifdef CONFIG_TRACEPOINTS + /** + * @version: Counter incremented on each successful + * landlock_add_rule(2), including when it only extends an existing + * rule's access rights. Used by tracepoints to correlate a domain with + * the exact ruleset state it was created from. Protected by @lock. + */ + u32 version; + /** + * @id: Unique identifier for this ruleset, used for tracing. + */ + u64 id; +#endif /* CONFIG_TRACEPOINTS */ + + /** + * @quiet_masks: Stores the quiet flags for an unmerged ruleset. For a + * merged domain, this is stored in each layer's struct + * landlock_hierarchy instead. + */ + struct access_masks quiet_masks; + /** + * @handled_masks: Contains the subset of filesystem and network actions + * that are handled by this ruleset. + */ + struct access_masks handled_masks; +}; + +struct landlock_ruleset * +landlock_create_ruleset(const access_mask_t access_mask_fs, + const access_mask_t access_mask_net, + const access_mask_t scope_mask); + +void landlock_put_ruleset(struct landlock_ruleset *const ruleset); + +DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *, + if (!IS_ERR_OR_NULL(_T)) landlock_put_ruleset(_T)) + +int landlock_insert_rule(struct landlock_ruleset *const ruleset, + const struct landlock_id id, + const access_mask_t access, const u32 flags); + +int landlock_store_rule(struct landlock_rules *const rules, + const struct landlock_id id, + const struct landlock_layer (*layers)[], + const size_t num_layers); + +void landlock_free_rules(struct landlock_rules *const rules); + +/** + * landlock_get_rule_root - Get the root of a rule tree by key type + * + * @rules: The rules storage to look up. + * @key_type: The type of key to select the tree for. + * + * Return: A pointer to the rb_root, or ERR_PTR(-EINVAL) on unknown type. + */ +static inline struct rb_root * +landlock_get_rule_root(struct landlock_rules *const rules, + const enum landlock_key_type key_type) +{ + switch (key_type) { + case LANDLOCK_KEY_INODE: + return &rules->root_inode; + +#if IS_ENABLED(CONFIG_INET) + case LANDLOCK_KEY_NET_PORT: + return &rules->root_net_port; +#endif /* IS_ENABLED(CONFIG_INET) */ + + default: + WARN_ON_ONCE(1); + return ERR_PTR(-EINVAL); + } +} + +static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset) +{ + if (ruleset) + refcount_inc(&ruleset->usage); +} + +#endif /* _SECURITY_LANDLOCK_RULESET_H */ diff --git a/security/landlock/setup.c b/security/landlock/setup.c new file mode 100644 index 000000000..47dac1736 --- /dev/null +++ b/security/landlock/setup.c @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock LSM - Security framework setup + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + */ + +#include <linux/bits.h> +#include <linux/init.h> +#include <linux/lsm_hooks.h> +#include <uapi/linux/lsm.h> + +#include "common.h" +#include "cred.h" +#include "errata.h" +#include "fs.h" +#include "id.h" +#include "net.h" +#include "setup.h" +#include "task.h" + +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), + .lbs_inode = sizeof(struct landlock_inode_security), + .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(); + landlock_add_net_hooks(); + landlock_init_id(); + landlock_initialized = true; + pr_info("Up and running.\n"); + return 0; +} + +DEFINE_LSM(LANDLOCK_NAME) = { + .id = &landlock_lsmid, + .init = landlock_init, + .blobs = &landlock_blob_sizes, +}; diff --git a/security/landlock/setup.h b/security/landlock/setup.h new file mode 100644 index 000000000..fca307c35 --- /dev/null +++ b/security/landlock/setup.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Security framework setup + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_SETUP_H +#define _SECURITY_LANDLOCK_SETUP_H + +#include <linux/lsm_hooks.h> + +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; + +#endif /* _SECURITY_LANDLOCK_SETUP_H */ diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c new file mode 100644 index 000000000..1d02d57f4 --- /dev/null +++ b/security/landlock/syscalls.c @@ -0,0 +1,706 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - System call implementations and user space interfaces + * + * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2018-2020 ANSSI + * Copyright © 2021-2025 Microsoft Corporation + */ + +#include <asm/current.h> +#include <linux/anon_inodes.h> +#include <linux/bitops.h> +#include <linux/build_bug.h> +#include <linux/capability.h> +#include <linux/cleanup.h> +#include <linux/compiler_types.h> +#include <linux/dcache.h> +#include <linux/err.h> +#include <linux/errno.h> +#include <linux/fs.h> +#include <linux/limits.h> +#include <linux/mount.h> +#include <linux/path.h> +#include <linux/sched.h> +#include <linux/sched/signal.h> +#include <linux/security.h> +#include <linux/stddef.h> +#include <linux/syscalls.h> +#include <linux/types.h> +#include <linux/uaccess.h> +#include <uapi/linux/landlock.h> + +#include "cred.h" +#include "domain.h" +#include "fs.h" +#include "limits.h" +#include "net.h" +#include "ruleset.h" +#include "setup.h" +#include "tsync.h" + +#include <trace/events/landlock.h> + +static bool is_initialized(void) +{ + if (likely(landlock_initialized)) + return true; + + pr_warn_once( + "Disabled but requested by user space. " + "You should enable Landlock at boot time: " + "https://docs.kernel.org/userspace-api/landlock.html#boot-time-configuration\n"); + return false; +} + +/** + * copy_min_struct_from_user - Safe future-proof argument copying + * + * Extend copy_struct_from_user() to check for consistent user buffer. + * + * @dst: Kernel space pointer or NULL. + * @ksize: Actual size of the data pointed to by @dst. + * @ksize_min: Minimal required size to be copied. + * @src: User space pointer or NULL. + * @usize: (Alleged) size of the data pointed to by @src. + * + * Return: 0 on success, -errno on failure. + */ +static __always_inline int +copy_min_struct_from_user(void *const dst, const size_t ksize, + const size_t ksize_min, const void __user *const src, + const size_t usize) +{ + /* Checks buffer inconsistencies. */ + BUILD_BUG_ON(!dst); + if (!src) + return -EFAULT; + + /* Checks size ranges. */ + BUILD_BUG_ON(ksize <= 0); + BUILD_BUG_ON(ksize < ksize_min); + if (usize < ksize_min) + return -EINVAL; + if (usize > PAGE_SIZE) + return -E2BIG; + + /* Copies user buffer and fills with zeros. */ + return copy_struct_from_user(dst, ksize, src, usize); +} + +/* + * This function only contains arithmetic operations with constants, leading to + * BUILD_BUG_ON(). The related code is evaluated and checked at build time, + * but it is then ignored thanks to compiler optimizations. + */ +static void build_check_abi(void) +{ + struct landlock_ruleset_attr ruleset_attr; + struct landlock_path_beneath_attr path_beneath_attr; + struct landlock_net_port_attr net_port_attr; + size_t ruleset_size, path_beneath_size, net_port_size; + + /* + * For each user space ABI structures, first checks that there is no + * hole in them, then checks that all architectures have the same + * struct size. + */ + ruleset_size = sizeof(ruleset_attr.handled_access_fs); + ruleset_size += sizeof(ruleset_attr.handled_access_net); + ruleset_size += sizeof(ruleset_attr.scoped); + ruleset_size += sizeof(ruleset_attr.quiet_access_fs); + ruleset_size += sizeof(ruleset_attr.quiet_access_net); + ruleset_size += sizeof(ruleset_attr.quiet_scoped); + BUILD_BUG_ON(sizeof(ruleset_attr) != ruleset_size); + BUILD_BUG_ON(sizeof(ruleset_attr) != 48); + + path_beneath_size = sizeof(path_beneath_attr.allowed_access); + path_beneath_size += sizeof(path_beneath_attr.parent_fd); + BUILD_BUG_ON(sizeof(path_beneath_attr) != path_beneath_size); + BUILD_BUG_ON(sizeof(path_beneath_attr) != 12); + + net_port_size = sizeof(net_port_attr.allowed_access); + net_port_size += sizeof(net_port_attr.port); + BUILD_BUG_ON(sizeof(net_port_attr) != net_port_size); + BUILD_BUG_ON(sizeof(net_port_attr) != 16); +} + +/* Ruleset handling */ + +static int fop_ruleset_release(struct inode *const inode, + struct file *const filp) +{ + struct landlock_ruleset *ruleset = filp->private_data; + + landlock_put_ruleset(ruleset); + return 0; +} + +static ssize_t fop_dummy_read(struct file *const filp, char __user *const buf, + const size_t size, loff_t *const ppos) +{ + /* Dummy handler to enable FMODE_CAN_READ. */ + return -EINVAL; +} + +static ssize_t fop_dummy_write(struct file *const filp, + const char __user *const buf, const size_t size, + loff_t *const ppos) +{ + /* Dummy handler to enable FMODE_CAN_WRITE. */ + return -EINVAL; +} + +/* + * A ruleset file descriptor enables to build a ruleset by adding (i.e. + * writing) rule after rule, without relying on the task's context. This + * reentrant design is also used in a read way to enforce the ruleset on the + * current task. + */ +static const struct file_operations ruleset_fops = { + .release = fop_ruleset_release, + .read = fop_dummy_read, + .write = fop_dummy_write, +}; + +/* + * 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. When incrementing, the date in + * Documentation/userspace-api/landlock.rst should be updated to reflect the + * UAPI change. + * If the change involves a fix that requires userspace awareness, also update + * the errata documentation in Documentation/userspace-api/landlock.rst . + */ +const int landlock_abi_version = 11; + +/** + * sys_landlock_create_ruleset - Create a new ruleset + * + * @attr: Pointer to a &struct landlock_ruleset_attr identifying the scope of + * the new ruleset. + * @size: Size of the pointed &struct landlock_ruleset_attr (needed for + * backward and forward compatibility). + * @flags: Supported values: + * + * - %LANDLOCK_CREATE_RULESET_VERSION + * - %LANDLOCK_CREATE_RULESET_ERRATA + * + * This system call enables to create a new Landlock ruleset. + * + * If %LANDLOCK_CREATE_RULESET_VERSION or %LANDLOCK_CREATE_RULESET_ERRATA is + * set, then @attr must be NULL and @size must be 0. + * + * Return: The ruleset file descriptor on success, the Landlock ABI version if + * %LANDLOCK_CREATE_RULESET_VERSION is set, the errata value if + * %LANDLOCK_CREATE_RULESET_ERRATA is set, or -errno on failure. Possible + * returned errors are: + * + * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; + * - %EINVAL: unknown @flags, or unknown access, or unknown scope, or too small + * @size; + * - %EINVAL: quiet_access_fs, quiet_access_net, or quiet_scoped is not a + * subset of the corresponding handled_access_fs, handled_access_net, or + * scoped; + * - %E2BIG: @attr or @size inconsistencies; + * - %EFAULT: @attr or @size inconsistencies; + * - %ENOMSG: empty &landlock_ruleset_attr.handled_access_fs. + * + * .. kernel-doc:: include/uapi/linux/landlock.h + * :identifiers: landlock_create_ruleset_flags + */ +SYSCALL_DEFINE3(landlock_create_ruleset, + const struct landlock_ruleset_attr __user *const, attr, + const size_t, size, const __u32, flags) +{ + struct landlock_ruleset_attr ruleset_attr; + struct landlock_ruleset *ruleset; + int err, ruleset_fd; + + /* Build-time checks. */ + build_check_abi(); + + if (!is_initialized()) + return -EOPNOTSUPP; + + if (flags) { + 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; + } + + /* Copies raw user space buffer. */ + err = copy_min_struct_from_user(&ruleset_attr, sizeof(ruleset_attr), + offsetofend(typeof(ruleset_attr), + handled_access_fs), + attr, size); + if (err) + return err; + + /* Checks content (and 32-bits cast). */ + if ((ruleset_attr.handled_access_fs | LANDLOCK_MASK_ACCESS_FS) != + LANDLOCK_MASK_ACCESS_FS) + return -EINVAL; + + /* Checks network content (and 32-bits cast). */ + if ((ruleset_attr.handled_access_net | LANDLOCK_MASK_ACCESS_NET) != + LANDLOCK_MASK_ACCESS_NET) + return -EINVAL; + + /* Checks IPC scoping content (and 32-bits cast). */ + if ((ruleset_attr.scoped | LANDLOCK_MASK_SCOPE) != LANDLOCK_MASK_SCOPE) + return -EINVAL; + + /* + * Check that quiet masks are subsets of the respective handled masks. + * Because of the checks above this is sufficient to also ensure that + * the quiet masks are valid access masks. + */ + if ((ruleset_attr.quiet_access_fs | ruleset_attr.handled_access_fs) != + ruleset_attr.handled_access_fs) + return -EINVAL; + if ((ruleset_attr.quiet_access_net | ruleset_attr.handled_access_net) != + ruleset_attr.handled_access_net) + return -EINVAL; + if ((ruleset_attr.quiet_scoped | ruleset_attr.scoped) != + ruleset_attr.scoped) + return -EINVAL; + + /* Checks arguments and transforms to kernel struct. */ + ruleset = landlock_create_ruleset(ruleset_attr.handled_access_fs, + ruleset_attr.handled_access_net, + ruleset_attr.scoped); + if (IS_ERR(ruleset)) + return PTR_ERR(ruleset); + + ruleset->quiet_masks.fs = ruleset_attr.quiet_access_fs; + ruleset->quiet_masks.net = ruleset_attr.quiet_access_net; + ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped; + + /* + * Emits before anon_inode_getfd() installs the file descriptor, while + * the ruleset is still private to this thread: no lock is needed, and + * the event cannot race a concurrent close() freeing the ruleset under + * the tracepoint's BTF read. This is the last point at which the + * ruleset is guaranteed alive and unshared. + */ + trace_landlock_create_ruleset(ruleset); + + /* Creates anonymous FD referring to the ruleset. */ + ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops, + ruleset, O_RDWR | O_CLOEXEC); + if (ruleset_fd < 0) + landlock_put_ruleset(ruleset); + return ruleset_fd; +} + +/* + * Returns an owned ruleset from a FD. It is thus needed to call + * landlock_put_ruleset() on the return value. + */ +static struct landlock_ruleset *get_ruleset_from_fd(const int fd, + const fmode_t mode) +{ + CLASS(fd, ruleset_f)(fd); + struct landlock_ruleset *ruleset; + + if (fd_empty(ruleset_f)) + return ERR_PTR(-EBADF); + + /* Checks FD type and access right. */ + if (fd_file(ruleset_f)->f_op != &ruleset_fops) + return ERR_PTR(-EBADFD); + if (!(fd_file(ruleset_f)->f_mode & mode)) + return ERR_PTR(-EPERM); + ruleset = fd_file(ruleset_f)->private_data; + landlock_get_ruleset(ruleset); + return ruleset; +} + +/* Path handling */ + +/* + * @path: Must call put_path(@path) after the call if it succeeded. + */ +static int get_path_from_fd(const s32 fd, struct path *const path) +{ + CLASS(fd_raw, f)(fd); + + BUILD_BUG_ON(!__same_type( + fd, ((struct landlock_path_beneath_attr *)NULL)->parent_fd)); + + if (fd_empty(f)) + return -EBADF; + /* + * Forbids ruleset FDs, internal filesystems (e.g. nsfs), including + * pseudo filesystems that will never be mountable (e.g. sockfs, + * pipefs). + */ + if ((fd_file(f)->f_op == &ruleset_fops) || + (fd_file(f)->f_path.mnt->mnt_flags & MNT_INTERNAL) || + (fd_file(f)->f_path.dentry->d_sb->s_flags & SB_NOUSER) || + IS_PRIVATE(d_backing_inode(fd_file(f)->f_path.dentry))) + return -EBADFD; + + *path = fd_file(f)->f_path; + path_get(path); + return 0; +} + +static int add_rule_path_beneath(struct landlock_ruleset *const ruleset, + const void __user *const rule_attr, u32 flags) +{ + struct landlock_path_beneath_attr path_beneath_attr; + struct path path; + int res, err; + access_mask_t mask; + + /* Copies raw user space buffer. */ + res = copy_from_user(&path_beneath_attr, rule_attr, + sizeof(path_beneath_attr)); + if (res) + return -EFAULT; + + /* + * Informs about useless rule: empty allowed_access (i.e. deny rules) + * are ignored in path walks. However, the rule is not useless if it is + * there to hold a quiet flag. + */ + if (!flags && !path_beneath_attr.allowed_access) + return -ENOMSG; + + /* Checks that allowed_access matches the @ruleset constraints. */ + mask = ruleset->handled_masks.fs; + if ((path_beneath_attr.allowed_access | mask) != mask) + return -EINVAL; + + /* Checks for useless quiet flag. */ + if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.fs) + return -EINVAL; + + /* Gets and checks the new rule. */ + err = get_path_from_fd(path_beneath_attr.parent_fd, &path); + if (err) + return err; + + /* Imports the new rule. */ + err = landlock_append_fs_rule(ruleset, &path, + path_beneath_attr.allowed_access, flags); + path_put(&path); + return err; +} + +static int add_rule_net_port(struct landlock_ruleset *ruleset, + const void __user *const rule_attr, u32 flags) +{ + struct landlock_net_port_attr net_port_attr; + int res; + access_mask_t mask; + + /* Copies raw user space buffer. */ + res = copy_from_user(&net_port_attr, rule_attr, sizeof(net_port_attr)); + if (res) + return -EFAULT; + + /* + * Informs about useless rule: empty allowed_access (i.e. deny rules) + * are ignored by network actions. However, the rule is not useless if + * it is there to hold a quiet flag. + */ + if (!flags && !net_port_attr.allowed_access) + return -ENOMSG; + + /* Checks that allowed_access matches the @ruleset constraints. */ + mask = ruleset->handled_masks.net; + if ((net_port_attr.allowed_access | mask) != mask) + return -EINVAL; + + /* Checks for useless quiet flag. */ + if (flags & LANDLOCK_ADD_RULE_QUIET && !ruleset->quiet_masks.net) + return -EINVAL; + + /* Denies inserting a rule with port greater than 65535. */ + if (net_port_attr.port > U16_MAX) + return -EINVAL; + + /* Imports the new rule. */ + return landlock_append_net_rule(ruleset, net_port_attr.port, + net_port_attr.allowed_access, flags); +} + +/** + * sys_landlock_add_rule - Add a new rule to a ruleset + * + * @ruleset_fd: File descriptor tied to the ruleset that should be extended + * with the new rule. + * @rule_type: Identify the structure type pointed to by @rule_attr: + * %LANDLOCK_RULE_PATH_BENEATH or %LANDLOCK_RULE_NET_PORT. + * @rule_attr: Pointer to a rule (matching the @rule_type). + * @flags: Must be 0 or %LANDLOCK_ADD_RULE_QUIET. + * + * This system call enables to define a new rule and add it to an existing + * ruleset. + * + * Return: 0 on success, or -errno on failure. Possible returned errors are: + * + * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; + * - %EAFNOSUPPORT: @rule_type is %LANDLOCK_RULE_NET_PORT but TCP/IP is not + * supported by the running kernel; + * - %EINVAL: @flags is not valid; + * - %EINVAL: The rule accesses are inconsistent (i.e. + * &landlock_path_beneath_attr.allowed_access or + * &landlock_net_port_attr.allowed_access is not a subset of the ruleset + * handled accesses) + * - %EINVAL: &landlock_net_port_attr.port is greater than 65535; + * - %EINVAL: LANDLOCK_ADD_RULE_QUIET is passed but the ruleset has no + * quiet access bits set for the corresponding rule type. + * - %ENOMSG: Empty accesses (e.g. &landlock_path_beneath_attr.allowed_access is + * 0) and no flags; + * - %EBADF: @ruleset_fd is not a file descriptor for the current thread, or a + * member of @rule_attr is not a file descriptor as expected; + * - %EBADFD: @ruleset_fd is not a ruleset file descriptor, or a member of + * @rule_attr is not the expected file descriptor type; + * - %EPERM: @ruleset_fd has no write access to the underlying ruleset; + * - %EFAULT: @rule_attr was not a valid address. + * + * .. kernel-doc:: include/uapi/linux/landlock.h + * :identifiers: landlock_add_rule_flags + */ +SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, + const enum landlock_rule_type, rule_type, + const void __user *const, rule_attr, const __u32, flags) +{ + struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL; + + if (!is_initialized()) + return -EOPNOTSUPP; + + if (flags && flags != LANDLOCK_ADD_RULE_QUIET) + return -EINVAL; + + /* Gets and checks the ruleset. */ + ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_WRITE); + if (IS_ERR(ruleset)) + return PTR_ERR(ruleset); + + switch (rule_type) { + case LANDLOCK_RULE_PATH_BENEATH: + return add_rule_path_beneath(ruleset, rule_attr, flags); + case LANDLOCK_RULE_NET_PORT: + return add_rule_net_port(ruleset, rule_attr, flags); + default: + return -EINVAL; + } +} + +/* Enforcement */ + +/** + * 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: Supported values: + * + * - %LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF + * - %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON + * - %LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF + * - %LANDLOCK_RESTRICT_SELF_TSYNC + * - %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS + * + * This system call enforces 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. + * + * With %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS, the no_new_privs attribute of the + * calling thread is set only once the enforcement of the ruleset succeeded, + * which fulfills the above requirement: no_new_privs is set if and only if the + * call succeeds. + * + * Return: 0 on success, or -errno on failure. Possible returned errors are: + * + * - %EOPNOTSUPP: Landlock is supported by the kernel but disabled at boot time; + * - %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 + * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS is not set while the current thread + * is not running with no_new_privs and doesn't have %CAP_SYS_ADMIN in its + * namespace. + * - %E2BIG: The maximum number of stacked rulesets is reached for the current + * thread. + * + * .. kernel-doc:: include/uapi/linux/landlock.h + * :identifiers: landlock_restrict_self_flags + */ +SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, + flags) +{ + struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL; + struct landlock_domain *new_dom = NULL; + struct cred *new_cred; + struct landlock_cred_security *new_llcred; + bool process_wide; + bool __maybe_unused log_same_exec, log_new_exec, log_subdomains, + prev_log_subdomains; + + if (!is_initialized()) + return -EOPNOTSUPP; + + if ((flags | LANDLOCK_MASK_RESTRICT_SELF) != + LANDLOCK_MASK_RESTRICT_SELF) + return -EINVAL; + + /* + * Similar checks as for seccomp(2), except that an -EPERM may be + * returned. LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS fulfills this + * requirement. + */ + if (!(flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) && + !task_no_new_privs(current) && + !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) + return -EPERM; + + /* 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); + /* Translates "off" flag to boolean. */ + log_subdomains = !(flags & LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF); + + /* + * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with + * -1 as ruleset_fd, optionally combined with + * LANDLOCK_RESTRICT_SELF_TSYNC to propagate this configuration to all + * threads. No other flag must be set. + */ + if (!(ruleset_fd == -1 && + (flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) == + 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(); + if (!new_cred) + return -ENOMEM; + + new_llcred = landlock_cred(new_cred); + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + prev_log_subdomains = !new_llcred->log_subdomains_off; + new_llcred->log_subdomains_off = !prev_log_subdomains || + !log_subdomains; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + /* + * The only case when a ruleset may not be set is if + * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with + * LANDLOCK_RESTRICT_SELF_TSYNC) 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) { + /* + * There is no possible race condition while copying and + * manipulating the current credentials because they are + * dedicated per thread. + */ + mutex_lock(&ruleset->lock); + new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset); + if (IS_ERR(new_dom)) { + mutex_unlock(&ruleset->lock); + abort_creds(new_cred); + return PTR_ERR(new_dom); + } + /* + * Emits the domain-creation event while @ruleset->lock is still + * held, right after the merge, so an eBPF program attached to + * the tracepoint reads the exact ruleset that was merged into + * the domain: a consistent snapshot that a concurrent + * landlock_add_rule() (which holds the same lock) cannot + * modify. + * + * This must come before the thread-sync wait below. Holding + * @ruleset->lock across landlock_restrict_sibling_threads() + * would hang: a sibling thread blocked in landlock_add_rule() + * on the same @ruleset->lock cannot run the task_work that + * thread-sync waits for (the lock wait is uninterruptible). + * Emitting here keeps the lock off the thread-sync path. + * + * The trade-off is that the event fires for a domain that a + * later (rare) thread-sync failure aborts. That path emits the + * matching free_domain event so the create/free pair stays + * balanced (see the thread-sync error path below). + */ + trace_landlock_create_domain(new_dom, ruleset); + mutex_unlock(&ruleset->lock); + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + new_dom->hierarchy->log_same_exec = log_same_exec; + new_dom->hierarchy->log_new_exec = log_new_exec; + /* + * The creation event fired above, so move the domain out of + * LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire + * too, even if a thread-sync failure aborts it below. Audit + * logging may still be disabled (DISABLED); tracing observes it + * anyway. + */ + if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains) + new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED; + else + new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + /* Replaces the old (prepared) domain. */ + landlock_put_domain(new_llcred->domain); + new_llcred->domain = new_dom; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + new_llcred->domain_exec |= BIT(new_dom->num_layers - 1); +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + } + + if (flags & LANDLOCK_RESTRICT_SELF_TSYNC) { + const int err = landlock_restrict_sibling_threads( + current_cred(), new_cred, flags); + if (err) { + /* + * Thread-sync failed (rare), so the new domain is + * aborted instead of committed. Its creation event + * already fired above, so the imminent free must emit + * the matching free_domain event to keep the + * create/free pair balanced; no special log_status is + * set here. + */ + abort_creds(new_cred); + return err; + } + } + + /* Sets no_new_privs past the last point of failure. */ + if (flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) + task_set_no_new_privs(current); + + /* Whole process: thread-sync swept siblings, or single-threaded. */ + process_wide = (flags & LANDLOCK_RESTRICT_SELF_TSYNC) || + get_nr_threads(current) == 1; + commit_creds(new_cred); + + /* The caller commits last, so its event concludes the operation. */ + if (ruleset) + trace_landlock_enforce_domain(new_dom, true, process_wide, + task_no_new_privs(current)); + + return 0; +} diff --git a/security/landlock/task.c b/security/landlock/task.c new file mode 100644 index 000000000..4491ce31a --- /dev/null +++ b/security/landlock/task.c @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Ptrace and scope hooks + * + * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019-2020 ANSSI + * Copyright © 2024-2025 Microsoft Corporation + */ + +#include <asm/current.h> +#include <linux/cleanup.h> +#include <linux/cred.h> +#include <linux/errno.h> +#include <linux/kernel.h> +#include <linux/lsm_audit.h> +#include <linux/lsm_hooks.h> +#include <linux/rcupdate.h> +#include <linux/sched.h> +#include <linux/sched/signal.h> +#include <net/af_unix.h> +#include <net/sock.h> + +#include "common.h" +#include "cred.h" +#include "domain.h" +#include "fs.h" +#include "log.h" +#include "ruleset.h" +#include "setup.h" +#include "task.h" + +/** + * domain_scope_le - Checks domain ordering for scoped ptrace + * + * @parent: Parent domain. + * @child: Potential child of @parent. + * + * Checks if the @parent domain is less or equal to (i.e. an ancestor, which + * means a subset of) the @child domain. + * + * Return: True if @parent is an ancestor of or equal to @child, false + * otherwise. + */ +static bool domain_scope_le(const struct landlock_domain *const parent, + const struct landlock_domain *const child) +{ + 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 int domain_ptrace(const struct landlock_domain *const parent, + const struct landlock_domain *const child) +{ + if (domain_scope_le(parent, child)) + return 0; + + return -EPERM; +} + +/** + * hook_ptrace_access_check - Determines whether the current process may access + * another + * + * @child: Process to be accessed. + * @mode: Mode of attachment. + * + * If the current task has Landlock rules, then the child must have at least + * the same rules. Else denied. + * + * Return: 0 if permission is granted, -errno if denied. + */ +static int hook_ptrace_access_check(struct task_struct *const child, + const unsigned int mode) +{ + const struct landlock_cred_security *parent_subject; + u64 tracee_domain_id = 0; + int err; + + /* Quick return for non-landlocked tasks. */ + parent_subject = landlock_cred(current_cred()); + if (!parent_subject) + return 0; + + scoped_guard(rcu) { + const struct landlock_domain *const child_dom = + landlock_get_task_domain(child); + err = domain_ptrace(parent_subject->domain, child_dom); +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + if (child_dom) + tracee_domain_id = child_dom->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + } + + 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, + .other_domain_id = tracee_domain_id, + }); + + return err; +} + +/** + * hook_ptrace_traceme - Determines whether another process may trace the + * current one + * + * @parent: Task proposed to be the tracer. + * + * If the parent has Landlock rules, then the current task must have the same + * or more rules. Else denied. + * + * Return: 0 if permission is granted, -errno if denied. + */ +static int hook_ptrace_traceme(struct task_struct *const parent) +{ + const struct landlock_cred_security *parent_subject; + const struct landlock_domain *child_dom; + u64 tracee_domain_id = 0; + 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; + +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + /* The tracee is the current task; its domain is stable here. */ + if (child_dom) + tracee_domain_id = child_dom->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + + /* + * 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, + .other_domain_id = tracee_domain_id, + }); + return err; +} + +/** + * domain_is_scoped - Check if an interaction from a client/sender to a + * server/receiver should be restricted based on scope controls. + * + * @client: IPC sender domain. + * @server: IPC receiver domain. + * @scope: The scope restriction criteria. + * + * Return: True if @server is in a different domain from @client and @client + * is scoped to access @server (i.e. access should be denied), false otherwise. + */ +static bool domain_is_scoped(const struct landlock_domain *const client, + const struct landlock_domain *const server, + access_mask_t scope) +{ + int client_layer, server_layer; + const struct landlock_hierarchy *client_walker, *server_walker; + + /* Quick return if client has no domain */ + if (WARN_ON_ONCE(!client)) + return false; + + client_layer = client->num_layers - 1; + client_walker = client->hierarchy; + /* + * client_layer must be able to represent all numbers from + * LANDLOCK_MAX_NUM_LAYERS - 1 to -1 for the loop below to terminate. + * (It must be large enough, and it must be signed.) + */ + BUILD_BUG_ON(!is_signed_type(typeof(client_layer))); + BUILD_BUG_ON(LANDLOCK_MAX_NUM_LAYERS - 1 > + type_max(typeof(client_layer))); + + server_layer = server ? (server->num_layers - 1) : -1; + server_walker = server ? server->hierarchy : NULL; + + /* + * Walks client's parent domains down to the same hierarchy level + * as the server's domain, and checks that none of these client's + * parent domains are scoped. + */ + for (; client_layer > server_layer; client_layer--) { + if (landlock_get_scope_mask(client, client_layer) & scope) + return true; + + client_walker = client_walker->parent; + } + /* + * Walks server's parent domains down to the same hierarchy level as + * the client's domain. + */ + for (; server_layer > client_layer; server_layer--) + server_walker = server_walker->parent; + + for (; client_layer >= 0; client_layer--) { + if (landlock_get_scope_mask(client, client_layer) & scope) { + /* + * Client and server are at the same level in the + * hierarchy. If the client is scoped, the request is + * only allowed if this domain is also a server's + * ancestor. + */ + return server_walker != client_walker; + } + client_walker = client_walker->parent; + server_walker = server_walker->parent; + } + return false; +} + +static bool sock_is_scoped(struct sock *const other, + const struct landlock_domain *const domain, + u64 *const peer_domain_id) +{ + const struct landlock_domain *dom_other; + + /* The credentials will not change. */ + lockdep_assert_held(&unix_sk(other)->lock); + + /* + * A live kernel socket (e.g. from sock_create_kern()) has no backing + * file, hence no Landlock domain, so treat it as unscoped. The + * sk_socket check only guards that dereference; sk_socket is NULL + * solely for a dead peer, which the caller already excludes under the + * held lock, so no separate SOCK_DEAD check is needed. + */ + if (unlikely(!other->sk_socket || !other->sk_socket->file)) + return false; + + dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain; +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + *peer_domain_id = dom_other ? dom_other->hierarchy->id : 0; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + return domain_is_scoped(domain, dom_other, + LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET); +} + +static bool is_abstract_socket(struct sock *const sock) +{ + struct unix_address *addr = unix_sk(sock)->addr; + + if (!addr) + return false; + + if (addr->len >= offsetof(struct sockaddr_un, sun_path) + 1 && + addr->name->sun_path[0] == '\0') + return true; + + return false; +} + +static const struct access_masks unix_scope = { + .scope = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, +}; + +static int hook_unix_stream_connect(struct sock *const sock, + struct sock *const other, + struct sock *const newsk) +{ + size_t handle_layer; + u64 peer_domain_id = 0; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), unix_scope, + &handle_layer); + + /* Quick return for non-landlocked tasks. */ + if (!subject) + return 0; + + if (!is_abstract_socket(other)) + return 0; + + if (!sock_is_scoped(other, subject->domain, &peer_domain_id)) + 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, + .other_domain_id = peer_domain_id, + }); + return -EPERM; +} + +static int hook_unix_may_send(struct socket *const sock, + struct socket *const other) +{ + size_t handle_layer; + u64 peer_domain_id = 0; + const struct landlock_cred_security *const subject = + landlock_get_applicable_subject(current_cred(), unix_scope, + &handle_layer); + + if (!subject) + return 0; + + /* + * Checks if this datagram socket was already allowed to be connected + * to other. + */ + if (unix_peer(sock->sk) == other->sk) + return 0; + + if (!is_abstract_socket(other->sk)) + return 0; + + if (!sock_is_scoped(other->sk, subject->domain, &peer_domain_id)) + 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, + .other_domain_id = peer_domain_id, + }); + return -EPERM; +} + +static const struct access_masks signal_scope = { + .scope = LANDLOCK_SCOPE_SIGNAL, +}; + +static int hook_task_kill(struct task_struct *const p, + struct kernel_siginfo *const info, const int sig, + const struct cred *cred) +{ + bool is_scoped; + size_t handle_layer; + u64 target_domain_id = 0; + const struct landlock_cred_security *subject; + + if (!cred) { + /* + * 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; + + /* Not dealing with USB IO. */ + cred = current_cred(); + } + + subject = landlock_get_applicable_subject(cred, signal_scope, + &handle_layer); + + /* Quick return for non-landlocked tasks. */ + if (!subject) + return 0; + + scoped_guard(rcu) { + const struct landlock_domain *const other = + landlock_get_task_domain(p); + + is_scoped = domain_is_scoped(subject->domain, other, + signal_scope.scope); +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + if (other) + target_domain_id = other->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + } + + 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, + .other_domain_id = target_domain_id, + }); + return -EPERM; +} + +static int hook_file_send_sigiotask(struct task_struct *tsk, + struct fown_struct *fown, int signum) +{ + const struct landlock_cred_security *subject; + bool is_scoped = false; + u64 target_domain_id = 0; + + /* Lock already held by send_sigio() and send_sigurg(). */ + lockdep_assert_held(&fown->lock); + subject = &landlock_file(fown->file)->fown_subject; + + /* + * 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; + + /* + * Always allow delivery to the file owner's own process, including a + * thread-group leader reached through a process-group owner. This + * mirrors hook_task_kill()'s same-process exemption and preserves the + * guarantee of commit 18eb75f3af40 ("landlock: Always allow signals + * between threads of the same process"), which the registration-time + * check cannot honor for a process-group target. + */ + if (task_tgid(tsk) == landlock_file(fown->file)->fown_tg) + return 0; + + scoped_guard(rcu) { + const struct landlock_domain *const other = + landlock_get_task_domain(tsk); + + is_scoped = domain_is_scoped(subject->domain, other, + signal_scope.scope); +#ifdef CONFIG_SECURITY_LANDLOCK_LOG + if (other) + target_domain_id = other->hierarchy->id; +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + } + + 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_SECURITY_LANDLOCK_LOG + .layer_plus_one = landlock_file(fown->file)->fown_layer + 1, +#endif /* CONFIG_SECURITY_LANDLOCK_LOG */ + .other_domain_id = target_domain_id, + }); + return -EPERM; +} + +static struct security_hook_list landlock_hooks[] __ro_after_init = { + LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check), + LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme), + + LSM_HOOK_INIT(unix_stream_connect, hook_unix_stream_connect), + LSM_HOOK_INIT(unix_may_send, hook_unix_may_send), + + LSM_HOOK_INIT(task_kill, hook_task_kill), + LSM_HOOK_INIT(file_send_sigiotask, hook_file_send_sigiotask), +}; + +__init void landlock_add_task_hooks(void) +{ + security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), + &landlock_lsmid); +} diff --git a/security/landlock/task.h b/security/landlock/task.h new file mode 100644 index 000000000..7c0036021 --- /dev/null +++ b/security/landlock/task.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock LSM - Ptrace hooks + * + * Copyright © 2017-2019 Mickaël Salaün <mic@digikod.net> + * Copyright © 2019 ANSSI + */ + +#ifndef _SECURITY_LANDLOCK_TASK_H +#define _SECURITY_LANDLOCK_TASK_H + +__init void landlock_add_task_hooks(void); + +#endif /* _SECURITY_LANDLOCK_TASK_H */ diff --git a/security/landlock/trace.c b/security/landlock/trace.c new file mode 100644 index 000000000..8c21e5de6 --- /dev/null +++ b/security/landlock/trace.c @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Tracepoint helpers + * + * Copyright © 2025 Microsoft Corporation + * Copyright © 2026 Cloudflare, Inc. + */ + +#include <kunit/test.h> +#include <linux/cleanup.h> +#include <linux/dcache.h> +#include <linux/err.h> +#include <linux/fs.h> +#include <linux/lsm_audit.h> +#include <net/sock.h> + +#include "access.h" +#include "domain.h" +#include "fs.h" +#include "log.h" +#include "ruleset.h" +#include "trace.h" + +/* + * Generates the tracepoint definitions in this translation unit. The trace + * event header dereferences the traced objects in TP_fast_assign, so the full + * struct definitions (e.g. ruleset.h, domain.h) must be included before it. + */ +#define CREATE_TRACE_POINTS +#include <trace/events/landlock.h> + +/** + * landlock_trace_free_domain - Emit a tracepoint on domain deallocation + * + * @hierarchy: The domain's hierarchy being deallocated. + * + * Fires only for a hierarchy whose creation event was emitted, i.e. one that + * left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self(). This keeps the + * create/free pair balanced: a hierarchy that never became observable is freed + * silently, while a domain that landlock_restrict_self() created and a + * thread-sync failure then aborted still fires free_domain, because its + * creation event already fired. + * + * Called from landlock_log_free_domain(). + */ +void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy) +{ + /* + * The log_status read is a correctness guard (keep the create/free pair + * balanced), not a cost guard, so this cold path needs no + * trace_..._enabled() check: the tracepoint is a static-branch no-op + * when disabled. The denial path guards trace_..._enabled() instead + * because it does expensive __getname()/path work before emitting. + */ + if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_UNCOMMITTED) + trace_landlock_free_domain(hierarchy); +} + +/** + * landlock_trace_denial - Emit a tracepoint for a denied access request + * + * @request: Detail of the user space request. + * @youngest_denied: The youngest hierarchy node that denied the access. + * @missing: The set of denied access rights. + * @same_exec: Whether the current task is the same executable that called + * landlock_restrict_self() for the denying domain, as computed + * by landlock_log_denial(). + * @logged: Whether the domain's policy selects this denial for logging, as + * computed by landlock_log_denial(). + * + * Emits the tracepoint matching @request->type when its event is enabled. + * Unlike audit, fires regardless of @logged; the value is recorded in the event + * so consumers can filter on it. + * + * Called from landlock_log_denial(). + */ +void landlock_trace_denial( + const struct landlock_request *const request, + const struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool same_exec, const bool logged) +{ + switch (request->type) { + case LANDLOCK_REQUEST_FS_ACCESS: + case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY: + if (trace_landlock_deny_access_fs_enabled()) { + char *buf __free(__putname) = __getname(); + struct path dentry_path; + const char *pathname; + const struct path *path = NULL; + + /* + * Selects the path from the audit data type, as + * dump_common_audit_data() does. A FS_ACCESS denial + * carries a file (hook_file_truncate) or an ioctl op + * (hook_file_ioctl) rather than a path; + * FS_CHANGE_TOPOLOGY carries a path or a bare dentry. + * Reading the wrong union member would dereference + * garbage, so every reachable type is handled here. + */ + switch (request->audit.type) { + case LSM_AUDIT_DATA_FILE: + path = &request->audit.u.file->f_path; + break; + case LSM_AUDIT_DATA_IOCTL_OP: + path = &request->audit.u.op->path; + break; + case LSM_AUDIT_DATA_DENTRY: + /* + * Build a path on the stack with the real + * dentry so TP_fast_assign can extract dev and + * ino; the mnt field is unused there. + */ + dentry_path = (struct path){ + .dentry = request->audit.u.dentry, + }; + path = &dentry_path; + break; + case LSM_AUDIT_DATA_PATH: + path = &request->audit.u.path; + break; + default: + WARN_ONCE(1, + "Unhandled Landlock FS audit type %d", + request->audit.type); + break; + } + + if (!path) + break; + + if (!buf) { + pathname = "<no_mem>"; + } else if (request->audit.type == + LSM_AUDIT_DATA_DENTRY) { + /* No vfsmount: render the dentry path alone. */ + pathname = dentry_path_raw( + request->audit.u.dentry, buf, PATH_MAX); + if (IS_ERR(pathname)) + pathname = + PTR_ERR(pathname) == + -ENAMETOOLONG ? + "<too_long>" : + "<unreachable>"; + } else { + pathname = resolve_path_for_trace(path, buf); + } + + trace_landlock_deny_access_fs(youngest_denied, + same_exec, logged, + missing, path, pathname); + } + break; + case LANDLOCK_REQUEST_NET_ACCESS: + if (trace_landlock_deny_access_net_enabled()) + trace_landlock_deny_access_net( + youngest_denied, same_exec, logged, missing, + request->audit.u.net->sk, + ntohs(request->audit.u.net->sport), + ntohs(request->audit.u.net->dport)); + break; + case LANDLOCK_REQUEST_PTRACE: + if (trace_landlock_deny_ptrace_enabled()) + trace_landlock_deny_ptrace(youngest_denied, same_exec, + logged, + request->other_domain_id, + request->audit.u.tsk); + break; + case LANDLOCK_REQUEST_SCOPE_SIGNAL: + if (trace_landlock_deny_scope_signal_enabled()) + trace_landlock_deny_scope_signal( + youngest_denied, same_exec, logged, + request->other_domain_id, request->audit.u.tsk); + break; + case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET: + if (trace_landlock_deny_scope_abstract_unix_socket_enabled()) + trace_landlock_deny_scope_abstract_unix_socket( + youngest_denied, same_exec, logged, + request->other_domain_id, + request->audit.u.net->sk); + break; + default: + WARN_ONCE(1, "Unhandled Landlock request type %d", + request->type); + break; + } +} + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_trace_seq_init(struct trace_seq *const seq, const size_t size) +{ + memset(seq, 0, sizeof(*seq)); + seq_buf_init(&seq->seq, seq->buffer, size); +} + +static void test_untrusted_str_data(struct kunit *const test) +{ + const char binary[] = { 'a', '\0', '<' }; + static const char ellipsis[] = "\xe2\x80\xa6"; + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + const char *output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, "<too_long>", 10); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "<too_long>"); + + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, binary, sizeof(binary)); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "a\\000<"); + + /* Input ellipsis bytes are escaped and cannot mimic the raw marker. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, ellipsis, + sizeof(ellipsis) - 1); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "\\342\\200\\246"); +} + +static void test_untrusted_str_boundaries(struct kunit *const test) +{ + static const char escaped_space[] = "\\040"; + const size_t output_size = TRACE_UNTRUSTED_STR_OUTPUT_SIZE; + const size_t marker_len = sizeof(TRACE_TRUNCATION_MARKER) - 1; + const size_t escape_len = sizeof(escaped_space) - 1; + const size_t exact_prefix_len = + output_size - marker_len - 1 - escape_len; + const size_t short_prefix_len = exact_prefix_len + 1; + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + char *const input = kunit_kmalloc(test, output_size + 1, GFP_KERNEL); + char *const expected = kunit_kmalloc(test, output_size, GFP_KERNEL); + const char *output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + KUNIT_ASSERT_NOT_NULL(test, input); + KUNIT_ASSERT_NOT_NULL(test, expected); + + /* The escaped string and its trailing NUL exactly fit the limit. */ + memset(input, 'a', output_size - 1); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, input, output_size - 1); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_EQ(test, seq->seq.len, output_size); + KUNIT_EXPECT_EQ(test, memcmp(output, input, output_size - 1), 0); + + /* Stop before a four-byte escape when only three bytes remain. */ + memset(input, 'a', short_prefix_len); + input[short_prefix_len] = ' '; + memset(input + short_prefix_len + 1, 'b', 5); + memset(expected, 'a', short_prefix_len); + memcpy(expected + short_prefix_len, TRACE_TRUNCATION_MARKER, + marker_len + 1); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, input, short_prefix_len + 6); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, expected); + + /* Include a four-byte escape that exactly fills the prefix capacity. */ + memset(input, 'a', exact_prefix_len); + input[exact_prefix_len] = ' '; + memset(input + exact_prefix_len + 1, 'b', marker_len + 1); + memset(expected, 'a', exact_prefix_len); + memcpy(expected + exact_prefix_len, escaped_space, escape_len); + memcpy(expected + exact_prefix_len + escape_len, + TRACE_TRUNCATION_MARKER, marker_len + 1); + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, input, + exact_prefix_len + marker_len + 2); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, expected); + + /* Literal backslashes remain escaped in complete output. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + output = __trace_print_untrusted_str(seq, "/\\000", 5); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "/\\\\000"); +} + +static void test_untrusted_str_cursor(struct kunit *const test) +{ + const size_t padding_len = + TRACE_SEQ_BUFFER_SIZE - TRACE_UNTRUSTED_STR_OUTPUT_SIZE + 1; + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + char *const padding = kunit_kzalloc(test, padding_len, GFP_KERNEL); + const char *output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + KUNIT_ASSERT_NOT_NULL(test, padding); + + /* Accept available space exactly equal to the fixed reservation. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + trace_seq_putmem(seq, padding, padding_len - 1); + output = __trace_print_untrusted_str(seq, "/a", 2); + KUNIT_ASSERT_NOT_NULL(test, output); + KUNIT_EXPECT_STREQ(test, output, "/a"); + KUNIT_EXPECT_EQ(test, seq->seq.len, padding_len - 1 + sizeof("/a")); + + /* Reject one byte less without changing the scratch cursor. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + trace_seq_putmem(seq, padding, padding_len); + output = __trace_print_untrusted_str(seq, "/a", 2); + KUNIT_EXPECT_NULL(test, output); + KUNIT_EXPECT_EQ(test, seq->seq.len, padding_len); +} + +static void test_untrusted_str_composition(struct kunit *const test) +{ + static const struct trace_print_flags flags[] = { + { .mask = 1, .name = "read" }, + }; + const size_t output_size = TRACE_UNTRUSTED_STR_OUTPUT_SIZE; + const size_t prefix_len = output_size - sizeof(TRACE_TRUNCATION_MARKER); + struct trace_seq *const seq = + kunit_kzalloc(test, sizeof(*seq), GFP_KERNEL); + char *const expected = kunit_kmalloc(test, output_size, GFP_KERNEL); + char *const path = kunit_kmalloc(test, output_size, GFP_KERNEL); + const char *flags_output, *path_output; + + KUNIT_ASSERT_NOT_NULL(test, seq); + KUNIT_ASSERT_NOT_NULL(test, expected); + KUNIT_ASSERT_NOT_NULL(test, path); + memset(path, 'a', output_size); + memset(expected, 'a', prefix_len); + memcpy(expected + prefix_len, TRACE_TRUNCATION_MARKER, + sizeof(TRACE_TRUNCATION_MARKER)); + + /* Exercise both legal TP_printk() sibling evaluation orders. */ + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + path_output = __trace_print_untrusted_str(seq, path, output_size); + flags_output = + trace_print_flags_seq(seq, "|", 1, flags, ARRAY_SIZE(flags)); + KUNIT_ASSERT_NOT_NULL(test, path_output); + KUNIT_EXPECT_STREQ(test, path_output, expected); + KUNIT_EXPECT_STREQ(test, flags_output, "read"); + + test_trace_seq_init(seq, TRACE_SEQ_BUFFER_SIZE); + flags_output = + trace_print_flags_seq(seq, "|", 1, flags, ARRAY_SIZE(flags)); + path_output = __trace_print_untrusted_str(seq, path, output_size); + KUNIT_ASSERT_NOT_NULL(test, path_output); + KUNIT_EXPECT_STREQ(test, path_output, expected); + KUNIT_EXPECT_STREQ(test, flags_output, "read"); +} + +static struct kunit_case test_cases[] = { + /* clang-format off */ + KUNIT_CASE(test_untrusted_str_data), + KUNIT_CASE(test_untrusted_str_boundaries), + KUNIT_CASE(test_untrusted_str_cursor), + KUNIT_CASE(test_untrusted_str_composition), + {} + /* clang-format on */ +}; + +static struct kunit_suite test_suite = { + .name = "landlock_trace", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/trace.h b/security/landlock/trace.h new file mode 100644 index 000000000..7be98e748 --- /dev/null +++ b/security/landlock/trace.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Tracepoint helpers + * + * Copyright © 2025 Microsoft Corporation + * Copyright © 2026 Cloudflare, Inc. + */ + +#ifndef _SECURITY_LANDLOCK_TRACE_H +#define _SECURITY_LANDLOCK_TRACE_H + +#include "access.h" + +struct landlock_hierarchy; +struct landlock_request; + +#ifdef CONFIG_TRACEPOINTS + +void landlock_trace_free_domain( + const struct landlock_hierarchy *const hierarchy); + +void landlock_trace_denial( + const struct landlock_request *const request, + const struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool same_exec, const bool logged); + +#else /* CONFIG_TRACEPOINTS */ + +static inline void +landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy) +{ +} + +static inline void +landlock_trace_denial(const struct landlock_request *const request, + const struct landlock_hierarchy *const youngest_denied, + const access_mask_t missing, const bool same_exec, + const bool logged) +{ +} + +#endif /* CONFIG_TRACEPOINTS */ + +#endif /* _SECURITY_LANDLOCK_TRACE_H */ diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c new file mode 100644 index 000000000..cfa73a2a6 --- /dev/null +++ b/security/landlock/tsync.c @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Landlock - Cross-thread ruleset enforcement + * + * Copyright © 2025 Google LLC + */ + +#include <linux/atomic.h> +#include <linux/cleanup.h> +#include <linux/completion.h> +#include <linux/cred.h> +#include <linux/errno.h> +#include <linux/overflow.h> +#include <linux/rcupdate.h> +#include <linux/sched.h> +#include <linux/sched/signal.h> +#include <linux/sched/task.h> +#include <linux/slab.h> +#include <linux/task_work.h> +#include <uapi/linux/landlock.h> + +#include "cred.h" +#include "tsync.h" + +#include <trace/events/landlock.h> + +/* + * Shared state between multiple threads which are enforcing Landlock rulesets + * in lockstep with each other. + */ +struct tsync_shared_context { + /* The old and tentative new creds of the calling thread. */ + const struct cred *old_cred; + const struct cred *new_cred; + + /* True if sibling tasks need to set the no_new_privs flag. */ + bool set_no_new_privs; + + /* An error encountered in preparation step, or 0. */ + atomic_t preparation_error; + + /* + * Barrier after preparation step in restrict_one_thread. + * The calling thread waits for completion. + * + * Re-initialized on every round of looking for newly spawned threads. + */ + atomic_t num_preparing; + struct completion all_prepared; + + /* Sibling threads wait for completion. */ + struct completion ready_to_commit; + + /* + * Barrier after commit step (used by syscall impl to wait for + * completion). + */ + atomic_t num_unfinished; + struct completion all_finished; +}; + +struct tsync_work { + struct callback_head work; + struct task_struct *task; + struct tsync_shared_context *shared_ctx; +}; + +/* + * restrict_one_thread - update a thread's Landlock domain in lockstep with the + * other threads in the same process + * + * When this is run, the same function gets run in all other threads in the same + * process (except for the calling thread which called landlock_restrict_self). + * The concurrently running invocations of restrict_one_thread coordinate + * through the shared ctx object to do their work in lockstep to implement + * all-or-nothing semantics for enforcing the new Landlock domain. + * + * Afterwards, depending on the presence of an error, all threads either commit + * or abort the prepared credentials. The commit operation can not fail any + * more. + */ +static void restrict_one_thread(struct tsync_shared_context *ctx) +{ + const struct landlock_domain *new_dom = + landlock_cred(ctx->new_cred)->domain; + int err; + struct cred *cred = NULL; + + if (current_cred() == ctx->old_cred) { + /* + * Switch out old_cred with new_cred, if possible. + * + * In the common case, where all threads initially point to the + * same struct cred, this optimization avoids creating separate + * redundant credentials objects for each, which would all have + * the same contents. + * + * Note: We are intentionally dropping the const qualifier + * here, because it is required by commit_creds() and + * abort_creds(). + */ + cred = (struct cred *)get_cred(ctx->new_cred); + } else { + /* Else, prepare new creds and populate them. */ + cred = prepare_creds(); + + if (!cred) { + atomic_set(&ctx->preparation_error, -ENOMEM); + + /* + * Even on error, we need to adhere to the protocol and + * coordinate with concurrently running invocations. + */ + if (atomic_dec_return(&ctx->num_preparing) == 0) + complete_all(&ctx->all_prepared); + + goto out; + } + + landlock_cred_copy(landlock_cred(cred), + landlock_cred(ctx->new_cred)); + } + + /* + * Barrier: Wait until all threads are done preparing. + * After this point, we can have no more failures. + */ + if (atomic_dec_return(&ctx->num_preparing) == 0) + complete_all(&ctx->all_prepared); + + /* + * Wait for signal from calling thread that it's safe to read the + * preparation error now and we are ready to commit (or abort). + */ + wait_for_completion(&ctx->ready_to_commit); + + /* Abort the commit if any of the other threads had an error. */ + err = atomic_read(&ctx->preparation_error); + if (err) { + abort_creds(cred); + goto out; + } + + /* + * Make sure that all sibling tasks fulfill the no_new_privs + * prerequisite. (This is in line with Seccomp's + * SECCOMP_FILTER_FLAG_TSYNC logic in kernel/seccomp.c) + */ + if (ctx->set_no_new_privs) + task_set_no_new_privs(current); + + commit_creds(cred); + + /* + * Emitted strictly after commit_creds() and before the out: label, so + * it fires only for a thread now enforcing new_dom, and every + * non-concluding (complete == false) event happens-before the + * operation's single concluding one. Skipped on the flags-only path, + * where old_cred and new_cred carry the same domain. A sibling never + * concludes the operation and its enforcement is always process-wide. + */ + if (new_dom != landlock_cred(ctx->old_cred)->domain) + trace_landlock_enforce_domain(new_dom, false, true, + task_no_new_privs(current)); + +out: + /* Notify the calling thread once all threads are done */ + if (atomic_dec_return(&ctx->num_unfinished) == 0) + complete_all(&ctx->all_finished); +} + +/* + * restrict_one_thread_callback - task_work callback for restricting a thread + * + * Calls restrict_one_thread with the struct landlock_shared_tsync_context. + */ +static void restrict_one_thread_callback(struct callback_head *work) +{ + struct tsync_work *ctx = container_of(work, struct tsync_work, work); + + restrict_one_thread(ctx->shared_ctx); +} + +/* + * struct tsync_works - a growable array of per-task contexts + * + * The zero-initialized struct represents the empty array. + */ +struct tsync_works { + struct tsync_work **works; + size_t size; + size_t capacity; +}; + +/* + * tsync_works_provide - provides a preallocated tsync_work for the given task + * + * This also stores a task pointer in the context and increments the reference + * count of the task. + * + * This function may fail in the case where we did not preallocate sufficient + * capacity. This can legitimately happen if new threads get started after we + * grew the capacity. + * + * Return: A pointer to the preallocated context struct with task filled in, or + * NULL if preallocated context structs ran out. + */ +static struct tsync_work *tsync_works_provide(struct tsync_works *s, + struct task_struct *task) +{ + struct tsync_work *ctx; + + if (s->size >= s->capacity) + return NULL; + + ctx = s->works[s->size]; + s->size++; + + ctx->task = get_task_struct(task); + return ctx; +} + +/** + * tsync_works_trim - Put the last tsync_work element + * + * @s: TSYNC works to trim. + * + * Put the last task and decrement the size of @s. + * + * This helper does not cancel a running task, but just reset the last element + * to zero. + */ +static void tsync_works_trim(struct tsync_works *s) +{ + struct tsync_work *ctx; + + if (WARN_ON_ONCE(s->size <= 0)) + return; + + ctx = s->works[s->size - 1]; + + /* + * For consistency, remove the task from ctx so that it does not look + * like we handed it a task_work. + */ + put_task_struct(ctx->task); + *ctx = (typeof(*ctx)){}; + + /* + * Cancel the tsync_works_provide() change to recycle the reserved + * memory for the next thread, if any. This also ensures that + * cancel_tsync_works() and tsync_works_release() do not see any NULL + * task pointers. + */ + s->size--; +} + +/* + * tsync_works_grow_by - preallocates space for n more contexts in s + * + * On a successful return, the subsequent n calls to tsync_works_provide() are + * guaranteed to succeed. (size + n <= capacity) + * + * Return: 0 if sufficient space for n more elements could be provided, -ENOMEM + * on allocation errors, -EOVERFLOW in case of integer overflow. + */ +static int tsync_works_grow_by(struct tsync_works *s, size_t n, gfp_t flags) +{ + size_t i; + size_t new_capacity; + struct tsync_work **works; + struct tsync_work *work; + + if (check_add_overflow(s->size, n, &new_capacity)) + return -EOVERFLOW; + + /* No need to reallocate if s already has sufficient capacity. */ + if (new_capacity <= s->capacity) + return 0; + + works = krealloc_array(s->works, new_capacity, sizeof(s->works[0]), + flags); + if (!works) + return -ENOMEM; + + s->works = works; + + for (i = s->capacity; i < new_capacity; i++) { + work = kzalloc_obj(*work, flags); + if (!work) { + /* + * Leave the object in a consistent state, + * but return an error. + */ + s->capacity = i; + return -ENOMEM; + } + s->works[i] = work; + } + s->capacity = new_capacity; + return 0; +} + +/* + * tsync_works_contains - checks for presence of task in s + */ +static bool tsync_works_contains_task(const struct tsync_works *s, + const struct task_struct *task) +{ + size_t i; + + for (i = 0; i < s->size; i++) + if (s->works[i]->task == task) + return true; + + return false; +} + +/* + * tsync_works_release - frees memory held by s and drops all task references + * + * This does not free s itself, only the data structures held by it. + */ +static void tsync_works_release(struct tsync_works *s) +{ + size_t i; + + for (i = 0; i < s->size; i++) { + if (WARN_ON_ONCE(!s->works[i]->task)) + continue; + + put_task_struct(s->works[i]->task); + } + + for (i = 0; i < s->capacity; i++) + kfree(s->works[i]); + + kfree(s->works); + s->works = NULL; + s->size = 0; + s->capacity = 0; +} + +/* + * count_additional_threads - counts the sibling threads that are not in works + */ +static size_t count_additional_threads(const struct tsync_works *works) +{ + const struct task_struct *caller, *thread; + size_t n = 0; + + caller = current; + + guard(rcu)(); + + for_each_thread(caller, thread) { + /* Skip current, since it is initiating the sync. */ + if (thread == caller) + continue; + + /* Skip exited threads. */ + if (thread->flags & PF_EXITING) + continue; + + /* Skip threads that we have already seen. */ + if (tsync_works_contains_task(works, thread)) + continue; + + n++; + } + return n; +} + +/* + * schedule_task_work - adds task_work for all eligible sibling threads + * which have not been scheduled yet + * + * For each added task_work, atomically increments shared_ctx->num_preparing and + * shared_ctx->num_unfinished. + * + * Return: True if at least one eligible sibling thread was found, false + * otherwise. + */ +static bool schedule_task_work(struct tsync_works *works, + struct tsync_shared_context *shared_ctx) +{ + int err; + const struct task_struct *caller; + struct task_struct *thread; + struct tsync_work *ctx; + bool found_more_threads = false; + + caller = current; + + guard(rcu)(); + + for_each_thread(caller, thread) { + /* Skip current, since it is initiating the sync. */ + if (thread == caller) + continue; + + /* Skip exited threads. */ + if (thread->flags & PF_EXITING) + continue; + + /* Skip threads that we already looked at. */ + if (tsync_works_contains_task(works, thread)) + continue; + + /* + * We found a sibling thread that is not doing its task_work + * yet, and which might spawn new threads before our task work + * runs, so we need at least one more round in the outer loop. + */ + found_more_threads = true; + + ctx = tsync_works_provide(works, thread); + if (!ctx) { + /* + * We ran out of preallocated contexts -- we need to + * try again with this thread at a later time! + * found_more_threads is already true at this point. + */ + break; + } + + ctx->shared_ctx = shared_ctx; + + atomic_inc(&shared_ctx->num_preparing); + atomic_inc(&shared_ctx->num_unfinished); + + init_task_work(&ctx->work, restrict_one_thread_callback); + err = task_work_add(thread, &ctx->work, TWA_SIGNAL); + if (unlikely(err)) { + /* + * task_work_add() only fails if the task is about to + * exit. We checked that earlier, but it can happen as + * a race. Resume without setting an error, as the + * task is probably gone in the next loop iteration. + */ + tsync_works_trim(works); + + atomic_dec(&shared_ctx->num_preparing); + atomic_dec(&shared_ctx->num_unfinished); + } + } + + return found_more_threads; +} + +/* + * cancel_tsync_works - cancel all task works where it is possible + * + * Task works can be canceled as long as they are still queued and have not + * started running. If they get canceled, we decrement + * shared_ctx->num_preparing and shared_ctx->num_unfished and mark the two + * completions if needed, as if the task was never scheduled. + */ +static void cancel_tsync_works(const struct tsync_works *works, + struct tsync_shared_context *shared_ctx) +{ + size_t i; + + for (i = 0; i < works->size; i++) { + if (WARN_ON_ONCE(!works->works[i]->task)) + continue; + + if (!task_work_cancel(works->works[i]->task, + &works->works[i]->work)) + continue; + + /* After dequeueing, act as if the task work had executed. */ + + if (atomic_dec_return(&shared_ctx->num_preparing) == 0) + complete_all(&shared_ctx->all_prepared); + + if (atomic_dec_return(&shared_ctx->num_unfinished) == 0) + complete_all(&shared_ctx->all_finished); + } +} + +/* + * restrict_sibling_threads - enables a Landlock policy for all sibling threads + */ +int landlock_restrict_sibling_threads(const struct cred *old_cred, + const struct cred *new_cred, + const u32 restrict_flags) +{ + int err; + struct tsync_shared_context shared_ctx; + struct tsync_works works = {}; + size_t newly_discovered_threads; + bool found_more_threads; + + atomic_set(&shared_ctx.preparation_error, 0); + init_completion(&shared_ctx.all_prepared); + init_completion(&shared_ctx.ready_to_commit); + atomic_set(&shared_ctx.num_unfinished, 1); + init_completion(&shared_ctx.all_finished); + shared_ctx.old_cred = old_cred; + shared_ctx.new_cred = new_cred; + shared_ctx.set_no_new_privs = + (restrict_flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) || + task_no_new_privs(current); + + /* + * Serialize concurrent TSYNC operations to prevent deadlocks when + * multiple threads call landlock_restrict_self() simultaneously. + * If the lock is already held, we gracefully yield by restarting the + * syscall. This allows the current thread to process pending + * task_works before retrying. + */ + if (!down_write_trylock(¤t->signal->exec_update_lock)) + return restart_syscall(); + + /* + * We schedule a pseudo-signal task_work for each of the calling task's + * sibling threads. In the task work, each thread: + * + * 1) runs prepare_creds() and writes back the error to + * shared_ctx.preparation_error, if needed. + * + * 2) signals that it's done with prepare_creds() to the calling task. + * (completion "all_prepared"). + * + * 3) waits for the completion "ready_to_commit". This is sent by the + * calling task after ensuring that all sibling threads have done + * with the "preparation" stage. + * + * After this barrier is reached, it's safe to read + * shared_ctx.preparation_error. + * + * 4) reads shared_ctx.preparation_error and then either does + * commit_creds() or abort_creds(). + * + * 5) signals that it's done altogether (barrier synchronization + * "all_finished") + * + * Unlike seccomp, which modifies sibling tasks directly, we do not + * need to acquire the cred_guard_mutex and sighand->siglock: + * + * - As in our case, all threads are themselves exchanging their own + * struct cred through the credentials API, no locks are needed for + * that. + * - Our for_each_thread() loops are protected by RCU. + * - We do not acquire a lock to keep the list of sibling threads + * stable between our for_each_thread loops. If the list of + * available sibling threads changes between these for_each_thread + * loops, we make up for that by continuing to look for threads until + * they are all discovered and have entered their task_work, where + * they are unable to spawn new threads. + */ + do { + /* In RCU read-lock, count the threads we need. */ + newly_discovered_threads = count_additional_threads(&works); + + if (newly_discovered_threads == 0) + break; /* done */ + + err = tsync_works_grow_by(&works, newly_discovered_threads, + GFP_KERNEL_ACCOUNT); + if (err) { + atomic_set(&shared_ctx.preparation_error, err); + break; + } + + /* + * The "all_prepared" barrier is used locally to the loop body, + * this use of for_each_thread(). We can reset it on each loop + * iteration because all previous loop iterations are done with + * it already. + * + * num_preparing is initialized to 1 so that the counter can + * not go to 0 and mark the completion as done before all task + * works are registered. We decrement it at the end of the + * loop body. + */ + atomic_set(&shared_ctx.num_preparing, 1); + reinit_completion(&shared_ctx.all_prepared); + + /* + * In RCU read-lock, schedule task work on newly discovered + * sibling tasks. + */ + found_more_threads = schedule_task_work(&works, &shared_ctx); + + /* + * Decrement num_preparing for current, to undo that we + * initialized it to 1 a few lines above. + */ + if (atomic_dec_return(&shared_ctx.num_preparing) > 0) { + if (wait_for_completion_interruptible( + &shared_ctx.all_prepared)) { + /* + * In case of interruption, we need to retry + * the system call. + */ + atomic_set(&shared_ctx.preparation_error, + -ERESTARTNOINTR); + + /* + * Opportunistic improvement: try to cancel task + * works for tasks that did not start running + * yet. We do not have a guarantee that it + * cancels any of the enqueued task works + * because task_work_run() might already have + * dequeued them. + */ + cancel_tsync_works(&works, &shared_ctx); + + /* + * Break the loop with error. The cleanup code + * after the loop unblocks the remaining + * task_works. + */ + break; + } + } + } while (found_more_threads && + !atomic_read(&shared_ctx.preparation_error)); + + /* + * We now have either (a) all sibling threads blocking and in "prepared" + * state in the task work, or (b) the preparation error is set. Ask all + * threads to commit (or abort). + */ + complete_all(&shared_ctx.ready_to_commit); + + /* + * Decrement num_unfinished for current, to undo that we initialized it + * to 1 at the beginning. + */ + if (atomic_dec_return(&shared_ctx.num_unfinished) > 0) + wait_for_completion(&shared_ctx.all_finished); + + tsync_works_release(&works); + up_write(¤t->signal->exec_update_lock); + return atomic_read(&shared_ctx.preparation_error); +} diff --git a/security/landlock/tsync.h b/security/landlock/tsync.h new file mode 100644 index 000000000..2ae4f938c --- /dev/null +++ b/security/landlock/tsync.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Landlock - Cross-thread ruleset enforcement + * + * Copyright © 2025 Google LLC + */ + +#ifndef _SECURITY_LANDLOCK_TSYNC_H +#define _SECURITY_LANDLOCK_TSYNC_H + +#include <linux/cred.h> +#include <linux/types.h> + +int landlock_restrict_sibling_threads(const struct cred *old_cred, + const struct cred *new_cred, + u32 restrict_flags); + +#endif /* _SECURITY_LANDLOCK_TSYNC_H */ |
