Linux Security Modules development
 help / color / mirror / Atom feed
* [PATCH v8 03/12] landlock: Replace union access_masks_all with helper functions
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, kernel test robot, linux-security-module,
	Tingmao Wang, Justin Suess, Samasth Norway Ananda,
	Matthieu Buffet, Mikhail Ivanov, konstantin.meskhidze,
	Demi Marie Obenour, Alyssa Ross, Jann Horn, Tahera Fahimi,
	Sebastian Andrzej Siewior, Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

* Stop using a union for access_masks_all.
* Expose helper functions for intersection checks and union operations.

The memory layout of bitfields is only loosely defined by the C
standard, so our static assertion that expects a fixed size was
brittle, and it broke on some compilers when we attempted to add a
17th file system access right.

Reported-by: kernel test robot <lkp@intel.com>
Closes: https://lore.kernel.org/oe-kbuild-all/202603261438.jBx2DGNe-lkp@intel.com/
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 security/landlock/access.h  | 21 ++++++++++++++-------
 security/landlock/cred.h    | 10 ++--------
 security/landlock/ruleset.h | 13 ++++---------
 3 files changed, 20 insertions(+), 24 deletions(-)

diff --git a/security/landlock/access.h b/security/landlock/access.h
index 42c95747d7bd..277b6ed7f7bb 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -52,14 +52,21 @@ struct access_masks {
 	access_mask_t scope : LANDLOCK_NUM_SCOPE;
 };
 
-union access_masks_all {
-	struct access_masks masks;
-	u32 all;
-};
+/* Checks whether two access masks have any common bit set. */
+static inline bool access_masks_intersect(const struct access_masks a,
+					  const struct access_masks b)
+{
+	return (a.fs & b.fs) || (a.net & b.net) || (a.scope & b.scope);
+}
 
-/* Makes sure all fields are covered. */
-static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
-	      sizeof(typeof_member(union access_masks_all, all)));
+/* ORs the bits of @src into @dst. */
+static inline void access_masks_merge(struct access_masks *dst,
+				      const struct access_masks src)
+{
+	dst->fs |= src.fs;
+	dst->net |= src.net;
+	dst->scope |= src.scope;
+}
 
 /**
  * struct layer_access_masks - A boolean matrix of layers and access rights
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index f287c56b5fd4..207a6db1c086 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -123,9 +123,6 @@ landlock_get_applicable_subject(const struct cred *const cred,
 				const struct access_masks masks,
 				size_t *const handle_layer)
 {
-	const union access_masks_all masks_all = {
-		.masks = masks,
-	};
 	const struct landlock_ruleset *domain;
 	ssize_t layer_level;
 
@@ -138,11 +135,8 @@ landlock_get_applicable_subject(const struct cred *const cred,
 
 	for (layer_level = domain->num_layers - 1; layer_level >= 0;
 	     layer_level--) {
-		union access_masks_all layer = {
-			.masks = domain->access_masks[layer_level],
-		};
-
-		if (layer.all & masks_all.all) {
+		if (access_masks_intersect(domain->access_masks[layer_level],
+					   masks)) {
 			if (handle_layer)
 				*handle_layer = layer_level;
 
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 889f4b30301a..9f8b33815c2c 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -229,18 +229,13 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
 static inline struct access_masks
 landlock_union_access_masks(const struct landlock_ruleset *const domain)
 {
-	union access_masks_all matches = {};
+	struct access_masks matches = {};
 	size_t layer_level;
 
-	for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
-		union access_masks_all layer = {
-			.masks = domain->access_masks[layer_level],
-		};
+	for (layer_level = 0; layer_level < domain->num_layers; layer_level++)
+		access_masks_merge(&matches, domain->access_masks[layer_level]);
 
-		matches.all |= layer.all;
-	}
-
-	return matches.masks;
+	return matches;
 }
 
 static inline void
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 04/12] landlock: Control pathname UNIX domain socket resolution by path
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, Tingmao Wang, Justin Suess,
	Sebastian Andrzej Siewior, Kuniyuki Iwashima, Jann Horn,
	linux-security-module, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Tahera Fahimi, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

* Add a new access right LANDLOCK_ACCESS_FS_RESOLVE_UNIX, which
  controls the lookup operations for named UNIX domain sockets.  The
  resolution happens during connect() and sendmsg() (depending on
  socket type).
* Change access_mask_t from u16 to u32 (see below)
* Hook into the path lookup in unix_find_bsd() in af_unix.c, using a
  LSM hook.  Make policy decisions based on the new access rights
* Increment the Landlock ABI version.
* Minor test adaptations to keep the tests working.
* Document the design rationale for scoped access rights,
  and cross-reference it from the header documentation.

With this access right, access is granted if either of the following
conditions is met:

* The target socket's filesystem path was allow-listed using a
  LANDLOCK_RULE_PATH_BENEATH rule, *or*:
* The target socket was created in the same Landlock domain in which
  LANDLOCK_ACCESS_FS_RESOLVE_UNIX was restricted.

In case of a denial, connect() and sendmsg() return EACCES, which is
the same error as it is returned if the user does not have the write
bit in the traditional UNIX file system permissions of that file.

The access_mask_t type grows from u16 to u32 to make space for the new
access right.  This also doubles the size of struct layer_access_masks
from 32 byte to 64 byte.

Document the (possible future) interaction between scoped flags and
other access rights in struct landlock_ruleset_attr, and summarize the
rationale, as discussed in code review leading up to [2].

This feature was created with substantial discussion and input from
Justin Suess, Tingmao Wang and Mickaël Salaün.

Cc: Tingmao Wang <m@maowtm.org>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Mickaël Salaün <mic@digikod.net>
Cc: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
Cc: Kuniyuki Iwashima <kuniyu@google.com>
Suggested-by: Jann Horn <jannh@google.com>
Link[1]: https://github.com/landlock-lsm/linux/issues/36
Link[2]: https://lore.kernel.org/all/20260205.8531e4005118@gnoack.org/
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 Documentation/security/landlock.rst          |  42 +++++-
 Documentation/userspace-api/landlock.rst     |   2 +-
 include/uapi/linux/landlock.h                |  21 +++
 security/landlock/access.h                   |   2 +-
 security/landlock/audit.c                    |   1 +
 security/landlock/fs.c                       | 130 ++++++++++++++++++-
 security/landlock/limits.h                   |   2 +-
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 tools/testing/selftests/landlock/fs_test.c   |   5 +-
 10 files changed, 200 insertions(+), 9 deletions(-)

diff --git a/Documentation/security/landlock.rst b/Documentation/security/landlock.rst
index 3e4d4d04cfae..c3f8f43073a7 100644
--- a/Documentation/security/landlock.rst
+++ b/Documentation/security/landlock.rst
@@ -7,7 +7,7 @@ Landlock LSM: kernel documentation
 ==================================
 
 :Author: Mickaël Salaün
-:Date: September 2025
+:Date: March 2026
 
 Landlock's goal is to create scoped access-control (i.e. sandboxing).  To
 harden a whole system, this feature should be available to any process,
@@ -89,6 +89,46 @@ this is required to keep access controls consistent over the whole system, and
 this avoids unattended bypasses through file descriptor passing (i.e. confused
 deputy attack).
 
+.. _scoped-flags-interaction:
+
+Interaction between scoped flags and other access rights
+--------------------------------------------------------
+
+The ``scoped`` flags in ``struct landlock_ruleset_attr`` restrict the
+use of *outgoing* IPC from the created Landlock domain, while they
+permit reaching out to IPC endpoints *within* the created Landlock
+domain.
+
+In the future, scoped flags *may* interact with other access rights,
+e.g. so that abstract UNIX sockets can be allow-listed by name, or so
+that signals can be allow-listed by signal number or target process.
+
+When introducing ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``, we defined it to
+implicitly have the same scoping semantics as a
+``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` flag would have: connecting to
+UNIX sockets within the same domain (where
+``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` is used) is unconditionally
+allowed.
+
+The reasoning is:
+
+* Like other IPC mechanisms, connecting to named UNIX sockets in the
+  same domain should be expected and harmless.  (If needed, users can
+  further refine their Landlock policies with nested domains or by
+  restricting ``LANDLOCK_ACCESS_FS_MAKE_SOCK``.)
+* We reserve the option to still introduce
+  ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the future.  (This would
+  be useful if we wanted to have a Landlock rule to permit IPC access
+  to other Landlock domains.)
+* But we can postpone the point in time when users have to deal with
+  two interacting flags visible in the userspace API.  (In particular,
+  it is possible that it won't be needed in practice, in which case we
+  can avoid the second flag altogether.)
+* If we *do* introduce ``LANDLOCK_SCOPE_PATHNAME_UNIX_SOCKET`` in the
+  future, setting this scoped flag in a ruleset does *not reduce* the
+  restrictions, because access within the same scope is already
+  allowed based on ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX``.
+
 Tests
 =====
 
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 13134bccdd39..1490f879f621 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -8,7 +8,7 @@ Landlock: unprivileged access control
 =====================================
 
 :Author: Mickaël Salaün
-:Date: January 2026
+:Date: March 2026
 
 The goal of Landlock is to enable restriction of ambient rights (e.g. global
 filesystem or network access) for a set of processes.  Because Landlock
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index f88fa1f68b77..3157d257555b 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -248,6 +248,26 @@ struct landlock_net_port_attr {
  *
  *   This access right is available since the fifth version of the Landlock
  *   ABI.
+ * - %LANDLOCK_ACCESS_FS_RESOLVE_UNIX: Look up pathname UNIX domain sockets
+ *   (:manpage:`unix(7)`).  On UNIX domain sockets, this restricts both calls to
+ *   :manpage:`connect(2)` as well as calls to :manpage:`sendmsg(2)` with an
+ *   explicit recipient address.
+ *
+ *   This access right only applies to connections to UNIX server sockets which
+ *   were created outside of the newly created Landlock domain (e.g. from within
+ *   a parent domain or from an unrestricted process).  Newly created UNIX
+ *   servers within the same Landlock domain continue to be accessible.  In this
+ *   regard, %LANDLOCK_ACCESS_FS_RESOLVE_UNIX has the same semantics as the
+ *   ``LANDLOCK_SCOPE_*`` flags.
+ *
+ *   If a resolve attempt is denied, the operation returns an ``EACCES`` error,
+ *   in line with other filesystem access rights (but different to denials for
+ *   abstract UNIX domain sockets).
+ *
+ *   This access right is available since the ninth version of the Landlock ABI.
+ *
+ *   The rationale for this design is described in
+ *   :ref:`Documentation/security/landlock.rst <scoped-flags-interaction>`.
  *
  * Whether an opened file can be truncated with :manpage:`ftruncate(2)` or used
  * with `ioctl(2)` is determined during :manpage:`open(2)`, in the same way as
@@ -333,6 +353,7 @@ struct landlock_net_port_attr {
 #define LANDLOCK_ACCESS_FS_REFER			(1ULL << 13)
 #define LANDLOCK_ACCESS_FS_TRUNCATE			(1ULL << 14)
 #define LANDLOCK_ACCESS_FS_IOCTL_DEV			(1ULL << 15)
+#define LANDLOCK_ACCESS_FS_RESOLVE_UNIX			(1ULL << 16)
 /* clang-format on */
 
 /**
diff --git a/security/landlock/access.h b/security/landlock/access.h
index 277b6ed7f7bb..99c709f7979e 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -34,7 +34,7 @@
 	LANDLOCK_ACCESS_FS_IOCTL_DEV)
 /* clang-format on */
 
-typedef u16 access_mask_t;
+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);
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 60ff217ab95b..8d0edf94037d 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -37,6 +37,7 @@ static const char *const fs_access_strings[] = {
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
 	[BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
+	[BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
 };
 
 static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 97065d51685a..fcf69b3d734d 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -27,6 +27,7 @@
 #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>
@@ -36,6 +37,7 @@
 #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>
 
@@ -314,7 +316,8 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 /* clang-format on */
 
 /*
@@ -1557,6 +1560,130 @@ 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_ruleset *const client,
+				 const struct landlock_ruleset *const server,
+				 struct layer_access_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 a signed integer with greater capacity
+	 * than client->num_layers to ensure the following loop stops.
+	 */
+	BUILD_BUG_ON(sizeof(client_layer) > sizeof(client->num_layers));
+
+	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->access[client_layer] & access &&
+		    client_walker == server_walker)
+			masks->access[client_layer] &= ~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_ruleset *dom_other;
+	const struct landlock_cred_security *subject;
+	struct layer_access_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 */
 
 /**
@@ -1834,6 +1961,7 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = {
 	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),
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index eb584f47288d..b454ad73b15e 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -19,7 +19,7 @@
 #define LANDLOCK_MAX_NUM_LAYERS		16
 #define LANDLOCK_MAX_NUM_RULES		U32_MAX
 
-#define LANDLOCK_LAST_ACCESS_FS		LANDLOCK_ACCESS_FS_IOCTL_DEV
+#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)
 
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 3b33839b80c7..a6e23657f3ce 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -166,7 +166,7 @@ static const struct file_operations ruleset_fops = {
  * If the change involves a fix that requires userspace awareness, also update
  * the errata documentation in Documentation/userspace-api/landlock.rst .
  */
-const int landlock_abi_version = 8;
+const int landlock_abi_version = 9;
 
 /**
  * sys_landlock_create_ruleset - Create a new ruleset
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index 0fea236ef4bd..30d37234086c 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,7 +76,7 @@ TEST(abi_version)
 	const struct landlock_ruleset_attr ruleset_attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
 	};
-	ASSERT_EQ(8, landlock_create_ruleset(NULL, 0,
+	ASSERT_EQ(9, landlock_create_ruleset(NULL, 0,
 					     LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 968a91c927a4..b318627e7561 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -575,9 +575,10 @@ TEST_F_FORK(layout1, inval)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
-#define ACCESS_LAST LANDLOCK_ACCESS_FS_IOCTL_DEV
+#define ACCESS_LAST LANDLOCK_ACCESS_FS_RESOLVE_UNIX
 
 #define ACCESS_ALL ( \
 	ACCESS_FILE | \
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 05/12] landlock: Clarify BUILD_BUG_ON check in scoping logic
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, linux-security-module, Tingmao Wang,
	Justin Suess, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

The BUILD_BUG_ON check in domain_is_scoped() and
unmask_scoped_access() should check that the loop that counts down
client_layer finishes.  We therefore check that the numbers
LANDLOCK_MAX_NUM_LAYERS-1 and -1 are both representable by that
integer.  If they are representable, the numbers in between are
representable too, and the loop finishes.

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 security/landlock/fs.c   | 9 ++++++---
 security/landlock/task.c | 9 ++++++---
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index fcf69b3d734d..c1ecfe239032 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -1595,10 +1595,13 @@ static void unmask_scoped_access(const struct landlock_ruleset *const client,
 		return;
 
 	/*
-	 * client_layer must be a signed integer with greater capacity
-	 * than client->num_layers to ensure the following loop stops.
+	 * 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(sizeof(client_layer) > sizeof(client->num_layers));
+	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;
diff --git a/security/landlock/task.c b/security/landlock/task.c
index f2dbdebf2770..6d46042132ce 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -191,10 +191,13 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
 	client_layer = client->num_layers - 1;
 	client_walker = client->hierarchy;
 	/*
-	 * client_layer must be a signed integer with greater capacity
-	 * than client->num_layers to ensure the following loop stops.
+	 * 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(sizeof(client_layer) > sizeof(client->num_layers));
+	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;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 06/12] samples/landlock: Add support for named UNIX domain socket restrictions
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, Justin Suess, linux-security-module,
	Tingmao Wang, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

The access right for UNIX domain socket lookups is grouped with the
read-write rights in the sample tool.  Rationale: In the general case,
any operations are possible through a UNIX domain socket, including
data-mutating operations.

Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 samples/landlock/sandboxer.c | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 9f21088c0855..66e56ae275c6 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -111,7 +111,8 @@ static int parse_path(char *env_path, const char ***const path_list)
 	LANDLOCK_ACCESS_FS_WRITE_FILE | \
 	LANDLOCK_ACCESS_FS_READ_FILE | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
 /* clang-format on */
 
@@ -295,11 +296,12 @@ static bool check_ruleset_scope(const char *const env_var,
 	LANDLOCK_ACCESS_FS_MAKE_SYM | \
 	LANDLOCK_ACCESS_FS_REFER | \
 	LANDLOCK_ACCESS_FS_TRUNCATE | \
-	LANDLOCK_ACCESS_FS_IOCTL_DEV)
+	LANDLOCK_ACCESS_FS_IOCTL_DEV | \
+	LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
 
 /* clang-format on */
 
-#define LANDLOCK_ABI_LAST 8
+#define LANDLOCK_ABI_LAST 9
 
 #define XSTR(s) #s
 #define STR(s) XSTR(s)
@@ -438,6 +440,10 @@ int main(const int argc, char *const argv[], char *const *const envp)
 			~LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
 		__attribute__((fallthrough));
 	case 7:
+	case 8:
+		/* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
+		ruleset_attr.handled_access_fs &=
+			~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
 		/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
 		fprintf(stderr,
 			"Hint: You should update the running kernel "
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 07/12] selftests/landlock: Replace access_fs_16 with ACCESS_ALL in fs_test
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, linux-security-module, Tingmao Wang,
	Justin Suess, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

The access_fs_16 variable was originally intended to stay frozen at 16
access rights so that audit tests would not need updating when new
access rights are added.  Now that we have 17 access rights, the name
is confusing.

Replace all uses of access_fs_16 with ACCESS_ALL and delete the
variable.

Suggested-by: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 tools/testing/selftests/landlock/fs_test.c | 54 +++++++---------------
 1 file changed, 17 insertions(+), 37 deletions(-)

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index b318627e7561..9fdd3b8f7b11 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -7161,26 +7161,6 @@ TEST_F(audit_layout1, execute_make)
  * only the blocked ones are logged.
  */
 
-/* clang-format off */
-static const __u64 access_fs_16 =
-	LANDLOCK_ACCESS_FS_EXECUTE |
-	LANDLOCK_ACCESS_FS_WRITE_FILE |
-	LANDLOCK_ACCESS_FS_READ_FILE |
-	LANDLOCK_ACCESS_FS_READ_DIR |
-	LANDLOCK_ACCESS_FS_REMOVE_DIR |
-	LANDLOCK_ACCESS_FS_REMOVE_FILE |
-	LANDLOCK_ACCESS_FS_MAKE_CHAR |
-	LANDLOCK_ACCESS_FS_MAKE_DIR |
-	LANDLOCK_ACCESS_FS_MAKE_REG |
-	LANDLOCK_ACCESS_FS_MAKE_SOCK |
-	LANDLOCK_ACCESS_FS_MAKE_FIFO |
-	LANDLOCK_ACCESS_FS_MAKE_BLOCK |
-	LANDLOCK_ACCESS_FS_MAKE_SYM |
-	LANDLOCK_ACCESS_FS_REFER |
-	LANDLOCK_ACCESS_FS_TRUNCATE |
-	LANDLOCK_ACCESS_FS_IOCTL_DEV;
-/* clang-format on */
-
 TEST_F(audit_layout1, execute_read)
 {
 	struct audit_records records;
@@ -7190,7 +7170,7 @@ TEST_F(audit_layout1, execute_read)
 	test_check_exec(_metadata, 0, file1_s1d1);
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	/*
@@ -7214,7 +7194,7 @@ TEST_F(audit_layout1, write_file)
 	struct audit_records records;
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY));
@@ -7231,7 +7211,7 @@ TEST_F(audit_layout1, read_file)
 	struct audit_records records;
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -7248,7 +7228,7 @@ TEST_F(audit_layout1, read_dir)
 	struct audit_records records;
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(EACCES, test_open(dir_s1d1, O_DIRECTORY));
@@ -7268,7 +7248,7 @@ TEST_F(audit_layout1, remove_dir)
 	EXPECT_EQ(0, unlink(file2_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, rmdir(dir_s1d3));
@@ -7291,7 +7271,7 @@ TEST_F(audit_layout1, remove_file)
 	struct audit_records records;
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, unlink(file1_s1d3));
@@ -7311,7 +7291,7 @@ TEST_F(audit_layout1, make_char)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, 0));
@@ -7331,7 +7311,7 @@ TEST_F(audit_layout1, make_dir)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, mkdir(file1_s1d3, 0755));
@@ -7351,7 +7331,7 @@ TEST_F(audit_layout1, make_reg)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFREG | 0644, 0));
@@ -7371,7 +7351,7 @@ TEST_F(audit_layout1, make_sock)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFSOCK | 0644, 0));
@@ -7391,7 +7371,7 @@ TEST_F(audit_layout1, make_fifo)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFIFO | 0644, 0));
@@ -7411,7 +7391,7 @@ TEST_F(audit_layout1, make_block)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFBLK | 0644, 0));
@@ -7431,7 +7411,7 @@ TEST_F(audit_layout1, make_sym)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, symlink("target", file1_s1d3));
@@ -7501,7 +7481,7 @@ TEST_F(audit_layout1, refer_rename)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(EACCES, test_rename(file1_s1d2, file1_s2d3));
@@ -7523,7 +7503,7 @@ TEST_F(audit_layout1, refer_exchange)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	/*
@@ -7586,7 +7566,7 @@ TEST_F(audit_layout1, truncate)
 	struct audit_records records;
 
 	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = access_fs_16,
+					      .handled_access_fs = ACCESS_ALL,
 				      });
 
 	EXPECT_EQ(-1, truncate(file1_s1d3, 0));
@@ -7607,7 +7587,7 @@ TEST_F(audit_layout1, ioctl_dev)
 	drop_access_rights(_metadata,
 			   &(struct landlock_ruleset_attr){
 				   .handled_access_fs =
-					   access_fs_16 &
+					   ACCESS_ALL &
 					   ~LANDLOCK_ACCESS_FS_READ_FILE,
 			   });
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 08/12] selftests/landlock: Test LANDLOCK_ACCESS_FS_RESOLVE_UNIX
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, Justin Suess, Tingmao Wang,
	linux-security-module, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

* Extract common helpers from an existing IOCTL test that
  also uses pathname unix(7) sockets.
* These tests use the common scoped domains fixture which is also used
  in other Landlock scoping tests and which was used in Tingmao Wang's
  earlier patch set in [1].

These tests exercise the cross product of the following scenarios:

* Stream connect(), Datagram connect(), Datagram sendmsg() and
  Seqpacket connect().
* Child-to-parent and parent-to-child communication
* The Landlock policy configuration as listed in the scoped_domains
  fixture.
  * In the default variant, Landlock domains are only placed where
    prescribed in the fixture.
  * In the "ALL_DOMAINS" variant, Landlock domains are also placed in
    the places where the fixture says to omit them, but with a
    LANDLOCK_RULE_PATH_BENEATH that allows connection.

Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Tingmao Wang <m@maowtm.org>
Cc: Mickaël Salaün <mic@digikod.net>
Link[1]: https://lore.kernel.org/all/53b9883648225d5a08e82d2636ab0b4fda003bc9.1767115163.git.m@maowtm.org/
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 tools/testing/selftests/landlock/fs_test.c | 390 ++++++++++++++++++++-
 1 file changed, 374 insertions(+), 16 deletions(-)

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 9fdd3b8f7b11..f8cfd31335e1 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -4358,30 +4358,66 @@ TEST_F_FORK(layout1, named_pipe_ioctl)
 	ASSERT_EQ(child_pid, waitpid(child_pid, NULL, 0));
 }
 
+/*
+ * set_up_named_unix_server - Create a pathname unix socket
+ *
+ * If the socket type is not SOCK_DGRAM, also invoke listen(2).
+ *
+ * Return: The listening FD - it is the caller responsibility to close it.
+ */
+static int set_up_named_unix_server(struct __test_metadata *const _metadata,
+				    int type, const char *const path)
+{
+	int fd;
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+	};
+
+	fd = socket(AF_UNIX, type, 0);
+	ASSERT_LE(0, fd);
+
+	ASSERT_LT(strlen(path), sizeof(addr.sun_path));
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
+
+	ASSERT_EQ(0, bind(fd, (struct sockaddr *)&addr, sizeof(addr)));
+
+	if (type != SOCK_DGRAM)
+		ASSERT_EQ(0, listen(fd, 10 /* qlen */));
+	return fd;
+}
+
+/*
+ * test_connect_named_unix - connect to the given named UNIX socket
+ *
+ * Return: The errno from connect(), or 0
+ */
+static int test_connect_named_unix(struct __test_metadata *const _metadata,
+				   int fd, const char *const path)
+{
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+	};
+
+	ASSERT_LT(strlen(path), sizeof(addr.sun_path));
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
+
+	if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
+		return errno;
+	return 0;
+}
+
 /* For named UNIX domain sockets, no IOCTL restrictions apply. */
 TEST_F_FORK(layout1, named_unix_domain_socket_ioctl)
 {
 	const char *const path = file1_s1d1;
 	int srv_fd, cli_fd, ruleset_fd;
-	struct sockaddr_un srv_un = {
-		.sun_family = AF_UNIX,
-	};
-	struct sockaddr_un cli_un = {
-		.sun_family = AF_UNIX,
-	};
 	const struct landlock_ruleset_attr attr = {
 		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
 	};
 
 	/* Sets up a server */
 	ASSERT_EQ(0, unlink(path));
-	srv_fd = socket(AF_UNIX, SOCK_STREAM, 0);
-	ASSERT_LE(0, srv_fd);
-
-	strncpy(srv_un.sun_path, path, sizeof(srv_un.sun_path));
-	ASSERT_EQ(0, bind(srv_fd, (struct sockaddr *)&srv_un, sizeof(srv_un)));
-
-	ASSERT_EQ(0, listen(srv_fd, 10 /* qlen */));
+	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path);
 
 	/* Enables Landlock. */
 	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
@@ -4393,9 +4429,7 @@ TEST_F_FORK(layout1, named_unix_domain_socket_ioctl)
 	cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
 	ASSERT_LE(0, cli_fd);
 
-	strncpy(cli_un.sun_path, path, sizeof(cli_un.sun_path));
-	ASSERT_EQ(0,
-		  connect(cli_fd, (struct sockaddr *)&cli_un, sizeof(cli_un)));
+	ASSERT_EQ(0, test_connect_named_unix(_metadata, cli_fd, path));
 
 	/* FIONREAD and other IOCTLs should not be forbidden. */
 	EXPECT_EQ(0, test_fionread_ioctl(cli_fd));
@@ -4570,6 +4604,330 @@ TEST_F_FORK(ioctl, handle_file_access_file)
 	ASSERT_EQ(0, close(file_fd));
 }
 
+/*
+ * test_sendto_named_unix - sendto to the given named UNIX socket
+ *
+ * sendto() is equivalent to sendmsg() in this respect.
+ *
+ * Return: The errno from sendto(), or 0
+ */
+static int test_sendto_named_unix(struct __test_metadata *const _metadata,
+				  int fd, const char *const path)
+{
+	static const char buf[] = "dummy";
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+	};
+
+	ASSERT_LT(strlen(path), sizeof(addr.sun_path));
+	strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);
+
+	if (sendto(fd, buf, sizeof(buf), 0, (struct sockaddr *)&addr,
+		   sizeof(addr)) == -1)
+		return errno;
+	return 0;
+}
+
+/* clang-format off */
+FIXTURE(scoped_domains) {};
+/* clang-format on */
+
+#include "scoped_base_variants.h"
+
+FIXTURE_SETUP(scoped_domains)
+{
+	drop_caps(_metadata);
+};
+
+FIXTURE_TEARDOWN(scoped_domains)
+{
+}
+
+static void enforce_fs_resolve_unix(struct __test_metadata *const _metadata,
+				    const struct rule rules[])
+{
+	if (rules) {
+		int fd = create_ruleset(_metadata,
+					LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
+		enforce_ruleset(_metadata, fd);
+		EXPECT_EQ(0, close(fd));
+	} else {
+		struct landlock_ruleset_attr attr = {
+			.handled_access_fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+		};
+		drop_access_rights(_metadata, &attr);
+	}
+}
+
+/*
+ * Flags for test_connect_to_parent and test_connect_to_child:
+ *
+ * USE_SENDTO: Use sendto() instead of connect() (for SOCK_DGRAM only)
+ * ENFORCE_ALL: Enforce a Landlock domain even when the variant says
+ *   we shouldn't.  We enforce a domain where the path is allow-listed,
+ *   and expect the behavior to be the same as if none was used.
+ */
+#define USE_SENDTO (1 << 0)
+#define ENFORCE_ALL (1 << 1)
+
+static void test_connect_to_parent(struct __test_metadata *const _metadata,
+				   const FIXTURE_VARIANT(scoped_domains) *
+					   variant,
+				   int sock_type, int flags)
+{
+	const char *const path = "sock";
+	const struct rule rules[] = {
+		{
+			.path = ".",
+			.access = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+		},
+		{},
+	};
+	int cli_fd, srv_fd, res, status;
+	pid_t child_pid;
+	int readiness_pipe[2];
+	char buf[1];
+
+	if (variant->domain_both)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	unlink(path);
+	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
+
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+
+	if (child_pid == 0) {
+		if (variant->domain_child)
+			enforce_fs_resolve_unix(_metadata, NULL);
+		else if (flags & ENFORCE_ALL)
+			enforce_fs_resolve_unix(_metadata, rules);
+
+		/* Wait for server to be available. */
+		EXPECT_EQ(0, close(readiness_pipe[1]));
+		EXPECT_EQ(1, read(readiness_pipe[0], &buf, 1));
+		EXPECT_EQ(0, close(readiness_pipe[0]));
+
+		/* Talk to server. */
+		cli_fd = socket(AF_UNIX, sock_type, 0);
+		ASSERT_LE(0, cli_fd);
+
+		if (flags & USE_SENDTO)
+			res = test_sendto_named_unix(_metadata, cli_fd, path);
+		else
+			res = test_connect_named_unix(_metadata, cli_fd, path);
+
+		EXPECT_EQ(variant->domain_child ? EACCES : 0, res);
+
+		/* Clean up. */
+		EXPECT_EQ(0, close(cli_fd));
+
+		_exit(_metadata->exit_code);
+		return;
+	}
+
+	if (variant->domain_parent)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
+
+	/* Tell the child that it can connect. */
+	EXPECT_EQ(0, close(readiness_pipe[0]));
+	EXPECT_EQ(sizeof(buf), write(readiness_pipe[1], buf, sizeof(buf)));
+	EXPECT_EQ(0, close(readiness_pipe[1]));
+
+	/* Wait for child. */
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	EXPECT_EQ(1, WIFEXITED(status));
+	EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+	/* Clean up. */
+	EXPECT_EQ(0, close(srv_fd));
+	EXPECT_EQ(0, unlink(path));
+}
+
+static void test_connect_to_child(struct __test_metadata *const _metadata,
+				  const FIXTURE_VARIANT(scoped_domains) *
+					  variant,
+				  int sock_type, int flags)
+{
+	const char *const path = "sock";
+	const struct rule rules[] = {
+		{
+			.path = ".",
+			.access = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+		},
+		{},
+	};
+	int readiness_pipe[2];
+	int shutdown_pipe[2];
+	int cli_fd, srv_fd, res, status;
+	pid_t child_pid;
+	char buf[1];
+
+	if (variant->domain_both)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	unlink(path);
+	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
+	ASSERT_EQ(0, pipe2(shutdown_pipe, O_CLOEXEC));
+
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+
+	if (child_pid == 0) {
+		if (variant->domain_child)
+			enforce_fs_resolve_unix(_metadata, NULL);
+		else if (flags & ENFORCE_ALL)
+			enforce_fs_resolve_unix(_metadata, rules);
+
+		srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
+
+		/* Tell the parent that it can connect. */
+		EXPECT_EQ(0, close(readiness_pipe[0]));
+		EXPECT_EQ(sizeof(buf),
+			  write(readiness_pipe[1], buf, sizeof(buf)));
+		EXPECT_EQ(0, close(readiness_pipe[1]));
+
+		/* Wait until it is time to shut down. */
+		EXPECT_EQ(0, close(shutdown_pipe[1]));
+		EXPECT_EQ(1, read(shutdown_pipe[0], &buf, 1));
+		EXPECT_EQ(0, close(shutdown_pipe[0]));
+
+		/* Cleanup */
+		EXPECT_EQ(0, close(srv_fd));
+		EXPECT_EQ(0, unlink(path));
+
+		_exit(_metadata->exit_code);
+		return;
+	}
+
+	if (variant->domain_parent)
+		enforce_fs_resolve_unix(_metadata, NULL);
+	else if (flags & ENFORCE_ALL)
+		enforce_fs_resolve_unix(_metadata, rules);
+
+	/* Wait for server to be available. */
+	EXPECT_EQ(0, close(readiness_pipe[1]));
+	EXPECT_EQ(1, read(readiness_pipe[0], &buf, 1));
+	EXPECT_EQ(0, close(readiness_pipe[0]));
+
+	/* Talk to server. */
+	cli_fd = socket(AF_UNIX, sock_type, 0);
+	ASSERT_LE(0, cli_fd);
+
+	if (flags & USE_SENDTO)
+		res = test_sendto_named_unix(_metadata, cli_fd, path);
+	else
+		res = test_connect_named_unix(_metadata, cli_fd, path);
+
+	EXPECT_EQ(variant->domain_parent ? EACCES : 0, res);
+
+	/* Clean up. */
+	EXPECT_EQ(0, close(cli_fd));
+
+	/* Tell the server to shut down. */
+	EXPECT_EQ(0, close(shutdown_pipe[0]));
+	EXPECT_EQ(sizeof(buf), write(shutdown_pipe[1], buf, sizeof(buf)));
+	EXPECT_EQ(0, close(shutdown_pipe[1]));
+
+	/* Wait for child. */
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	EXPECT_EQ(1, WIFEXITED(status));
+	EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_STREAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM, USE_SENDTO);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_parent)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_SEQPACKET, 0);
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_STREAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_DGRAM,
+			       USE_SENDTO | ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_parent_full)
+{
+	test_connect_to_parent(_metadata, variant, SOCK_SEQPACKET, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_STREAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM, 0);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM, USE_SENDTO);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_child)
+{
+	test_connect_to_child(_metadata, variant, SOCK_SEQPACKET, 0);
+}
+
+TEST_F(scoped_domains, unix_stream_connect_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_STREAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_connect_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM, ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_dgram_sendmsg_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_DGRAM,
+			      USE_SENDTO | ENFORCE_ALL);
+}
+
+TEST_F(scoped_domains, unix_seqpacket_connect_to_child_full)
+{
+	test_connect_to_child(_metadata, variant, SOCK_SEQPACKET, ENFORCE_ALL);
+}
+
+#undef USE_SENDTO
+#undef ENFORCE_ALL
+
 /* clang-format off */
 FIXTURE(layout1_bind) {};
 /* clang-format on */
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 09/12] selftests/landlock: Audit test for LANDLOCK_ACCESS_FS_RESOLVE_UNIX
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, linux-security-module, Tingmao Wang,
	Justin Suess, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

Add an audit test to check that Landlock denials from
LANDLOCK_ACCESS_FS_RESOLVE_UNIX result in audit logs in the expected
format.  (There is one audit test for each filesystem access right, so
we should add one for LANDLOCK_ACCESS_FS_RESOLVE_UNIX as well.)

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 tools/testing/selftests/landlock/fs_test.c | 40 ++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index f8cfd31335e1..3dad643741f7 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -7961,6 +7961,46 @@ TEST_F(audit_layout1, ioctl_dev)
 	EXPECT_EQ(1, records.domain);
 }
 
+TEST_F(audit_layout1, resolve_unix)
+{
+	struct audit_records records;
+	const char *const path = "sock";
+	int srv_fd, cli_fd, status;
+	pid_t child_pid;
+
+	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path);
+
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+	if (!child_pid) {
+		drop_access_rights(_metadata,
+				   &(struct landlock_ruleset_attr){
+					   .handled_access_fs = ACCESS_ALL,
+				   });
+
+		cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
+		ASSERT_LE(0, cli_fd);
+		EXPECT_EQ(EACCES,
+			  test_connect_named_unix(_metadata, cli_fd, path));
+
+		EXPECT_EQ(0, close(cli_fd));
+		_exit(_metadata->exit_code);
+	}
+
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	EXPECT_EQ(1, WIFEXITED(status));
+	EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+
+	EXPECT_EQ(0, matches_log_fs_extra(_metadata, self->audit_fd,
+					  "fs\\.resolve_unix", path, NULL));
+
+	EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+	EXPECT_EQ(0, records.access);
+	EXPECT_EQ(1, records.domain);
+
+	EXPECT_EQ(0, close(srv_fd));
+}
+
 TEST_F(audit_layout1, mount)
 {
 	struct audit_records records;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 10/12] selftests/landlock: Check that coredump sockets stay unrestricted
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, linux-security-module, Tingmao Wang,
	Justin Suess, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

Even when a process is restricted with the new
LANDLOCK_ACCESS_FS_RESOLVE_UNIX right, the kernel can continue writing
its coredump to the configured coredump socket.

In the test, we create a local server and rewire the system to write
coredumps into it.  We then create a child process within a Landlock
domain where LANDLOCK_ACCESS_FS_RESOLVE_UNIX is restricted and make
the process crash.  The test uses SO_PEERCRED to check that the
connecting client process is the expected one.

Includes a fix by Mickaël Salaün for setting the EUID to 0 (see [1]).

Link[1]: https://lore.kernel.org/all/20260218.ohth8theu8Yi@digikod.net/
Suggested-by: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 tools/testing/selftests/landlock/fs_test.c | 143 +++++++++++++++++++++
 1 file changed, 143 insertions(+)

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 3dad643741f7..af0f0b16129a 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -22,6 +22,7 @@
 #include <sys/ioctl.h>
 #include <sys/mount.h>
 #include <sys/prctl.h>
+#include <sys/resource.h>
 #include <sys/sendfile.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
@@ -4928,6 +4929,148 @@ TEST_F(scoped_domains, unix_seqpacket_connect_to_child_full)
 #undef USE_SENDTO
 #undef ENFORCE_ALL
 
+static void read_core_pattern(struct __test_metadata *const _metadata,
+			      char *buf, size_t buf_size)
+{
+	int fd;
+	ssize_t ret;
+
+	fd = open("/proc/sys/kernel/core_pattern", O_RDONLY | O_CLOEXEC);
+	ASSERT_LE(0, fd);
+
+	ret = read(fd, buf, buf_size - 1);
+	ASSERT_LE(0, ret);
+	EXPECT_EQ(0, close(fd));
+
+	buf[ret] = '\0';
+}
+
+static void set_core_pattern(struct __test_metadata *const _metadata,
+			     const char *pattern)
+{
+	int fd;
+	size_t len = strlen(pattern);
+
+	/*
+	 * Writing to /proc/sys/kernel/core_pattern requires EUID 0 because
+	 * sysctl_perm() checks that, ignoring capabilities like
+	 * CAP_SYS_ADMIN or CAP_DAC_OVERRIDE.
+	 *
+	 * Switching EUID clears the dumpable flag, which must be restored
+	 * afterwards to allow coredumps.
+	 */
+	set_cap(_metadata, CAP_SETUID);
+	ASSERT_EQ(0, seteuid(0));
+	clear_cap(_metadata, CAP_SETUID);
+
+	fd = open("/proc/sys/kernel/core_pattern", O_WRONLY | O_CLOEXEC);
+	ASSERT_LE(0, fd)
+	{
+		TH_LOG("Failed to open core_pattern for writing: %s",
+		       strerror(errno));
+	}
+
+	ASSERT_EQ(len, write(fd, pattern, len));
+	EXPECT_EQ(0, close(fd));
+
+	set_cap(_metadata, CAP_SETUID);
+	ASSERT_EQ(0, seteuid(getuid()));
+	clear_cap(_metadata, CAP_SETUID);
+
+	/* Restore dumpable flag cleared by seteuid(). */
+	ASSERT_EQ(0, prctl(PR_SET_DUMPABLE, 1, 0, 0, 0));
+}
+
+FIXTURE(coredump)
+{
+	char original_core_pattern[256];
+};
+
+FIXTURE_SETUP(coredump)
+{
+	disable_caps(_metadata);
+	read_core_pattern(_metadata, self->original_core_pattern,
+			  sizeof(self->original_core_pattern));
+}
+
+FIXTURE_TEARDOWN_PARENT(coredump)
+{
+	set_core_pattern(_metadata, self->original_core_pattern);
+}
+
+/*
+ * Test that even when a process is restricted with
+ * LANDLOCK_ACCESS_FS_RESOLVE_UNIX, the kernel can still initiate a connection
+ * to the coredump socket on the processes' behalf.
+ */
+TEST_F_FORK(coredump, socket_not_restricted)
+{
+	static const char core_pattern[] = "@/tmp/landlock_coredump_test.sock";
+	const char *const sock_path = core_pattern + 1;
+	int srv_fd, conn_fd, status;
+	pid_t child_pid;
+	struct ucred cred;
+	socklen_t cred_len = sizeof(cred);
+	char buf[4096];
+
+	/* Set up the coredump server socket. */
+	unlink(sock_path);
+	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, sock_path);
+
+	/* Point coredumps at our socket. */
+	set_core_pattern(_metadata, core_pattern);
+
+	/* Restrict LANDLOCK_ACCESS_FS_RESOLVE_UNIX. */
+	drop_access_rights(_metadata,
+			   &(struct landlock_ruleset_attr){
+				   .handled_access_fs =
+					   LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+			   });
+
+	/* Fork a child that crashes. */
+	child_pid = fork();
+	ASSERT_LE(0, child_pid);
+	if (child_pid == 0) {
+		struct rlimit rl = {
+			.rlim_cur = RLIM_INFINITY,
+			.rlim_max = RLIM_INFINITY,
+		};
+
+		ASSERT_EQ(0, setrlimit(RLIMIT_CORE, &rl));
+
+		/* Crash on purpose. */
+		kill(getpid(), SIGSEGV);
+		_exit(1);
+	}
+
+	/*
+	 * Accept the coredump connection.  If Landlock incorrectly denies the
+	 * kernel's coredump connect, accept() will block forever, so the test
+	 * would time out.
+	 */
+	conn_fd = accept(srv_fd, NULL, NULL);
+	ASSERT_LE(0, conn_fd);
+
+	/* Check that the connection came from the crashing child. */
+	ASSERT_EQ(0, getsockopt(conn_fd, SOL_SOCKET, SO_PEERCRED, &cred,
+				&cred_len));
+	EXPECT_EQ(child_pid, cred.pid);
+
+	/* Drain the coredump data so the kernel can finish. */
+	while (read(conn_fd, buf, sizeof(buf)) > 0)
+		;
+
+	EXPECT_EQ(0, close(conn_fd));
+
+	/* Wait for the child and verify it coredumped. */
+	ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0));
+	ASSERT_TRUE(WIFSIGNALED(status));
+	ASSERT_TRUE(WCOREDUMP(status));
+
+	EXPECT_EQ(0, close(srv_fd));
+	EXPECT_EQ(0, unlink(sock_path));
+}
+
 /* clang-format off */
 FIXTURE(layout1_bind) {};
 /* clang-format on */
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 11/12] selftests/landlock: fs_test: Simplify ruleset creation and enforcement
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, linux-security-module, Tingmao Wang,
	Justin Suess, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

* Add enforce_fs() for defining and enforcing a ruleset in one step
* In some places, dropped "ASSERT_LE(0, fd)" checks after
  create_ruleset() call -- create_ruleset() already checks that.
* In some places, rename "file_fd" to "fd" if it is not needed to
  disambiguate any more.

Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 tools/testing/selftests/landlock/fs_test.c | 821 ++++++---------------
 1 file changed, 210 insertions(+), 611 deletions(-)

diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index af0f0b16129a..cdb47fc1fc0a 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -767,15 +767,6 @@ static int create_ruleset(struct __test_metadata *const _metadata,
 		.handled_access_fs = handled_access_fs,
 	};
 
-	ASSERT_NE(NULL, rules)
-	{
-		TH_LOG("No rule list");
-	}
-	ASSERT_NE(NULL, rules[0].path)
-	{
-		TH_LOG("Empty rule list");
-	}
-
 	ruleset_fd =
 		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
 	ASSERT_LE(0, ruleset_fd)
@@ -783,16 +774,26 @@ static int create_ruleset(struct __test_metadata *const _metadata,
 		TH_LOG("Failed to create a ruleset: %s", strerror(errno));
 	}
 
-	for (i = 0; rules[i].path; i++) {
-		if (!rules[i].access)
-			continue;
+	if (rules)
+		for (i = 0; rules[i].path; i++) {
+			if (!rules[i].access)
+				continue;
 
-		add_path_beneath(_metadata, ruleset_fd, rules[i].access,
-				 rules[i].path);
-	}
+			add_path_beneath(_metadata, ruleset_fd, rules[i].access,
+					 rules[i].path);
+		}
 	return ruleset_fd;
 }
 
+static void enforce_fs(struct __test_metadata *const _metadata,
+		       const __u64 access_fs, const struct rule rules[])
+{
+	const int ruleset_fd = create_ruleset(_metadata, access_fs, rules);
+
+	enforce_ruleset(_metadata, ruleset_fd);
+	EXPECT_EQ(0, close(ruleset_fd));
+}
+
 TEST_F_FORK(layout0, proc_nsfs)
 {
 	const struct rule rules[] = {
@@ -879,13 +880,10 @@ TEST_F_FORK(layout1, effective_access)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 	char buf;
 	int reg_fd;
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Tests on a directory (with or without O_PATH). */
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -932,12 +930,9 @@ TEST_F_FORK(layout1, unhandled_access)
 		},
 		{},
 	};
-	/* Here, we only handle read accesses, not write accesses. */
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RO, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	/* Here, we only handle read accesses, not write accesses. */
+	enforce_fs(_metadata, ACCESS_RO, rules);
 
 	/*
 	 * Because the policy does not handle LANDLOCK_ACCESS_FS_WRITE_FILE,
@@ -966,11 +961,8 @@ TEST_F_FORK(layout1, ruleset_overlap)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks s1d1 hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1022,11 +1014,8 @@ TEST_F_FORK(layout1, layer_rule_unions)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1);
 
 	/* Checks s1d1 hierarchy with layer1. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1048,10 +1037,7 @@ TEST_F_FORK(layout1, layer_rule_unions)
 	ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY));
 
 	/* Doesn't change anything from layer1. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer2);
 
 	/* Checks s1d1 hierarchy with layer2. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1073,10 +1059,7 @@ TEST_F_FORK(layout1, layer_rule_unions)
 	ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY | O_DIRECTORY));
 
 	/* Only allows write (but not read) to dir_s1d3. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer3);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer3);
 
 	/* Checks s1d1 hierarchy with layer3. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -1114,27 +1097,18 @@ TEST_F_FORK(layout1, non_overlapping_accesses)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_REG, layer1);
 
 	ASSERT_EQ(-1, mknod(file1_s1d1, S_IFREG | 0700, 0));
 	ASSERT_EQ(EACCES, errno);
 	ASSERT_EQ(0, mknod(file1_s1d2, S_IFREG | 0700, 0));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REMOVE_FILE,
-				    layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REMOVE_FILE, layer2);
 
 	/* Unchanged accesses for file creation. */
 	ASSERT_EQ(-1, mknod(file1_s1d1, S_IFREG | 0700, 0));
@@ -1238,37 +1212,24 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 		},
 		{},
 	};
-	int ruleset_fd;
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer1_read);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer1_read);
 
 	/* Checks that read access is granted for file1_s1d3 with layer 1. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 	ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY));
 
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_READ_FILE |
-					    LANDLOCK_ACCESS_FS_WRITE_FILE,
-				    layer2_read_write);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   layer2_read_write);
 
 	/* Checks that previous access rights are unchanged with layer 2. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 	ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer3_read);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer3_read);
 
 	/* Checks that previous access rights are unchanged with layer 3. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDWR));
@@ -1276,13 +1237,9 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(0, test_open(file2_s1d3, O_WRONLY));
 
 	/* This time, denies write access for the file hierarchy. */
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_READ_FILE |
-					    LANDLOCK_ACCESS_FS_WRITE_FILE,
-				    layer4_read_write);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   layer4_read_write);
 
 	/*
 	 * Checks that the only change with layer 4 is that write access is
@@ -1293,11 +1250,7 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer5_read);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer5_read);
 
 	/* Checks that previous access rights are unchanged with layer 5. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY));
@@ -1305,11 +1258,7 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_EXECUTE,
-				    layer6_execute);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, layer6_execute);
 
 	/* Checks that previous access rights are unchanged with layer 6. */
 	ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY));
@@ -1317,13 +1266,9 @@ TEST_F_FORK(layout1, interleaved_masked_accesses)
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_WRONLY));
 	ASSERT_EQ(EACCES, test_open(file2_s1d3, O_RDONLY));
 
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_READ_FILE |
-					    LANDLOCK_ACCESS_FS_WRITE_FILE,
-				    layer7_read_write);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   layer7_read_write);
 
 	/* Checks read access is now denied with layer 7. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY));
@@ -1344,7 +1289,6 @@ TEST_F_FORK(layout1, inherit_subset)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	enforce_ruleset(_metadata, ruleset_fd);
 
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY));
@@ -1460,7 +1404,6 @@ TEST_F_FORK(layout1, inherit_superset)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	enforce_ruleset(_metadata, ruleset_fd);
 
 	/* Readdir access is denied for dir_s1d2. */
@@ -1476,7 +1419,7 @@ TEST_F_FORK(layout1, inherit_superset)
 				 LANDLOCK_ACCESS_FS_READ_DIR,
 			 dir_s1d2);
 	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	EXPECT_EQ(0, close(ruleset_fd));
 
 	/* Readdir access is still denied for dir_s1d2. */
 	ASSERT_EQ(EACCES, test_open(dir_s1d2, O_RDONLY | O_DIRECTORY));
@@ -1498,7 +1441,6 @@ TEST_F_FORK(layout0, max_layers)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	for (i = 0; i < 16; i++)
 		enforce_ruleset(_metadata, ruleset_fd);
 
@@ -1507,7 +1449,7 @@ TEST_F_FORK(layout0, max_layers)
 		ASSERT_EQ(-1, err);
 		ASSERT_EQ(E2BIG, errno);
 	}
-	ASSERT_EQ(0, close(ruleset_fd));
+	EXPECT_EQ(0, close(ruleset_fd));
 }
 
 TEST_F_FORK(layout1, empty_or_same_ruleset)
@@ -1521,20 +1463,15 @@ TEST_F_FORK(layout1, empty_or_same_ruleset)
 	ASSERT_LE(-1, ruleset_fd);
 	ASSERT_EQ(ENOMSG, errno);
 
-	/* Enforces policy which deny read access to all files. */
-	ruleset_attr.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE;
-	ruleset_fd =
-		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
+	/* Enforces policy which denies read access to all files. */
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, NULL);
+
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
-	/* Nests a policy which deny read access to all directories. */
-	ruleset_attr.handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR;
+	/* Nests a policy which denies read access to all directories. */
 	ruleset_fd =
-		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
-	ASSERT_LE(0, ruleset_fd);
+		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, NULL);
 	enforce_ruleset(_metadata, ruleset_fd);
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
 	ASSERT_EQ(EACCES, test_open(dir_s1d1, O_RDONLY));
@@ -1558,11 +1495,8 @@ TEST_F_FORK(layout1, rule_on_mountpoint)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
@@ -1587,11 +1521,8 @@ TEST_F_FORK(layout1, rule_over_mountpoint)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
@@ -1615,21 +1546,15 @@ TEST_F_FORK(layout1, rule_over_root_allow_then_deny)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks allowed access. */
 	ASSERT_EQ(0, test_open("/", O_RDONLY));
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 
 	rules[0].access = LANDLOCK_ACCESS_FS_READ_FILE;
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks denied access (on a directory). */
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -1645,11 +1570,8 @@ TEST_F_FORK(layout1, rule_over_root_deny)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	/* Checks denied access (on a directory). */
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -1665,7 +1587,6 @@ TEST_F_FORK(layout1, rule_inside_mount_ns)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(0, syscall(__NR_pivot_root, dir_s3d2, dir_s3d3))
@@ -1675,10 +1596,7 @@ TEST_F_FORK(layout1, rule_inside_mount_ns)
 	ASSERT_EQ(0, chdir("/"));
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	ASSERT_EQ(0, test_open("s3d3", O_RDONLY));
 	ASSERT_EQ(EACCES, test_open("/", O_RDONLY));
@@ -1693,11 +1611,8 @@ TEST_F_FORK(layout1, mount_and_pivot)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_RDONLY, NULL));
@@ -1716,9 +1631,6 @@ TEST_F_FORK(layout1, move_mount)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(0, syscall(__NR_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD,
@@ -1731,8 +1643,7 @@ TEST_F_FORK(layout1, move_mount)
 			     dir_s3d2, 0));
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, rules);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(-1, syscall(__NR_move_mount, AT_FDCWD, dir_s3d2, AT_FDCWD,
@@ -1747,14 +1658,9 @@ TEST_F_FORK(layout1, topology_changes_with_net_only)
 		.handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
 	};
-	int ruleset_fd;
 
 	/* Add network restrictions. */
-	ruleset_fd =
-		landlock_create_ruleset(&ruleset_net, sizeof(ruleset_net), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	drop_access_rights(_metadata, &ruleset_net);
 
 	/* Mount, remount, move_mount, umount, and pivot_root checks. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -1775,14 +1681,9 @@ TEST_F_FORK(layout1, topology_changes_with_net_and_fs)
 				      LANDLOCK_ACCESS_NET_CONNECT_TCP,
 		.handled_access_fs = LANDLOCK_ACCESS_FS_EXECUTE,
 	};
-	int ruleset_fd;
 
 	/* Add network and filesystem restrictions. */
-	ruleset_fd = landlock_create_ruleset(&ruleset_net_fs,
-					     sizeof(ruleset_net_fs), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	drop_access_rights(_metadata, &ruleset_net_fs);
 
 	/* Mount, remount, move_mount, umount, and pivot_root checks. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -1819,14 +1720,13 @@ TEST_F_FORK(layout1, release_inodes)
 	};
 	const int ruleset_fd = create_ruleset(_metadata, ACCESS_RW, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	/* Unmount a file hierarchy while it is being used by a ruleset. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_EQ(0, umount(dir_s3d2));
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 
 	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	EXPECT_EQ(0, close(ruleset_fd));
 
 	ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
 	ASSERT_EQ(EACCES, test_open(dir_s3d2, O_RDONLY));
@@ -1858,7 +1758,6 @@ TEST_F_FORK(layout1, covered_rule)
 	/* Creates a ruleset with the future hidden directory. */
 	ruleset_fd =
 		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, layer1);
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Covers with a new mount point. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -1908,10 +1807,7 @@ static void test_relative_path(struct __test_metadata *const _metadata,
 	};
 	int dirfd, ruleset_fd;
 
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1_base);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1_base);
 
 	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2_subs);
 
@@ -2092,10 +1988,7 @@ TEST_F_FORK(layout1, execute)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
 
-	ASSERT_LE(0, ruleset_fd);
 	copy_file(_metadata, bin_true, file1_s1d1);
 	copy_file(_metadata, bin_true, file1_s1d2);
 	copy_file(_metadata, bin_true, file1_s1d3);
@@ -2104,8 +1997,7 @@ TEST_F_FORK(layout1, execute)
 	test_execute(_metadata, 0, file1_s1d1);
 	test_check_exec(_metadata, 0, file1_s1d1);
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
 	ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
@@ -2216,16 +2108,12 @@ TEST_F_FORK(layout1, link)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata, layer1[0].access, layer1);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer1[0].access, layer1);
 
 	ASSERT_EQ(-1, link(file2_s1d1, file1_s1d1));
 	ASSERT_EQ(EACCES, errno);
@@ -2245,10 +2133,7 @@ TEST_F_FORK(layout1, link)
 	ASSERT_EQ(0, unlink(file2_s1d2));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	ruleset_fd = create_ruleset(_metadata, layer2[0].access, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer2[0].access, layer2);
 
 	/* Checks that linkind doesn't require the ability to delete a file. */
 	ASSERT_EQ(0, link(file1_s1d2, file2_s1d2));
@@ -2298,15 +2183,10 @@ TEST_F_FORK(layout1, rename_file)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/*
 	 * Tries to replace a file, from a directory that allows file removal,
@@ -2380,17 +2260,12 @@ TEST_F_FORK(layout1, rename_dir)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Empties dir_s1d3 to allow renaming. */
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/* Exchanges and renames directory to a different parent. */
 	ASSERT_EQ(-1, renameat2(AT_FDCWD, dir_s2d3, AT_FDCWD, dir_s1d3,
@@ -2444,12 +2319,8 @@ TEST_F_FORK(layout1, reparent_refer)
 		},
 		{},
 	};
-	int ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
 	ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d1));
 	ASSERT_EQ(EXDEV, errno);
@@ -2479,14 +2350,9 @@ static void refer_denied_by_default(struct __test_metadata *const _metadata,
 				    const int layer1_err,
 				    const struct rule layer2[])
 {
-	int ruleset_fd;
-
 	ASSERT_EQ(0, unlink(file1_s1d2));
 
-	ruleset_fd = create_ruleset(_metadata, layer1[0].access, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer1[0].access, layer1);
 
 	/*
 	 * If the first layer handles LANDLOCK_ACCESS_FS_REFER (according to
@@ -2498,10 +2364,7 @@ static void refer_denied_by_default(struct __test_metadata *const _metadata,
 	ASSERT_EQ(layer1_err, test_exchange(file2_s1d1, file2_s1d2));
 	ASSERT_EQ(layer1_err, test_exchange(file2_s1d2, file2_s1d1));
 
-	ruleset_fd = create_ruleset(_metadata, layer2[0].access, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, layer2[0].access, layer2);
 
 	/*
 	 * Now, either the first or the second layer does not handle
@@ -2587,10 +2450,7 @@ TEST_F_FORK(layout1, refer_denied_by_default4)
  */
 TEST_F_FORK(layout1, refer_mount_root_deny)
 {
-	const struct landlock_ruleset_attr ruleset_attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_MAKE_DIR,
-	};
-	int root_fd, ruleset_fd;
+	int root_fd;
 
 	/* Creates a mount object from a non-mount point. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -2600,13 +2460,7 @@ TEST_F_FORK(layout1, refer_mount_root_deny)
 	clear_cap(_metadata, CAP_SYS_ADMIN);
 	ASSERT_LE(0, root_fd);
 
-	ruleset_fd =
-		landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-
-	ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
-	ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, NULL);
 
 	/* Link denied by Landlock: EACCES. */
 	EXPECT_EQ(-1, linkat(root_fd, ".", root_fd, "does_not_exist", 0));
@@ -2641,18 +2495,12 @@ TEST_F_FORK(layout1, refer_part_mount_tree_is_allowed)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	ASSERT_EQ(0, unlink(file1_s3d3));
-	ruleset_fd = create_ruleset(_metadata,
-				    LANDLOCK_ACCESS_FS_REFER |
-					    LANDLOCK_ACCESS_FS_MAKE_REG |
-					    LANDLOCK_ACCESS_FS_REMOVE_FILE,
-				    layer1);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG |
+			   LANDLOCK_ACCESS_FS_REMOVE_FILE,
+		   layer1);
 
 	ASSERT_EQ(0, rename(file1_s3d4, file1_s3d3));
 }
@@ -2678,13 +2526,10 @@ TEST_F_FORK(layout1, reparent_link)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   layer1);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
@@ -2756,13 +2601,10 @@ TEST_F_FORK(layout1, reparent_rename)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   layer1);
 
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
@@ -2902,13 +2744,9 @@ reparent_exdev_layers_enforce1(struct __test_metadata *const _metadata)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER, layer1);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   layer1);
 }
 
 static void
@@ -2925,12 +2763,7 @@ reparent_exdev_layers_enforce2(struct __test_metadata *const _metadata)
 	 * Same checks as before but with a second layer and a new MAKE_DIR
 	 * rule (and no explicit handling of REFER).
 	 */
-	const int ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, layer2);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_MAKE_DIR, layer2);
 }
 
 TEST_F_FORK(layout1, reparent_exdev_layers_rename1)
@@ -3199,15 +3032,11 @@ TEST_F_FORK(layout1, reparent_remove)
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_REMOVE_DIR |
-			LANDLOCK_ACCESS_FS_REMOVE_FILE,
-		layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_REMOVE_DIR |
+			   LANDLOCK_ACCESS_FS_REMOVE_FILE,
+		   layer1);
 
 	/* Access denied because of wrong/swapped remove file/dir. */
 	ASSERT_EQ(-1, rename(file1_s1d1, dir_s2d2));
@@ -3271,17 +3100,13 @@ TEST_F_FORK(layout1, reparent_dom_superset)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(_metadata,
-					LANDLOCK_ACCESS_FS_REFER |
-						LANDLOCK_ACCESS_FS_EXECUTE |
-						LANDLOCK_ACCESS_FS_MAKE_SOCK |
-						LANDLOCK_ACCESS_FS_READ_FILE |
-						LANDLOCK_ACCESS_FS_MAKE_FIFO,
-					layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE |
+			   LANDLOCK_ACCESS_FS_MAKE_SOCK |
+			   LANDLOCK_ACCESS_FS_READ_FILE |
+			   LANDLOCK_ACCESS_FS_MAKE_FIFO,
+		   layer1);
 
 	ASSERT_EQ(-1, rename(file1_s1d2, file1_s2d1));
 	ASSERT_EQ(EXDEV, errno);
@@ -3344,18 +3169,13 @@ TEST_F_FORK(layout1, remove_dir)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(0, rmdir(dir_s1d3));
 	ASSERT_EQ(0, mkdir(dir_s1d3, 0700));
@@ -3381,12 +3201,8 @@ TEST_F_FORK(layout1, remove_file)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(-1, unlink(file1_s1d1));
 	ASSERT_EQ(EACCES, errno);
@@ -3407,9 +3223,6 @@ static void test_make_file(struct __test_metadata *const _metadata,
 		},
 		{},
 	};
-	const int ruleset_fd = create_ruleset(_metadata, access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file2_s1d1));
@@ -3425,8 +3238,7 @@ static void test_make_file(struct __test_metadata *const _metadata,
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, access, rules);
 
 	ASSERT_EQ(-1, mknod(file1_s1d1, mode | 0400, dev));
 	ASSERT_EQ(EACCES, errno);
@@ -3495,10 +3307,6 @@ TEST_F_FORK(layout1, make_sym)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file2_s1d1));
@@ -3510,8 +3318,7 @@ TEST_F_FORK(layout1, make_sym)
 	ASSERT_EQ(0, unlink(file1_s1d3));
 	ASSERT_EQ(0, unlink(file2_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	ASSERT_EQ(-1, symlink("none", file1_s1d1));
 	ASSERT_EQ(EACCES, errno);
@@ -3540,17 +3347,12 @@ TEST_F_FORK(layout1, make_dir)
 		},
 		{},
 	};
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
-
-	ASSERT_LE(0, ruleset_fd);
 
 	ASSERT_EQ(0, unlink(file1_s1d1));
 	ASSERT_EQ(0, unlink(file1_s1d2));
 	ASSERT_EQ(0, unlink(file1_s1d3));
 
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/* Uses file_* as directory names. */
 	ASSERT_EQ(-1, mkdir(file1_s1d1, 0700));
@@ -3581,14 +3383,10 @@ TEST_F_FORK(layout1, proc_unlinked_file)
 		{},
 	};
 	int reg_fd, proc_fd;
-	const int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
-		rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   rules);
 
 	ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDWR));
 	ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY));
@@ -3624,13 +3422,9 @@ TEST_F_FORK(layout1, proc_pipe)
 		},
 		{},
 	};
-	/* Limits read and write access to files tied to the filesystem. */
-	const int ruleset_fd =
-		create_ruleset(_metadata, rules[0].access, rules);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	/* Limits read and write access to files tied to the filesystem. */
+	enforce_fs(_metadata, rules[0].access, rules);
 
 	/* Checks enforcement for normal files. */
 	ASSERT_EQ(0, test_open(file1_s1d2, O_RDWR));
@@ -3720,16 +3514,10 @@ TEST_F_FORK(layout1, truncate_unhandled)
 		{},
 	};
 
-	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
-			      LANDLOCK_ACCESS_FS_WRITE_FILE;
-	int ruleset_fd;
-
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, handled, rules);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+		   rules);
 
 	/*
 	 * Checks read right: truncate and open with O_TRUNC work, unless the
@@ -3802,17 +3590,13 @@ TEST_F_FORK(layout1, truncate)
 		},
 		{},
 	};
-	const __u64 handled = LANDLOCK_ACCESS_FS_READ_FILE |
-			      LANDLOCK_ACCESS_FS_WRITE_FILE |
-			      LANDLOCK_ACCESS_FS_TRUNCATE;
-	int ruleset_fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, handled, rules);
-
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_READ_FILE |
+			   LANDLOCK_ACCESS_FS_WRITE_FILE |
+			   LANDLOCK_ACCESS_FS_TRUNCATE,
+		   rules);
 
 	/* Checks read, write and truncate rights: truncation works. */
 	EXPECT_EQ(0, test_truncate(file_rwt));
@@ -3912,34 +3696,25 @@ TEST_F_FORK(layout1, ftruncate)
 		},
 		{},
 	};
-	int fd_layer0, fd_layer1, fd_layer2, fd_layer3, ruleset_fd;
+	int fd_layer0, fd_layer1, fd_layer2, fd_layer3;
 
 	fd_layer0 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
 
-	ruleset_fd = create_ruleset(_metadata, handled1, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, handled1, layer1);
 
 	fd_layer1 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
 	EXPECT_EQ(0, test_ftruncate(fd_layer1));
 
-	ruleset_fd = create_ruleset(_metadata, handled2, layer2);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, handled2, layer2);
 
 	fd_layer2 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
 	EXPECT_EQ(0, test_ftruncate(fd_layer1));
 	EXPECT_EQ(0, test_ftruncate(fd_layer2));
 
-	ruleset_fd = create_ruleset(_metadata, handled3, layer3);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, handled3, layer3);
 
 	fd_layer3 = open(path, O_WRONLY);
 	EXPECT_EQ(0, test_ftruncate(fd_layer0));
@@ -4031,13 +3806,10 @@ TEST_F_FORK(ftruncate, open_and_ftruncate)
 		},
 		{},
 	};
-	int fd, ruleset_fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
 	fd = open(path, O_WRONLY);
 	EXPECT_EQ(variant->expected_open_result, (fd < 0 ? errno : 0));
@@ -4072,12 +3844,9 @@ TEST_F_FORK(ftruncate, open_and_ftruncate_in_different_processes)
 			},
 			{},
 		};
-		int fd, ruleset_fd;
+		int fd;
 
-		ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-		ASSERT_LE(0, ruleset_fd);
-		enforce_ruleset(_metadata, ruleset_fd);
-		ASSERT_EQ(0, close(ruleset_fd));
+		enforce_fs(_metadata, variant->handled, rules);
 
 		fd = open(path, O_WRONLY);
 		ASSERT_EQ(variant->expected_open_result, (fd < 0 ? errno : 0));
@@ -4122,10 +3891,7 @@ static int test_fs_ioc_getflags_ioctl(int fd)
 
 TEST(memfd_ftruncate_and_ioctl)
 {
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = ACCESS_ALL,
-	};
-	int ruleset_fd, fd, i;
+	int fd, i;
 
 	/*
 	 * We exercise the same test both with and without Landlock enabled, to
@@ -4147,10 +3913,7 @@ TEST(memfd_ftruncate_and_ioctl)
 		ASSERT_EQ(0, close(fd));
 
 		/* Enables Landlock. */
-		ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-		ASSERT_LE(0, ruleset_fd);
-		enforce_ruleset(_metadata, ruleset_fd);
-		ASSERT_EQ(0, close(ruleset_fd));
+		enforce_fs(_metadata, ACCESS_ALL, NULL);
 	}
 }
 
@@ -4165,10 +3928,7 @@ static int test_fionread_ioctl(int fd)
 
 TEST_F_FORK(layout1, o_path_ftruncate_and_ioctl)
 {
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = ACCESS_ALL,
-	};
-	int ruleset_fd, fd;
+	int fd;
 
 	/*
 	 * Checks that for files opened with O_PATH, both ioctl(2) and
@@ -4184,10 +3944,7 @@ TEST_F_FORK(layout1, o_path_ftruncate_and_ioctl)
 	ASSERT_EQ(0, close(fd));
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	/*
 	 * Checks that after enabling Landlock,
@@ -4261,16 +4018,10 @@ struct space_resv {
  */
 TEST_F_FORK(layout1, blanket_permitted_ioctls)
 {
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-	};
-	int ruleset_fd, fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL);
 
 	fd = open("/dev/null", O_RDWR | O_CLOEXEC);
 	ASSERT_LE(0, fd);
@@ -4323,20 +4074,14 @@ TEST_F_FORK(layout1, blanket_permitted_ioctls)
 TEST_F_FORK(layout1, named_pipe_ioctl)
 {
 	pid_t child_pid;
-	int fd, ruleset_fd;
+	int fd;
 	const char *const path = file1_s1d1;
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-	};
 
 	ASSERT_EQ(0, unlink(path));
 	ASSERT_EQ(0, mkfifo(path, 0600));
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL);
 
 	/* The child process opens the pipe for writing. */
 	child_pid = fork();
@@ -4411,20 +4156,14 @@ static int test_connect_named_unix(struct __test_metadata *const _metadata,
 TEST_F_FORK(layout1, named_unix_domain_socket_ioctl)
 {
 	const char *const path = file1_s1d1;
-	int srv_fd, cli_fd, ruleset_fd;
-	const struct landlock_ruleset_attr attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_IOCTL_DEV,
-	};
+	int srv_fd, cli_fd;
 
 	/* Sets up a server */
 	ASSERT_EQ(0, unlink(path));
 	srv_fd = set_up_named_unix_server(_metadata, SOCK_STREAM, path);
 
 	/* Enables Landlock. */
-	ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_IOCTL_DEV, NULL);
 
 	/* Sets up a client connection to it */
 	cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
@@ -4497,29 +4236,25 @@ TEST_F_FORK(ioctl, handle_dir_access_file)
 		},
 		{},
 	};
-	int file_fd, ruleset_fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
-	file_fd = open("/dev/zero", variant->open_mode);
-	ASSERT_LE(0, file_fd);
+	fd = open("/dev/zero", variant->open_mode);
+	ASSERT_LE(0, fd);
 
 	/* Checks that IOCTL commands return the expected errors. */
-	EXPECT_EQ(variant->expected_fionread_result,
-		  test_fionread_ioctl(file_fd));
+	EXPECT_EQ(variant->expected_fionread_result, test_fionread_ioctl(fd));
 
 	/* Checks that unrestrictable commands are unrestricted. */
-	EXPECT_EQ(0, ioctl(file_fd, FIOCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONBIO, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIOASYNC, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIGETBSZ, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONBIO, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOASYNC, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIGETBSZ, &flag));
 
-	ASSERT_EQ(0, close(file_fd));
+	ASSERT_EQ(0, close(fd));
 }
 
 TEST_F_FORK(ioctl, handle_dir_access_dir)
@@ -4532,13 +4267,10 @@ TEST_F_FORK(ioctl, handle_dir_access_dir)
 		},
 		{},
 	};
-	int dir_fd, ruleset_fd;
+	int dir_fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
 	/*
 	 * Ignore variant->open_mode for this test, as we intend to open a
@@ -4577,32 +4309,28 @@ TEST_F_FORK(ioctl, handle_file_access_file)
 		},
 		{},
 	};
-	int file_fd, ruleset_fd;
+	int fd;
 
 	/* Enables Landlock. */
-	ruleset_fd = create_ruleset(_metadata, variant->handled, rules);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, variant->handled, rules);
 
-	file_fd = open("/dev/zero", variant->open_mode);
-	ASSERT_LE(0, file_fd)
+	fd = open("/dev/zero", variant->open_mode);
+	ASSERT_LE(0, fd)
 	{
 		TH_LOG("Failed to open /dev/zero: %s", strerror(errno));
 	}
 
 	/* Checks that IOCTL commands return the expected errors. */
-	EXPECT_EQ(variant->expected_fionread_result,
-		  test_fionread_ioctl(file_fd));
+	EXPECT_EQ(variant->expected_fionread_result, test_fionread_ioctl(fd));
 
 	/* Checks that unrestrictable commands are unrestricted. */
-	EXPECT_EQ(0, ioctl(file_fd, FIOCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONCLEX));
-	EXPECT_EQ(0, ioctl(file_fd, FIONBIO, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIOASYNC, &flag));
-	EXPECT_EQ(0, ioctl(file_fd, FIGETBSZ, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONCLEX));
+	EXPECT_EQ(0, ioctl(fd, FIONBIO, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIOASYNC, &flag));
+	EXPECT_EQ(0, ioctl(fd, FIGETBSZ, &flag));
 
-	ASSERT_EQ(0, close(file_fd));
+	ASSERT_EQ(0, close(fd));
 }
 
 /*
@@ -4644,22 +4372,6 @@ FIXTURE_TEARDOWN(scoped_domains)
 {
 }
 
-static void enforce_fs_resolve_unix(struct __test_metadata *const _metadata,
-				    const struct rule rules[])
-{
-	if (rules) {
-		int fd = create_ruleset(_metadata,
-					LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
-		enforce_ruleset(_metadata, fd);
-		EXPECT_EQ(0, close(fd));
-	} else {
-		struct landlock_ruleset_attr attr = {
-			.handled_access_fs = LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
-		};
-		drop_access_rights(_metadata, &attr);
-	}
-}
-
 /*
  * Flags for test_connect_to_parent and test_connect_to_child:
  *
@@ -4690,9 +4402,9 @@ static void test_connect_to_parent(struct __test_metadata *const _metadata,
 	char buf[1];
 
 	if (variant->domain_both)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	unlink(path);
 	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
@@ -4702,9 +4414,11 @@ static void test_connect_to_parent(struct __test_metadata *const _metadata,
 
 	if (child_pid == 0) {
 		if (variant->domain_child)
-			enforce_fs_resolve_unix(_metadata, NULL);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   NULL);
 		else if (flags & ENFORCE_ALL)
-			enforce_fs_resolve_unix(_metadata, rules);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   rules);
 
 		/* Wait for server to be available. */
 		EXPECT_EQ(0, close(readiness_pipe[1]));
@@ -4730,9 +4444,9 @@ static void test_connect_to_parent(struct __test_metadata *const _metadata,
 	}
 
 	if (variant->domain_parent)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
 
@@ -4771,9 +4485,9 @@ static void test_connect_to_child(struct __test_metadata *const _metadata,
 	char buf[1];
 
 	if (variant->domain_both)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	unlink(path);
 	ASSERT_EQ(0, pipe2(readiness_pipe, O_CLOEXEC));
@@ -4784,9 +4498,11 @@ static void test_connect_to_child(struct __test_metadata *const _metadata,
 
 	if (child_pid == 0) {
 		if (variant->domain_child)
-			enforce_fs_resolve_unix(_metadata, NULL);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   NULL);
 		else if (flags & ENFORCE_ALL)
-			enforce_fs_resolve_unix(_metadata, rules);
+			enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
+				   rules);
 
 		srv_fd = set_up_named_unix_server(_metadata, sock_type, path);
 
@@ -4810,9 +4526,9 @@ static void test_connect_to_child(struct __test_metadata *const _metadata,
 	}
 
 	if (variant->domain_parent)
-		enforce_fs_resolve_unix(_metadata, NULL);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 	else if (flags & ENFORCE_ALL)
-		enforce_fs_resolve_unix(_metadata, rules);
+		enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, rules);
 
 	/* Wait for server to be available. */
 	EXPECT_EQ(0, close(readiness_pipe[1]));
@@ -5021,11 +4737,7 @@ TEST_F_FORK(coredump, socket_not_restricted)
 	set_core_pattern(_metadata, core_pattern);
 
 	/* Restrict LANDLOCK_ACCESS_FS_RESOLVE_UNIX. */
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
-			   });
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_RESOLVE_UNIX, NULL);
 
 	/* Fork a child that crashes. */
 	child_pid = fork();
@@ -5212,13 +4924,9 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 		},
 		{},
 	};
-	int ruleset_fd;
 
 	/* Sets rules for the parent directories. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1_parent);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1_parent);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(0, test_open(file1_s1d1, O_RDONLY));
@@ -5237,10 +4945,7 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 	ASSERT_EQ(0, test_open(dir_s2d2, O_RDONLY | O_DIRECTORY));
 
 	/* Sets rules for the mount points. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2_mount_point);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer2_mount_point);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
@@ -5261,10 +4966,7 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 	ASSERT_EQ(0, test_open(bind_dir_s1d3, O_RDONLY | O_DIRECTORY));
 
 	/* Sets a (shared) rule only on the source. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer3_source);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer3_source);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d2, O_RDONLY));
@@ -5285,10 +4987,7 @@ TEST_F_FORK(layout1_bind, same_content_same_file)
 	ASSERT_EQ(EACCES, test_open(bind_dir_s1d3, O_RDONLY | O_DIRECTORY));
 
 	/* Sets a (shared) rule only on the destination. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer4_destination);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer4_destination);
 
 	/* Checks source hierarchy. */
 	ASSERT_EQ(EACCES, test_open(file1_s1d3, O_RDONLY));
@@ -5313,13 +5012,10 @@ TEST_F_FORK(layout1_bind, reparent_cross_mount)
 		},
 		{},
 	};
-	int ruleset_fd = create_ruleset(
-		_metadata,
-		LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_EXECUTE,
+		   layer1);
 
 	/* Checks basic denied move. */
 	ASSERT_EQ(-1, rename(file1_s1d1, file1_s1d2));
@@ -5376,10 +5072,6 @@ TEST_F_FORK(layout1_bind, path_disconnected)
 		create_ruleset(_metadata, ACCESS_RW, layer3_only_s1d2);
 	int bind_s1d3_fd;
 
-	ASSERT_LE(0, ruleset_fd_l1);
-	ASSERT_LE(0, ruleset_fd_l2);
-	ASSERT_LE(0, ruleset_fd_l3);
-
 	enforce_ruleset(_metadata, ruleset_fd_l1);
 	EXPECT_EQ(0, close(ruleset_fd_l1));
 
@@ -5483,8 +5175,6 @@ TEST_F_FORK(layout1_bind, path_disconnected_rename)
 	ruleset_fd_l1 = create_ruleset(_metadata, ACCESS_ALL, layer1);
 	ruleset_fd_l2 = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
 				       layer2_only_s1d2);
-	ASSERT_LE(0, ruleset_fd_l1);
-	ASSERT_LE(0, ruleset_fd_l2);
 
 	enforce_ruleset(_metadata, ruleset_fd_l1);
 	EXPECT_EQ(0, close(ruleset_fd_l1));
@@ -5630,7 +5320,7 @@ TEST_F_FORK(layout1_bind, path_disconnected_link)
 		},
 		{}
 	};
-	int ruleset_fd, bind_s1d3_fd;
+	int bind_s1d3_fd;
 
 	/* Removes unneeded files created by layout1, otherwise it will EEXIST. */
 	ASSERT_EQ(0, unlink(file1_s1d2));
@@ -5653,10 +5343,7 @@ TEST_F_FORK(layout1_bind, path_disconnected_link)
 		TH_LOG("Failed to create %s: %s", dir_s4d2, strerror(errno));
 	}
 
-	ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, layer1);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_ALL, layer1);
 
 	/* From disconnected to connected. */
 	ASSERT_EQ(0, linkat(bind_s1d3_fd, file1_name, AT_FDCWD, file1_s2d2, 0))
@@ -6194,7 +5881,6 @@ TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange)
 	int ruleset_fd, s1d41_bind_fd, s1d42_bind_fd;
 
 	ruleset_fd = create_ruleset(_metadata, handled_access, rules);
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Adds rule for the covered directory. */
 	if (variant->allowed_s2d2) {
@@ -7127,7 +6813,6 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 		},
 		{},
 	};
-	int ruleset_fd;
 	size_t i;
 	const char *path_entry;
 
@@ -7135,10 +6820,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 		SKIP(return, "overlayfs is not supported (test)");
 
 	/* Sets rules on base directories (i.e. outside overlay scope). */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer1_base);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer1_base);
 
 	/* Checks lower layer. */
 	for_each_path(lower_base_files, path_entry, i) {
@@ -7183,10 +6865,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Sets rules on data directories (i.e. inside overlay scope). */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer2_data);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer2_data);
 
 	/* Checks merge. */
 	for_each_path(merge_base_files, path_entry, i) {
@@ -7200,10 +6879,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Same checks with tighter rules. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer3_subdirs);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer3_subdirs);
 
 	/* Checks changes for lower layer. */
 	for_each_path(lower_base_files, path_entry, i) {
@@ -7225,10 +6901,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Sets rules directly on overlayed files. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer4_files);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer4_files);
 
 	/* Checks unchanged accesses on lower layer. */
 	for_each_path(lower_sub_files, path_entry, i) {
@@ -7253,10 +6926,7 @@ TEST_F_FORK(layout2_overlay, same_content_different_file)
 	}
 
 	/* Only allowes access to the merge hierarchy. */
-	ruleset_fd = create_ruleset(_metadata, ACCESS_RW, layer5_merge_only);
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, ACCESS_RW, layer5_merge_only);
 
 	/* Checks new accesses on lower layer. */
 	for_each_path(lower_sub_files, path_entry, i) {
@@ -7442,11 +7112,7 @@ static void layer3_fs_tag_inode(struct __test_metadata *const _metadata,
 		},
 		{},
 	};
-	const struct landlock_ruleset_attr layer2_deny_everything_attr = {
-		.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
-	};
 	const char *const dev_null_path = "/dev/null";
-	int ruleset_fd;
 
 	if (self->skip_test)
 		SKIP(return, "this filesystem is not supported (test)");
@@ -7455,22 +7121,14 @@ static void layer3_fs_tag_inode(struct __test_metadata *const _metadata,
 	EXPECT_EQ(0, test_open(dev_null_path, O_RDONLY | O_CLOEXEC));
 	EXPECT_EQ(0, test_open(variant->file_path, O_RDONLY | O_CLOEXEC));
 
-	ruleset_fd = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
-				    layer1_allow_read_file);
-	EXPECT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE,
+		   layer1_allow_read_file);
 
 	EXPECT_EQ(EACCES, test_open(dev_null_path, O_RDONLY | O_CLOEXEC));
 	EXPECT_EQ(0, test_open(variant->file_path, O_RDONLY | O_CLOEXEC));
 
 	/* Forbids directory reading. */
-	ruleset_fd =
-		landlock_create_ruleset(&layer2_deny_everything_attr,
-					sizeof(layer2_deny_everything_attr), 0);
-	EXPECT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	EXPECT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, NULL);
 
 	/* Checks with Landlock and forbidden access. */
 	EXPECT_EQ(EACCES, test_open(dev_null_path, O_RDONLY | O_CLOEXEC));
@@ -7532,7 +7190,6 @@ TEST_F_FORK(layout3_fs, release_inodes)
 
 	ruleset_fd =
 		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, layer1);
-	ASSERT_LE(0, ruleset_fd);
 
 	/* Unmount the filesystem while it is being used by a ruleset. */
 	set_cap(_metadata, CAP_SYS_ADMIN);
@@ -7639,11 +7296,7 @@ TEST_F(audit_layout1, execute_make)
 	test_execute(_metadata, 0, file1_s1d1);
 	test_check_exec(_metadata, 0, file1_s1d1);
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   LANDLOCK_ACCESS_FS_EXECUTE,
-			   });
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, NULL);
 
 	test_execute(_metadata, EACCES, file1_s1d1);
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.execute",
@@ -7670,9 +7323,7 @@ TEST_F(audit_layout1, execute_read)
 	test_execute(_metadata, 0, file1_s1d1);
 	test_check_exec(_metadata, 0, file1_s1d1);
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	/*
 	 * The only difference with the previous audit_layout1.execute_read test is
@@ -7694,9 +7345,7 @@ TEST_F(audit_layout1, write_file)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(EACCES, test_open(file1_s1d1, O_WRONLY));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
@@ -7711,9 +7360,7 @@ TEST_F(audit_layout1, read_file)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(EACCES, test_open(file1_s1d1, O_RDONLY));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_file",
@@ -7728,9 +7375,7 @@ TEST_F(audit_layout1, read_dir)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(EACCES, test_open(dir_s1d1, O_DIRECTORY));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd, "fs\\.read_dir",
@@ -7748,9 +7393,7 @@ TEST_F(audit_layout1, remove_dir)
 	EXPECT_EQ(0, unlink(file1_s1d3));
 	EXPECT_EQ(0, unlink(file2_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, rmdir(dir_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7771,9 +7414,7 @@ TEST_F(audit_layout1, remove_file)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, unlink(file1_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7791,9 +7432,7 @@ TEST_F(audit_layout1, make_char)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFCHR | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7811,9 +7450,7 @@ TEST_F(audit_layout1, make_dir)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, mkdir(file1_s1d3, 0755));
 	EXPECT_EQ(EACCES, errno);
@@ -7831,9 +7468,7 @@ TEST_F(audit_layout1, make_reg)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFREG | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7851,9 +7486,7 @@ TEST_F(audit_layout1, make_sock)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFSOCK | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7871,9 +7504,7 @@ TEST_F(audit_layout1, make_fifo)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFIFO | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7891,9 +7522,7 @@ TEST_F(audit_layout1, make_block)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, mknod(file1_s1d3, S_IFBLK | 0644, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -7911,9 +7540,7 @@ TEST_F(audit_layout1, make_sym)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, symlink("target", file1_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7931,10 +7558,7 @@ TEST_F(audit_layout1, refer_handled)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs =
-						      LANDLOCK_ACCESS_FS_REFER,
-				      });
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, NULL);
 
 	EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3));
 	EXPECT_EQ(EXDEV, errno);
@@ -7956,12 +7580,9 @@ TEST_F(audit_layout1, refer_make)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   LANDLOCK_ACCESS_FS_MAKE_REG |
-					   LANDLOCK_ACCESS_FS_REFER,
-			   });
+	enforce_fs(_metadata,
+		   LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_REFER,
+		   NULL);
 
 	EXPECT_EQ(-1, link(file1_s1d1, file1_s1d3));
 	EXPECT_EQ(EACCES, errno);
@@ -7981,9 +7602,7 @@ TEST_F(audit_layout1, refer_rename)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(EACCES, test_rename(file1_s1d2, file1_s2d3));
 	EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
@@ -8003,9 +7622,7 @@ TEST_F(audit_layout1, refer_exchange)
 
 	EXPECT_EQ(0, unlink(file1_s1d3));
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	/*
 	 * The only difference with the previous audit_layout1.refer_rename test is
@@ -8043,12 +7660,8 @@ TEST_F(audit_layout1, refer_rename_half)
 		},
 		{},
 	};
-	int ruleset_fd =
-		create_ruleset(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
-	ASSERT_LE(0, ruleset_fd);
-	enforce_ruleset(_metadata, ruleset_fd);
-	ASSERT_EQ(0, close(ruleset_fd));
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER, layer1);
 
 	ASSERT_EQ(-1, rename(dir_s1d2, dir_s2d3));
 	ASSERT_EQ(EXDEV, errno);
@@ -8066,9 +7679,7 @@ TEST_F(audit_layout1, truncate)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata, &(struct landlock_ruleset_attr){
-					      .handled_access_fs = ACCESS_ALL,
-				      });
+	enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 	EXPECT_EQ(-1, truncate(file1_s1d3, 0));
 	EXPECT_EQ(EACCES, errno);
@@ -8085,12 +7696,7 @@ TEST_F(audit_layout1, ioctl_dev)
 	struct audit_records records;
 	int fd;
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   ACCESS_ALL &
-					   ~LANDLOCK_ACCESS_FS_READ_FILE,
-			   });
+	enforce_fs(_metadata, ACCESS_ALL & ~LANDLOCK_ACCESS_FS_READ_FILE, NULL);
 
 	fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
 	ASSERT_LE(0, fd);
@@ -8116,10 +7722,7 @@ TEST_F(audit_layout1, resolve_unix)
 	child_pid = fork();
 	ASSERT_LE(0, child_pid);
 	if (!child_pid) {
-		drop_access_rights(_metadata,
-				   &(struct landlock_ruleset_attr){
-					   .handled_access_fs = ACCESS_ALL,
-				   });
+		enforce_fs(_metadata, ACCESS_ALL, NULL);
 
 		cli_fd = socket(AF_UNIX, SOCK_STREAM, 0);
 		ASSERT_LE(0, cli_fd);
@@ -8148,11 +7751,7 @@ TEST_F(audit_layout1, mount)
 {
 	struct audit_records records;
 
-	drop_access_rights(_metadata,
-			   &(struct landlock_ruleset_attr){
-				   .handled_access_fs =
-					   LANDLOCK_ACCESS_FS_EXECUTE,
-			   });
+	enforce_fs(_metadata, LANDLOCK_ACCESS_FS_EXECUTE, NULL);
 
 	set_cap(_metadata, CAP_SYS_ADMIN);
 	EXPECT_EQ(-1, mount(NULL, dir_s3d2, NULL, MS_RDONLY, NULL));
-- 
2.53.0


^ permalink raw reply related

* [PATCH v8 12/12] landlock: Document FS access right for pathname UNIX sockets
From: Günther Noack @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Mickaël Salaün, John Johansen
  Cc: Günther Noack, Justin Suess, linux-security-module,
	Tingmao Wang, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-1-gnoack3000@gmail.com>

Add LANDLOCK_ACCESS_FS_RESOLVE_UNIX to the example code, and explain
it in the section about previous limitations.  The bulk of the
interesting flag documentation lives in the kernel header and is
included in the Sphinx rendering.

Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Mickaël Salaün <mic@digikod.net>
Signed-off-by: Günther Noack <gnoack3000@gmail.com>
---
 Documentation/userspace-api/landlock.rst | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 1490f879f621..6624af085a6a 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -77,7 +77,8 @@ to be explicit about the denied-by-default access rights.
             LANDLOCK_ACCESS_FS_MAKE_SYM |
             LANDLOCK_ACCESS_FS_REFER |
             LANDLOCK_ACCESS_FS_TRUNCATE |
-            LANDLOCK_ACCESS_FS_IOCTL_DEV,
+            LANDLOCK_ACCESS_FS_IOCTL_DEV |
+            LANDLOCK_ACCESS_FS_RESOLVE_UNIX,
         .handled_access_net =
             LANDLOCK_ACCESS_NET_BIND_TCP |
             LANDLOCK_ACCESS_NET_CONNECT_TCP,
@@ -127,6 +128,10 @@ version, and only use the available subset of access rights:
         /* Removes LANDLOCK_SCOPE_* for ABI < 6 */
         ruleset_attr.scoped &= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
                                  LANDLOCK_SCOPE_SIGNAL);
+        __attribute__((fallthrough));
+    case 6 ... 8:
+        /* Removes LANDLOCK_ACCESS_FS_RESOLVE_UNIX for ABI < 9 */
+        ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_RESOLVE_UNIX;
     }
 
 This enables the creation of an inclusive ruleset that will contain our rules.
@@ -685,6 +690,13 @@ enforce Landlock rulesets across all threads of the calling process
 using the ``LANDLOCK_RESTRICT_SELF_TSYNC`` flag passed to
 sys_landlock_restrict_self().
 
+Pathname UNIX sockets (ABI < 9)
+-------------------------------
+
+Starting with the Landlock ABI version 9, it is possible to restrict
+connections to pathname UNIX domain sockets (:manpage:`unix(7)`) using
+the new ``LANDLOCK_ACCESS_FS_RESOLVE_UNIX`` right.
+
 .. _kernel_support:
 
 Kernel support
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v4 11/13] ima: Support staging and deleting N measurements entries
From: Roberto Sassu @ 2026-03-27 17:02 UTC (permalink / raw)
  To: steven chen, corbet, skhan, zohar, dmitry.kasatkin, eric.snowberg,
	paul, jmorris, serge
  Cc: linux-doc, linux-kernel, linux-integrity, linux-security-module,
	gregorylumen, nramas, Roberto Sassu
In-Reply-To: <0e186faf-8111-4fd9-a7df-bff30f7fb20a@linux.microsoft.com>

On Thu, 2026-03-26 at 16:19 -0700, steven chen wrote:
> On 3/26/2026 10:30 AM, Roberto Sassu wrote:
> > From: Roberto Sassu <roberto.sassu@huawei.com>
> > 
> > Add support for sending a value N between 1 and ULONG_MAX to the staging
> > interface. This value represents the number of measurements that should be
> > deleted from the current measurements list.
> > 
> > This staging method allows the remote attestation agents to easily separate
> > the measurements that were verified (staged and deleted) from those that
> > weren't due to the race between taking a TPM quote and reading the
> > measurements list.
> > 
> > In order to minimize the locking time of ima_extend_list_mutex, deleting
> > N entries is realized by staging the entire current measurements list
> > (with the lock), by determining the N-th staged entry (without the lock),
> > and by splicing the entries in excess back to the current measurements list
> > (with the lock). Finally, the N entries are deleted (without the lock).
> > 
> > Flushing the hash table is not supported for N entries, since it would
> > require removing the N entries one by one from the hash table under the
> > ima_extend_list_mutex lock, which would increase the locking time.
> > 
> > The ima_extend_list_mutex lock is necessary in ima_dump_measurement_list()
> > because ima_queue_staged_delete_partial() uses __list_cut_position() to
> > modify ima_measurements_staged, for which no RCU-safe variant exists. For
> > the staging with prompt flavor alone, list_replace_rcu() could have been
> > used instead, but since both flavors share the same kexec serialization
> > path, the mutex is required regardless.
> > 
> > Link: https://github.com/linux-integrity/linux/issues/1
> > Suggested-by: Steven Chen <chenste@linux.microsoft.com>
> > Signed-off-by: Roberto Sassu <roberto.sassu@huawei.com>
> > ---
> >   security/integrity/ima/Kconfig     |  3 ++
> >   security/integrity/ima/ima.h       |  1 +
> >   security/integrity/ima/ima_fs.c    | 22 +++++++++-
> >   security/integrity/ima/ima_queue.c | 70 ++++++++++++++++++++++++++++++
> >   4 files changed, 95 insertions(+), 1 deletion(-)
> > 
> > diff --git a/security/integrity/ima/Kconfig b/security/integrity/ima/Kconfig
> > index e714726f3384..6ddb4e77bff5 100644
> > --- a/security/integrity/ima/Kconfig
> > +++ b/security/integrity/ima/Kconfig
> > @@ -341,6 +341,9 @@ config IMA_STAGING
> >   	  It allows user space to stage the measurements list for deletion and
> >   	  to delete the staged measurements after confirmation.
> >   
> > +	  Or, alternatively, it allows user space to specify N measurements
> > +	  entries to be deleted.
> > +
> >   	  On kexec, staging is reverted and staged measurements are prepended
> >   	  to the current measurements list when measurements are copied to the
> >   	  secondary kernel.
> > diff --git a/security/integrity/ima/ima.h b/security/integrity/ima/ima.h
> > index 699b735dec7d..de0693fce53c 100644
> > --- a/security/integrity/ima/ima.h
> > +++ b/security/integrity/ima/ima.h
> > @@ -319,6 +319,7 @@ struct ima_template_desc *lookup_template_desc(const char *name);
> >   bool ima_template_has_modsig(const struct ima_template_desc *ima_template);
> >   int ima_queue_stage(void);
> >   int ima_queue_staged_delete_all(void);
> > +int ima_queue_staged_delete_partial(unsigned long req_value);
> >   int ima_restore_measurement_entry(struct ima_template_entry *entry);
> >   int ima_restore_measurement_list(loff_t bufsize, void *buf);
> >   int ima_measurements_show(struct seq_file *m, void *v);
> > diff --git a/security/integrity/ima/ima_fs.c b/security/integrity/ima/ima_fs.c
> > index 39d9128e9f22..eb3f343c1138 100644
> > --- a/security/integrity/ima/ima_fs.c
> > +++ b/security/integrity/ima/ima_fs.c
> > @@ -28,6 +28,7 @@
> >    * Requests:
> >    * 'A\n': stage the entire measurements list
> >    * 'D\n': delete all staged measurements
> > + * '[1, ULONG_MAX]\n' delete N measurements entries
> >    */
> >   #define STAGED_REQ_LENGTH 21
> >   
> > @@ -319,6 +320,7 @@ static ssize_t ima_measurements_staged_write(struct file *file,
> >   					     size_t datalen, loff_t *ppos)
> >   {
> >   	char req[STAGED_REQ_LENGTH];
> > +	unsigned long req_value;
> >   	int ret;
> >   
> >   	if (*ppos > 0 || datalen < 2 || datalen > STAGED_REQ_LENGTH)
> > @@ -346,7 +348,25 @@ static ssize_t ima_measurements_staged_write(struct file *file,
> >   		ret = ima_queue_staged_delete_all();
> >   		break;
> >   	default:
> > -		ret = -EINVAL;
> > +		if (ima_flush_htable) {
> > +			pr_debug("Deleting staged N measurements not supported when flushing the hash table is requested\n");
> > +			return -EINVAL;
> > +		}
> > +
> > +		ret = kstrtoul(req, 10, &req_value);
> > +		if (ret < 0)
> > +			return ret;
> > +
> > +		if (req_value == 0) {
> > +			pr_debug("Must delete at least one entry\n");
> > +			return -EINVAL;
> > +		}
> > +
> > +		ret = ima_queue_stage();
> > +		if (ret < 0)
> > +			return ret;
> > +
> > +		ret = ima_queue_staged_delete_partial(req_value);
> The default processing is "Trim N" idea plus performance improvement.
> 
> Here do everything in one time. And this is what I said in v3.
> 
> [PATCH v3 1/3] ima: Remove ima_h_table structure 
> <https://lore.kernel.org/linux-integrity/c61aeaa79929a98cb3a6d30835972891fac3570f.camel@linux.ibm.com/T/#t>

In your approach you do:

lock ima_extend_measure_list_mutex
scan entries until N
cut list staged -> trim
unlock ima_extend_measure_list_mutex


In my approach I do:
lock ima_extend_measure_list_mutex
list replace active -> staged
unlock ima_extend_measure_list_mutex

scan entries until N

lock ima_extend_measure_list_mutex
cut list staged -> trim
splice staged ->active
unlock ima_extend_measure_list_mutex

So, I guess if you refer to less user space locking time, you mean one
lock/unlock and one list replace + list splice less in your solution.

In exchange, you propose to hold the lock in the kernel while scanning
N. I think it is a significant increase of kernel locking time vs a
negligible increase of user space locking time (in the kernel, all
processes need to wait for the ima_extend_measure_list_mutex to be
released, in user space it is just the agent waiting).

Roberto

> The important two parts of trimming is "trim N" and performance improvement.
> 
> The performance improvement include two parts:
>      hash table staging
>      active log list staging
> 
> And I think "Trim N" plus performance improvement is the right direction 
> to go.
> Lots of code for two steps "stage and trim" "stage" part can be removed.
> 
> Also race condition may happen if not holding the list all time in user 
> space
> during attestation period: from stage, read list, attestation and trimming.
> 
> So in order to improve the above user space lock time, "Trim T:N" can be 
> used
> not to hold list long in user space during attestation.
> 
> For Trim T:N, T represent total log trimmed since system boot up. Please 
> refer to
> https://lore.kernel.org/linux-integrity/20260205235849.7086-1-chenste@linux.microsoft.com/T/#t
> 
> Thanks,
> 
> Steven
> >   	}
> >   
> >   	if (ret < 0)
> > diff --git a/security/integrity/ima/ima_queue.c b/security/integrity/ima/ima_queue.c
> > index f5c18acfbc43..4fb557d61a88 100644
> > --- a/security/integrity/ima/ima_queue.c
> > +++ b/security/integrity/ima/ima_queue.c
> > @@ -371,6 +371,76 @@ int ima_queue_staged_delete_all(void)
> >   	return 0;
> >   }
> >   
> > +int ima_queue_staged_delete_partial(unsigned long req_value)
> > +{
> > +	unsigned long req_value_copy = req_value;
> > +	unsigned long size_to_remove = 0, num_to_remove = 0;
> > +	struct list_head *cut_pos = NULL;
> > +	LIST_HEAD(ima_measurements_trim);
> > +	struct ima_queue_entry *qe;
> > +	int ret = 0;
> > +
> > +	/*
> > +	 * Safe walk (no concurrent write), not under ima_extend_list_mutex
> > +	 * for performance reasons.
> > +	 */
> > +	list_for_each_entry(qe, &ima_measurements_staged, later) {
> > +		size_to_remove += get_binary_runtime_size(qe->entry);
> > +		num_to_remove++;
> > +
> > +		if (--req_value_copy == 0) {
> > +			/* qe->later always points to a valid list entry. */
> > +			cut_pos = &qe->later;
> > +			break;
> > +		}
> > +	}
> > +
> > +	/* Nothing to remove, undoing staging. */
> > +	if (req_value_copy > 0) {
> > +		size_to_remove = 0;
> > +		num_to_remove = 0;
> > +		ret = -ENOENT;
> > +	}
> > +
> > +	mutex_lock(&ima_extend_list_mutex);
> > +	if (list_empty(&ima_measurements_staged)) {
> > +		mutex_unlock(&ima_extend_list_mutex);
> > +		return -ENOENT;
> > +	}
> > +
> > +	if (cut_pos != NULL)
> > +		/*
> > +		 * ima_dump_measurement_list() does not modify the list,
> > +		 * cut_pos remains the same even if it was computed before
> > +		 * the lock.
> > +		 */
> > +		__list_cut_position(&ima_measurements_trim,
> > +				    &ima_measurements_staged, cut_pos);
> > +
> > +	atomic_long_sub(num_to_remove, &ima_num_entries[BINARY_STAGED]);
> > +	atomic_long_add(atomic_long_read(&ima_num_entries[BINARY_STAGED]),
> > +			&ima_num_entries[BINARY]);
> > +	atomic_long_set(&ima_num_entries[BINARY_STAGED], 0);
> > +
> > +	if (IS_ENABLED(CONFIG_IMA_KEXEC)) {
> > +		binary_runtime_size[BINARY_STAGED] -= size_to_remove;
> > +		binary_runtime_size[BINARY] +=
> > +					binary_runtime_size[BINARY_STAGED];
> > +		binary_runtime_size[BINARY_STAGED] = 0;
> > +	}
> > +
> > +	/*
> > +	 * Splice (prepend) any remaining non-deleted staged entries to the
> > +	 * active list (RCU not needed, there cannot be concurrent readers).
> > +	 */
> > +	list_splice(&ima_measurements_staged, &ima_measurements);
> > +	INIT_LIST_HEAD(&ima_measurements_staged);
> > +	mutex_unlock(&ima_extend_list_mutex);
> > +
> > +	ima_queue_delete(&ima_measurements_trim, false);
> > +	return ret;
> > +}
> > +
> >   static void ima_queue_delete(struct list_head *head, bool flush_htable)
> >   {
> >   	struct ima_queue_entry *qe, *qe_tmp;
> 


^ permalink raw reply

* Re: [PATCH v3 4/9] lsm: framework for BPF integrity verification
From: Blaise Boscaccy @ 2026-03-27 17:54 UTC (permalink / raw)
  To: Song Liu
  Cc: Jonathan Corbet, Paul Moore, James Morris, Serge E. Hallyn,
	Mickaël Salaün, Günther Noack,
	Dr. David Alan Gilbert, Andrew Morton, James.Bottomley, dhowells,
	Fan Wu, Ryan Foster, Randy Dunlap, linux-security-module,
	linux-doc, linux-kernel, bpf
In-Reply-To: <CAPhsuW6P-K=nTPxBk0_Wd0E1wDCBqb=uUQzmzpEg2NJub=L+dg@mail.gmail.com>

Song Liu <song@kernel.org> writes:

> On Wed, Mar 25, 2026 at 11:07 PM Blaise Boscaccy
> <bboscaccy@linux.microsoft.com> wrote:
> [...]
>> The first new callback, bpf_prog_load_integrity(), located within the
>> security_bpf_prog_load() hook, is necessary to ensure that the integrity
>> verification callbacks are executed before any of the existing LSMs
>> are executed via the bpf_prog_load() callback.  Reusing the existing
>> bpf_prog_load() callback for integrity verification could result in LSMs
>> not having access to the integrity verification results when asked to
>> authorize the BPF program load in the bpf_prog_load() callback.
>>
>> The new LSM hook, security_bpf_prog_load_post_integrity(), is intended
>> to be called from within LSMs performing BPF program integrity
>> verification.  It is used to report the verdict of the integrity
>> verification to other LSMs enforcing access control policy on BPF
>> program loads.  LSMs enforcing such access controls should register a
>> bpf_prog_load_post_integrity() callback to receive integrity verdicts.
>
> bpf_prog_load_post_integrity() is weird. Some questions about it:
>
> 1. Is it possible to call it from other LSMs (not hornet)? Specifically, is it
>    possible to call it from BPF LSM?

There is nothing hornet exclusive about that security hook. If the BPF
LSM folks wanted to use it they would probably need to implement a
kfunc to invoke it.  

> 2. This set does not include any LSMs that attach functions to
>    bpf_prog_load_post_integrity. This is against the new LSM hook policy.
>    I guess the plan is to add LSM users in follow up patches? Could you
>    please include at least some of such code in this patchset? This will
>    help folks understand the use case.
>

Sure thing. I will be sending out a v4 with additional patches that has
a concrete user of bpf_prog_load_post_integrity.

> Thanks,
> Song
>
> [...]

^ permalink raw reply

* Re: [PATCH v8 01/12] lsm: Add LSM hook security_unix_find
From: Paul Moore @ 2026-03-27 17:55 UTC (permalink / raw)
  To: Günther Noack
  Cc: Mickaël Salaün, John Johansen, James Morris,
	Serge E . Hallyn, Tingmao Wang, Justin Suess,
	linux-security-module, Samasth Norway Ananda, Matthieu Buffet,
	Mikhail Ivanov, konstantin.meskhidze, Demi Marie Obenour,
	Alyssa Ross, Jann Horn, Tahera Fahimi, Sebastian Andrzej Siewior,
	Kuniyuki Iwashima, Georgia Garcia, Simon Horman, netdev,
	Alexander Viro, Christian Brauner
In-Reply-To: <20260327164838.38231-2-gnoack3000@gmail.com>

On Fri, Mar 27, 2026 at 12:49 PM Günther Noack <gnoack3000@gmail.com> wrote:
>
> From: Justin Suess <utilityemal77@gmail.com>
>
> Add an LSM hook security_unix_find.
>
> This hook is called to check the path of a named UNIX socket before a
> connection is initiated. The peer socket may be inspected as well.
>
> Why existing hooks are unsuitable:
>
> Existing socket hooks, security_unix_stream_connect(),
> security_unix_may_send(), and security_socket_connect() don't provide
> TOCTOU-free / namespace independent access to the paths of sockets.
>
> (1) We cannot resolve the path from the struct sockaddr in existing hooks.
> This requires another path lookup. A change in the path between the
> two lookups will cause a TOCTOU bug.
>
> (2) We cannot use the struct path from the listening socket, because it
> may be bound to a path in a different namespace than the caller,
> resulting in a path that cannot be referenced at policy creation time.
>
> Consumers of the hook wishing to reference @other are responsible
> for acquiring the unix_state_lock and checking for the SOCK_DEAD flag
> therein, ensuring the socket hasn't died since lookup.
>
> Cc: Günther Noack <gnoack3000@gmail.com>
> Cc: Tingmao Wang <m@maowtm.org>
> Cc: Mickaël Salaün <mic@digikod.net>
> Cc: Paul Moore <paul@paul-moore.com>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>  include/linux/lsm_hook_defs.h |  5 +++++
>  include/linux/security.h      | 11 +++++++++++
>  net/unix/af_unix.c            | 10 +++++++---
>  security/security.c           | 20 ++++++++++++++++++++
>  4 files changed, 43 insertions(+), 3 deletions(-)

This patch doesn't look like it changed significantly in this
revision, is there a reason you dropped the tags from Georgia and I?

-- 
paul-moore.com

^ permalink raw reply

* Re: [PATCH 0/7] lsm: Replace security_sb_mount with granular mount hooks
From: Song Liu @ 2026-03-27 18:23 UTC (permalink / raw)
  To: Paul Moore
  Cc: linux-security-module, linux-fsdevel, selinux, apparmor,
	john.johansen, mic, jmorris, serge, viro, brauner, jack,
	stephen.smalley.work, omosnace, gnoack, takedakn, penguin-kernel,
	herton, kernel-team
In-Reply-To: <CAHC9VhQUU9L9OTtyo+2mdi0P5jCk1RXnJBxT1+YUyywMSfLuPg@mail.gmail.com>

On Thu, Mar 26, 2026 at 6:06 PM Paul Moore <paul@paul-moore.com> wrote:
>
> On Thu, Mar 26, 2026 at 8:31 PM Song Liu <song@kernel.org> wrote:
> >
> > Hi folks, especially SELinux, AppArmor, and LandLock maintainers,
> >
> > Could you please share your comments on this set?
>
> I think we are all aware of this patchset, but most of us are fairly
> busy at the moment.  This patchset is in my queue to review, but it
> isn't something I'm going to be able to review this week.
>
> > If there are no issues with these changes, can we land the set in 7.1
> > kernels?
>
> Given that we are near the end of -rc5, that is highly unlikely.
>
> https://github.com/LinuxSecurityModule/kernel?tab=readme-ov-file#kernel-development-process

Thanks for sharing this information! Looking forward to more detailed
review/feedback.

Song

^ permalink raw reply

* Re: [PATCH v3 4/9] lsm: framework for BPF integrity verification
From: Song Liu @ 2026-03-27 18:24 UTC (permalink / raw)
  To: Blaise Boscaccy
  Cc: Jonathan Corbet, Paul Moore, James Morris, Serge E. Hallyn,
	Mickaël Salaün, Günther Noack,
	Dr. David Alan Gilbert, Andrew Morton, James.Bottomley, dhowells,
	Fan Wu, Ryan Foster, Randy Dunlap, linux-security-module,
	linux-doc, linux-kernel, bpf
In-Reply-To: <871ph5f99z.fsf@microsoft.com>

On Fri, Mar 27, 2026 at 10:54 AM Blaise Boscaccy
<bboscaccy@linux.microsoft.com> wrote:
>
> Song Liu <song@kernel.org> writes:
>
> > On Wed, Mar 25, 2026 at 11:07 PM Blaise Boscaccy
> > <bboscaccy@linux.microsoft.com> wrote:
> > [...]
> >> The first new callback, bpf_prog_load_integrity(), located within the
> >> security_bpf_prog_load() hook, is necessary to ensure that the integrity
> >> verification callbacks are executed before any of the existing LSMs
> >> are executed via the bpf_prog_load() callback.  Reusing the existing
> >> bpf_prog_load() callback for integrity verification could result in LSMs
> >> not having access to the integrity verification results when asked to
> >> authorize the BPF program load in the bpf_prog_load() callback.
> >>
> >> The new LSM hook, security_bpf_prog_load_post_integrity(), is intended
> >> to be called from within LSMs performing BPF program integrity
> >> verification.  It is used to report the verdict of the integrity
> >> verification to other LSMs enforcing access control policy on BPF
> >> program loads.  LSMs enforcing such access controls should register a
> >> bpf_prog_load_post_integrity() callback to receive integrity verdicts.
> >
> > bpf_prog_load_post_integrity() is weird. Some questions about it:
> >
> > 1. Is it possible to call it from other LSMs (not hornet)? Specifically, is it
> >    possible to call it from BPF LSM?
>
> There is nothing hornet exclusive about that security hook. If the BPF
> LSM folks wanted to use it they would probably need to implement a
> kfunc to invoke it.

Please also include the kfunc in v4.

Thanks,
Song

^ permalink raw reply

* [PATCH v3 0/2] Fix incorrect overlayfs mmap() and mprotect() LSM access controls
From: Paul Moore @ 2026-03-27 22:04 UTC (permalink / raw)
  To: linux-security-module, selinux, linux-fsdevel, linux-unionfs,
	linux-erofs
  Cc: Amir Goldstein, Gao Xiang, Christian Brauner

A very minor update to the v2 patchset[2] posted earlier this week.  The
changelog is below.  The primary reason for posting such a lightly revised
patchset is to drop the "RFC" qualifier as I've had the opportunity to do
additional testing and I'm reasonably happy with the results.  As always,
anyone reading this is welcome, and encouraged, to do any additional
testing they believe might be helpful.

I plan to merge this into lsm/stable-7.0 either later tonight, or sometime
over the weekend, so the patchset has some time in linux-next.  As we're
fairly close to the v7.1 merge window, I may decide to hold this for Linus
until then; let's see how things turn out with linux-next as well as any
additional review comments.

[2] https://lore.kernel.org/linux-security-module/20260323042510.3331778-4-paul@paul-moore.com/

--
CHANGELOG:
v3:
- fix the LSM hook stubs (kernel robot, Ryan Lee)
- fix the lsm_backing_file_cache allocation size (Ryan Lee)
- minor style, simplicity tweaks to the SELinux patch
v2:
- remove the user O_PATH file patch from Amir
- add the backing_file LSM blob and lifecycle hooks
- update the SELinux code to reflect the other changes
v1:
- initial version

--
Paul Moore (2):
      lsm: add backing_file LSM hooks
      selinux: fix overlayfs mmap() and mprotect() access checks

 fs/backing-file.c                 |   18 +-
 fs/erofs/ishare.c                 |   10 +
 fs/file_table.c                   |   21 ++
 fs/fuse/passthrough.c             |    2 
 fs/internal.h                     |    3 
 fs/overlayfs/dir.c                |    2 
 fs/overlayfs/file.c               |    2 
 include/linux/backing-file.h      |    4 
 include/linux/fs.h                |    1 
 include/linux/lsm_audit.h         |    2 
 include/linux/lsm_hook_defs.h     |    5 
 include/linux/lsm_hooks.h         |    1 
 include/linux/security.h          |   22 ++
 security/lsm.h                    |    1 
 security/lsm_init.c               |    9 +
 security/security.c               |  100 +++++++++++
 security/selinux/hooks.c          |  256 +++++++++++++++++++++---------
 security/selinux/include/objsec.h |   17 +
 18 files changed, 389 insertions(+), 87 deletions(-)


^ permalink raw reply

* [PATCH v3 1/2] lsm: add backing_file LSM hooks
From: Paul Moore @ 2026-03-27 22:04 UTC (permalink / raw)
  To: linux-security-module, selinux, linux-fsdevel, linux-unionfs,
	linux-erofs
  Cc: Amir Goldstein, Gao Xiang, Christian Brauner
In-Reply-To: <20260327220446.353103-4-paul@paul-moore.com>

Stacked filesystems such as overlayfs do not currently provide the
necessary mechanisms for LSMs to properly enforce access controls on the
mmap() and mprotect() operations.  In order to resolve this gap, a LSM
security blob is being added to the backing_file struct and the following
new LSM hooks are being created:

 security_backing_file_alloc()
 security_backing_file_free()
 security_mmap_backing_file()

The first two hooks are to manage the lifecycle of the LSM security blob
in the backing_file struct, while the third provides a new mmap() access
control point for the underlying backing file.  It is also expected that
LSMs will likely want to update their security_file_mprotect() callback
to address issues with their mprotect() controls, but that does not
require a change to the security_file_mprotect() LSM hook.

There are a two other small changes to support these new LSM hooks.  We
pass the user file associated with a backing file down to
alloc_empty_backing_file() so it can be included in the
security_backing_file_alloc() hook, and we constify the file struct field
in the LSM common_audit_data struct to better support LSMs that need to
pass a const file struct pointer into the common LSM audit code.

Thanks to Arnd Bergmann for identifying the missing EXPORT_SYMBOL_GPL()
and supplying a fixup.

Cc: stable@vger.kernel.org
Acked-by: Christian Brauner <brauner@kernel.org>
Signed-off-by: Paul Moore <paul@paul-moore.com>
---
 fs/backing-file.c             |  18 ++++--
 fs/erofs/ishare.c             |  10 +++-
 fs/file_table.c               |  21 ++++++-
 fs/fuse/passthrough.c         |   2 +-
 fs/internal.h                 |   3 +-
 fs/overlayfs/dir.c            |   2 +-
 fs/overlayfs/file.c           |   2 +-
 include/linux/backing-file.h  |   4 +-
 include/linux/fs.h            |   1 +
 include/linux/lsm_audit.h     |   2 +-
 include/linux/lsm_hook_defs.h |   5 ++
 include/linux/lsm_hooks.h     |   1 +
 include/linux/security.h      |  22 ++++++++
 security/lsm.h                |   1 +
 security/lsm_init.c           |   9 +++
 security/security.c           | 100 ++++++++++++++++++++++++++++++++++
 16 files changed, 187 insertions(+), 16 deletions(-)

diff --git a/fs/backing-file.c b/fs/backing-file.c
index 45da8600d564..1f3bbfc75882 100644
--- a/fs/backing-file.c
+++ b/fs/backing-file.c
@@ -12,6 +12,7 @@
 #include <linux/backing-file.h>
 #include <linux/splice.h>
 #include <linux/mm.h>
+#include <linux/security.h>
 
 #include "internal.h"
 
@@ -29,14 +30,15 @@
  * returned file into a container structure that also stores the stacked
  * file's path, which can be retrieved using backing_file_user_path().
  */
-struct file *backing_file_open(const struct path *user_path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
 			       const struct path *real_path,
 			       const struct cred *cred)
 {
+	const struct path *user_path = &user_file->f_path;
 	struct file *f;
 	int error;
 
-	f = alloc_empty_backing_file(flags, cred);
+	f = alloc_empty_backing_file(flags, cred, user_file);
 	if (IS_ERR(f))
 		return f;
 
@@ -52,15 +54,16 @@ struct file *backing_file_open(const struct path *user_path, int flags,
 }
 EXPORT_SYMBOL_GPL(backing_file_open);
 
-struct file *backing_tmpfile_open(const struct path *user_path, int flags,
+struct file *backing_tmpfile_open(const struct file *user_file, int flags,
 				  const struct path *real_parentpath,
 				  umode_t mode, const struct cred *cred)
 {
 	struct mnt_idmap *real_idmap = mnt_idmap(real_parentpath->mnt);
+	const struct path *user_path = &user_file->f_path;
 	struct file *f;
 	int error;
 
-	f = alloc_empty_backing_file(flags, cred);
+	f = alloc_empty_backing_file(flags, cred, user_file);
 	if (IS_ERR(f))
 		return f;
 
@@ -336,8 +339,13 @@ int backing_file_mmap(struct file *file, struct vm_area_struct *vma,
 
 	vma_set_file(vma, file);
 
-	scoped_with_creds(ctx->cred)
+	scoped_with_creds(ctx->cred) {
+		ret = security_mmap_backing_file(vma, file, user_file);
+		if (ret)
+			return ret;
+
 		ret = vfs_mmap(vma->vm_file, vma);
+	}
 
 	if (ctx->accessed)
 		ctx->accessed(user_file);
diff --git a/fs/erofs/ishare.c b/fs/erofs/ishare.c
index 829d50d5c717..ec3fc5ac1a55 100644
--- a/fs/erofs/ishare.c
+++ b/fs/erofs/ishare.c
@@ -4,6 +4,7 @@
  */
 #include <linux/xxhash.h>
 #include <linux/mount.h>
+#include <linux/security.h>
 #include "internal.h"
 #include "xattr.h"
 
@@ -106,7 +107,8 @@ static int erofs_ishare_file_open(struct inode *inode, struct file *file)
 
 	if (file->f_flags & O_DIRECT)
 		return -EINVAL;
-	realfile = alloc_empty_backing_file(O_RDONLY|O_NOATIME, current_cred());
+	realfile = alloc_empty_backing_file(O_RDONLY|O_NOATIME, current_cred(),
+					    file);
 	if (IS_ERR(realfile))
 		return PTR_ERR(realfile);
 	ihold(sharedinode);
@@ -150,8 +152,14 @@ static ssize_t erofs_ishare_file_read_iter(struct kiocb *iocb,
 static int erofs_ishare_mmap(struct file *file, struct vm_area_struct *vma)
 {
 	struct file *realfile = file->private_data;
+	int err;
 
 	vma_set_file(vma, realfile);
+
+	err = security_mmap_backing_file(vma, realfile, file);
+	if (err)
+		return err;
+
 	return generic_file_readonly_mmap(file, vma);
 }
 
diff --git a/fs/file_table.c b/fs/file_table.c
index aaa5faaace1e..0bdc26cae138 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -50,6 +50,7 @@ struct backing_file {
 		struct path user_path;
 		freeptr_t bf_freeptr;
 	};
+	void *security;
 };
 
 #define backing_file(f) container_of(f, struct backing_file, file)
@@ -66,6 +67,11 @@ void backing_file_set_user_path(struct file *f, const struct path *path)
 }
 EXPORT_SYMBOL_GPL(backing_file_set_user_path);
 
+void *backing_file_security(const struct file *f)
+{
+	return backing_file(f)->security;
+}
+
 static inline void file_free(struct file *f)
 {
 	security_file_free(f);
@@ -73,8 +79,11 @@ static inline void file_free(struct file *f)
 		percpu_counter_dec(&nr_files);
 	put_cred(f->f_cred);
 	if (unlikely(f->f_mode & FMODE_BACKING)) {
-		path_put(backing_file_user_path(f));
-		kmem_cache_free(bfilp_cachep, backing_file(f));
+		struct backing_file *ff = backing_file(f);
+
+		security_backing_file_free(&ff->security);
+		path_put(&ff->user_path);
+		kmem_cache_free(bfilp_cachep, ff);
 	} else {
 		kmem_cache_free(filp_cachep, f);
 	}
@@ -290,7 +299,8 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
  * This is only for kernel internal use, and the allocate file must not be
  * installed into file tables or such.
  */
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+				      const struct file *user_file)
 {
 	struct backing_file *ff;
 	int error;
@@ -306,6 +316,11 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
 	}
 
 	ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
+	error = security_backing_file_alloc(&ff->security, user_file);
+	if (unlikely(error)) {
+		fput(&ff->file);
+		return ERR_PTR(error);
+	}
 	return &ff->file;
 }
 EXPORT_SYMBOL_GPL(alloc_empty_backing_file);
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 72de97c03d0e..f2d08ac2459b 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -167,7 +167,7 @@ struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id)
 		goto out;
 
 	/* Allocate backing file per fuse file to store fuse path */
-	backing_file = backing_file_open(&file->f_path, file->f_flags,
+	backing_file = backing_file_open(file, file->f_flags,
 					 &fb->file->f_path, fb->cred);
 	err = PTR_ERR(backing_file);
 	if (IS_ERR(backing_file)) {
diff --git a/fs/internal.h b/fs/internal.h
index cbc384a1aa09..77e90e4124e0 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -106,7 +106,8 @@ extern void chroot_fs_refs(const struct path *, const struct path *);
  */
 struct file *alloc_empty_file(int flags, const struct cred *cred);
 struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred);
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred);
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+				      const struct file *user_file);
 void backing_file_set_user_path(struct file *f, const struct path *path);
 
 static inline void file_put_write_access(struct file *file)
diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c
index ff3dbd1ca61f..f2f20a611af3 100644
--- a/fs/overlayfs/dir.c
+++ b/fs/overlayfs/dir.c
@@ -1374,7 +1374,7 @@ static int ovl_create_tmpfile(struct file *file, struct dentry *dentry,
 				return PTR_ERR(cred);
 
 			ovl_path_upper(dentry->d_parent, &realparentpath);
-			realfile = backing_tmpfile_open(&file->f_path, flags, &realparentpath,
+			realfile = backing_tmpfile_open(file, flags, &realparentpath,
 							mode, current_cred());
 			err = PTR_ERR_OR_ZERO(realfile);
 			pr_debug("tmpfile/open(%pd2, 0%o) = %i\n", realparentpath.dentry, mode, err);
diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c
index 97bed2286030..27cc07738f33 100644
--- a/fs/overlayfs/file.c
+++ b/fs/overlayfs/file.c
@@ -48,7 +48,7 @@ static struct file *ovl_open_realfile(const struct file *file,
 			if (!inode_owner_or_capable(real_idmap, realinode))
 				flags &= ~O_NOATIME;
 
-			realfile = backing_file_open(file_user_path(file),
+			realfile = backing_file_open(file,
 						     flags, realpath, current_cred());
 		}
 	}
diff --git a/include/linux/backing-file.h b/include/linux/backing-file.h
index 1476a6ed1bfd..c939cd222730 100644
--- a/include/linux/backing-file.h
+++ b/include/linux/backing-file.h
@@ -18,10 +18,10 @@ struct backing_file_ctx {
 	void (*end_write)(struct kiocb *iocb, ssize_t);
 };
 
-struct file *backing_file_open(const struct path *user_path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
 			       const struct path *real_path,
 			       const struct cred *cred);
-struct file *backing_tmpfile_open(const struct path *user_path, int flags,
+struct file *backing_tmpfile_open(const struct file *user_file, int flags,
 				  const struct path *real_parentpath,
 				  umode_t mode, const struct cred *cred);
 ssize_t backing_file_read_iter(struct file *file, struct iov_iter *iter,
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 8b3dd145b25e..8f5702cfb5e0 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2474,6 +2474,7 @@ struct file *dentry_open_nonotify(const struct path *path, int flags,
 struct file *dentry_create(struct path *path, int flags, umode_t mode,
 			   const struct cred *cred);
 const struct path *backing_file_user_path(const struct file *f);
+void *backing_file_security(const struct file *f);
 
 /*
  * When mmapping a file on a stackable filesystem (e.g., overlayfs), the file
diff --git a/include/linux/lsm_audit.h b/include/linux/lsm_audit.h
index 382c56a97bba..584db296e43b 100644
--- a/include/linux/lsm_audit.h
+++ b/include/linux/lsm_audit.h
@@ -94,7 +94,7 @@ struct common_audit_data {
 #endif
 		char *kmod_name;
 		struct lsm_ioctlop_audit *op;
-		struct file *file;
+		const struct file *file;
 		struct lsm_ibpkey_audit *ibpkey;
 		struct lsm_ibendport_audit *ibendport;
 		int reason;
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 8c42b4bde09c..2c4da40757ad 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -191,6 +191,9 @@ LSM_HOOK(int, 0, file_permission, struct file *file, int mask)
 LSM_HOOK(int, 0, file_alloc_security, struct file *file)
 LSM_HOOK(void, LSM_RET_VOID, file_release, struct file *file)
 LSM_HOOK(void, LSM_RET_VOID, file_free_security, struct file *file)
+LSM_HOOK(int, 0, backing_file_alloc, void *backing_file_blobp,
+	 const struct file *user_file)
+LSM_HOOK(void, LSM_RET_VOID, backing_file_free, void *backing_file_blobp)
 LSM_HOOK(int, 0, file_ioctl, struct file *file, unsigned int cmd,
 	 unsigned long arg)
 LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
@@ -198,6 +201,8 @@ LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
 LSM_HOOK(int, 0, mmap_addr, unsigned long addr)
 LSM_HOOK(int, 0, mmap_file, struct file *file, unsigned long reqprot,
 	 unsigned long prot, unsigned long flags)
+LSM_HOOK(int, 0, mmap_backing_file, struct vm_area_struct *vma,
+	 struct file *backing_file, struct file *user_file)
 LSM_HOOK(int, 0, file_mprotect, struct vm_area_struct *vma,
 	 unsigned long reqprot, unsigned long prot)
 LSM_HOOK(int, 0, file_lock, struct file *file, unsigned int cmd)
diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h
index d48bf0ad26f4..b4f8cad53ddb 100644
--- a/include/linux/lsm_hooks.h
+++ b/include/linux/lsm_hooks.h
@@ -104,6 +104,7 @@ struct security_hook_list {
 struct lsm_blob_sizes {
 	unsigned int lbs_cred;
 	unsigned int lbs_file;
+	unsigned int lbs_backing_file;
 	unsigned int lbs_ib;
 	unsigned int lbs_inode;
 	unsigned int lbs_sock;
diff --git a/include/linux/security.h b/include/linux/security.h
index 83a646d72f6f..0a726bb70479 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -471,11 +471,17 @@ int security_file_permission(struct file *file, int mask);
 int security_file_alloc(struct file *file);
 void security_file_release(struct file *file);
 void security_file_free(struct file *file);
+int security_backing_file_alloc(void **backing_file_blobp,
+				const struct file *user_file);
+void security_backing_file_free(void **backing_file_blobp);
 int security_file_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
 int security_file_ioctl_compat(struct file *file, unsigned int cmd,
 			       unsigned long arg);
 int security_mmap_file(struct file *file, unsigned long prot,
 			unsigned long flags);
+int security_mmap_backing_file(struct vm_area_struct *vma,
+			       struct file *backing_file,
+			       struct file *user_file);
 int security_mmap_addr(unsigned long addr);
 int security_file_mprotect(struct vm_area_struct *vma, unsigned long reqprot,
 			   unsigned long prot);
@@ -1140,6 +1146,15 @@ static inline void security_file_release(struct file *file)
 static inline void security_file_free(struct file *file)
 { }
 
+static inline int security_backing_file_alloc(void **backing_file_blobp,
+					      const struct file *user_file)
+{
+	return 0;
+}
+
+static inline void security_backing_file_free(void **backing_file_blobp)
+{ }
+
 static inline int security_file_ioctl(struct file *file, unsigned int cmd,
 				      unsigned long arg)
 {
@@ -1159,6 +1174,13 @@ static inline int security_mmap_file(struct file *file, unsigned long prot,
 	return 0;
 }
 
+static inline int security_mmap_backing_file(struct vm_area_struct *vma,
+					     struct file *backing_file,
+					     struct file *user_file)
+{
+	return 0;
+}
+
 static inline int security_mmap_addr(unsigned long addr)
 {
 	return cap_mmap_addr(addr);
diff --git a/security/lsm.h b/security/lsm.h
index db77cc83e158..32f808ad4335 100644
--- a/security/lsm.h
+++ b/security/lsm.h
@@ -29,6 +29,7 @@ extern struct lsm_blob_sizes blob_sizes;
 
 /* LSM blob caches */
 extern struct kmem_cache *lsm_file_cache;
+extern struct kmem_cache *lsm_backing_file_cache;
 extern struct kmem_cache *lsm_inode_cache;
 
 /* LSM blob allocators */
diff --git a/security/lsm_init.c b/security/lsm_init.c
index 573e2a7250c4..7c0fd17f1601 100644
--- a/security/lsm_init.c
+++ b/security/lsm_init.c
@@ -293,6 +293,8 @@ static void __init lsm_prepare(struct lsm_info *lsm)
 	blobs = lsm->blobs;
 	lsm_blob_size_update(&blobs->lbs_cred, &blob_sizes.lbs_cred);
 	lsm_blob_size_update(&blobs->lbs_file, &blob_sizes.lbs_file);
+	lsm_blob_size_update(&blobs->lbs_backing_file,
+			     &blob_sizes.lbs_backing_file);
 	lsm_blob_size_update(&blobs->lbs_ib, &blob_sizes.lbs_ib);
 	/* inode blob gets an rcu_head in addition to LSM blobs. */
 	if (blobs->lbs_inode && blob_sizes.lbs_inode == 0)
@@ -441,6 +443,8 @@ int __init security_init(void)
 	if (lsm_debug) {
 		lsm_pr("blob(cred) size %d\n", blob_sizes.lbs_cred);
 		lsm_pr("blob(file) size %d\n", blob_sizes.lbs_file);
+		lsm_pr("blob(backing_file) size %d\n",
+		       blob_sizes.lbs_backing_file);
 		lsm_pr("blob(ib) size %d\n", blob_sizes.lbs_ib);
 		lsm_pr("blob(inode) size %d\n", blob_sizes.lbs_inode);
 		lsm_pr("blob(ipc) size %d\n", blob_sizes.lbs_ipc);
@@ -462,6 +466,11 @@ int __init security_init(void)
 		lsm_file_cache = kmem_cache_create("lsm_file_cache",
 						   blob_sizes.lbs_file, 0,
 						   SLAB_PANIC, NULL);
+	if (blob_sizes.lbs_backing_file)
+		lsm_backing_file_cache = kmem_cache_create(
+						   "lsm_backing_file_cache",
+						   blob_sizes.lbs_backing_file,
+						   0, SLAB_PANIC, NULL);
 	if (blob_sizes.lbs_inode)
 		lsm_inode_cache = kmem_cache_create("lsm_inode_cache",
 						    blob_sizes.lbs_inode, 0,
diff --git a/security/security.c b/security/security.c
index 67af9228c4e9..651a0d643c9f 100644
--- a/security/security.c
+++ b/security/security.c
@@ -81,6 +81,7 @@ const struct lsm_id *lsm_idlist[MAX_LSM_COUNT];
 struct lsm_blob_sizes blob_sizes;
 
 struct kmem_cache *lsm_file_cache;
+struct kmem_cache *lsm_backing_file_cache;
 struct kmem_cache *lsm_inode_cache;
 
 #define SECURITY_HOOK_ACTIVE_KEY(HOOK, IDX) security_hook_active_##HOOK##_##IDX
@@ -172,6 +173,28 @@ static int lsm_file_alloc(struct file *file)
 	return 0;
 }
 
+/**
+ * lsm_backing_file_alloc - allocate a composite backing file blob
+ * @backing_file_blobp: pointer to the backing file LSM blob pointer
+ *
+ * Allocate the backing file blob for all the modules.
+ *
+ * Returns 0, or -ENOMEM if memory can't be allocated.
+ */
+static int lsm_backing_file_alloc(void **backing_file_blobp)
+{
+	if (!lsm_backing_file_cache) {
+		*backing_file_blobp = NULL;
+		return 0;
+	}
+
+	*backing_file_blobp = kmem_cache_zalloc(lsm_backing_file_cache,
+						GFP_KERNEL);
+	if (*backing_file_blobp == NULL)
+		return -ENOMEM;
+	return 0;
+}
+
 /**
  * lsm_blob_alloc - allocate a composite blob
  * @dest: the destination for the blob
@@ -2417,6 +2440,57 @@ void security_file_free(struct file *file)
 	}
 }
 
+/**
+ * security_backing_file_alloc() - Allocate and setup a backing file blob
+ * @backing_file_blobp: pointer to the backing file LSM blob pointer
+ * @user_file: the associated user visible file
+ *
+ * Allocate a backing file LSM blob and perform any necessary initialization of
+ * the LSM blob.  There will be some operations where the LSM will not have
+ * access to @user_file after this point, so any important state associated
+ * with @user_file that is important to the LSM should be captured in the
+ * backing file's LSM blob.
+ *
+ * LSM's should avoid taking a reference to @user_file in this hook as it will
+ * result in problems later when the system attempts to drop/put the file
+ * references due to a circular dependency.
+ *
+ * Return: Return 0 if the hook is successful, negative values otherwise.
+ */
+int security_backing_file_alloc(void **backing_file_blobp,
+				const struct file *user_file)
+{
+	int rc;
+
+	rc = lsm_backing_file_alloc(backing_file_blobp);
+	if (rc)
+		return rc;
+	rc = call_int_hook(backing_file_alloc, *backing_file_blobp, user_file);
+	if (unlikely(rc))
+		security_backing_file_free(backing_file_blobp);
+
+	return rc;
+}
+
+/**
+ * security_backing_file_free() - Free a backing file blob
+ * @backing_file_blobp: pointer to the backing file LSM blob pointer
+ *
+ * Free any LSM state associate with a backing file's LSM blob, including the
+ * blob itself.
+ */
+void security_backing_file_free(void **backing_file_blobp)
+{
+	void *backing_file_blob = *backing_file_blobp;
+
+	call_void_hook(backing_file_free, backing_file_blob);
+
+	if (backing_file_blob) {
+		*backing_file_blobp = NULL;
+		kmem_cache_free(lsm_backing_file_cache, backing_file_blob);
+	}
+}
+
 /**
  * security_file_ioctl() - Check if an ioctl is allowed
  * @file: associated file
@@ -2505,6 +2579,32 @@ int security_mmap_file(struct file *file, unsigned long prot,
 			     flags);
 }
 
+/**
+ * security_mmap_backing_file - Check if mmap'ing a backing file is allowed
+ * @vma: the vm_area_struct for the mmap'd region
+ * @backing_file: the backing file being mmap'd
+ * @user_file: the user file being mmap'd
+ *
+ * Check permissions for a mmap operation on a stacked filesystem.  This hook
+ * is called after the security_mmap_file() and is responsible for authorizing
+ * the mmap on @backing_file.  It is important to note that the mmap operation
+ * on @user_file has already been authorized and the @vma->vm_file has been
+ * set to @backing_file.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_mmap_backing_file(struct vm_area_struct *vma,
+			       struct file *backing_file,
+			       struct file *user_file)
+{
+	/* recommended by the stackable filesystem devs */
+	if (WARN_ON_ONCE(!(backing_file->f_mode & FMODE_BACKING)))
+		return -EIO;
+
+	return call_int_hook(mmap_backing_file, vma, backing_file, user_file);
+}
+EXPORT_SYMBOL_GPL(security_mmap_backing_file);
+
 /**
  * security_mmap_addr() - Check if mmap'ing an address is allowed
  * @addr: address
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 2/2] selinux: fix overlayfs mmap() and mprotect() access checks
From: Paul Moore @ 2026-03-27 22:04 UTC (permalink / raw)
  To: linux-security-module, selinux, linux-fsdevel, linux-unionfs,
	linux-erofs
  Cc: Amir Goldstein, Gao Xiang, Christian Brauner
In-Reply-To: <20260327220446.353103-4-paul@paul-moore.com>

The existing SELinux security model for overlayfs is to allow access if
the current task is able to access the top level file (the "user" file)
and the mounter's credentials are sufficient to access the lower
level file (the "backing" file).  Unfortunately, the current code does
not properly enforce these access controls for both mmap() and mprotect()
operations on overlayfs filesystems.

This patch makes use of the newly created security_mmap_backing_file()
LSM hook to provide the missing backing file enforcement for mmap()
operations, and leverages the backing file API and new LSM blob to
provide the necessary information to properly enforce the mprotect()
access controls.

Cc: stable@vger.kernel.org
Signed-off-by: Paul Moore <paul@paul-moore.com>
---
 security/selinux/hooks.c          | 256 +++++++++++++++++++++---------
 security/selinux/include/objsec.h |  17 ++
 2 files changed, 202 insertions(+), 71 deletions(-)

diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index d8224ea113d1..d8557da79480 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -1745,6 +1745,60 @@ static inline int file_path_has_perm(const struct cred *cred,
 static int bpf_fd_pass(const struct file *file, u32 sid);
 #endif
 
+static int __file_has_perm(const struct cred *cred, const struct file *file,
+			   u32 av, bool bf_user_file)
+
+{
+	struct common_audit_data ad;
+	struct inode *inode;
+	u32 ssid = cred_sid(cred);
+	u32 tsid_fd;
+	int rc;
+
+	if (bf_user_file) {
+		struct backing_file_security_struct *bfsec;
+		const struct path *path;
+
+		if (WARN_ON(!(file->f_mode & FMODE_BACKING)))
+			return -EIO;
+
+		bfsec = selinux_backing_file(file);
+		path = backing_file_user_path(file);
+		tsid_fd = bfsec->uf_sid;
+		inode = d_inode(path->dentry);
+
+		ad.type = LSM_AUDIT_DATA_PATH;
+		ad.u.path = *path;
+	} else {
+		struct file_security_struct *fsec = selinux_file(file);
+
+		tsid_fd = fsec->sid;
+		inode = file_inode(file);
+
+		ad.type = LSM_AUDIT_DATA_FILE;
+		ad.u.file = file;
+	}
+
+	if (ssid != tsid_fd) {
+		rc = avc_has_perm(ssid, tsid_fd, SECCLASS_FD, FD__USE, &ad);
+		if (rc)
+			return rc;
+	}
+
+#ifdef CONFIG_BPF_SYSCALL
+	/* regardless of backing vs user file, use the underlying file here */
+	rc = bpf_fd_pass(file, ssid);
+	if (rc)
+		return rc;
+#endif
+
+	/* av is zero if only checking access to the descriptor. */
+	if (av)
+		return inode_has_perm(cred, inode, av, &ad);
+
+	return 0;
+}
+
 /* Check whether a task can use an open file descriptor to
    access an inode in a given way.  Check access to the
    descriptor itself, and then use dentry_has_perm to
@@ -1753,41 +1807,10 @@ static int bpf_fd_pass(const struct file *file, u32 sid);
    has the same SID as the process.  If av is zero, then
    access to the file is not checked, e.g. for cases
    where only the descriptor is affected like seek. */
-static int file_has_perm(const struct cred *cred,
-			 struct file *file,
-			 u32 av)
+static inline int file_has_perm(const struct cred *cred,
+				const struct file *file, u32 av)
 {
-	struct file_security_struct *fsec = selinux_file(file);
-	struct inode *inode = file_inode(file);
-	struct common_audit_data ad;
-	u32 sid = cred_sid(cred);
-	int rc;
-
-	ad.type = LSM_AUDIT_DATA_FILE;
-	ad.u.file = file;
-
-	if (sid != fsec->sid) {
-		rc = avc_has_perm(sid, fsec->sid,
-				  SECCLASS_FD,
-				  FD__USE,
-				  &ad);
-		if (rc)
-			goto out;
-	}
-
-#ifdef CONFIG_BPF_SYSCALL
-	rc = bpf_fd_pass(file, cred_sid(cred));
-	if (rc)
-		return rc;
-#endif
-
-	/* av is zero if only checking access to the descriptor. */
-	rc = 0;
-	if (av)
-		rc = inode_has_perm(cred, inode, av, &ad);
-
-out:
-	return rc;
+	return __file_has_perm(cred, file, av, false);
 }
 
 /*
@@ -3825,6 +3848,17 @@ static int selinux_file_alloc_security(struct file *file)
 	return 0;
 }
 
+static int selinux_backing_file_alloc(void *backing_file_blob,
+				      const struct file *user_file)
+{
+	struct backing_file_security_struct *bfsec;
+
+	bfsec = selinux_backing_file_raw(backing_file_blob);
+	bfsec->uf_sid = selinux_file(user_file)->sid;
+
+	return 0;
+}
+
 /*
  * Check whether a task has the ioctl permission and cmd
  * operation to an inode.
@@ -3942,42 +3976,55 @@ static int selinux_file_ioctl_compat(struct file *file, unsigned int cmd,
 
 static int default_noexec __ro_after_init;
 
-static int file_map_prot_check(struct file *file, unsigned long prot, int shared)
+static int __file_map_prot_check(const struct cred *cred,
+				 const struct file *file, unsigned long prot,
+				 bool shared, bool bf_user_file)
 {
-	const struct cred *cred = current_cred();
-	u32 sid = cred_sid(cred);
-	int rc = 0;
+	struct inode *inode = NULL;
+	bool prot_exec = prot & PROT_EXEC;
+	bool prot_write = prot & PROT_WRITE;
+
+	if (file) {
+		if (bf_user_file)
+			inode = d_inode(backing_file_user_path(file)->dentry);
+		else
+			inode = file_inode(file);
+	}
+
+	if (default_noexec && prot_exec &&
+	    (!file || IS_PRIVATE(inode) || (!shared && prot_write))) {
+		int rc;
+		u32 sid = cred_sid(cred);
 
-	if (default_noexec &&
-	    (prot & PROT_EXEC) && (!file || IS_PRIVATE(file_inode(file)) ||
-				   (!shared && (prot & PROT_WRITE)))) {
 		/*
-		 * We are making executable an anonymous mapping or a
-		 * private file mapping that will also be writable.
-		 * This has an additional check.
+		 * We are making executable an anonymous mapping or a private
+		 * file mapping that will also be writable.
 		 */
-		rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
-				  PROCESS__EXECMEM, NULL);
+		rc = avc_has_perm(sid, sid, SECCLASS_PROCESS, PROCESS__EXECMEM,
+				  NULL);
 		if (rc)
-			goto error;
+			return rc;
 	}
 
 	if (file) {
-		/* read access is always possible with a mapping */
+		/* "read" always possible, "write" only if shared */
 		u32 av = FILE__READ;
-
-		/* write access only matters if the mapping is shared */
-		if (shared && (prot & PROT_WRITE))
+		if (shared && prot_write)
 			av |= FILE__WRITE;
-
-		if (prot & PROT_EXEC)
+		if (prot_exec)
 			av |= FILE__EXECUTE;
 
-		return file_has_perm(cred, file, av);
+		return __file_has_perm(cred, file, av, bf_user_file);
 	}
 
-error:
-	return rc;
+	return 0;
+}
+
+static inline int file_map_prot_check(const struct cred *cred,
+				      const struct file *file,
+				      unsigned long prot, bool shared)
+{
+	return __file_map_prot_check(cred, file, prot, shared, false);
 }
 
 static int selinux_mmap_addr(unsigned long addr)
@@ -3993,36 +4040,80 @@ static int selinux_mmap_addr(unsigned long addr)
 	return rc;
 }
 
-static int selinux_mmap_file(struct file *file,
-			     unsigned long reqprot __always_unused,
-			     unsigned long prot, unsigned long flags)
+static int selinux_mmap_file_common(const struct cred *cred, struct file *file,
+				    unsigned long prot, bool shared)
 {
-	struct common_audit_data ad;
-	int rc;
-
 	if (file) {
+		int rc;
+		struct common_audit_data ad;
+
 		ad.type = LSM_AUDIT_DATA_FILE;
 		ad.u.file = file;
-		rc = inode_has_perm(current_cred(), file_inode(file),
-				    FILE__MAP, &ad);
+		rc = inode_has_perm(cred, file_inode(file), FILE__MAP, &ad);
 		if (rc)
 			return rc;
 	}
 
-	return file_map_prot_check(file, prot,
-				   (flags & MAP_TYPE) == MAP_SHARED);
+	return file_map_prot_check(cred, file, prot, shared);
+}
+
+static int selinux_mmap_file(struct file *file,
+			     unsigned long reqprot __always_unused,
+			     unsigned long prot, unsigned long flags)
+{
+	return selinux_mmap_file_common(current_cred(), file, prot,
+					(flags & MAP_TYPE) == MAP_SHARED);
+}
+
+/**
+ * selinux_mmap_backing_file - Check mmap permissions on a backing file
+ * @vma: memory region
+ * @backing_file: stacked filesystem backing file
+ * @user_file: user visible file
+ *
+ * This is called after selinux_mmap_file() on stacked filesystems, and it
+ * is this function's responsibility to verify access to @backing_file and
+ * setup the SELinux state for possible later use in the mprotect() code path.
+ *
+ * By the time this function is called, mmap() access to @user_file has already
+ * been authorized and @vma->vm_file has been set to point to @backing_file.
+ *
+ * Return zero on success, negative values otherwise.
+ */
+static int selinux_mmap_backing_file(struct vm_area_struct *vma,
+				     struct file *backing_file,
+				     struct file *user_file __always_unused)
+{
+	unsigned long prot = 0;
+
+	/* translate vma->vm_flags perms into PROT perms */
+	if (vma->vm_flags & VM_READ)
+		prot |= PROT_READ;
+	if (vma->vm_flags & VM_WRITE)
+		prot |= PROT_WRITE;
+	if (vma->vm_flags & VM_EXEC)
+		prot |= PROT_EXEC;
+
+	return selinux_mmap_file_common(backing_file->f_cred, backing_file,
+					prot, vma->vm_flags & VM_SHARED);
 }
 
 static int selinux_file_mprotect(struct vm_area_struct *vma,
 				 unsigned long reqprot __always_unused,
 				 unsigned long prot)
 {
+	int rc;
 	const struct cred *cred = current_cred();
 	u32 sid = cred_sid(cred);
+	const struct file *file = vma->vm_file;
+	bool backing_file;
+	bool shared = vma->vm_flags & VM_SHARED;
+
+	/* check if we need to trigger the "backing files are awful" mode */
+	backing_file = file && (file->f_mode & FMODE_BACKING);
 
 	if (default_noexec &&
 	    (prot & PROT_EXEC) && !(vma->vm_flags & VM_EXEC)) {
-		int rc = 0;
 		/*
 		 * We don't use the vma_is_initial_heap() helper as it has
 		 * a history of problems and is currently broken on systems
@@ -4036,11 +4127,15 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
 		    vma->vm_end <= vma->vm_mm->brk) {
 			rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
 					  PROCESS__EXECHEAP, NULL);
-		} else if (!vma->vm_file && (vma_is_initial_stack(vma) ||
+			if (rc)
+				return rc;
+		} else if (!file && (vma_is_initial_stack(vma) ||
 			    vma_is_stack_for_current(vma))) {
 			rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
 					  PROCESS__EXECSTACK, NULL);
-		} else if (vma->vm_file && vma->anon_vma) {
+			if (rc)
+				return rc;
+		} else if (file && vma->anon_vma) {
 			/*
 			 * We are making executable a file mapping that has
 			 * had some COW done. Since pages might have been
@@ -4048,13 +4143,29 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
 			 * modified content.  This typically should only
 			 * occur for text relocations.
 			 */
-			rc = file_has_perm(cred, vma->vm_file, FILE__EXECMOD);
+			rc = __file_has_perm(cred, file, FILE__EXECMOD,
+					     backing_file);
+			if (rc)
+				return rc;
+			if (backing_file) {
+				rc = file_has_perm(file->f_cred, file,
+						   FILE__EXECMOD);
+				if (rc)
+					return rc;
+			}
 		}
+	}
+
+	rc = __file_map_prot_check(cred, file, prot, shared, backing_file);
+	if (rc)
+		return rc;
+	if (backing_file) {
+		rc = file_map_prot_check(file->f_cred, file, prot, shared);
 		if (rc)
 			return rc;
 	}
 
-	return file_map_prot_check(vma->vm_file, prot, vma->vm_flags&VM_SHARED);
+	return 0;
 }
 
 static int selinux_file_lock(struct file *file, unsigned int cmd)
@@ -7393,6 +7504,7 @@ struct lsm_blob_sizes selinux_blob_sizes __ro_after_init = {
 	.lbs_cred = sizeof(struct cred_security_struct),
 	.lbs_task = sizeof(struct task_security_struct),
 	.lbs_file = sizeof(struct file_security_struct),
+	.lbs_backing_file = sizeof(struct backing_file_security_struct),
 	.lbs_inode = sizeof(struct inode_security_struct),
 	.lbs_ipc = sizeof(struct ipc_security_struct),
 	.lbs_key = sizeof(struct key_security_struct),
@@ -7498,9 +7610,11 @@ static struct security_hook_list selinux_hooks[] __ro_after_init = {
 
 	LSM_HOOK_INIT(file_permission, selinux_file_permission),
 	LSM_HOOK_INIT(file_alloc_security, selinux_file_alloc_security),
+	LSM_HOOK_INIT(backing_file_alloc, selinux_backing_file_alloc),
 	LSM_HOOK_INIT(file_ioctl, selinux_file_ioctl),
 	LSM_HOOK_INIT(file_ioctl_compat, selinux_file_ioctl_compat),
 	LSM_HOOK_INIT(mmap_file, selinux_mmap_file),
+	LSM_HOOK_INIT(mmap_backing_file, selinux_mmap_backing_file),
 	LSM_HOOK_INIT(mmap_addr, selinux_mmap_addr),
 	LSM_HOOK_INIT(file_mprotect, selinux_file_mprotect),
 	LSM_HOOK_INIT(file_lock, selinux_file_lock),
diff --git a/security/selinux/include/objsec.h b/security/selinux/include/objsec.h
index 5bddd28ea5cb..8ec493064aa2 100644
--- a/security/selinux/include/objsec.h
+++ b/security/selinux/include/objsec.h
@@ -88,6 +88,10 @@ struct file_security_struct {
 	u32 pseqno; /* Policy seqno at the time of file open */
 };
 
+struct backing_file_security_struct {
+	u32 uf_sid; /* associated user file fsec->sid */
+};
+
 struct superblock_security_struct {
 	u32 sid; /* SID of file system superblock */
 	u32 def_sid; /* default SID for labeling */
@@ -195,6 +199,19 @@ static inline struct file_security_struct *selinux_file(const struct file *file)
 	return file->f_security + selinux_blob_sizes.lbs_file;
 }
 
+static inline struct backing_file_security_struct *
+selinux_backing_file_raw(void *blob)
+{
+	return blob + selinux_blob_sizes.lbs_backing_file;
+}
+
+static inline struct backing_file_security_struct *
+selinux_backing_file(const struct file *backing_file)
+{
+	void *blob = backing_file_security(backing_file);
+	return selinux_backing_file_raw(blob);
+}
+
 static inline struct inode_security_struct *
 selinux_inode(const struct inode *inode)
 {
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v3 6/9] security: Hornet LSM
From: kernel test robot @ 2026-03-28  2:55 UTC (permalink / raw)
  To: Blaise Boscaccy, Jonathan Corbet, Paul Moore, James Morris,
	Serge E. Hallyn, Mickaël Salaün, Günther Noack,
	Dr. David Alan Gilbert, Andrew Morton, James.Bottomley, dhowells,
	Fan Wu, Ryan Foster, Randy Dunlap, linux-security-module,
	linux-doc, linux-kernel, bpf
  Cc: oe-kbuild-all, Linux Memory Management List
In-Reply-To: <20260326060655.2550595-7-bboscaccy@linux.microsoft.com>

Hi Blaise,

kernel test robot noticed the following build errors:

[auto build test ERROR on herbert-cryptodev-2.6/master]
[also build test ERROR on herbert-crypto-2.6/master shuah-kselftest/next shuah-kselftest/fixes linus/master v7.0-rc5 next-20260327]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]

url:    https://github.com/intel-lab-lkp/linux/commits/Blaise-Boscaccy/crypto-pkcs7-add-flag-for-validated-trust-on-a-signed-info-block/20260327-145024
base:   https://git.kernel.org/pub/scm/linux/kernel/git/herbert/cryptodev-2.6.git master
patch link:    https://lore.kernel.org/r/20260326060655.2550595-7-bboscaccy%40linux.microsoft.com
patch subject: [PATCH v3 6/9] security: Hornet LSM
config: x86_64-randconfig-102-20260328 (https://download.01.org/0day-ci/archive/20260328/202603281030.AIoqyOy3-lkp@intel.com/config)
compiler: clang version 20.1.8 (https://github.com/llvm/llvm-project 87f0227cb60147a26a1eeb4fb06e3b505e9c7261)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20260328/202603281030.AIoqyOy3-lkp@intel.com/reproduce)

If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202603281030.AIoqyOy3-lkp@intel.com/

All errors (new ones prefixed by >>):

>> security/hornet/hornet_lsm.c:194:6: error: call to undeclared function 'verify_pkcs7_message_sig'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
     194 |         if (verify_pkcs7_message_sig(prog->insnsi, prog->len * sizeof(struct bpf_insn), msg,
         |             ^
   1 error generated.


vim +/verify_pkcs7_message_sig +194 security/hornet/hornet_lsm.c

   155	
   156	static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
   157					struct bpf_token *token, bool is_kernel,
   158					enum lsm_integrity_verdict *verdict)
   159	{
   160		struct hornet_maps maps = {0};
   161		bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
   162		struct pkcs7_message *msg;
   163		struct hornet_parse_context *ctx;
   164		void *sig;
   165		int err;
   166		const void *authattrs;
   167		size_t authattrs_len;
   168	
   169		if (!attr->signature) {
   170			*verdict = LSM_INT_VERDICT_UNSIGNED;
   171			return 0;
   172		}
   173	
   174		ctx = kzalloc(sizeof(struct hornet_parse_context), GFP_KERNEL);
   175		if (!ctx)
   176			return -ENOMEM;
   177	
   178		maps.fd_array = make_bpfptr(attr->fd_array, is_kernel);
   179		sig = kzalloc(attr->signature_size, GFP_KERNEL);
   180		if (!sig) {
   181			err = -ENOMEM;
   182			goto out;
   183		}
   184		err = copy_from_bpfptr(sig, usig, attr->signature_size);
   185		if (err != 0)
   186			goto cleanup_sig;
   187	
   188		msg = pkcs7_parse_message(sig, attr->signature_size);
   189		if (IS_ERR(msg)) {
   190			err = LSM_INT_VERDICT_BADSIG;
   191			goto cleanup_sig;
   192		}
   193	
 > 194		if (verify_pkcs7_message_sig(prog->insnsi, prog->len * sizeof(struct bpf_insn), msg,
   195					     VERIFY_USE_SECONDARY_KEYRING,
   196					     VERIFYING_BPF_SIGNATURE,
   197					     NULL, NULL)) {
   198			err = LSM_INT_VERDICT_UNKNOWNKEY;
   199			goto cleanup_msg;
   200		}
   201	
   202		if (pkcs7_get_authattr(msg, OID_hornet_data,
   203				       &authattrs, &authattrs_len) == -ENODATA) {
   204			err = LSM_INT_VERDICT_PARTIALSIG;
   205			goto cleanup_msg;
   206		}
   207	
   208		err = asn1_ber_decoder(&hornet_decoder, ctx, authattrs, authattrs_len);
   209		if (err < 0 || authattrs == NULL) {
   210			err = LSM_INT_VERDICT_BADSIG;
   211			goto cleanup_msg;
   212		}
   213	
   214		err = hornet_verify_hashes(&maps, ctx, prog);
   215	
   216	cleanup_msg:
   217		pkcs7_free_message(msg);
   218	cleanup_sig:
   219		kfree(sig);
   220	out:
   221		kfree(ctx);
   222		return err;
   223	}
   224	

-- 
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* Re: [PATCH v3 1/2] lsm: add backing_file LSM hooks
From: Amir Goldstein @ 2026-03-28  8:29 UTC (permalink / raw)
  To: Paul Moore
  Cc: linux-security-module, selinux, linux-fsdevel, linux-unionfs,
	linux-erofs, Gao Xiang, Christian Brauner, Miklos Szeredi
In-Reply-To: <20260327220446.353103-5-paul@paul-moore.com>

On Fri, Mar 27, 2026 at 11:05 PM Paul Moore <paul@paul-moore.com> wrote:
>
> Stacked filesystems such as overlayfs do not currently provide the
> necessary mechanisms for LSMs to properly enforce access controls on the
> mmap() and mprotect() operations.  In order to resolve this gap, a LSM
> security blob is being added to the backing_file struct and the following
> new LSM hooks are being created:
>
>  security_backing_file_alloc()
>  security_backing_file_free()
>  security_mmap_backing_file()
>
> The first two hooks are to manage the lifecycle of the LSM security blob
> in the backing_file struct, while the third provides a new mmap() access
> control point for the underlying backing file.  It is also expected that
> LSMs will likely want to update their security_file_mprotect() callback
> to address issues with their mprotect() controls, but that does not
> require a change to the security_file_mprotect() LSM hook.
>
> There are a two other small changes to support these new LSM hooks.  We
> pass the user file associated with a backing file down to
> alloc_empty_backing_file() so it can be included in the
> security_backing_file_alloc() hook, and we constify the file struct field
> in the LSM common_audit_data struct to better support LSMs that need to
> pass a const file struct pointer into the common LSM audit code.
>
> Thanks to Arnd Bergmann for identifying the missing EXPORT_SYMBOL_GPL()
> and supplying a fixup.
>
> Cc: stable@vger.kernel.org
> Acked-by: Christian Brauner <brauner@kernel.org>
> Signed-off-by: Paul Moore <paul@paul-moore.com>
> ---

I 100% agree with Christian.
This is much better than my O_PATH file hack
It is also what Miklos had initially suggested.

I have a minor suggestion for API change though

>  fs/backing-file.c             |  18 ++++--
>  fs/erofs/ishare.c             |  10 +++-
>  fs/file_table.c               |  21 ++++++-
>  fs/fuse/passthrough.c         |   2 +-
>  fs/internal.h                 |   3 +-
>  fs/overlayfs/dir.c            |   2 +-
>  fs/overlayfs/file.c           |   2 +-
>  include/linux/backing-file.h  |   4 +-
>  include/linux/fs.h            |   1 +
>  include/linux/lsm_audit.h     |   2 +-
>  include/linux/lsm_hook_defs.h |   5 ++
>  include/linux/lsm_hooks.h     |   1 +
>  include/linux/security.h      |  22 ++++++++
>  security/lsm.h                |   1 +
>  security/lsm_init.c           |   9 +++
>  security/security.c           | 100 ++++++++++++++++++++++++++++++++++
>  16 files changed, 187 insertions(+), 16 deletions(-)
>
> diff --git a/fs/backing-file.c b/fs/backing-file.c
> index 45da8600d564..1f3bbfc75882 100644
> --- a/fs/backing-file.c
> +++ b/fs/backing-file.c
> @@ -12,6 +12,7 @@
>  #include <linux/backing-file.h>
>  #include <linux/splice.h>
>  #include <linux/mm.h>
> +#include <linux/security.h>
>
>  #include "internal.h"
>
> @@ -29,14 +30,15 @@
>   * returned file into a container structure that also stores the stacked
>   * file's path, which can be retrieved using backing_file_user_path().
>   */
> -struct file *backing_file_open(const struct path *user_path, int flags,
> +struct file *backing_file_open(const struct file *user_file, int flags,
>                                const struct path *real_path,
>                                const struct cred *cred)
>  {
> +       const struct path *user_path = &user_file->f_path;
>         struct file *f;
>         int error;
>
> -       f = alloc_empty_backing_file(flags, cred);
> +       f = alloc_empty_backing_file(flags, cred, user_file);
>         if (IS_ERR(f))
>                 return f;
>
> @@ -52,15 +54,16 @@ struct file *backing_file_open(const struct path *user_path, int flags,
>  }
>  EXPORT_SYMBOL_GPL(backing_file_open);
>
> -struct file *backing_tmpfile_open(const struct path *user_path, int flags,
> +struct file *backing_tmpfile_open(const struct file *user_file, int flags,
>                                   const struct path *real_parentpath,
>                                   umode_t mode, const struct cred *cred)
>  {
>         struct mnt_idmap *real_idmap = mnt_idmap(real_parentpath->mnt);
> +       const struct path *user_path = &user_file->f_path;
>         struct file *f;
>         int error;
>
> -       f = alloc_empty_backing_file(flags, cred);
> +       f = alloc_empty_backing_file(flags, cred, user_file);
>         if (IS_ERR(f))
>                 return f;
>
> @@ -336,8 +339,13 @@ int backing_file_mmap(struct file *file, struct vm_area_struct *vma,
>
>         vma_set_file(vma, file);
>
> -       scoped_with_creds(ctx->cred)
> +       scoped_with_creds(ctx->cred) {
> +               ret = security_mmap_backing_file(vma, file, user_file);
> +               if (ret)
> +                       return ret;
> +
>                 ret = vfs_mmap(vma->vm_file, vma);
> +       }
>
>         if (ctx->accessed)
>                 ctx->accessed(user_file);
> diff --git a/fs/erofs/ishare.c b/fs/erofs/ishare.c
> index 829d50d5c717..ec3fc5ac1a55 100644
> --- a/fs/erofs/ishare.c
> +++ b/fs/erofs/ishare.c
> @@ -4,6 +4,7 @@
>   */
>  #include <linux/xxhash.h>
>  #include <linux/mount.h>
> +#include <linux/security.h>
>  #include "internal.h"
>  #include "xattr.h"
>
> @@ -106,7 +107,8 @@ static int erofs_ishare_file_open(struct inode *inode, struct file *file)
>
>         if (file->f_flags & O_DIRECT)
>                 return -EINVAL;
> -       realfile = alloc_empty_backing_file(O_RDONLY|O_NOATIME, current_cred());
> +       realfile = alloc_empty_backing_file(O_RDONLY|O_NOATIME, current_cred(),
> +                                           file);
>         if (IS_ERR(realfile))
>                 return PTR_ERR(realfile);
>         ihold(sharedinode);
> @@ -150,8 +152,14 @@ static ssize_t erofs_ishare_file_read_iter(struct kiocb *iocb,
>  static int erofs_ishare_mmap(struct file *file, struct vm_area_struct *vma)
>  {
>         struct file *realfile = file->private_data;
> +       int err;
>
>         vma_set_file(vma, realfile);
> +
> +       err = security_mmap_backing_file(vma, realfile, file);
> +       if (err)
> +               return err;
> +
>         return generic_file_readonly_mmap(file, vma);
>  }
>
> diff --git a/fs/file_table.c b/fs/file_table.c
> index aaa5faaace1e..0bdc26cae138 100644
> --- a/fs/file_table.c
> +++ b/fs/file_table.c
> @@ -50,6 +50,7 @@ struct backing_file {
>                 struct path user_path;
>                 freeptr_t bf_freeptr;
>         };

Shouldn't we wrap this with
#ifdef CONFIG_SECURITY

> +       void *security;

please initialize it in init_file()

>  };
>
>  #define backing_file(f) container_of(f, struct backing_file, file)
> @@ -66,6 +67,11 @@ void backing_file_set_user_path(struct file *f, const struct path *path)
>  }
>  EXPORT_SYMBOL_GPL(backing_file_set_user_path);
>
> +void *backing_file_security(const struct file *f)
> +{
> +       return backing_file(f)->security;

I think LSM code should be completely responsible for this ptr
assignment/free so you should export

void **backing_file_security_ptr(const struct file *f)
{
       return &backing_file(f)->security;
}

> +
>  static inline void file_free(struct file *f)
>  {
>         security_file_free(f);
> @@ -73,8 +79,11 @@ static inline void file_free(struct file *f)
>                 percpu_counter_dec(&nr_files);
>         put_cred(f->f_cred);
>         if (unlikely(f->f_mode & FMODE_BACKING)) {
> -               path_put(backing_file_user_path(f));
> -               kmem_cache_free(bfilp_cachep, backing_file(f));
> +               struct backing_file *ff = backing_file(f);
> +
> +               security_backing_file_free(&ff->security);

Why do you need to add this in vfs code?

Can't you do the same in security_file_free(f)?
        if (unlikely(f->f_mode & FMODE_BACKING))
                security_backing_file_free(backing_file_security_ptr(f));


> +               path_put(&ff->user_path);
> +               kmem_cache_free(bfilp_cachep, ff);
>         } else {
>                 kmem_cache_free(filp_cachep, f);
>         }
> @@ -290,7 +299,8 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
>   * This is only for kernel internal use, and the allocate file must not be
>   * installed into file tables or such.
>   */
> -struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
> +struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
> +                                     const struct file *user_file)
>  {
>         struct backing_file *ff;
>         int error;
> @@ -306,6 +316,11 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
>         }
>
>         ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
> +       error = security_backing_file_alloc(&ff->security, user_file);> +       if (unlikely(error)) {
> +               fput(&ff->file);
> +               return ERR_PTR(error);
> +       }
>         return &ff->file;
>  }
>  EXPORT_SYMBOL_GPL(alloc_empty_backing_file);

Maybe, and I am not sure,
alloc_empty_backing_file() should call ONLY
            error = security_backing_file_alloc(&ff->file, user_file);

Instead of security_file_alloc() AND security_backing_file_alloc()
and security_backing_file_alloc() can make use of
backing_file_security_ptr() accessor internally?

I think this will further abstract LSM implementation details from vfs
and avoid the need to spray #ifdef SECURITY in vfs code.

WDYT?

Thanks for following through with this elegant and clean API!
Amir.

^ permalink raw reply

* Re: [PATCH v3 1/2] lsm: add backing_file LSM hooks
From: Paul Moore @ 2026-03-28 16:34 UTC (permalink / raw)
  To: Amir Goldstein
  Cc: linux-security-module, selinux, linux-fsdevel, linux-unionfs,
	linux-erofs, Gao Xiang, Christian Brauner, Miklos Szeredi
In-Reply-To: <CAOQ4uxjvCYRLcRM-JGgtCPXKRB1agCBacN1tmR7hDR9TLdVS4w@mail.gmail.com>

On Sat, Mar 28, 2026 at 4:29 AM Amir Goldstein <amir73il@gmail.com> wrote:
> On Fri, Mar 27, 2026 at 11:05 PM Paul Moore <paul@paul-moore.com> wrote:
> >
> > Stacked filesystems such as overlayfs do not currently provide the
> > necessary mechanisms for LSMs to properly enforce access controls on the
> > mmap() and mprotect() operations.  In order to resolve this gap, a LSM
> > security blob is being added to the backing_file struct and the following
> > new LSM hooks are being created:
> >
> >  security_backing_file_alloc()
> >  security_backing_file_free()
> >  security_mmap_backing_file()
> >
> > The first two hooks are to manage the lifecycle of the LSM security blob
> > in the backing_file struct, while the third provides a new mmap() access
> > control point for the underlying backing file.  It is also expected that
> > LSMs will likely want to update their security_file_mprotect() callback
> > to address issues with their mprotect() controls, but that does not
> > require a change to the security_file_mprotect() LSM hook.
> >
> > There are a two other small changes to support these new LSM hooks.  We
> > pass the user file associated with a backing file down to
> > alloc_empty_backing_file() so it can be included in the
> > security_backing_file_alloc() hook, and we constify the file struct field
> > in the LSM common_audit_data struct to better support LSMs that need to
> > pass a const file struct pointer into the common LSM audit code.
> >
> > Thanks to Arnd Bergmann for identifying the missing EXPORT_SYMBOL_GPL()
> > and supplying a fixup.
> >
> > Cc: stable@vger.kernel.org
> > Acked-by: Christian Brauner <brauner@kernel.org>
> > Signed-off-by: Paul Moore <paul@paul-moore.com>
> > ---
>
> I 100% agree with Christian.
> This is much better than my O_PATH file hack

I'm not surprised that both you and Christian prefer this solution, it
moves all the pain of resolving this issue to the individual LSMs.
Just look at how the SELinux code has changed, even trying to pretty
it up as best as possible, it's objectively much uglier now, not to
mention more complicated.

From my perspective the root cause of this issue lies in the
overlayfs/backing-file design, specifically how overlayfs hides
multiple files under a single file so that it can plug into the
existing VFS/userspace paradigm of a single file.  While the design
and abstraction are no doubt very clever things, and for the most part
they "just work", there are definitely some corner cases that require
special handling, e.g. LSM access controls around mprotect().  In my
opinion, the burden of hiding any ugliness associated with this
special handling lies with the subsystem implementing the abstraction,
which is why I was pushing for a solution where the VFS and/or
backing-file layer would provide the user file (or a stand-in) for the
LSMs to use.  Unfortunately, that was not something the overlayfs and
VFS communities were willing to tolerate, so those of us in the LSM
space were left with a terrible choice: accept that the overlayfs/VFS
folks don't care and hack around the shortcomings of overlayfs, or
leave a public vulnerability for an unknown period of time while the
overlayfs/VFS folks argue over a solution, with the a non-trivial
chance that the LSMs would need to hack around the problem anyway.

That's my view of were things were at, and why I begrudgingly took
this approach.  I'm sure you have your own perspecitve, and I'm not
going to be surprised if you view this primarily as a LSM problem;
it's a common viewpoint amongst Linux kernel maintainers not
responsible for a LSM.

> It is also what Miklos had initially suggested.

Perhaps I've lost the mail, but going back to when this issue was
first discovered, I don't see anything from Miklos relating to this in
either my inbox or the mailing lists.

> > diff --git a/fs/file_table.c b/fs/file_table.c
> > index aaa5faaace1e..0bdc26cae138 100644
> > --- a/fs/file_table.c
> > +++ b/fs/file_table.c
> > @@ -50,6 +50,7 @@ struct backing_file {
> >                 struct path user_path;
> >                 freeptr_t bf_freeptr;
> >         };
>
> Shouldn't we wrap this with
> #ifdef CONFIG_SECURITY

Sure, I'll change that.

> > +       void *security;
>
> please initialize it in init_file()

We lack a clean way to access the backing_file struct in init_file().
I placed the security_backing_file_alloc() initializer in
alloc_empty_backing_file() as it was the first place where we could
both allocate the necessary LSM blob and initialize it with the data
from the user file at the same time.

If you want to intialize backing_file->security in init_file(),
init_file() we will need to add a FMODE_BACKING check in init_file and
split the security_backing_file_alloc() hook into two: one in
init_file() to do the allocation and basic init, one in
alloc_empty_backing_file() to capture the necessary user file data.

Do you still prefer to move the backing_file->security initializtion
into init_file()?

> > +void *backing_file_security(const struct file *f)
> > +{
> > +       return backing_file(f)->security;
>
> I think LSM code should be completely responsible for this ptr
> assignment/free so you should export
>
> void **backing_file_security_ptr(const struct file *f)
> {
>        return &backing_file(f)->security;
> }

Doing so would require us to also move the backing_file struct
definition into include/linux/backing-file.h (or similar), which I
tried very hard to avoid as I suspected you would not approve of that.
I figured if you had wanted to expose the struct definition you would
have defined it in backing-file.h as opposed to file_table.c.

Would you like me to move the backing_file struct definition into
include/linux/backing-file.h?

> > @@ -73,8 +79,11 @@ static inline void file_free(struct file *f)
> >                 percpu_counter_dec(&nr_files);
> >         put_cred(f->f_cred);
> >         if (unlikely(f->f_mode & FMODE_BACKING)) {
> > -               path_put(backing_file_user_path(f));
> > -               kmem_cache_free(bfilp_cachep, backing_file(f));
> > +               struct backing_file *ff = backing_file(f);
> > +
> > +               security_backing_file_free(&ff->security);
>
> Why do you need to add this in vfs code?
>
> Can't you do the same in security_file_free(f)?
>         if (unlikely(f->f_mode & FMODE_BACKING))
>                 security_backing_file_free(backing_file_security_ptr(f));

See my comments above regarding the visibility of the backing_file struct.

> > +               path_put(&ff->user_path);
> > +               kmem_cache_free(bfilp_cachep, ff);
> >         } else {
> >                 kmem_cache_free(filp_cachep, f);
> >         }
> > @@ -290,7 +299,8 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
> >   * This is only for kernel internal use, and the allocate file must not be
> >   * installed into file tables or such.
> >   */
> > -struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
> > +struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
> > +                                     const struct file *user_file)
> >  {
> >         struct backing_file *ff;
> >         int error;
> > @@ -306,6 +316,11 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
> >         }
> >
> >         ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
> > +       error = security_backing_file_alloc(&ff->security, user_file);> +       if (unlikely(error)) {
> > +               fput(&ff->file);
> > +               return ERR_PTR(error);
> > +       }
> >         return &ff->file;
> >  }
> >  EXPORT_SYMBOL_GPL(alloc_empty_backing_file);
>
> Maybe, and I am not sure,
> alloc_empty_backing_file() should call ONLY
>             error = security_backing_file_alloc(&ff->file, user_file);
>
> Instead of security_file_alloc() AND security_backing_file_alloc()
> and security_backing_file_alloc() can make use of
> backing_file_security_ptr() accessor internally?

This is another case of the code being structured so that we don't
need to expose the backing_file struct definition to the LSMs.  If you
would prefer to expose the backing_file struct in include/linux I can
probably make a few additional simplifications to the code.

-- 
paul-moore.com

^ permalink raw reply

* Re: LSM namespacing API
From: Dr. Greg @ 2026-03-29 16:09 UTC (permalink / raw)
  To: Paul Moore
  Cc: Stephen Smalley, Ondrej Mosnacek, linux-security-module, selinux,
	John Johansen
In-Reply-To: <CAHC9VhRgi8_gdx0nKwkOws1VD6EFG+bHNTN5Q8YCxZ3HOCu5PQ@mail.gmail.com>

On Tue, Mar 24, 2026 at 05:31:09PM -0400, Paul Moore wrote:

Good afternoon, I hope the weekend has gone well for everyone.

A few comments on the LSM namespace architecture for when the current
overlayfs drama subsides... :-)

> On Tue, Mar 3, 2026 at 11:46???AM Paul Moore <paul@paul-moore.com> wrote:
> >
> > I'd really like to hear from some of the other LSMs before we start
> > diving into the code.  It may sound funny, but from my perspective
> > doing the work to get the API definition "right" is far more important
> > than implementing it.

> It's been three weeks now, and I haven't seen any strong arguments for
> supporting the clone() API at this time, so we can leave that out for
> now and stick with just the unshare() API for an initial attempt.  We
> can always add a clone() API at a later date if needed; going small
> and expanding over time is usually a better decision anyway.
> 
> So to quickly summarize, here is where I think the discussion landed:
> 
> * Implement the lsm_unshare() syscall
> 
> I expect it would look something like 'lsm_unshare(struct lsm_ctx
> *ctx, u32 size, u32 flags)' with @ctx specifying the particular LSM
> being unshared, and @flags being 0/unused at this point in time
> (unless we can think of something we want to specify here).  Like
> lsm_set_self_attr(), only one @ctx can be specified at a time, so you
> can only unshare one LSM at a time.

Unless we miss something, it would seem that there needs to be
additional thought as to how a process moves, atomically, from one
effective security configuration to the next.

At a minimum, if we restrict ourselves to the model of simply changing
the namespace for a single LSM, there would seem to be a need to have
a 2-step process in order to atomically transition from one security
model/policy to the next.

The logical first step would seem to be to signal an LSM that a
namespace change is impending, with the second step being to tell the
LSM to actually execute the transition.

Presumably in the first step, an LSM would allocate an LSM namespace
memory blob for the new security context and it would also seem like a
good place to determine whether or not the namespace change should be
allowed, secondary to an understanding of possible TOCTOU issues.

The interim between the first and second steps would allow an
orchestrator to configure the new namespace and load new namespace
specific policy into the security namespace blob allocated in the
first step.

It would seem that the flags variable might be a good option to use to
handle this 2-stage transition, for example LSM_NS_INIT and
LSM_NS_CHANGE, respectively, to specify the initialization and
execution phases of the transition.

A simple unshare call becomes much more problematic in the face of an
orchestrator that may wish to create a set of new LSM namespaces for a
new process/container environment.  The inability to atomically
activate the entire new representation of the LSM stack would seem to
be problematic.

The other unanswered issue, or perhaps we missed it, are the security
controls that should be associated with the unshare call.

For example:

Will there be a new LSM hook that allows other LSM's to veto the
creation of a namespace either for itself or for another LSM?  We've
mentioned this before, but it would seem logical that the ability to
deny a change in overall system security policy would be something
that the 'lockdown' LSM would want to do.

Is there a need to have yet another kernel command-line parameter that
would completely deny the ability to create security namespaces?

Is CAP_MAC_ADMIN appropriate as the required capability to create a
new namespace or does there need to be, for security rigor, a specific
capability (CAP_LSM_NS?) that gates the ability to execute whatever
form of the system call is adopted?

Should there be an option to completely compile LSM namespaces out of
the kernel?

> * Implement /proc/pid/ns/lsm and setns(CLONE_NEWLSM)
> 
> As discussed previously, this allows us to move a process into an
> existing, established LSM namespace set.  The caller cannot
> selectively choose which individual LSM namespaces they join from the
> given LSM namespace set, they receive the same LSM namespace
> configuration as the target process.

As an initial aside.  It would be assumed that a positive result of a
setns call would be to cause the calling process to atomically change
its security namespace set.  This would further suggest the need to
have the security namespace creation process also execute atomically
in a multi-LSM namespace change environment.

We may be the only group that has significant field experience with
this, but when it comes to LSM security namespaces, there is a larger
security issue at hand.  That is the concept of whether or not a setns
call, for any resource namespace, should also force a security
namespace change if the security namespace of the calling process
differs from that of the target process.

This, of course, runs up against the meme that containers are not a
kernel concept, but it seems safe to assume, for all practical
purposes, that this horse has bolted from the barn.

A gedanken experiment that should be near and dear to participants in
this conversation, Microsoft's Confidential Containers.

The current predicate for 'trust' based architectures is cryptographic
based integrity measurements and attestation.  If a resource
orchestrator has elected to place a container workload in an alternate
integrity namespace, should another process be allowed to enter, for
example the mount namespace of that process, without also entering
the integrity namespace for the process.

That is just the tip of the iceberg on this issue.

> Any comments, corrections, etc.?  If not, if someone wants to send me
> a patch{set} implementing these changes we can merge them into
> lsm/dev-staging until we have a LSM which implements support for the
> new API.

The above issues come from 10 years of experience in dealing with all
of the issues that arise, particularly in production environments,
with security namespaces.

Without solid answers to these issues the community would be remiss in
cementing down any API's, perhaps that is not a challenge with
existence only in staging.

We would be happy to test fire any API's, but if operational sentiment
is that only in-kernel LSM's and experience are relevant, the odds are
that this functionality isn't going to get done right.  The number of
individuals/people with first hand practical experience with these
issues can probably be comfortably enumerated with one hand.

> paul-moore.com

Have a good week.

As always,
Dr. Greg

The Quixote Project - Flailing at the Travails of Cybersecurity
              https://github.com/Quixote-Project

^ permalink raw reply

* Re: LSM namespacing API
From: Paul Moore @ 2026-03-30  0:56 UTC (permalink / raw)
  To: Dr. Greg
  Cc: Stephen Smalley, Ondrej Mosnacek, linux-security-module, selinux,
	John Johansen
In-Reply-To: <aclOtS61nbG5Wf3p@wind.enjellic.com>

On Sun, Mar 29, 2026 at 12:09 PM Dr. Greg <greg@enjellic.com> wrote:
> On Tue, Mar 24, 2026 at 05:31:09PM -0400, Paul Moore wrote:
> > On Tue, Mar 3, 2026 at 11:46???AM Paul Moore <paul@paul-moore.com> wrote:
> > >
> > > I'd really like to hear from some of the other LSMs before we start
> > > diving into the code.  It may sound funny, but from my perspective
> > > doing the work to get the API definition "right" is far more important
> > > than implementing it.
>
> > It's been three weeks now, and I haven't seen any strong arguments for
> > supporting the clone() API at this time, so we can leave that out for
> > now and stick with just the unshare() API for an initial attempt.  We
> > can always add a clone() API at a later date if needed; going small
> > and expanding over time is usually a better decision anyway.
> >
> > So to quickly summarize, here is where I think the discussion landed:
> >
> > * Implement the lsm_unshare() syscall
> >
> > I expect it would look something like 'lsm_unshare(struct lsm_ctx
> > *ctx, u32 size, u32 flags)' with @ctx specifying the particular LSM
> > being unshared, and @flags being 0/unused at this point in time
> > (unless we can think of something we want to specify here).  Like
> > lsm_set_self_attr(), only one @ctx can be specified at a time, so you
> > can only unshare one LSM at a time.
>
> Unless we miss something, it would seem that there needs to be
> additional thought as to how a process moves, atomically, from one
> effective security configuration to the next.
>
> At a minimum, if we restrict ourselves to the model of simply changing
> the namespace for a single LSM, there would seem to be a need to have
> a 2-step process in order to atomically transition from one security
> model/policy to the next.

That depends on the individual LSMs, they are free to interpret the
unshare request and handle it however they like.

> The interim between the first and second steps would allow an
> orchestrator to configure the new namespace and load new namespace
> specific policy into the security namespace ...

As discussed previously, the LSM policy load syscalls might include
some LSM namespace options. However, I first want to focus on
finalizing the most basic namespace API, which on Linux is arguably
the unshare() syscall concept.

> It would seem that the flags variable might be a good option to use to
> handle this 2-stage transition, for example LSM_NS_INIT and
> LSM_NS_CHANGE, respectively, to specify the initialization and
> execution phases of the transition.

No.  The lsm_unshare() syscall is intended to mimic the existing
unshare() syscall as a single step process from a user's perspective.
If it returns successfully the caller will be in a new LSM namespace
as defined by the individual LSM specified in the syscall.

> The other unanswered issue, or perhaps we missed it, are the security
> controls that should be associated with the unshare call.

Each LSM is free to implement whatever access controls it deems
necessary in its lsm_unshare() callback.

> Will there be a new LSM hook that allows other LSM's to veto the
> creation of a namespace either for itself or for another LSM?

I would expect the lsm_unshare() syscall to operate similarly to the
lsm_set_self_attr() syscall in this regard.

> Is there a need to have yet another kernel command-line parameter that
> would completely deny the ability to create security namespaces?

No, at least not at this point in time.

Individual LSMs can decide how they want to gate their own namespace
functionality, if they implement namespaces at all.

> Is CAP_MAC_ADMIN appropriate as the required capability to create a
> new namespace or does there need to be, for security rigor, a specific
> capability (CAP_LSM_NS?) that gates the ability to execute whatever
> form of the system call is adopted?

Once again, this is up to the individual LSMs, not the framework layer.

> Should there be an option to completely compile LSM namespaces out of
> the kernel?

That doesn't belong in the LSM framework layer, that is up to the
individual LSMs.

> > * Implement /proc/pid/ns/lsm and setns(CLONE_NEWLSM)
> >
> > As discussed previously, this allows us to move a process into an
> > existing, established LSM namespace set.  The caller cannot
> > selectively choose which individual LSM namespaces they join from the
> > given LSM namespace set, they receive the same LSM namespace
> > configuration as the target process.
>
> As an initial aside.  It would be assumed that a positive result of a
> setns call would be to cause the calling process to atomically change
> its security namespace set.  This would further suggest the need to
> have the security namespace creation process also execute atomically
> in a multi-LSM namespace change environment.

In the setns case no new LSM namespaces should be created, the process
simply joins an existing set of LSM namespaces.

> ... That is the concept of whether or not a setns
> call, for any resource namespace, should also force a security
> namespace change if the security namespace of the calling process
> differs from that of the target process.

That decision is left to the individual LSMs.

-- 
paul-moore.com

^ permalink raw reply

* Re: [PATCH v3 1/2] lsm: add backing_file LSM hooks
From: Amir Goldstein @ 2026-03-30  8:35 UTC (permalink / raw)
  To: Paul Moore
  Cc: linux-security-module, selinux, linux-fsdevel, linux-unionfs,
	linux-erofs, Gao Xiang, Christian Brauner
In-Reply-To: <20260327220446.353103-5-paul@paul-moore.com>

[-- Attachment #1: Type: text/plain, Size: 6230 bytes --]

On Fri, Mar 27, 2026 at 11:05 PM Paul Moore <paul@paul-moore.com> wrote:
>
> Stacked filesystems such as overlayfs do not currently provide the
> necessary mechanisms for LSMs to properly enforce access controls on the
> mmap() and mprotect() operations.  In order to resolve this gap, a LSM
> security blob is being added to the backing_file struct and the following
> new LSM hooks are being created:
>
>  security_backing_file_alloc()
>  security_backing_file_free()
>  security_mmap_backing_file()
>
> The first two hooks are to manage the lifecycle of the LSM security blob
> in the backing_file struct, while the third provides a new mmap() access
> control point for the underlying backing file.  It is also expected that
> LSMs will likely want to update their security_file_mprotect() callback
> to address issues with their mprotect() controls, but that does not
> require a change to the security_file_mprotect() LSM hook.
>
> There are a two other small changes to support these new LSM hooks.  We
> pass the user file associated with a backing file down to
> alloc_empty_backing_file() so it can be included in the
> security_backing_file_alloc() hook, and we constify the file struct field
> in the LSM common_audit_data struct to better support LSMs that need to
> pass a const file struct pointer into the common LSM audit code.
>
> Thanks to Arnd Bergmann for identifying the missing EXPORT_SYMBOL_GPL()
> and supplying a fixup.
>
> Cc: stable@vger.kernel.org
> Acked-by: Christian Brauner <brauner@kernel.org>
> Signed-off-by: Paul Moore <paul@paul-moore.com>
> ---
>  fs/backing-file.c             |  18 ++++--
>  fs/erofs/ishare.c             |  10 +++-
>  fs/file_table.c               |  21 ++++++-
>  fs/fuse/passthrough.c         |   2 +-
>  fs/internal.h                 |   3 +-
>  fs/overlayfs/dir.c            |   2 +-
>  fs/overlayfs/file.c           |   2 +-
>  include/linux/backing-file.h  |   4 +-
>  include/linux/fs.h            |   1 +
>  include/linux/lsm_audit.h     |   2 +-
>  include/linux/lsm_hook_defs.h |   5 ++
>  include/linux/lsm_hooks.h     |   1 +
>  include/linux/security.h      |  22 ++++++++
>  security/lsm.h                |   1 +
>  security/lsm_init.c           |   9 +++
>  security/security.c           | 100 ++++++++++++++++++++++++++++++++++
>  16 files changed, 187 insertions(+), 16 deletions(-)
>

That looks like a nice clean abstraction to me.

...

> diff --git a/fs/file_table.c b/fs/file_table.c
> index aaa5faaace1e..0bdc26cae138 100644
> --- a/fs/file_table.c
> +++ b/fs/file_table.c
> @@ -50,6 +50,7 @@ struct backing_file {
>                 struct path user_path;
>                 freeptr_t bf_freeptr;
>         };
> +       void *security;

This needs ifdef SECURITY
and the name should be user_security

>  };
>
>  #define backing_file(f) container_of(f, struct backing_file, file)
> @@ -66,6 +67,11 @@ void backing_file_set_user_path(struct file *f, const struct path *path)
>  }
>  EXPORT_SYMBOL_GPL(backing_file_set_user_path);
>
> +void *backing_file_security(const struct file *f)
> +{
> +       return backing_file(f)->security;
> +}

I prefer the name backing_file_user_security()

Terminology here is very confusing but when saying
"backing file" it is more natural that one is referring to the
backing xfs file with overlayfs has opened.

The "backing file" already has an LSM blob f->f_security
which is fair the call it the "backing file's LSM blob"

Therefore, I think we need to make a distinction, as we did
with backing_file_user_path() and refer to this as something along
the lines of the "backing file's user LSM blob".

> +
>  static inline void file_free(struct file *f)
>  {
>         security_file_free(f);
> @@ -73,8 +79,11 @@ static inline void file_free(struct file *f)
>                 percpu_counter_dec(&nr_files);
>         put_cred(f->f_cred);
>         if (unlikely(f->f_mode & FMODE_BACKING)) {
> -               path_put(backing_file_user_path(f));
> -               kmem_cache_free(bfilp_cachep, backing_file(f));
> +               struct backing_file *ff = backing_file(f);
> +
> +               security_backing_file_free(&ff->security);
> +               path_put(&ff->user_path);
> +               kmem_cache_free(bfilp_cachep, ff);

Not directly related to your patch, but as this is growing, IMO
this would look cleaner with backing_file_free() inline helper
(see attached path).

>         } else {
>                 kmem_cache_free(filp_cachep, f);
>         }
> @@ -290,7 +299,8 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
>   * This is only for kernel internal use, and the allocate file must not be
>   * installed into file tables or such.
>   */
> -struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
> +struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
> +                                     const struct file *user_file)
>  {
>         struct backing_file *ff;
>         int error;
> @@ -306,6 +316,11 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
>         }
>
>         ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
> +       error = security_backing_file_alloc(&ff->security, user_file);
> +       if (unlikely(error)) {
> +               fput(&ff->file);
> +               return ERR_PTR(error);
> +       }
>         return &ff->file;
>  }

There is an API issue here.
in order to call fput() we must ensure that user_security was initialized to
NULL (or allocated).

I don't think that we want security_backing_file_alloc() to provide this
semantic and the current patch does not implement it.

Furthermore, user_path is also not initialized in the error case.

Attached UNTESTED fixup patch to suggest a cleanup with
init_backing_file() helper.

It also changes the variable and helper name to user_security
and plays some trick to avoid many ifdef SECURITY.
Feel free to take whichever bits you like with/without attribution.

If you prefer, attached also a proper prep patch.
compile tested only.

Thanks,
Amir.

[-- Attachment #2: 0001-backing_file_user_security.patch --]
[-- Type: text/x-patch, Size: 3765 bytes --]

From 4858f610d960454ab4de0f29f3557016e80848bd Mon Sep 17 00:00:00 2001
From: Amir Goldstein <amir73il@gmail.com>
Date: Mon, 30 Mar 2026 08:26:01 +0200
Subject: [PATCH] backing_file_user_security

---
 fs/file_table.c    | 47 ++++++++++++++++++++++++++++++++++++----------
 include/linux/fs.h |  2 +-
 2 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/fs/file_table.c b/fs/file_table.c
index 0bdc26cae1389..4666e88ba687d 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -43,14 +43,19 @@ static struct kmem_cache *bfilp_cachep __ro_after_init;
 
 static struct percpu_counter nr_files __cacheline_aligned_in_smp;
 
-/* Container for backing file with optional user path */
+/* Container for backing file with optional user path and security blob */
 struct backing_file {
 	struct file file;
 	union {
 		struct path user_path;
 		freeptr_t bf_freeptr;
+		void *dummy_security;
 	};
-	void *security;
+#ifdef CONFIG_SECURITY
+	void *user_security;
+#else
+#define user_security dummy_security
+#endif
 };
 
 #define backing_file(f) container_of(f, struct backing_file, file)
@@ -67,9 +72,16 @@ void backing_file_set_user_path(struct file *f, const struct path *path)
 }
 EXPORT_SYMBOL_GPL(backing_file_set_user_path);
 
-void *backing_file_security(const struct file *f)
+void *backing_file_user_security(struct file *f)
 {
-	return backing_file(f)->security;
+	return backing_file(f)->user_security;
+}
+
+static inline void backing_file_free(struct backing_file *ff)
+{
+	security_backing_file_free(&ff->user_security);
+	path_put(&ff->user_path);
+	kmem_cache_free(bfilp_cachep, ff);
 }
 
 static inline void file_free(struct file *f)
@@ -79,11 +91,7 @@ static inline void file_free(struct file *f)
 		percpu_counter_dec(&nr_files);
 	put_cred(f->f_cred);
 	if (unlikely(f->f_mode & FMODE_BACKING)) {
-		struct backing_file *ff = backing_file(f);
-
-		security_backing_file_free(&ff->security);
-		path_put(&ff->user_path);
-		kmem_cache_free(bfilp_cachep, ff);
+		backing_file_free(backing_file(f));
 	} else {
 		kmem_cache_free(filp_cachep, f);
 	}
@@ -292,6 +300,23 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
 	return f;
 }
 
+static int init_backing_file(struct backing_file *ff,
+			     const struct file *user_file)
+{
+	int error;
+
+	memset(&ff->user_path, 0, sizeof(ff->user_path));
+	ff->user_security = NULL;
+
+	error = security_backing_file_alloc(&ff->user_security, user_file);
+	if (unlikely(error)) {
+		fput(&ff->file);
+		return ERR_PTR(error);
+	}
+
+	return 0;
+}
+
 /*
  * Variant of alloc_empty_file() that allocates a backing_file container
  * and doesn't check and modify nr_files.
@@ -315,12 +340,14 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
 		return ERR_PTR(error);
 	}
 
+	// The f_mode flags must be set before fput()
 	ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
-	error = security_backing_file_alloc(&ff->security, user_file);
+	error = init_backing_file(ff, user_file);
 	if (unlikely(error)) {
 		fput(&ff->file);
 		return ERR_PTR(error);
 	}
+
 	return &ff->file;
 }
 EXPORT_SYMBOL_GPL(alloc_empty_backing_file);
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 8f5702cfb5e0b..60450a0790add 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2474,7 +2474,7 @@ struct file *dentry_open_nonotify(const struct path *path, int flags,
 struct file *dentry_create(struct path *path, int flags, umode_t mode,
 			   const struct cred *cred);
 const struct path *backing_file_user_path(const struct file *f);
-void *backing_file_security(const struct file *f);
+void *backing_file_user_security(const struct file *f);
 
 /*
  * When mmapping a file on a stackable filesystem (e.g., overlayfs), the file
-- 
2.53.0


[-- Attachment #3: 0001-fs-prepare-for-adding-user_security-block-to-backing.patch --]
[-- Type: text/x-patch, Size: 2128 bytes --]

From cad1df280bcc935289c787f5f4deb4a23ea20fcd Mon Sep 17 00:00:00 2001
From: Amir Goldstein <amir73il@gmail.com>
Date: Mon, 30 Mar 2026 10:27:51 +0200
Subject: [PATCH] fs: prepare for adding user_security block to backing_file

In preparation to adding user_security blob to backing_file struct,
factor out helpers init_backing_file() and backing_file_free().

Signed-off-by: Amir Goldstein <amir73il@gmail.com>
---
 fs/file_table.c | 22 ++++++++++++++++++++--
 1 file changed, 20 insertions(+), 2 deletions(-)

diff --git a/fs/file_table.c b/fs/file_table.c
index aaa5faaace1e9..9e02c1f16db3c 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -66,6 +66,12 @@ void backing_file_set_user_path(struct file *f, const struct path *path)
 }
 EXPORT_SYMBOL_GPL(backing_file_set_user_path);
 
+static inline void backing_file_free(struct backing_file *ff)
+{
+	path_put(&ff->user_path);
+	kmem_cache_free(bfilp_cachep, ff);
+}
+
 static inline void file_free(struct file *f)
 {
 	security_file_free(f);
@@ -73,8 +79,7 @@ static inline void file_free(struct file *f)
 		percpu_counter_dec(&nr_files);
 	put_cred(f->f_cred);
 	if (unlikely(f->f_mode & FMODE_BACKING)) {
-		path_put(backing_file_user_path(f));
-		kmem_cache_free(bfilp_cachep, backing_file(f));
+		backing_file_free(backing_file(f));
 	} else {
 		kmem_cache_free(filp_cachep, f);
 	}
@@ -283,6 +288,12 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
 	return f;
 }
 
+static int init_backing_file(struct backing_file *ff)
+{
+	memset(&ff->user_path, 0, sizeof(ff->user_path));
+	return 0;
+}
+
 /*
  * Variant of alloc_empty_file() that allocates a backing_file container
  * and doesn't check and modify nr_files.
@@ -305,7 +316,14 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
 		return ERR_PTR(error);
 	}
 
+	// The f_mode flags must be set before fput()
 	ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
+	error = init_backing_file(ff);
+	if (unlikely(error)) {
+		fput(&ff->file);
+		return ERR_PTR(error);
+	}
+
 	return &ff->file;
 }
 EXPORT_SYMBOL_GPL(alloc_empty_backing_file);
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH v8 03/12] landlock: Replace union access_masks_all with helper functions
From: Mickaël Salaün @ 2026-03-30  9:56 UTC (permalink / raw)
  To: Günther Noack
  Cc: John Johansen, kernel test robot, linux-security-module,
	Tingmao Wang, Justin Suess, Samasth Norway Ananda,
	Matthieu Buffet, Mikhail Ivanov, konstantin.meskhidze,
	Demi Marie Obenour, Alyssa Ross, Jann Horn, Tahera Fahimi,
	Sebastian Andrzej Siewior, Kuniyuki Iwashima, Georgia Garcia
In-Reply-To: <20260327164838.38231-4-gnoack3000@gmail.com>

On Fri, Mar 27, 2026 at 05:48:28PM +0100, Günther Noack wrote:
> * Stop using a union for access_masks_all.
> * Expose helper functions for intersection checks and union operations.
> 
> The memory layout of bitfields is only loosely defined by the C
> standard, so our static assertion that expects a fixed size was
> brittle, and it broke on some compilers when we attempted to add a
> 17th file system access right.
> 
> Reported-by: kernel test robot <lkp@intel.com>
> Closes: https://lore.kernel.org/oe-kbuild-all/202603261438.jBx2DGNe-lkp@intel.com/
> Signed-off-by: Günther Noack <gnoack3000@gmail.com>
> ---
>  security/landlock/access.h  | 21 ++++++++++++++-------
>  security/landlock/cred.h    | 10 ++--------
>  security/landlock/ruleset.h | 13 ++++---------
>  3 files changed, 20 insertions(+), 24 deletions(-)

I'd prefer this approach:

diff --git a/security/landlock/access.h b/security/landlock/access.h
index 89dc8e7b93da..bc9efbb5c900 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -50,7 +50,7 @@ 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;

 union access_masks_all {
        struct access_masks masks;
@@ -58,7 +58,7 @@ union access_masks_all {
 };

 /* Makes sure all fields are covered. */
-static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
+static_assert(sizeof(typeof_member(union access_masks_all, masks)) <=
              sizeof(typeof_member(union access_masks_all, all)));

 /**


This keep the check and make sure new field will not introduce issues, but more
importantly it save memory, which was one of the goal of struct access_masks.

If that's good with you I'll replace your patch and squash this packed
annotation with the following patch.


> 
> diff --git a/security/landlock/access.h b/security/landlock/access.h
> index 42c95747d7bd..277b6ed7f7bb 100644
> --- a/security/landlock/access.h
> +++ b/security/landlock/access.h
> @@ -52,14 +52,21 @@ struct access_masks {
>  	access_mask_t scope : LANDLOCK_NUM_SCOPE;
>  };
>  
> -union access_masks_all {
> -	struct access_masks masks;
> -	u32 all;
> -};
> +/* Checks whether two access masks have any common bit set. */
> +static inline bool access_masks_intersect(const struct access_masks a,
> +					  const struct access_masks b)
> +{
> +	return (a.fs & b.fs) || (a.net & b.net) || (a.scope & b.scope);
> +}
>  
> -/* Makes sure all fields are covered. */
> -static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
> -	      sizeof(typeof_member(union access_masks_all, all)));
> +/* ORs the bits of @src into @dst. */
> +static inline void access_masks_merge(struct access_masks *dst,
> +				      const struct access_masks src)
> +{
> +	dst->fs |= src.fs;
> +	dst->net |= src.net;
> +	dst->scope |= src.scope;
> +}
>  
>  /**
>   * struct layer_access_masks - A boolean matrix of layers and access rights
> diff --git a/security/landlock/cred.h b/security/landlock/cred.h
> index f287c56b5fd4..207a6db1c086 100644
> --- a/security/landlock/cred.h
> +++ b/security/landlock/cred.h
> @@ -123,9 +123,6 @@ landlock_get_applicable_subject(const struct cred *const cred,
>  				const struct access_masks masks,
>  				size_t *const handle_layer)
>  {
> -	const union access_masks_all masks_all = {
> -		.masks = masks,
> -	};
>  	const struct landlock_ruleset *domain;
>  	ssize_t layer_level;
>  
> @@ -138,11 +135,8 @@ landlock_get_applicable_subject(const struct cred *const cred,
>  
>  	for (layer_level = domain->num_layers - 1; layer_level >= 0;
>  	     layer_level--) {
> -		union access_masks_all layer = {
> -			.masks = domain->access_masks[layer_level],
> -		};
> -
> -		if (layer.all & masks_all.all) {
> +		if (access_masks_intersect(domain->access_masks[layer_level],
> +					   masks)) {
>  			if (handle_layer)
>  				*handle_layer = layer_level;
>  
> diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
> index 889f4b30301a..9f8b33815c2c 100644
> --- a/security/landlock/ruleset.h
> +++ b/security/landlock/ruleset.h
> @@ -229,18 +229,13 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
>  static inline struct access_masks
>  landlock_union_access_masks(const struct landlock_ruleset *const domain)
>  {
> -	union access_masks_all matches = {};
> +	struct access_masks matches = {};
>  	size_t layer_level;
>  
> -	for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
> -		union access_masks_all layer = {
> -			.masks = domain->access_masks[layer_level],
> -		};
> +	for (layer_level = 0; layer_level < domain->num_layers; layer_level++)
> +		access_masks_merge(&matches, domain->access_masks[layer_level]);
>  
> -		matches.all |= layer.all;
> -	}
> -
> -	return matches.masks;
> +	return matches;
>  }
>  
>  static inline void
> -- 
> 2.53.0
> 
> 

^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox