Linux Security Modules development
 help / color / mirror / Atom feed
From: Justin Suess <utilityemal77@gmail.com>
To: gnoack3000@gmail.com, mic@digikod.net
Cc: linux-kernel@vger.kernel.org,
	linux-security-module@vger.kernel.org,
	Justin Suess <utilityemal77@gmail.com>
Subject: [PATCH v8 06/10] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
Date: Thu, 28 May 2026 21:52:05 -0400	[thread overview]
Message-ID: <20260529015210.500291-7-utilityemal77@gmail.com> (raw)
In-Reply-To: <20260529015210.500291-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:
    v7..v8 changes:
    
      * Reworded commit message to describe the three pieces of the
        implementation (per-layer tagging, ancestor walk, LSM hook
        enforcement).
      * Reworked ancestor sealing in landlock_append_fs_rule(): use the
        new landlock_insert_rule() return value to tag flags directly on
        the inserted ancestor rule, removing the prior find_rule() round
        trip after insertion.
      * Moved no_inherit / has_no_inherit_descendant tracking from a
        separate collected_rule_flags struct into the existing
        layer_mask struct as a single per-layer 'no_inherit' bit.
        landlock_unmask_layers() now skips layers whose mask already has
        no_inherit set, and landlock_init_layer_masks() clears the new
        bit. The 'has_no_inherit_descendant' rule-layer flag is
        auto-set on the rule's own object when LANDLOCK_ADD_RULE_NO_INHERIT
        is passed, sealing it against topology changes without a separate
        blank-rule insertion.
      * Simplified deny_no_inherit_topology_change(): dropped the
        override_layers accumulator (it was always 0 in practice) and
        now just OR-collects sealed layers from no_inherit /
        has_no_inherit_descendant.
      * Updated kerneldoc comments on the new layer flags.

 security/landlock/access.h  |   4 ++
 security/landlock/fs.c      | 116 +++++++++++++++++++++++++++++++++++-
 security/landlock/ruleset.c |  30 +++++++++-
 security/landlock/ruleset.h |  13 ++++
 4 files changed, 159 insertions(+), 4 deletions(-)

diff --git a/security/landlock/access.h b/security/landlock/access.h
index 61a17b568652..ab5c1e0bc25d 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -71,12 +71,16 @@ static_assert(sizeof(typeof_member(union access_masks_all, masks)) ==
  *
  * @quiet is used to store whether we have encountered a rule with the
  * quiet flag for this layer, which will be used to control audit logging.
+ *
+ * @no_inherit is used to mark this layer as having a no_inherit rule, so
+ * that ancestor rules in the same layer do not contribute access rights.
  */
 struct layer_mask {
 	access_mask_t access:LANDLOCK_NUM_ACCESS_MAX;
 #ifdef CONFIG_AUDIT
 	bool quiet:1;
 #endif /* CONFIG_AUDIT */
+	bool no_inherit:1;
 };
 
 /*
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index ee7d9f5d7ee5..3aa7d898efe1 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -364,6 +364,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 	struct landlock_id id = {
 		.type = LANDLOCK_KEY_INODE,
 	};
+	struct path walker = *path;
 
 	/* Files only get access rights that make sense. */
 	if (!d_is_dir(path->dentry) &&
@@ -378,10 +379,47 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
 	id.key.object = get_inode_object(d_backing_inode(path->dentry));
 	if (IS_ERR(id.key.object))
 		return PTR_ERR(id.key.object);
+
 	mutex_lock(&ruleset->lock);
 	rule = landlock_insert_rule(ruleset, id, access_rights, flags);
-	if (IS_ERR(rule))
+	if (IS_ERR(rule)) {
 		err = PTR_ERR(rule);
+		goto out_unlock;
+	}
+	if (!(flags & LANDLOCK_ADD_RULE_NO_INHERIT))
+		goto out_unlock;
+
+	/*
+	 * Seal each ancestor up to the VFS root with a no-access rule
+	 * tagged @has_no_inherit_descendant so that topology-changing
+	 * operations (rename, rmdir, link, ...) on them are denied.
+	 */
+	path_get(&walker);
+	while (landlock_walk_path_up(&walker) != LANDLOCK_WALK_STOP_REAL_ROOT) {
+		struct landlock_rule *ancestor_rule;
+		struct inode *const ancestor_inode =
+			d_backing_inode(walker.dentry);
+		struct landlock_id ancestor_id = {
+			.type = LANDLOCK_KEY_INODE,
+			.key.object = get_inode_object(ancestor_inode),
+		};
+
+		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);
+
+out_unlock:
 	mutex_unlock(&ruleset->lock);
 	/*
 	 * No need to check for an error because landlock_insert_rule()
@@ -1106,6 +1144,54 @@ collect_domain_accesses(const struct landlock_ruleset *const domain,
 	return ret;
 }
 
+/**
+ * deny_no_inherit_topology_change - Deny topology changes on sealed paths
+ * @subject: Subject performing the operation.
+ * @dentry: Target of the topology modification.
+ *
+ * Returns -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)
+{
+	unsigned long sealed_layers = 0;
+	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];
+
+		if (layer->flags.no_inherit ||
+		    layer->flags.has_no_inherit_descendant)
+			sealed_layers |= BIT(layer->level - 1);
+	}
+	if (!sealed_layers)
+		return 0;
+
+	landlock_log_denial(subject, &(struct landlock_request) {
+		.type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
+		.audit = {
+			.type = LSM_AUDIT_DATA_DENTRY,
+			.u.dentry = dentry,
+		},
+		.layer_plus_one = __ffs(sealed_layers) + 1,
+	});
+	return -EACCES;
+}
+
 /**
  * current_check_refer_path - Check if a rename or link action is allowed
  *
@@ -1188,6 +1274,16 @@ static int current_check_refer_path(struct dentry *const old_dentry,
 	access_request_parent2 =
 		get_mode_access(d_backing_inode(old_dentry)->i_mode);
 	if (removable) {
+		int err = deny_no_inherit_topology_change(subject, old_dentry);
+
+		if (err)
+			return err;
+		if (exchange) {
+			err = deny_no_inherit_topology_change(subject,
+							      new_dentry);
+			if (err)
+				return err;
+		}
 		access_request_parent1 |= maybe_remove(old_dentry);
 		access_request_parent2 |= maybe_remove(new_dentry);
 	}
@@ -1579,12 +1675,30 @@ static int hook_path_symlink(const struct path *const dir,
 static int hook_path_unlink(const struct path *const dir,
 			    struct dentry *const dentry)
 {
+	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, LANDLOCK_ACCESS_FS_REMOVE_FILE);
 }
 
 static int hook_path_rmdir(const struct path *const dir,
 			   struct dentry *const dentry)
 {
+	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, LANDLOCK_ACCESS_FS_REMOVE_DIR);
 }
 
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 48397ab43a2d..c78e2b2d73ff 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 int 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 5b7f554e8442..249a736248db 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -40,6 +40,19 @@ struct landlock_layer {
 		 * down the file hierarchy.
 		 */
 		bool 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.
+		 */
+		bool 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.
+		 */
+		bool has_no_inherit_descendant:1;
 	} flags;
 	/**
 	 * @access: Bitfield of allowed actions on the kernel object.  They are
-- 
2.53.0


  parent reply	other threads:[~2026-05-29  1:52 UTC|newest]

Thread overview: 11+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-29  1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2026-05-29  1:52 ` [PATCH v8 01/10] landlock: Add landlock_walk_path_up() helper Justin Suess
2026-05-29  1:52 ` [PATCH v8 02/10] landlock: Use landlock_walk_path_up() in is_access_to_paths_allowed() Justin Suess
2026-05-29  1:52 ` [PATCH v8 03/10] landlock: Use landlock_walk_path_up() in collect_domain_accesses() Justin Suess
2026-05-29  1:52 ` [PATCH v8 04/10] landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT user API Justin Suess
2026-05-29  1:52 ` [PATCH v8 05/10] landlock: Return inserted rule from landlock_insert_rule() Justin Suess
2026-05-29  1:52 ` Justin Suess [this message]
2026-05-29  1:52 ` [PATCH v8 07/10] landlock: Add documentation for LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2026-05-29  1:52 ` [PATCH v8 08/10] samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to landlock-sandboxer Justin Suess
2026-05-29  1:52 ` [PATCH v8 09/10] selftests/landlock: Add selftests for LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2026-05-29  1:52 ` [PATCH v8 10/10] 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=20260529015210.500291-7-utilityemal77@gmail.com \
    --to=utilityemal77@gmail.com \
    --cc=gnoack3000@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --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