From: "Mickaël Salaün" <mic@digikod.net>
To: Justin Suess <utilityemal77@gmail.com>
Cc: "Tingmao Wang" <m@maowtm.org>,
"Günther Noack" <gnoack@google.com>, "Jan Kara" <jack@suse.cz>,
"Abhinav Saxena" <xandfury@gmail.com>,
linux-security-module@vger.kernel.org
Subject: Re: [PATCH v4 1/5] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
Date: Fri, 12 Dec 2025 14:27:11 +0100 [thread overview]
Message-ID: <20251212.Iuracooqu0es@digikod.net> (raw)
In-Reply-To: <20251207015132.800576-2-utilityemal77@gmail.com>
Thanks for this patch series. This feature would be useful, but the
related patches add a lot of new code. We should try to minimize that
as much as possible, especially when similar/close logic already exist
(e.g. path walk).
On Sat, Dec 06, 2025 at 08:51:27PM -0500, Justin Suess wrote:
> Implements a flag to prevent access grant inheritance within the filesystem
> hierarchy for landlock rules.
>
> If a landlock rule on an inode has this flag, any access grants on parent
> inodes will be ignored. Moreover, operations that involve altering the
> ancestors of the subject with LANDLOCK_ADD_RULE_NO_INHERIT will be
> denied up to the VFS root (new in v4).
>
> For example, if /a/b/c/ = read only + LANDLOCK_ADD_RULE_NO_INHERIT and
> / = read write, writes to files in /a/b/c will be denied. Moreover,
> moving /a to /bad, removing /a/b/c, or creating links to /a will be
> prohibited.
>
> Parent flag inheritance is automatically suppressed by the permission
> harvesting logic, which will finish processing early if all relevant
> layers are tagged with NO_INHERIT.
>
> And if / has LANDLOCK_ADD_RULE_QUIET, /a/b/c will still audit (handled)
> accesses. This is because LANDLOCK_ADD_RULE_NO_INHERIT also
> suppresses flag inheritance from parent objects.
>
> The parent directory restrictions mitigate sandbox-restart attacks. For
> example, if a sandboxed program is able to move a
> LANDLOCK_ADD_RULE_NO_INHERIT restricted directory, upon sandbox restart,
> the policy applied naively on the same filenames would be invalid.
> Preventing these operations mitigates these attacks.
>
Your Signed-off-by and Cc should be here, followed by "---", followed by
the changelog (to exclude it from the git message e.g., when using
git am).
> v3..v4 changes:
>
> * Rebased on v6 of Tingmao Wang's "quiet flag" series.
> * Removed unnecessary mask_no_inherit_descendant_layers and related
> code at Tingmao Wang's suggestion, simplifying patch.
> * Updated to use new disconnected directory handling.
> * Improved WARN_ON_ONCE usage. (Thanks Tingmao Wang!)
> * Removed redundant loop for single-layer rulesets (again thanks Tingmao
> Wang!)
> * Protections now apply up to the VFS root, not just the mountpoint.
> * Indentation fixes.
> * Removed redundant flag marker blocked_flag_masks.
>
> v2..v3 changes:
>
> * Parent directory topology protections now work by lazily
> inserting blank rules on parent inodes if they do not
> exist. This replaces the previous xarray implementation
> with simplified logic.
> * Added an optimization to skip further processing if all layers
> collected have no inherit.
> * Added support to block flag inheritance.
>
> Cc: Tingmao Wang <m@maowtm.org>
> Signed-off-by: Justin Suess <utilityemal77@gmail.com>
> ---
> security/landlock/fs.c | 389 +++++++++++++++++++++++++++++++++++-
> security/landlock/ruleset.c | 19 +-
> security/landlock/ruleset.h | 29 ++-
> 3 files changed, 433 insertions(+), 4 deletions(-)
>
> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
> index 0b589263ea42..7b0b77859778 100644
> --- a/security/landlock/fs.c
> +++ b/security/landlock/fs.c
> @@ -317,6 +317,207 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
> LANDLOCK_ACCESS_FS_IOCTL_DEV)
> /* clang-format on */
>
> +static const struct landlock_rule *find_rule(const struct landlock_ruleset *const domain,
> + const struct dentry *const dentry);
> +
> +/**
> + * landlock_domain_layers_mask - Build a mask covering all layers of a domain
> + * @domain: The ruleset (domain) to inspect.
> + *
> + * Return a layer mask with a 1 bit for each existing layer of @domain.
> + * If @domain has no layers 0 is returned. If the number of layers is
> + * greater than or equal to the number of bits in layer_mask_t, all bits
> + * are set.
> + */
> +static layer_mask_t landlock_domain_layers_mask(const struct landlock_ruleset
> + *const domain)
> +{
> + if (!domain || !domain->num_layers)
> + return 0;
> +
> + if (domain->num_layers >= sizeof(layer_mask_t) * BITS_PER_BYTE)
> + return (layer_mask_t)~0ULL;
> +
> + return GENMASK_ULL(domain->num_layers - 1, 0);
> +}
> +
> +/**
> + * rule_blocks_all_layers_no_inherit - check whether a rule disables inheritance
> + * @domain_layers_mask: Mask describing the domain's active layers.
> + * @rule: Rule to inspect.
> + *
> + * Return true if every layer present in @rule has its no_inherit flag set
> + * and the set of layers covered by the rule equals @domain_layers_mask.
> + * This indicates that the rule prevents inheritance on all layers of the
> + * domain and thus further walking for inheritance checks can stop.
> + */
> +static bool rule_blocks_all_layers_no_inherit(const layer_mask_t domain_layers_mask,
> + const struct landlock_rule *const rule)
> +{
> + layer_mask_t rule_layers = 0;
> + u32 layer_index;
> +
> + if (!domain_layers_mask || !rule)
> + return false;
> +
> + for (layer_index = 0; layer_index < rule->num_layers; layer_index++) {
> + const struct landlock_layer *const layer =
> + &rule->layers[layer_index];
> + const layer_mask_t layer_bit = BIT_ULL(layer->level - 1);
> +
> + if (!layer->flags.no_inherit)
> + return false;
> +
> + rule_layers |= layer_bit;
> + }
> +
> + return rule_layers && rule_layers == domain_layers_mask;
> +}
> +
> +/**
> + * ensure_rule_for_dentry - ensure a ruleset contains a rule entry for dentry,
> + * inserting a blank rule if needed.
> + * @ruleset: Ruleset to modify/inspect. Caller must hold @ruleset->lock.
> + * @dentry: Dentry to ensure a rule exists for.
> + *
> + * If no rule is currently associated with @dentry, insert an empty rule
> + * (with zero access) tied to the backing inode. Returns a pointer to the
> + * rule associated with @dentry on success, NULL when @dentry is negative, or
> + * an ERR_PTR()-encoded error if the rule cannot be created.
> + *
> + * This is useful for LANDLOCK_ADD_RULE_NO_INHERIT processing, where a rule
> + * may need to be created for an ancestor dentry that does not yet have one
> + * to properly track no_inherit flags.
> + *
> + * The flags are set to zero if a rule is newly created, and the caller
> + * is responsible for setting them appropriately.
> + *
> + * The returned rule pointer's lifetime is tied to @ruleset.
> + */
> +static const struct landlock_rule *
> +ensure_rule_for_dentry(struct landlock_ruleset *const ruleset,
> + struct dentry *const dentry)
> +{
> + struct landlock_id id = {
> + .type = LANDLOCK_KEY_INODE,
> + };
> + const struct landlock_rule *rule;
> + int err;
> +
> + if (WARN_ON_ONCE(!ruleset || !dentry || d_is_negative(dentry)))
> + return NULL;
> +
> + lockdep_assert_held(&ruleset->lock);
> +
> + rule = find_rule(ruleset, dentry);
> + if (rule)
> + return rule;
> +
> + id.key.object = get_inode_object(d_backing_inode(dentry));
> + if (IS_ERR(id.key.object))
> + return ERR_CAST(id.key.object);
> +
> + err = landlock_insert_rule(ruleset, id, 0, 0);
> + landlock_put_object(id.key.object);
> + if (err)
> + return ERR_PTR(err);
> +
> + rule = find_rule(ruleset, dentry);
> + if (WARN_ON_ONCE(!rule))
> + return ERR_PTR(-ENOENT);
> + return rule;
> +}
> +
> +/**
> + * mark_no_inherit_ancestors - mark ancestors as having no_inherit descendants
> + * @ruleset: Ruleset to modify. Caller must hold @ruleset->lock.
> + * @path: Path representing the descendant that carries no_inherit bits.
> + * @descendant_layers: Mask of layers from the descendant that should be
> + * advertised to ancestors via has_no_inherit_descendant.
> + *
> + * Walks upward from @dentry and ensures that any ancestor rule contains the
> + * has_no_inherit_descendant marker for the specified @descendant_layers so
> + * parent lookups can quickly detect descendant no_inherit influence.
> + *
> + * Returns 0 on success or a negative errno if ancestor bookkeeping fails.
> + */
> +static int mark_no_inherit_ancestors(struct landlock_ruleset *ruleset,
> + const struct path *const path,
> + layer_mask_t descendant_layers)
> +{
> + struct dentry *cursor;
> + struct path walk_path;
> + int err = 0;
> +
> + if (WARN_ON_ONCE(!ruleset || !path || !path->dentry || !path->mnt ||
> + !descendant_layers))
> + return -EINVAL;
> +
> + lockdep_assert_held(&ruleset->lock);
> +
> + walk_path.mnt = path->mnt;
> + walk_path.dentry = path->dentry;
> + path_get(&walk_path);
> +
> + cursor = dget(walk_path.dentry);
> + while (cursor) {
> + struct dentry *parent;
> + const struct landlock_rule *rule;
> +
> + /* Follow mounts all the way up to the root. */
> + if (IS_ROOT(cursor)) {
> + dput(cursor);
> + if (!follow_up(&walk_path)) {
This path walk is inconsistent and a duplicate of the (correct)
is_access_to_paths_allowed() one. Please add a patch to factor out the
common parts as much as possible. Ditto for the
collect_domain_accesses() and collect_topology_sealed_layers().
> + cursor = NULL;
> + continue;
> + }
> + cursor = dget(walk_path.dentry);
> + }
> +
> + parent = dget_parent(cursor);
> + dput(cursor);
> + if (!parent)
> + break;
> +
> + if (WARN_ON_ONCE(d_is_negative(parent))) {
> + dput(parent);
> + break;
> + }
> + /*
> + * Ensures a rule exists for the parent dentry,
> + * inserting a blank one if needed.
> + */
> + rule = ensure_rule_for_dentry(ruleset, parent);
> + if (IS_ERR(rule)) {
> + err = PTR_ERR(rule);
> + dput(parent);
> + cursor = NULL;
> + break;
> + }
> + if (rule) {
> + struct landlock_rule *mutable_rule =
> + (struct landlock_rule *)rule;
> + /*
> + * Unmerged rulesets should only have one layer.
> + */
> + if (WARN_ON_ONCE(mutable_rule->num_layers != 1)) {
> + dput(parent);
> + err = -EINVAL;
> + cursor = NULL;
> + break;
> + }
> +
> + if (descendant_layers & BIT_ULL(0))
> + mutable_rule->layers[0]
> + .flags.has_no_inherit_descendant = true;
> + }
> +
> + cursor = parent;
> + }
> + path_put(&walk_path);
> + return err;
> +}
> +/**
> + * collect_topology_sealed_layers - collect layers sealed against topology changes
> + * @domain: Ruleset to consult.
> + * @dentry: Starting dentry for the upward walk.
> + * @override_layers: Optional out parameter filled with layers that are
> + * present on ancestors but considered overrides (not
> + * sealing the topology for descendants).
> + *
> + * Walk upwards from @dentry and return a mask of layers where either the
> + * visited dentry contains a no_inherit rule or ancestors were previously
> + * marked as having a descendant with no_inherit. @override_layers, if not
> + * NULL, is filled with layers that would normally be overridden by more
> + * specific descendant rules.
> + *
> + * Returns a layer mask where set bits indicate layers that are "sealed"
> + * (topology changes like rename/rmdir are denied) for the subtree rooted at
> + * @dentry.
> + *
> + * Useful for LANDLOCK_ADD_RULE_NO_INHERIT parent directory enforcement to ensure
> + * that topology changes do not violate the no_inherit constraints.
> + */
> +static layer_mask_t
> +collect_topology_sealed_layers(const struct landlock_ruleset *const domain,
> + struct dentry *dentry,
> + layer_mask_t *const override_layers)
> +{
> + struct dentry *cursor, *parent;
> + bool include_descendants = true;
> + layer_mask_t sealed_layers = 0;
> +
> + if (override_layers)
> + *override_layers = 0;
> +
> + if (WARN_ON_ONCE(!domain || !dentry || d_is_negative(dentry)))
> + return 0;
> +
> + cursor = dget(dentry);
> + while (cursor) {
> + const struct landlock_rule *rule;
> + u32 layer_index;
> +
> + rule = find_rule(domain, cursor);
> + if (rule) {
> + for (layer_index = 0; layer_index < rule->num_layers;
> + layer_index++) {
> + const struct landlock_layer *layer =
> + &rule->layers[layer_index];
> + layer_mask_t layer_bit =
> + BIT_ULL(layer->level - 1);
> +
> + if (include_descendants &&
> + (layer->flags.no_inherit ||
> + layer->flags.has_no_inherit_descendant)) {
> + sealed_layers |= layer_bit;
> + } else if (override_layers) {
> + *override_layers |= layer_bit;
> + }
> + }
> + }
> +
> + if (sealed_layers || IS_ROOT(cursor))
> + break;
> +
> + parent = dget_parent(cursor);
> + dput(cursor);
> + if (!parent)
> + return sealed_layers;
> +
> + cursor = parent;
> + include_descendants = false;
> + }
> + dput(cursor);
> + return sealed_layers;
> +}
next prev parent reply other threads:[~2025-12-12 13:27 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-07 1:51 [PATCH v4 0/5] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2025-12-07 1:51 ` [PATCH v4 1/5] landlock: " Justin Suess
2025-12-12 13:27 ` Mickaël Salaün [this message]
2025-12-12 22:02 ` Justin Suess
2025-12-07 1:51 ` [PATCH v4 2/5] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT userspace api Justin Suess
2025-12-07 1:51 ` [PATCH v4 3/5] samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to landlock-sandboxer Justin Suess
2025-12-07 1:51 ` [PATCH v4 4/5] selftests/landlock: Implement selftests for LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
2025-12-07 1:51 ` [PATCH v4 5/5] landlock: Implement KUnit test " 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=20251212.Iuracooqu0es@digikod.net \
--to=mic@digikod.net \
--cc=gnoack@google.com \
--cc=jack@suse.cz \
--cc=linux-security-module@vger.kernel.org \
--cc=m@maowtm.org \
--cc=utilityemal77@gmail.com \
--cc=xandfury@gmail.com \
/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;
as well as URLs for NNTP newsgroup(s).