linux-security-module.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Justin Suess <utilityemal77@gmail.com>
To: mic@digikod.net
Cc: gnoack@google.com, jack@suse.cz,
	linux-security-module@vger.kernel.org, m@maowtm.org,
	utilityemal77@gmail.com, xandfury@gmail.com
Subject: Re: [PATCH v4 1/5] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
Date: Fri, 12 Dec 2025 17:02:59 -0500	[thread overview]
Message-ID: <20251212220300.542362-1-utilityemal77@gmail.com> (raw)
In-Reply-To: <20251212.Iuracooqu0es@digikod.net>

On 12/12/25 08:27, Mickaël Salaün wrote:
> 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).

Agreed. I would like to get this patch much smaller. I'm glad you think the
feature is useful, it was a personal usecase that motivated me to make this
patch originally but I think having it would benefit a lot of downstream
projects.

I appreciate your patience and willingness to work with someone new to
kernel development and landlock. :)

> 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).
>

Gotcha. I'll make sure to fix that.

>> 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().
>

Total agreement from me.

It makes sense to not duplicate the path traversal code. We could do
everything in is_access_to_paths_allowed but that function is already hairy
in my opinion. What do you think about a helper function for traversal?

        enum landlock_walk_result {
	        LANDLOCK_WALK_CONTINUE,
	        LANDLOCK_WALK_STOP_REAL_ROOT,
	        LANDLOCK_WALK_STOP_INTERNAL_ROOT,
        };

        static enum landlock_walk_result landlock_walk_path(struct path *const path)
        {
        jump_up:
        	if (path->dentry == path->mnt->mnt_root) {
        		if (follow_up(path))
        			goto jump_up;
        		return LANDLOCK_WALK_STOP_REAL_ROOT;
        	}
        
        	if (unlikely(IS_ROOT(path->dentry))) {
        		if (likely(path->mnt->mnt_flags & MNT_INTERNAL))
        			return LANDLOCK_WALK_STOP_INTERNAL_ROOT;
        		dput(path->dentry);
        		path->dentry = dget(path->mnt->mnt_root);
        		return LANDLOCK_WALK_CONTINUE;
        	}
        
        	{
        		struct dentry *const parent = dget_parent(path->dentry);
        
        		dput(path->dentry);
        		path->dentry = parent;
        	}
        	return LANDLOCK_WALK_CONTINUE;
        }

The caller gets a readable enum for where they are in the traversal. This
should centralize the logic and make it easier to
understand.

>> +				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;
>> +}

  reply	other threads:[~2025-12-12 22:03 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
2025-12-12 22:02     ` Justin Suess [this message]
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=20251212220300.542362-1-utilityemal77@gmail.com \
    --to=utilityemal77@gmail.com \
    --cc=gnoack@google.com \
    --cc=jack@suse.cz \
    --cc=linux-security-module@vger.kernel.org \
    --cc=m@maowtm.org \
    --cc=mic@digikod.net \
    --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).