Linux Security Modules development
 help / color / mirror / Atom feed
From: Justin Suess <utilityemal77@gmail.com>
To: linux-security-module@vger.kernel.org, mic@digikod.net
Cc: m@maowtm.org, gnoack@google.com, gnoack3000@gmail.com,
	matthieu@buffet.re, Justin Suess <utilityemal77@gmail.com>
Subject: [PATCH v9 5/9] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
Date: Sat, 20 Jun 2026 23:52:18 -0400	[thread overview]
Message-ID: <20260621035223.2651547-6-utilityemal77@gmail.com> (raw)
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>

Make %LANDLOCK_ADD_RULE_NO_INHERIT actually enforce its semantics:

- Tag the new rule's layer with @no_inherit and
  @has_no_inherit_descendant so landlock_unmask_layers() stops walking
  up the hierarchy once it has been seen, and so the rule's own object
  is sealed against topology changes.
- Walk from the rule's path up to the VFS root in
  landlock_append_fs_rule(), inserting a zero-access rule on each
  ancestor with @has_no_inherit_descendant set, so topology changes
  (rename, rmdir, link, ...) on any ancestor are denied too.
- Add deny_no_inherit_topology_change(), called from
  current_check_refer_path(), hook_path_unlink() and hook_path_rmdir()
  to enforce the seal at the LSM hook layer.

Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---

Notes:
    Changes since v8:
    - Extracted the ancestor-sealing walk out of landlock_append_fs_rule()
      into a new seal_ancestors() helper, removing the out_unlock goto.
    - The seal loop now handles LANDLOCK_WALK_INTERNAL explicitly (skips
      disconnected internal-mount roots and keeps walking), matching the
      updated landlock_walk_path_up() behavior.
    - Broadened current_check_refer_path() enforcement:
      deny_no_inherit_topology_change() is now applied to old_dentry and
      (when present) new_dentry unconditionally, covering hard links (link)
      in addition to rename/exchange.
    - Factored the unlink/rmdir hooks into a new current_check_remove()
      helper instead of duplicating the subject lookup and deny call.
    - Simplified deny_no_inherit_topology_change() to short-circuit and log
      on the first sealed layer, dropping the sealed_layers accumulator and
      the redundant no_inherit test.
    - Bumped the Landlock ABI to version 11 and updated the base_test
      abi_version expectation.
    - Rebased onto mic/next.

 security/landlock/access.h                   |   6 +
 security/landlock/fs.c                       | 156 ++++++++++++++++++-
 security/landlock/ruleset.c                  |  30 +++-
 security/landlock/ruleset.h                  |  13 ++
 security/landlock/syscalls.c                 |   2 +-
 tools/testing/selftests/landlock/base_test.c |   2 +-
 6 files changed, 202 insertions(+), 7 deletions(-)

diff --git a/security/landlock/access.h b/security/landlock/access.h
index d926078bf0a5..9df6c6de71e2 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -81,6 +81,12 @@ struct layer_mask {
 	 */
 	access_mask_t quiet : 1;
 #endif /* CONFIG_AUDIT */
+	/**
+	 * @no_inherit: Whether we have encountered a rule with the no-inherit
+	 * flag for this layer, so that ancestor rules do not grant additional
+	 * access rights.
+	 */
+	access_mask_t no_inherit : 1;
 } __packed __aligned(sizeof(access_mask_t));
 
 /*
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 34d1c245af92..44ccf6cede85 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -362,6 +362,70 @@ static enum landlock_walk_result landlock_walk_path_up(struct path *const path)
 	return LANDLOCK_WALK_CONTINUE;
 }
 
+/**
+ * seal_ancestors - Seal every ancestor of @walker up to the VFS root
+ *
+ * @ruleset: Ruleset (must be locked by the caller) to insert the seal rules
+ *           into.
+ * @walker: Path to the inode whose ancestors must be sealed.  It is walked up
+ *          to the real VFS root and is modified during the walk.
+ *
+ * Inserts a no-access rule tagged @has_no_inherit_descendant for each ancestor
+ * so that topology-changing operations (rename, rmdir, link, ...) on them are
+ * denied.
+ *
+ * On failure the walk stops early, so the ruleset may be left with the leaf
+ * rule and only some of its ancestors sealed.  The caller must therefore
+ * discard the ruleset on error rather than enforce it, since an enforced but
+ * partially sealed ruleset would provide a weaker guarantee than intended.
+ *
+ * Return: 0 on success or a negative error code on failure.
+ */
+static int seal_ancestors(struct landlock_ruleset *const ruleset,
+			  struct path *const walker)
+{
+	int err = 0;
+
+	path_get(walker);
+	for (;;) {
+		struct landlock_rule *ancestor_rule;
+		struct landlock_id ancestor_id = {
+			.type = LANDLOCK_KEY_INODE,
+		};
+		const enum landlock_walk_result res =
+			landlock_walk_path_up(walker);
+
+		/* Done once the real VFS root has been reached. */
+		if (res == LANDLOCK_WALK_STOP_REAL_ROOT)
+			break;
+		/*
+		 * @walker advanced past the disconnected root of an internal
+		 * mount, which does not name a sealable ancestor; keep walking
+		 * up so that every ancestor up to the real root is still
+		 * sealed.
+		 */
+		if (res == LANDLOCK_WALK_INTERNAL)
+			continue;
+
+		/* LANDLOCK_WALK_CONTINUE: seal this ancestor. */
+		ancestor_id.key.object =
+			get_inode_object(d_backing_inode(walker->dentry));
+		if (IS_ERR(ancestor_id.key.object)) {
+			err = PTR_ERR(ancestor_id.key.object);
+			break;
+		}
+		ancestor_rule = landlock_insert_rule(ruleset, ancestor_id, 0, 0);
+		landlock_put_object(ancestor_id.key.object);
+		if (IS_ERR(ancestor_rule)) {
+			err = PTR_ERR(ancestor_rule);
+			break;
+		}
+		ancestor_rule->layers[0].flags.has_no_inherit_descendant = true;
+	}
+	path_put(walker);
+	return err;
+}
+
 /*
  * @path: Should have been checked by get_path_from_fd().
  */
@@ -391,6 +455,20 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 	mutex_lock(&ruleset->lock);
 	rule = landlock_insert_rule(ruleset, id, access_rights, flags);
 	err = PTR_ERR_OR_ZERO(rule);
+	/*
+	 * Sealing an inode also seals all of its ancestors against topology
+	 * changes, so walk the path up to the VFS root and seal each ancestor
+	 * too.  This is best effort: a concurrent rename during the walk may
+	 * leave the seal incomplete.  Such changes to the hierarchy between
+	 * ruleset construction and enforcement are outside of Landlock's
+	 * threat model.  See the "Filesystem inheritance suppression" section
+	 * in Documentation/userspace-api/landlock.rst for the limitations.
+	 */
+	if (!err && (flags & LANDLOCK_ADD_RULE_NO_INHERIT)) {
+		struct path walker = *path;
+
+		err = seal_ancestors(ruleset, &walker);
+	}
 	mutex_unlock(&ruleset->lock);
 	/*
 	 * No need to check for an error because landlock_insert_rule()
@@ -1129,6 +1207,47 @@ log_fs_change_topology_dentry(const struct landlock_cred_security *const subject
 	});
 }
 
+/**
+ * deny_no_inherit_topology_change - Deny topology changes on sealed paths
+ * @subject: Subject performing the operation.
+ * @dentry: Target of the topology modification.
+ *
+ * Return: -EACCES (and emits an audit record) if any of the subject's
+ * domain layers seal @dentry against topology changes: either @dentry
+ * itself has a %LANDLOCK_ADD_RULE_NO_INHERIT rule, or one of its
+ * descendants does (recorded via @has_no_inherit_descendant on the
+ * dentry's rule). Returns 0 otherwise.
+ */
+static int
+deny_no_inherit_topology_change(const struct landlock_cred_security *subject,
+				struct dentry *const dentry)
+{
+	const struct landlock_rule *rule;
+
+	if (WARN_ON_ONCE(!subject || !dentry || d_is_negative(dentry)))
+		return 0;
+
+	rule = find_rule(subject->domain, dentry);
+	if (!rule)
+		return 0;
+
+	for (size_t i = 0; i < rule->num_layers; i++) {
+		const struct landlock_layer *const layer = &rule->layers[i];
+
+		/*
+		 * @has_no_inherit_descendant is a superset of @no_inherit: it is
+		 * set on the rule's own object when the no-inherit rule is
+		 * created, and on every ancestor.  Testing it alone seals both.
+		 */
+		if (layer->flags.has_no_inherit_descendant) {
+			log_fs_change_topology_dentry(subject, layer->level - 1,
+						      dentry);
+			return -EACCES;
+		}
+	}
+	return 0;
+}
+
 /**
  * current_check_refer_path - Check if a rename or link action is allowed
  *
@@ -1194,6 +1313,7 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 	struct path old_parent_path;
 	struct layer_masks layer_masks_parent1 = {}, layer_masks_parent2 = {};
 	struct landlock_request request1 = {}, request2 = {};
+	int err;
 
 	if (!subject)
 		return 0;
@@ -1210,6 +1330,22 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 	}
 	access_request_parent2 =
 		get_mode_access(d_backing_inode(old_dentry)->i_mode);
+	/*
+	 * A no-inherit seal forbids any topology change of the sealed inode or
+	 * its ancestors.  Deny renaming or linking the source out of its
+	 * hierarchy, as well as removing, overwriting or exchanging a sealed
+	 * destination.  This applies to both rename (@removable) and link
+	 * (!@removable) operations, so it is checked unconditionally.
+	 */
+	err = deny_no_inherit_topology_change(subject, old_dentry);
+	if (err)
+		return err;
+	if (!d_is_negative(new_dentry)) {
+		err = deny_no_inherit_topology_change(subject, new_dentry);
+		if (err)
+			return err;
+	}
+
 	if (removable) {
 		access_request_parent1 |= maybe_remove(old_dentry);
 		access_request_parent2 |= maybe_remove(new_dentry);
@@ -1586,16 +1722,32 @@ static int hook_path_symlink(const struct path *const dir,
 	return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_SYM);
 }
 
+static int current_check_remove(const struct path *const dir,
+				struct dentry *const dentry,
+				const access_mask_t access_request)
+{
+	const struct landlock_cred_security *const subject =
+		landlock_get_applicable_subject(current_cred(), any_fs, NULL);
+
+	if (subject) {
+		int err = deny_no_inherit_topology_change(subject, dentry);
+
+		if (err)
+			return err;
+	}
+	return current_check_access_path(dir, access_request);
+}
+
 static int hook_path_unlink(const struct path *const dir,
 			    struct dentry *const dentry)
 {
-	return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_FILE);
+	return current_check_remove(dir, dentry, LANDLOCK_ACCESS_FS_REMOVE_FILE);
 }
 
 static int hook_path_rmdir(const struct path *const dir,
 			   struct dentry *const dentry)
 {
-	return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR);
+	return current_check_remove(dir, dentry, LANDLOCK_ACCESS_FS_REMOVE_DIR);
 }
 
 static int hook_path_truncate(const struct path *const path)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index b8a35675bcbf..ca7cfa45c90a 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -258,6 +258,10 @@ insert_rule(struct landlock_ruleset *const ruleset,
 				return ERR_PTR(-EINVAL);
 			this->layers[0].access |= (*layers)[0].access;
 			this->layers[0].flags.quiet |= (*layers)[0].flags.quiet;
+			this->layers[0].flags.no_inherit |=
+				(*layers)[0].flags.no_inherit;
+			this->layers[0].flags.has_no_inherit_descendant |=
+				(*layers)[0].flags.has_no_inherit_descendant;
 			return this;
 		}
 
@@ -311,12 +315,20 @@ landlock_insert_rule(struct landlock_ruleset *const ruleset,
 		     const struct landlock_id id,
 		     const access_mask_t access, const u32 flags)
 {
+	const bool no_inherit = !!(flags & LANDLOCK_ADD_RULE_NO_INHERIT);
 	struct landlock_layer layers[] = { {
 		.access = access,
 		/* When @level is zero, insert_rule() extends @ruleset. */
 		.level = 0,
 		.flags = {
 			.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
+			.no_inherit = no_inherit,
+			/*
+			 * The rule's own object is also sealed against
+			 * topology changes, so mark it as if it had a
+			 * no-inherit descendant.
+			 */
+			.has_no_inherit_descendant = no_inherit,
 		},
 	} };
 
@@ -657,15 +669,25 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
 	 */
 	for (size_t i = 0; i < rule->num_layers; i++) {
 		const struct landlock_layer *const layer = &rule->layers[i];
+		struct layer_mask *const layer_mask =
+			&masks->layers[layer->level - 1];
+
+		/*
+		 * Skip layers that already have no_inherit set: these layers
+		 * should not inherit access rights from ancestor directories.
+		 */
+		if (layer_mask->no_inherit)
+			continue;
 
 		/* Clear the bits where the layer in the rule grants access. */
-		masks->layers[layer->level - 1].access &= ~layer->access;
+		layer_mask->access &= ~layer->access;
 
 #ifdef CONFIG_AUDIT
-		/* Collect rule flags for each layer. */
 		if (layer->flags.quiet)
-			masks->layers[layer->level - 1].quiet = true;
+			layer_mask->quiet = true;
 #endif /* CONFIG_AUDIT */
+		if (layer->flags.no_inherit)
+			layer_mask->no_inherit = true;
 	}
 
 	for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
@@ -731,6 +753,7 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
 #ifdef CONFIG_AUDIT
 		masks->layers[i].quiet = false;
 #endif /* CONFIG_AUDIT */
+		masks->layers[i].no_inherit = false;
 	}
 	for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
 	     i++) {
@@ -738,6 +761,7 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
 #ifdef CONFIG_AUDIT
 		masks->layers[i].quiet = false;
 #endif /* CONFIG_AUDIT */
+		masks->layers[i].no_inherit = false;
 	}
 
 	return handled_accesses;
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index c927bcb82fa3..8d3dd551bf77 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -40,6 +40,19 @@ struct landlock_layer {
 		 * down the file hierarchy.
 		 */
 		u8 quiet : 1;
+		/**
+		 * @no_inherit: Prevents this rule from inheriting access rights
+		 * from ancestor inodes.  Only used for filesystem rules; set
+		 * via %LANDLOCK_ADD_RULE_NO_INHERIT.
+		 */
+		u8 no_inherit : 1;
+		/**
+		 * @has_no_inherit_descendant: Marker used to deny topology
+		 * changes on the rule's object: either the object itself has
+		 * a no-inherit rule, or a descendant does.  Only used for
+		 * filesystem rules; set by Landlock, never by user space.
+		 */
+		u8 has_no_inherit_descendant : 1;
 	} flags;
 	/**
 	 * @access: Bitfield of allowed actions on the kernel object.  They are
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index b847b0be1cf7..3fea5492df0d 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -169,7 +169,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 = 10;
+const int landlock_abi_version = 11;
 
 /**
  * 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 cbd3c1669951..b8b5fa1042ba 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(10, landlock_create_ruleset(NULL, 0,
+	ASSERT_EQ(11, landlock_create_ruleset(NULL, 0,
 					      LANDLOCK_CREATE_RULESET_VERSION));
 
 	ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
-- 
2.54.0


  parent reply	other threads:[~2026-06-21  3:52 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-21  3:52 [PATCH v9 0/9] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2026-06-21  3:52 ` [PATCH v9 1/9] landlock: Add and use landlock_walk_path_up() helper Justin Suess
2026-06-21  3:52 ` [PATCH v9 2/9] landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT user API Justin Suess
2026-06-21  3:52 ` [PATCH v9 3/9] landlock: Return inserted rule from landlock_insert_rule() Justin Suess
2026-06-21  3:52 ` [PATCH v9 4/9] landlock: Move log_fs_change_topology_dentry() above current_check_refer_path() Justin Suess
2026-06-21  3:52 ` Justin Suess [this message]
2026-06-21  3:52 ` [PATCH v9 6/9] landlock: Add documentation for LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2026-06-21  3:52 ` [PATCH v9 7/9] samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to landlock-sandboxer Justin Suess
2026-06-21  3:52 ` [PATCH v9 8/9] selftests/landlock: Add selftests for LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2026-06-21  3:52 ` [PATCH v9 9/9] landlock: Add KUnit tests " Justin Suess

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260621035223.2651547-6-utilityemal77@gmail.com \
    --to=utilityemal77@gmail.com \
    --cc=gnoack3000@gmail.com \
    --cc=gnoack@google.com \
    --cc=linux-security-module@vger.kernel.org \
    --cc=m@maowtm.org \
    --cc=matthieu@buffet.re \
    --cc=mic@digikod.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox