summaryrefslogtreecommitdiffstats
path: root/security/landlock
diff options
context:
space:
mode:
authorSang-Heon Jeon <ekffu200098@gmail.com>2026-08-24 01:18:14 +0900
committerJulia Lawall <Julia.Lawall@inria.fr>2026-08-23 22:12:11 +0200
commit2bd30b8dc75b823dd7bbef9d7bc68e1fd7ecee5c (patch)
treedcc4da1d99f17541c5590ac63c77da4bf372a4ab /security/landlock
downloadlinux-stable-2bd30b8dc75b823dd7bbef9d7bc68e1fd7ecee5c.tar.gz
linux-stable-2bd30b8dc75b823dd7bbef9d7bc68e1fd7ecee5c.zip
coccinelle: alloc_cast: drop removed allocatorsgrafted
- pci_alloc_consistent() and pci_zalloc_consistent() were removed by commit 7968778914e5 ("PCI: Remove the deprecated "pci-dma-compat.h" API") - kmem_alloc() was removed by commit f078d4ea8276 ("xfs: convert kmem_alloc() to kmalloc()") - kmem_zalloc() was removed by commit 10634530f7ba ("xfs: convert kmem_zalloc() to kzalloc()") - kmem_zone_alloc() and kmem_zone_zalloc() were removed by commit bae633a4a283 ("xfs: remove xfs_zone_{alloc,zalloc} helpers") So drop them from the rules. No functional change. Signed-off-by: Sang-Heon Jeon <ekffu200098@gmail.com> Signed-off-by: Julia Lawall <Julia.Lawall@inria.fr>
Diffstat (limited to 'security/landlock')
-rw-r--r--security/landlock/.kunitconfig6
-rw-r--r--security/landlock/Kconfig42
-rw-r--r--security/landlock/Makefile22
-rw-r--r--security/landlock/access.h151
-rw-r--r--security/landlock/audit.c237
-rw-r--r--security/landlock/audit.h43
-rw-r--r--security/landlock/common.h22
-rw-r--r--security/landlock/cred.c69
-rw-r--r--security/landlock/cred.h157
-rw-r--r--security/landlock/domain.c775
-rw-r--r--security/landlock/domain.h326
-rw-r--r--security/landlock/errata.h99
-rw-r--r--security/landlock/errata/abi-1.h47
-rw-r--r--security/landlock/errata/abi-4.h22
-rw-r--r--security/landlock/errata/abi-6.h29
-rw-r--r--security/landlock/fs.c2136
-rw-r--r--security/landlock/fs.h188
-rw-r--r--security/landlock/id.c295
-rw-r--r--security/landlock/id.h25
-rw-r--r--security/landlock/limits.h42
-rw-r--r--security/landlock/log.c587
-rw-r--r--security/landlock/log.h86
-rw-r--r--security/landlock/net.c413
-rw-r--r--security/landlock/net.h34
-rw-r--r--security/landlock/object.c67
-rw-r--r--security/landlock/object.h91
-rw-r--r--security/landlock/ruleset.c354
-rw-r--r--security/landlock/ruleset.h250
-rw-r--r--security/landlock/setup.c81
-rw-r--r--security/landlock/setup.h22
-rw-r--r--security/landlock/syscalls.c706
-rw-r--r--security/landlock/task.c509
-rw-r--r--security/landlock/task.h14
-rw-r--r--security/landlock/trace.c185
-rw-r--r--security/landlock/trace.h44
-rw-r--r--security/landlock/tsync.c639
-rw-r--r--security/landlock/tsync.h18
37 files changed, 8833 insertions, 0 deletions
diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig
new file mode 100644
index 000000000..f9423f01a
--- /dev/null
+++ b/security/landlock/.kunitconfig
@@ -0,0 +1,6 @@
+CONFIG_AUDIT=y
+CONFIG_KUNIT=y
+CONFIG_NET=y
+CONFIG_SECURITY=y
+CONFIG_SECURITY_LANDLOCK=y
+CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y
diff --git a/security/landlock/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..93c7104fd
--- /dev/null
+++ b/security/landlock/domain.c
@@ -0,0 +1,775 @@
+// 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();
+ lockdep_assert_held(&ruleset->lock);
+ if (WARN_ON_ONCE(!ruleset))
+ return ERR_PTR(-EINVAL);
+
+ 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..30aa6ce13
--- /dev/null
+++ b/security/landlock/fs.c
@@ -0,0 +1,2136 @@
+// 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). We do not need to call dget(old_parent) because
+ * we keep a reference to old_dentry.
+ */
+ old_parent = (old_dentry == mnt_dir.dentry) ? old_dentry :
+ old_dentry->d_parent;
+
+ /* 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)
+ 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))
+ return 0;
+
+ if (request1.access) {
+ request1.audit.u.path.dentry = old_parent;
+ landlock_log_denial(subject, &request1);
+ }
+ if (request2.access) {
+ request2.audit.u.path.dentry = new_dir->dentry;
+ landlock_log_denial(subject, &request2);
+ }
+
+ /*
+ * This prioritizes EACCES over EXDEV for all actions, including
+ * renames with RENAME_EXCHANGE.
+ */
+ 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..0d0770752
--- /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 */
+
+ /* Should already be checked in landlock_create_ruleset(). */
+ 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..2ea7aac8d
--- /dev/null
+++ b/security/landlock/trace.c
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - Tracepoint helpers
+ *
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#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;
+ }
+}
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(&current->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(&current->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 */