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
next prev 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