* [PATCH v8 01/10] landlock: Add landlock_walk_path_up() helper
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
@ 2026-05-29 1:52 ` 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
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic
Cc: linux-kernel, linux-security-module, Justin Suess, Tingmao Wang
In preparation for centralizing path-walk logic, add
landlock_walk_path_up(), which moves @path one step toward the VFS
root. Its return value indicates whether the new position is an
internal mount point, the real root, or neither (i.e. the caller
should continue walking).
No functional change intended.
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Reworded commit message; no code changes.
security/landlock/fs.c | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 3b71f569a8f9..8e75583c3ca7 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -320,6 +320,38 @@ static struct landlock_object *get_inode_object(struct inode *const inode)
LANDLOCK_ACCESS_FS_RESOLVE_UNIX)
/* clang-format on */
+/**
+ * enum landlock_walk_result - Result codes for landlock_walk_path_up()
+ * @LANDLOCK_WALK_CONTINUE: Path is now neither the real root nor an internal mount point.
+ * @LANDLOCK_WALK_STOP_REAL_ROOT: Path has reached the real VFS root.
+ * @LANDLOCK_WALK_INTERNAL: Path has reached an internal mount point.
+ */
+enum landlock_walk_result {
+ LANDLOCK_WALK_CONTINUE,
+ LANDLOCK_WALK_STOP_REAL_ROOT,
+ LANDLOCK_WALK_INTERNAL,
+};
+
+static enum landlock_walk_result landlock_walk_path_up(struct path *const path)
+{
+ struct dentry *old;
+
+ while (path->dentry == path->mnt->mnt_root) {
+ if (!follow_up(path))
+ return LANDLOCK_WALK_STOP_REAL_ROOT;
+ }
+ old = path->dentry;
+ if (unlikely(IS_ROOT(old))) {
+ if (likely(path->mnt->mnt_flags & MNT_INTERNAL))
+ return LANDLOCK_WALK_INTERNAL;
+ path->dentry = dget(path->mnt->mnt_root);
+ } else {
+ path->dentry = dget_parent(old);
+ }
+ dput(old);
+ return LANDLOCK_WALK_CONTINUE;
+}
+
/*
* @path: Should have been checked by get_path_from_fd().
*/
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 02/10] landlock: Use landlock_walk_path_up() in is_access_to_paths_allowed()
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 ` Justin Suess
2026-05-29 1:52 ` [PATCH v8 03/10] landlock: Use landlock_walk_path_up() in collect_domain_accesses() Justin Suess
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Replace the open-coded path-walk loop with the new
landlock_walk_path_up() helper. This removes the backward goto and
keeps the traversal logic in a single place.
No functional change intended.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Reworded commit message.
* Reordered switch arms so the LANDLOCK_WALK_CONTINUE fast path comes
first, and moved the per-case explanatory comments inside the case
bodies. No functional change.
security/landlock/fs.c | 55 ++++++++++++++----------------------------
1 file changed, 18 insertions(+), 37 deletions(-)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 8e75583c3ca7..8fb0aa59e180 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -921,46 +921,27 @@ is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
if (allowed_parent1 && allowed_parent2)
break;
-jump_up:
- if (walker_path.dentry == walker_path.mnt->mnt_root) {
- if (follow_up(&walker_path)) {
- /* Ignores hidden mount points. */
- goto jump_up;
- } else {
- /*
- * Stops at the real root. Denies access
- * because not all layers have granted access.
- */
- break;
- }
- }
-
- if (unlikely(IS_ROOT(walker_path.dentry))) {
- if (likely(walker_path.mnt->mnt_flags & MNT_INTERNAL)) {
- /*
- * Stops and allows access when reaching disconnected root
- * directories that are part of internal filesystems (e.g. nsfs,
- * which is reachable through /proc/<pid>/ns/<namespace>).
- */
- allowed_parent1 = true;
- allowed_parent2 = true;
- break;
- }
-
+ switch (landlock_walk_path_up(&walker_path)) {
+ case LANDLOCK_WALK_CONTINUE:
+ continue;
+ case LANDLOCK_WALK_INTERNAL:
/*
- * We reached a disconnected root directory from a bind mount.
- * Let's continue the walk with the mount point we missed.
+ * Stops and allows access when reaching disconnected
+ * root directories that are part of internal
+ * filesystems (e.g. nsfs, which is reachable through
+ * /proc/<pid>/ns/<namespace>).
*/
- dput(walker_path.dentry);
- walker_path.dentry = walker_path.mnt->mnt_root;
- dget(walker_path.dentry);
- } else {
- struct dentry *const parent_dentry =
- dget_parent(walker_path.dentry);
-
- dput(walker_path.dentry);
- walker_path.dentry = parent_dentry;
+ allowed_parent1 = true;
+ allowed_parent2 = true;
+ break;
+ case LANDLOCK_WALK_STOP_REAL_ROOT:
+ /*
+ * Stops at the real root. Denies access because not
+ * all layers have granted access.
+ */
+ break;
}
+ break;
}
path_put(&walker_path);
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 03/10] landlock: Use landlock_walk_path_up() in collect_domain_accesses()
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 ` Justin Suess
2026-05-29 1:52 ` [PATCH v8 04/10] landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT user API Justin Suess
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Replace the open-coded loop with landlock_walk_path_up() and change the
function signature from (mnt_root, dir) to a single struct path. The
caller's mount point and starting dentry are now both carried in @path,
which keeps the traversal logic consistent with
is_access_to_paths_allowed().
No functional change intended.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Reworded commit message.
* Changed collect_domain_accesses() to take a single struct path *
instead of separate mnt_root/dir parameters, simplifying the
interface and matching is_access_to_paths_allowed().
* Tightened the disconnected-directory stop condition to require
!d_unhashed(walker_path.dentry) when comparing against the mount
root, so disconnected bind-mount roots are not mistaken for the
real mount root.
security/landlock/fs.c | 82 ++++++++++++++++++++++--------------------
1 file changed, 44 insertions(+), 38 deletions(-)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 8fb0aa59e180..6552351e0b9c 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -1032,48 +1032,51 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
* collect_domain_accesses - Walk through a file path and collect accesses
*
* @domain: Domain to check against.
- * @mnt_root: Last directory to check.
- * @dir: Directory to start the walk from.
+ * @path: Path to start the walk from and whose mount root is the last
+ * directory to check.
* @layer_masks_dom: Where to store the collected accesses.
*
- * This helper is useful to begin a path walk from the @dir directory to a
- * @mnt_root directory used as a mount point. This mount point is the common
- * ancestor between the source and the destination of a renamed and linked
- * file. While walking from @dir to @mnt_root, we record all the domain's
- * allowed accesses in @layer_masks_dom.
+ * This helper is useful to begin a path walk from @path to the mount root
+ * directory used as a mount point. This mount point is the common ancestor
+ * between the source and the destination of a renamed and linked file. While
+ * walking from @path to that mount root, we record all the domain's allowed
+ * accesses in @layer_masks_dom.
*
- * Because of disconnected directories, this walk may not reach @mnt_dir. In
- * this case, the walk will continue to @mnt_dir after this call.
+ * Because of disconnected directories, this walk may not reach that mount
+ * root. In this case, the walk will continue to the mount root after this
+ * call.
*
* This is similar to is_access_to_paths_allowed() but much simpler because it
* only handles walking on the same mount point and only checks one set of
* accesses.
*
- * Return: True if all the domain access rights are allowed for @dir, false if
- * the walk reached @mnt_root.
+ * Return: True if all the domain access rights are allowed for @path, false if
+ * the walk reached the mount root.
*/
-static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
- const struct dentry *const mnt_root,
- struct dentry *dir,
- struct layer_masks *layer_masks_dom)
+static bool
+collect_domain_accesses(const struct landlock_ruleset *const domain,
+ const struct path *const path,
+ struct layer_masks *layer_masks_dom)
{
bool ret = false;
+ struct path walker_path;
- if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom))
+ if (WARN_ON_ONCE(!domain || !path || !path->dentry || !path->mnt ||
+ !layer_masks_dom))
return true;
- if (is_nouser_or_private(dir))
+ if (is_nouser_or_private(path->dentry))
return true;
if (!landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
layer_masks_dom, LANDLOCK_KEY_INODE))
return true;
- dget(dir);
+ walker_path = *path;
+ path_get(&walker_path);
while (true) {
- struct dentry *parent_dentry;
-
/* Gets all layers allowing all domain accesses. */
- if (landlock_unmask_layers(find_rule(domain, dir),
+ if (landlock_unmask_layers(find_rule(domain,
+ walker_path.dentry),
layer_masks_dom)) {
/*
* Stops when all handled accesses are allowed by at
@@ -1084,17 +1087,19 @@ static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
}
/*
- * Stops at the mount point or the filesystem root for a disconnected
- * directory.
+ * Stops at the mount point or the filesystem root for a
+ * disconnected directory.
*/
- if (dir == mnt_root || unlikely(IS_ROOT(dir)))
+ if ((walker_path.dentry == path->mnt->mnt_root &&
+ walker_path.mnt == path->mnt) ||
+ unlikely(IS_ROOT(walker_path.dentry)))
break;
- parent_dentry = dget_parent(dir);
- dput(dir);
- dir = parent_dentry;
+ if (WARN_ON_ONCE(landlock_walk_path_up(&walker_path) !=
+ LANDLOCK_WALK_CONTINUE))
+ break;
}
- dput(dir);
+ path_put(&walker_path);
return ret;
}
@@ -1160,7 +1165,7 @@ static int current_check_refer_path(struct dentry *const old_dentry,
bool allow_parent1, allow_parent2;
access_mask_t access_request_parent1, access_request_parent2;
struct path mnt_dir;
- struct dentry *old_parent;
+ struct path old_parent_path;
struct layer_masks layer_masks_parent1 = {}, layer_masks_parent2 = {};
struct landlock_request request1 = {}, request2 = {};
@@ -1214,18 +1219,19 @@ static int current_check_refer_path(struct dentry *const old_dentry,
/*
* old_dentry may be the root of the common mount point and
* !IS_ROOT(old_dentry) at the same time (e.g. with open_tree() and
- * OPEN_TREE_CLONE). We do not need to call dget(old_parent) because
- * we keep a reference to old_dentry.
+ * OPEN_TREE_CLONE). We do not need to call path_get(&old_parent_path)
+ * because we keep a reference to old_dentry.
*/
- old_parent = (old_dentry == mnt_dir.dentry) ? old_dentry :
- old_dentry->d_parent;
+ old_parent_path.mnt = mnt_dir.mnt;
+ old_parent_path.dentry = (old_dentry == mnt_dir.dentry) ?
+ old_dentry :
+ old_dentry->d_parent;
/* new_dir->dentry is equal to new_dentry->d_parent */
- allow_parent1 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
- old_parent,
+ allow_parent1 = collect_domain_accesses(subject->domain,
+ &old_parent_path,
&layer_masks_parent1);
- allow_parent2 = collect_domain_accesses(subject->domain, mnt_dir.dentry,
- new_dir->dentry,
+ allow_parent2 = collect_domain_accesses(subject->domain, new_dir,
&layer_masks_parent2);
if (allow_parent1 && allow_parent2)
return 0;
@@ -1244,7 +1250,7 @@ static int current_check_refer_path(struct dentry *const old_dentry,
return 0;
if (request1.access) {
- request1.audit.u.path.dentry = old_parent;
+ request1.audit.u.path.dentry = old_parent_path.dentry;
landlock_log_denial(subject, &request1);
}
if (request2.access) {
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 04/10] landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT user API
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (2 preceding siblings ...)
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 ` Justin Suess
2026-05-29 1:52 ` [PATCH v8 05/10] landlock: Return inserted rule from landlock_insert_rule() Justin Suess
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Wire up the new LANDLOCK_ADD_RULE_NO_INHERIT flag for
sys_landlock_add_rule(). Define the constant in the UAPI header with
its documentation, accept it from user space for
%LANDLOCK_RULE_PATH_BENEATH only, and update the path-beneath useless-
rule check so that an empty allowed_access is still accepted when a
flag (quiet or no-inherit) is present.
The flag has no enforcement effect yet; that is added in a subsequent
patch.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Renamed patch from "Implement LANDLOCK_ADD_RULE_NO_INHERIT
userspace api" to "Add LANDLOCK_ADD_RULE_NO_INHERIT user API".
* Reworded the UAPI documentation for LANDLOCK_ADD_RULE_NO_INHERIT
in include/uapi/linux/landlock.h for clarity.
* Centralized flag validation in sys_landlock_add_rule(): rejects
unknown flags with a single ~(QUIET | NO_INHERIT) mask, and
rejects NO_INHERIT on non-path-beneath rule types from the
syscall entry point.
* Removed the now-redundant LANDLOCK_ADD_RULE_NO_INHERIT check in
add_rule_net_port().
* Documented the new EINVAL case for NO_INHERIT on unsupported rule
types in the syscall kernel-doc.
include/uapi/linux/landlock.h | 24 ++++++++++++++++++++++++
security/landlock/syscalls.c | 14 +++++++++++---
2 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 90a0752b61bf..d6de209ab961 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -124,10 +124,34 @@ struct landlock_ruleset_attr {
* allowed_access in the passed in rule_attr. When this flag is
* present, the caller is also allowed to pass in an empty
* allowed_access.
+ * %LANDLOCK_ADD_RULE_NO_INHERIT
+ * Disable the inheritance of access rights and flags from parent objects
+ * for the rule's object and its descendants.
+ *
+ * This flag currently applies only to filesystem rules. Passing it with
+ * any other rule type returns ``-EINVAL``.
+ *
+ * By default, Landlock filesystem rules inherit allowed accesses from
+ * ancestor directories: rights granted on a parent directory also apply
+ * to its children. A rule marked with %LANDLOCK_ADD_RULE_NO_INHERIT
+ * stops this propagation at its object; only the accesses explicitly
+ * allowed by the rule apply. Descendants of that object continue to
+ * inherit from it normally, unless they too carry this flag.
+ *
+ * This flag also enforces parent-directory restrictions: rename, rmdir,
+ * link, and other operations that would change the immediate parent of
+ * the rule's object or any of its ancestors are denied up to the VFS
+ * root. This prevents sandboxed processes from manipulating the
+ * filesystem hierarchy to evade restrictions (e.g. via sandbox-restart
+ * attacks).
+ *
+ * Inheritance of rule flags (such as %LANDLOCK_ADD_RULE_QUIET) from
+ * ancestor directories is also blocked at the rule's object.
*/
/* clang-format off */
#define LANDLOCK_ADD_RULE_QUIET (1U << 0)
+#define LANDLOCK_ADD_RULE_NO_INHERIT (1U << 1)
/* clang-format on */
/**
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 08b6045d6926..04dacfdfc9f3 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -361,7 +361,7 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
/*
* Informs about useless rule: empty allowed_access (i.e. deny rules)
* are ignored in path walks. However, the rule is not useless if it
- * is there to hold a quiet flag.
+ * carries a flag (quiet or no-inherit).
*/
if (!flags && !path_beneath_attr.allowed_access)
return -ENOMSG;
@@ -433,7 +433,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
* @rule_type: Identify the structure type pointed to by @rule_attr:
* %LANDLOCK_RULE_PATH_BENEATH or %LANDLOCK_RULE_NET_PORT.
* @rule_attr: Pointer to a rule (matching the @rule_type).
- * @flags: Must be 0 or %LANDLOCK_ADD_RULE_QUIET.
+ * @flags: Bitmask of %LANDLOCK_ADD_RULE_* flags.
*
* This system call enables to define a new rule and add it to an existing
* ruleset.
@@ -451,6 +451,8 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
* - %EINVAL: &landlock_net_port_attr.port is greater than 65535;
* - %EINVAL: LANDLOCK_ADD_RULE_QUIET is passed but the ruleset has no
* quiet access bits set for the corresponding rule type.
+ * - %EINVAL: LANDLOCK_ADD_RULE_NO_INHERIT is passed for a rule type
+ * that does not support it (e.g. %LANDLOCK_RULE_NET_PORT).
* - %ENOMSG: Empty accesses (e.g. &landlock_path_beneath_attr.allowed_access is
* 0) and no flags;
* - %EBADF: @ruleset_fd is not a file descriptor for the current thread, or a
@@ -472,7 +474,13 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd,
if (!is_initialized())
return -EOPNOTSUPP;
- if (flags && flags != LANDLOCK_ADD_RULE_QUIET)
+ /* Rejects unknown flags. */
+ if (flags & ~(LANDLOCK_ADD_RULE_QUIET | LANDLOCK_ADD_RULE_NO_INHERIT))
+ return -EINVAL;
+
+ /* LANDLOCK_ADD_RULE_NO_INHERIT only applies to path-beneath rules. */
+ if ((flags & LANDLOCK_ADD_RULE_NO_INHERIT) &&
+ rule_type != LANDLOCK_RULE_PATH_BENEATH)
return -EINVAL;
/* Gets and checks the ruleset. */
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 05/10] landlock: Return inserted rule from landlock_insert_rule()
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (3 preceding siblings ...)
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 ` Justin Suess
2026-05-29 1:52 ` [PATCH v8 06/10] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Change insert_rule() and landlock_insert_rule() to return the inserted
(or updated) struct landlock_rule pointer instead of an int errno.
Errors are propagated via ERR_PTR().
This gives callers a handle on the resulting rule so a subsequent change
can mutate per-layer flags on it (e.g. to mark ancestor rules created
for no-inherit topology sealing).
No functional change intended.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Replaced the v7 "Move find_rule definition above
landlock_append_fs_rule" patch with this new preparatory patch.
Instead of moving find_rule(), make landlock_insert_rule() (and
its static insert_rule() helper) return the inserted struct
landlock_rule * via ERR_PTR(), so callers can directly tag flags
on the resulting rule. Callers in net.c, merge_tree(), and
inherit_tree() updated accordingly. No functional change.
security/landlock/fs.c | 7 ++--
security/landlock/net.c | 8 +++--
security/landlock/ruleset.c | 68 ++++++++++++++++++-------------------
security/landlock/ruleset.h | 7 ++--
4 files changed, 48 insertions(+), 42 deletions(-)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 6552351e0b9c..ee7d9f5d7ee5 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -359,7 +359,8 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
const struct path *const path,
access_mask_t access_rights, const int flags)
{
- int err;
+ int err = 0;
+ struct landlock_rule *rule;
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
@@ -378,7 +379,9 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
if (IS_ERR(id.key.object))
return PTR_ERR(id.key.object);
mutex_lock(&ruleset->lock);
- err = landlock_insert_rule(ruleset, id, access_rights, flags);
+ rule = landlock_insert_rule(ruleset, id, access_rights, flags);
+ if (IS_ERR(rule))
+ err = PTR_ERR(rule);
mutex_unlock(&ruleset->lock);
/*
* No need to check for an error because landlock_insert_rule()
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 60894cff973e..f08be4be275a 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -23,11 +23,11 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
const u16 port, access_mask_t access_rights,
const int flags)
{
- int err;
const struct landlock_id id = {
.key.data = (__force uintptr_t)htons(port),
.type = LANDLOCK_KEY_NET_PORT,
};
+ struct landlock_rule *rule;
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
@@ -36,10 +36,12 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
~landlock_get_net_access_mask(ruleset, 0);
mutex_lock(&ruleset->lock);
- err = landlock_insert_rule(ruleset, id, access_rights, flags);
+ rule = landlock_insert_rule(ruleset, id, access_rights, flags);
mutex_unlock(&ruleset->lock);
- return err;
+ if (IS_ERR(rule))
+ return PTR_ERR(rule);
+ return 0;
}
static int current_check_access_socket(struct socket *const sock,
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index f01c3e14e55d..48397ab43a2d 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -203,12 +203,13 @@ static void build_check_ruleset(void)
* added to @ruleset as new constraints, similarly to a boolean AND between
* access rights.
*
- * Return: 0 on success, -errno on failure.
+ * Return: A pointer to the inserted or updated rule, or an ERR_PTR on failure.
*/
-static int insert_rule(struct landlock_ruleset *const ruleset,
- const struct landlock_id id,
- const struct landlock_layer (*layers)[],
- const size_t num_layers)
+static struct landlock_rule *
+insert_rule(struct landlock_ruleset *const ruleset,
+ const struct landlock_id id,
+ const struct landlock_layer (*layers)[],
+ const size_t num_layers)
{
struct rb_node **walker_node;
struct rb_node *parent_node = NULL;
@@ -218,14 +219,14 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
might_sleep();
lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!layers))
- return -ENOENT;
+ return ERR_PTR(-ENOENT);
if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object))
- return -ENOENT;
+ return ERR_PTR(-ENOENT);
root = get_root(ruleset, id.type);
if (IS_ERR(root))
- return PTR_ERR(root);
+ return ERR_CAST(root);
walker_node = &root->rb_node;
while (*walker_node) {
@@ -243,7 +244,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
/* Only a single-level layer should match an existing rule. */
if (WARN_ON_ONCE(num_layers != 1))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
/* If there is a matching rule, updates it. */
if ((*layers)[0].level == 0) {
@@ -252,16 +253,16 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
* landlock_add_rule(2), i.e. @ruleset is not a domain.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
if (WARN_ON_ONCE(this->layers[0].level != 0))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
this->layers[0].access |= (*layers)[0].access;
this->layers[0].flags.quiet |= (*layers)[0].flags.quiet;
- return 0;
+ return this;
}
if (WARN_ON_ONCE(this->layers[0].level == 0))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
/*
* Intersects access rights when it is a merge between a
@@ -270,23 +271,23 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
new_rule = create_rule(id, &this->layers, this->num_layers,
&(*layers)[0]);
if (IS_ERR(new_rule))
- return PTR_ERR(new_rule);
+ return ERR_CAST(new_rule);
rb_replace_node(&this->node, &new_rule->node, root);
free_rule(this, id.type);
- return 0;
+ return new_rule;
}
/* There is no match for @id. */
build_check_ruleset();
if (ruleset->num_rules >= LANDLOCK_MAX_NUM_RULES)
- return -E2BIG;
+ return ERR_PTR(-E2BIG);
new_rule = create_rule(id, layers, num_layers, NULL);
if (IS_ERR(new_rule))
- return PTR_ERR(new_rule);
+ return ERR_CAST(new_rule);
rb_link_node(&new_rule->node, parent_node, walker_node);
rb_insert_color(&new_rule->node, root);
ruleset->num_rules++;
- return 0;
+ return new_rule;
}
static void build_check_layer(void)
@@ -305,9 +306,10 @@ static void build_check_layer(void)
}
/* @ruleset must be locked by the caller. */
-int landlock_insert_rule(struct landlock_ruleset *const ruleset,
- const struct landlock_id id,
- const access_mask_t access, const int flags)
+struct landlock_rule *
+landlock_insert_rule(struct landlock_ruleset *const ruleset,
+ const struct landlock_id id,
+ const access_mask_t access, const int flags)
{
struct landlock_layer layers[] = { {
.access = access,
@@ -326,9 +328,8 @@ static int merge_tree(struct landlock_ruleset *const dst,
struct landlock_ruleset *const src,
const enum landlock_key_type key_type)
{
- struct landlock_rule *walker_rule, *next_rule;
+ struct landlock_rule *walker_rule, *next_rule, *rule;
struct rb_root *src_root;
- int err = 0;
might_sleep();
lockdep_assert_held(&dst->lock);
@@ -358,11 +359,11 @@ static int merge_tree(struct landlock_ruleset *const dst,
layers[0].access = walker_rule->layers[0].access;
layers[0].flags = walker_rule->layers[0].flags;
- err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
- if (err)
- return err;
+ rule = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
+ if (IS_ERR(rule))
+ return PTR_ERR(rule);
}
- return err;
+ return 0;
}
static int merge_ruleset(struct landlock_ruleset *const dst,
@@ -412,9 +413,8 @@ static int inherit_tree(struct landlock_ruleset *const parent,
struct landlock_ruleset *const child,
const enum landlock_key_type key_type)
{
- struct landlock_rule *walker_rule, *next_rule;
+ struct landlock_rule *walker_rule, *next_rule, *rule;
struct rb_root *parent_root;
- int err = 0;
might_sleep();
lockdep_assert_held(&parent->lock);
@@ -432,12 +432,12 @@ static int inherit_tree(struct landlock_ruleset *const parent,
.type = key_type,
};
- err = insert_rule(child, id, &walker_rule->layers,
- walker_rule->num_layers);
- if (err)
- return err;
+ rule = insert_rule(child, id, &walker_rule->layers,
+ walker_rule->num_layers);
+ if (IS_ERR(rule))
+ return PTR_ERR(rule);
}
- return err;
+ return 0;
}
static int inherit_ruleset(struct landlock_ruleset *const parent,
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index ff163e5db5f0..5b7f554e8442 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -217,9 +217,10 @@ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);
DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_ruleset(_T))
-int landlock_insert_rule(struct landlock_ruleset *const ruleset,
- const struct landlock_id id,
- const access_mask_t access, const int flags);
+struct landlock_rule *
+landlock_insert_rule(struct landlock_ruleset *const ruleset,
+ const struct landlock_id id,
+ const access_mask_t access, const int flags);
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 06/10] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (4 preceding siblings ...)
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
2026-05-29 1:52 ` [PATCH v8 07/10] landlock: Add documentation for LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
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
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 07/10] landlock: Add documentation for LANDLOCK_ADD_RULE_NO_INHERIT
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (5 preceding siblings ...)
2026-05-29 1:52 ` [PATCH v8 06/10] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
@ 2026-05-29 1:52 ` Justin Suess
2026-05-29 1:52 ` [PATCH v8 08/10] samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to landlock-sandboxer Justin Suess
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Adds documentation of the flag to the userspace api, describing
the functionality of the flag and parent directory protections.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Minor wording polish in the new 'Filesystem inheritance
suppression' documentation section; no semantic change.
Documentation/userspace-api/landlock.rst | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 138d504cb498..ae3136461b18 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -733,6 +733,24 @@ struct landlock_ruleset_attr. It is also now possible to suppress audit logs
for scope accesses via the ``quiet_scoped`` field of struct
landlock_ruleset_attr.
+Filesystem inheritance suppression (ABI < 10)
+---------------------------------------------
+
+Starting with the Landlock ABI version 10, it is possible to prevent a
+directory or file from inheriting its parent's access grants by using the
+``LANDLOCK_ADD_RULE_NO_INHERIT`` flag passed to sys_landlock_add_rule().
+This is useful for policies where a parent directory needs broader access
+than its children.
+
+To mitigate sandbox-restart attacks, the tagged inode and all of its
+ancestors up to the VFS root cannot be removed, renamed, reparented, or
+linked into or out of other directories.
+
+Inheritance of access grants from descendants of an inode tagged with
+``LANDLOCK_ADD_RULE_NO_INHERIT`` is unaffected: such descendants continue
+to inherit from the tagged inode normally, unless they also carry this
+flag.
+
.. _kernel_support:
Kernel support
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 08/10] samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to landlock-sandboxer
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (6 preceding siblings ...)
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 ` 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
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic
Cc: linux-kernel, linux-security-module, Justin Suess, Tingmao Wang
Add a new LL_FS_NO_INHERIT environment variable to the sandboxer.
Paths listed in it are added with the LANDLOCK_ADD_RULE_NO_INHERIT
flag, demonstrating how to set up a parent directory with broader
access than its children.
The flag is silently skipped on kernels older than ABI 10.
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Reworded commit message.
* Updated the ABI fallthrough comment to mention 'quiet or
no_inherit flags'. No code change beyond the comment update.
samples/landlock/sandboxer.c | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index 74ee53afed6a..b126ffd7cd4f 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -60,6 +60,7 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_FS_RW_NAME "LL_FS_RW"
#define ENV_FS_QUIET_NAME "LL_FS_QUIET"
#define ENV_FS_QUIET_ACCESS_NAME "LL_FS_QUIET_ACCESS"
+#define ENV_FS_NO_INHERIT_NAME "LL_FS_NO_INHERIT"
#define ENV_TCP_BIND_NAME "LL_TCP_BIND"
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_NET_QUIET_NAME "LL_NET_QUIET"
@@ -395,6 +396,7 @@ static const char help[] =
"but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n"
ENV_FS_QUIET_NAME " and " ENV_NET_QUIET_NAME ", both optional, can then be used "
"to make access to some denied paths or network ports not trigger audit logging.\n"
+ ENV_FS_NO_INHERIT_NAME " can be used to suppress access right propagation (ABI >= 10).\n"
ENV_FS_QUIET_ACCESS_NAME " and " ENV_NET_QUIET_ACCESS_NAME " can be used to specify "
"which accesses should be quieted (defaults to all):\n"
"* " ENV_FS_QUIET_ACCESS_NAME ": file system accesses to quiet\n"
@@ -447,6 +449,7 @@ int main(const int argc, char *const argv[], char *const *const envp)
};
bool quiet_supported = true;
+ bool no_inherit_supported = true;
int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
int set_restrict_flags = 0;
@@ -538,8 +541,9 @@ int main(const int argc, char *const argv[], char *const *const envp)
~(LANDLOCK_ACCESS_NET_BIND_UDP |
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
__attribute__((fallthrough));
- /* Don't add quiet flags for ABI < 10 later on. */
+ /* Don't add quiet or no_inherit flags for ABI < 10 later on. */
quiet_supported = false;
+ no_inherit_supported = false;
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
@@ -644,6 +648,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
goto err_close_ruleset;
}
+ /* Don't require this env to be present. */
+ if (no_inherit_supported && getenv(ENV_FS_NO_INHERIT_NAME)) {
+ if (populate_ruleset_fs(ENV_FS_NO_INHERIT_NAME, ruleset_fd, 0,
+ LANDLOCK_ADD_RULE_NO_INHERIT))
+ goto err_close_ruleset;
+ }
+
if (populate_ruleset_net(ENV_TCP_BIND_NAME, ruleset_fd,
LANDLOCK_ACCESS_NET_BIND_TCP, 0)) {
goto err_close_ruleset;
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 09/10] selftests/landlock: Add selftests for LANDLOCK_ADD_RULE_NO_INHERIT
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (7 preceding siblings ...)
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 ` Justin Suess
2026-05-29 1:52 ` [PATCH v8 10/10] landlock: Add KUnit tests " Justin Suess
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Add test coverage for the new flag:
- New layout1_no_inherit fixture with five variants covering NO_INHERIT
on leaf, middle, and root directories, RW-over-RO expansion, and a
regular file target. Three tests per variant exercise inheritance
blocking, topology sealing, and layered (multi-domain) NO_INHERIT.
- A new layout4_disconnected_leafs variant exercising NO_INHERIT applied
through a bind mount, asserting that ancestors in both the bind and
source paths are sealed.
- A new audit_no_inherit fixture verifying that the flag interacts
correctly with the quiet flag: a quiet ancestor does not suppress
audit on a descendant that has crossed a NO_INHERIT boundary.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Reorganized the new fs_test.c coverage around fixtures and
variants instead of one TEST_F_FORK per scenario:
- New layout1_no_inherit fixture with five FIXTURE_VARIANT_ADD
cases (rw_parent_ro_leaf, rw_parent_ro_middle, rw_parent_ro_root,
ro_parent_rw_middle, rw_parent_read_file) collapse what were
eight near-duplicate layout1 tests in v7 into three shared
tests (blocks_inheritance, seals_topology, layered_no_inherit).
- New layout4_disconnected_leafs variant 'no_inherit_mount' with a
single 'no_inherit_seals_mount' test replaces the four
v7 layout4 tests (no_inherit_mount_parent_{rename,rmdir,link}
and no_inherit_source_parent_rename) by exercising all four
sealed topology operations in one test.
- New audit_no_inherit fixture with three variants
(parent_is_logged, blocks_quiet_inheritance, quiet_parent)
covers the quiet/no_inherit interaction previously inlined
into an ad hoc audit test.
* Net change: 705 added lines in v7 -> 419 added lines in v8, with
equivalent coverage.
tools/testing/selftests/landlock/fs_test.c | 419 +++++++++++++++++++++
1 file changed, 419 insertions(+)
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 2e32295258f9..625ff1afecb0 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -1429,6 +1429,224 @@ TEST_F_FORK(layout1, inherit_superset)
ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY));
}
+FIXTURE(layout1_no_inherit) {};
+
+FIXTURE_SETUP(layout1_no_inherit)
+{
+ prepare_layout(_metadata);
+ create_layout1(_metadata);
+}
+
+FIXTURE_TEARDOWN_PARENT(layout1_no_inherit)
+{
+ remove_layout1(_metadata);
+ cleanup_layout(_metadata);
+}
+
+FIXTURE_VARIANT(layout1_no_inherit)
+{
+ const char *ni_path;
+ const __u64 ni_access;
+ const char *ni_file;
+ const char *desc_file;
+ const int expected_ni_write;
+ const int expected_ni_read;
+ const int expected_desc_write;
+ const int expected_desc_read;
+};
+
+/* NO_INHERIT on leaf directory: blocks parent's RW, grants only RO. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_ro_leaf) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2/s1d3",
+ .ni_access = ACCESS_RO,
+ .ni_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f2",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = EACCES,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT on middle directory: blocks parent's RW for all descendants. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_ro_middle) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2",
+ .ni_access = ACCESS_RO,
+ .ni_file = TMP_DIR "/s1d1/s1d2/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = EACCES,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT on root directory: blocks parent's RW for entire subtree. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_ro_root) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1",
+ .ni_access = ACCESS_RO,
+ .ni_file = TMP_DIR "/s1d1/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = EACCES,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT with RW access expands parent's RO to RW. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, ro_parent_rw_middle) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2",
+ .ni_access = ACCESS_RW,
+ .ni_file = TMP_DIR "/s1d1/s1d2/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .expected_ni_write = 0,
+ .expected_ni_read = 0,
+ .expected_desc_write = 0,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT on a file: file gets only its explicit READ_FILE access. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_read_file) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2/f1",
+ .ni_access = LANDLOCK_ACCESS_FS_READ_FILE,
+ .ni_file = TMP_DIR "/s1d1/s1d2/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/f2",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = 0,
+ .expected_desc_read = 0,
+};
+
+TEST_F_FORK(layout1_no_inherit, blocks_inheritance)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ };
+ int ruleset_fd;
+
+ /* RO variants: TMP_DIR gets RO instead of RW. */
+ if (variant->ni_access == ACCESS_RW)
+ ruleset_attr.handled_access_fs |= LANDLOCK_ACCESS_FS_READ_DIR;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ if (variant->ni_access == ACCESS_RW)
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, TMP_DIR, 0);
+ else
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, TMP_DIR, 0);
+
+ add_path_beneath(_metadata, ruleset_fd, variant->ni_access,
+ variant->ni_path, LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ EXPECT_EQ(variant->expected_ni_write,
+ test_open(variant->ni_file, O_WRONLY));
+ EXPECT_EQ(variant->expected_ni_read,
+ test_open(variant->ni_file, O_RDONLY));
+
+ if (variant->desc_file != variant->ni_file) {
+ EXPECT_EQ(variant->expected_desc_write,
+ test_open(variant->desc_file, O_WRONLY));
+ EXPECT_EQ(variant->expected_desc_read,
+ test_open(variant->desc_file, O_RDONLY));
+ }
+}
+
+TEST_F_FORK(layout1_no_inherit, seals_topology)
+{
+ int ruleset_fd;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ };
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ TMP_DIR, 0);
+ add_path_beneath(_metadata, ruleset_fd, variant->ni_access,
+ variant->ni_path, LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* The directory bearing NO_INHERIT cannot be renamed or removed. */
+ ASSERT_EQ(-1, rename(variant->ni_path, TMP_DIR "/ni_renamed"));
+ ASSERT_EQ(EACCES, errno);
+
+ /*
+ * Content inside the NO_INHERIT directory is still mutable
+ * (if the access rights permit it).
+ */
+ if (variant->ni_access & LANDLOCK_ACCESS_FS_REMOVE_FILE) {
+ ASSERT_EQ(0, unlink(variant->ni_file));
+ } else {
+ ASSERT_EQ(-1, unlink(variant->ni_file));
+ }
+
+ /* Unrelated operations outside the sealed branch still work. */
+ ASSERT_EQ(0, unlink(file1_s2d1));
+ ASSERT_EQ(0, mknod(file1_s2d1, S_IFREG | 0700, 0));
+}
+
+TEST_F_FORK(layout1_no_inherit, layered_no_inherit)
+{
+ const struct rule layer_rules[] = {
+ {
+ .path = TMP_DIR,
+ .access = ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ },
+ {},
+ };
+ int ruleset_fd;
+
+ /* Layer 1: RW on TMP_DIR. */
+ ruleset_fd = create_ruleset(_metadata,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ layer_rules);
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Layer 2: NO_INHERIT on the target. */
+ ruleset_fd = create_ruleset(_metadata,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ layer_rules);
+ ASSERT_LE(0, ruleset_fd);
+ add_path_beneath(_metadata, ruleset_fd, variant->ni_access,
+ variant->ni_path, LANDLOCK_ADD_RULE_NO_INHERIT);
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* The target path cannot be renamed. */
+ ASSERT_EQ(-1, rename(variant->ni_path, TMP_DIR "/ni_renamed_layered"));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Content at NI path respects the NO_INHERIT access from layer 2. */
+ EXPECT_EQ(variant->expected_ni_write,
+ test_open(variant->ni_file, O_WRONLY));
+ EXPECT_EQ(variant->expected_ni_read,
+ test_open(variant->ni_file, O_RDONLY));
+}
+
TEST_F_FORK(layout0, max_layers)
{
int i, err;
@@ -5571,6 +5789,25 @@ FIXTURE_VARIANT(layout4_disconnected_leafs)
const int expected_exchange_result;
/* Expected result of the call to renameat([fd:s1d42]/f4, [fd:s1d42]/f5). */
const int expected_same_dir_rename_result;
+
+ /*
+ * If true, a NO_INHERIT rule is set on s1d41 (via the bind mount
+ * at s2d2). Used by the no_inherit_mount test.
+ */
+ bool no_inherit_on_s1d41;
+ /*
+ * Access rights used for the optional NO_INHERIT rule on s1d41.
+ */
+ const __u64 no_inherit_access;
+ /*
+ * Expected result of renaming s1d31 (parent of s1d41 within the
+ * mount) when no_inherit_on_s1d41 is set.
+ */
+ const int expected_parent_rename;
+ /*
+ * Expected result of rmdir on s1d31, when no_inherit_on_s1d41 is set.
+ */
+ const int expected_parent_rmdir;
};
/* clang-format off */
@@ -5823,6 +6060,26 @@ FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, f1_f2_f3) {
.expected_exchange_result = EACCES,
};
+/*
+ * NO_INHERIT variant: s1d41 is protected with ACCESS_RO via the bind mount.
+ * Parents within the mount are sealed against topology changes.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, no_inherit_mount) {
+ /* clang-format on */
+ .allowed_f1 = LANDLOCK_ACCESS_FS_READ_FILE,
+ .allowed_f2 = LANDLOCK_ACCESS_FS_READ_FILE,
+ .allowed_f3 = LANDLOCK_ACCESS_FS_READ_FILE,
+ .expected_read_result = 0,
+ .expected_rename_result = EACCES,
+ .expected_exchange_result = EACCES,
+ .expected_same_dir_rename_result = EACCES,
+ .no_inherit_on_s1d41 = true,
+ .no_inherit_access = ACCESS_RO,
+ .expected_parent_rename = EACCES,
+ .expected_parent_rmdir = EACCES,
+};
+
TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange)
{
const __u64 handled_access =
@@ -5931,6 +6188,70 @@ TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange)
test_renameat(s1d42_bind_fd, "f4", s1d42_bind_fd, "f5"));
}
+/*
+ * When s1d41 (accessed via the bind mount at s2d2) is protected with
+ * NO_INHERIT, its parent directories within the mount are sealed from
+ * topology changes. Other variants do not exercise NO_INHERIT and skip
+ * this test.
+ */
+TEST_F_FORK(layout4_disconnected_leafs, no_inherit_seals_mount)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ };
+ int ruleset_fd, s1d41_bind_fd;
+
+ if (!variant->no_inherit_on_s1d41)
+ SKIP(return, "variant does not set NO_INHERIT on s1d41");
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ TMP_DIR, 0);
+
+ s1d41_bind_fd = open(TMP_DIR "/s2d1/s2d2/s1d31/s1d41",
+ O_DIRECTORY | O_PATH | O_CLOEXEC);
+ ASSERT_LE(0, s1d41_bind_fd);
+
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &(struct landlock_path_beneath_attr){
+ .parent_fd = s1d41_bind_fd,
+ .allowed_access =
+ variant->no_inherit_access,
+ },
+ LANDLOCK_ADD_RULE_NO_INHERIT));
+ EXPECT_EQ(0, close(s1d41_bind_fd));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Parent of s1d41 within the mount is sealed. */
+ ASSERT_EQ(-1, rmdir(TMP_DIR "/s2d1/s2d2/s1d31"));
+ ASSERT_EQ(variant->expected_parent_rmdir, errno);
+
+ ASSERT_EQ(-1, rename(TMP_DIR "/s2d1/s2d2/s1d31",
+ TMP_DIR "/s2d1/s2d2/s1d31_renamed"));
+ ASSERT_EQ(variant->expected_parent_rename, errno);
+
+ /* Sibling directories outside the sealed chain are free. */
+ ASSERT_EQ(0, rename(TMP_DIR "/s2d1/s2d2/s1d32",
+ TMP_DIR "/s2d1/s2d2/s1d32_renamed"));
+ ASSERT_EQ(0, rename(TMP_DIR "/s2d1/s2d2/s1d32_renamed",
+ TMP_DIR "/s2d1/s2d2/s1d32"));
+
+ /* The mount source parent hierarchy is also sealed. */
+ ASSERT_EQ(-1, rename(TMP_DIR "/s1d1/s1d2/s1d31",
+ TMP_DIR "/s1d1/s1d2/s1d31_renamed"));
+ ASSERT_EQ(variant->expected_parent_rename, errno);
+}
+
/*
* layout5_disconnected_branch before rename:
*
@@ -7358,6 +7679,104 @@ TEST_F(audit_layout1, write_file)
EXPECT_EQ(1, records.domain);
}
+FIXTURE(audit_no_inherit)
+{
+ struct audit_filter audit_filter;
+ int audit_fd;
+};
+
+FIXTURE_SETUP(audit_no_inherit)
+{
+ prepare_layout(_metadata);
+ create_layout1(_metadata);
+
+ set_cap(_metadata, CAP_AUDIT_CONTROL);
+ self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
+ EXPECT_LE(0, self->audit_fd);
+ clear_cap(_metadata, CAP_AUDIT_CONTROL);
+}
+
+FIXTURE_TEARDOWN_PARENT(audit_no_inherit)
+{
+ remove_layout1(_metadata);
+ cleanup_layout(_metadata);
+
+ EXPECT_EQ(0, audit_cleanup(-1, NULL));
+}
+
+FIXTURE_VARIANT(audit_no_inherit)
+{
+ bool parent_quiet;
+ const char *test_path;
+ bool expect_audit_log;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit_no_inherit, parent_is_logged) {
+ /* clang-format on */
+ .parent_quiet = false,
+ .test_path = TMP_DIR "/s1d1/s1d2/f1",
+ .expect_audit_log = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit_no_inherit, blocks_quiet_inheritance) {
+ /* clang-format on */
+ .parent_quiet = true,
+ .test_path = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .expect_audit_log = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit_no_inherit, quiet_parent) {
+ /* clang-format on */
+ .parent_quiet = true,
+ .test_path = TMP_DIR "/s1d1/f1",
+ .expect_audit_log = false,
+};
+
+TEST_F(audit_no_inherit, no_inherit_audit)
+{
+ struct audit_records records;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ .quiet_access_fs = variant->parent_quiet ? ACCESS_RW : 0,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ if (variant->parent_quiet)
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d1,
+ LANDLOCK_ADD_RULE_QUIET);
+ else
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d1, 0);
+
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d3,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+
+ EXPECT_EQ(EACCES, test_open(variant->test_path, O_WRONLY));
+ if (variant->expect_audit_log) {
+ EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+ "fs\\.write_file",
+ variant->test_path));
+ } else {
+ EXPECT_NE(0, matches_log_fs(_metadata, self->audit_fd,
+ "fs\\.write_file",
+ variant->test_path));
+ }
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(variant->expect_audit_log ? 1 : 0, records.domain);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
TEST_F(audit_layout1, read_file)
{
struct audit_records records;
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* [PATCH v8 10/10] landlock: Add KUnit tests for LANDLOCK_ADD_RULE_NO_INHERIT
2026-05-29 1:51 [PATCH v8 00/10] Implement LANDLOCK_ADD_RULE_NO_INHERIT Justin Suess
` (8 preceding siblings ...)
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 ` Justin Suess
9 siblings, 0 replies; 11+ messages in thread
From: Justin Suess @ 2026-05-29 1:52 UTC (permalink / raw)
To: gnoack3000, mic; +Cc: linux-kernel, linux-security-module, Justin Suess
Add the landlock_ruleset KUnit suite with five tests for the
no_inherit handling in landlock_unmask_layers():
- test_unmask_no_inherit_propagates: a rule with no_inherit unmasks
access and sets the no_inherit bit on the layer mask.
- test_unmask_no_inherit_skip: a layer with no_inherit already set in
the mask is skipped (no access removal).
- test_unmask_no_inherit_both_set: when both rule and mask have
no_inherit, the skip still happens and the bit stays set.
- test_unmask_multilayer_no_inherit: no_inherit on one layer of a
multi-layer rule only affects that layer.
- test_unmask_no_inherit_sequential: applying a descendant rule
(no_inherit) followed by an ancestor rule causes the ancestor to be
skipped, modeling a path walk.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
v7..v8 changes:
* Renamed patch from 'Implement KUnit test' to 'Add KUnit tests'
(now plural).
* Replaced the single test_unmask_layers_no_inherit() case with
five focused tests aligned with the new per-layer no_inherit
bit added to struct layer_mask in patch 6:
- test_unmask_no_inherit_propagates
- test_unmask_no_inherit_skip
- test_unmask_no_inherit_both_set
- test_unmask_multilayer_no_inherit
- test_unmask_no_inherit_sequential
* Added alloc_rule() and fill_masks() helpers to share setup
across the new tests.
security/landlock/ruleset.c | 182 ++++++++++++++++++++++++++++++++++++
1 file changed, 182 insertions(+)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index c78e2b2d73ff..9f47d106aca3 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -6,6 +6,7 @@
* Copyright © 2018-2020 ANSSI
*/
+#include <kunit/test.h>
#include <linux/bits.h>
#include <linux/bug.h>
#include <linux/cleanup.h>
@@ -766,3 +767,184 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
return handled_accesses;
}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+/*
+ * Helper to allocate a rule with @num_layers layers and initialize
+ * its num_layers field. Caller must fill in individual layers.
+ */
+static struct landlock_rule *alloc_rule(struct kunit *test, u32 num_layers)
+{
+ struct landlock_rule *rule;
+
+ rule = kzalloc(struct_size(rule, layers, num_layers), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, rule);
+ rule->num_layers = num_layers;
+ return rule;
+}
+
+/*
+ * Build a layer_masks with the first @num_layers layers' access set to
+ * @val, and all no_inherit flags cleared. Layers beyond @num_layers stay
+ * zeroed, matching what landlock_init_layer_masks() produces for a domain
+ * with that many layers.
+ */
+static void fill_masks(struct layer_masks *masks, access_mask_t val,
+ size_t num_layers)
+{
+ memset(masks, 0, sizeof(*masks));
+ for (size_t i = 0; i < num_layers; i++)
+ masks->layers[i].access = val;
+}
+
+/* Verify that a rule with no_inherit unmasks access and propagates the flag. */
+static void test_unmask_no_inherit_propagates(struct kunit *const test)
+{
+ struct landlock_rule *rule = alloc_rule(test, 1);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0) | BIT_ULL(1);
+
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+ rule->layers[0].flags.no_inherit = true;
+
+ fill_masks(&masks, req, 1);
+ landlock_unmask_layers(rule, &masks);
+
+ /* access bit 0 should be cleared, bit 1 remains */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access,
+ BIT_ULL(1));
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[1].access, 0);
+ kfree(rule);
+}
+
+/* Verify that a pre-set no_inherit in the mask causes the layer to be skipped. */
+static void test_unmask_no_inherit_skip(struct kunit *const test)
+{
+ struct landlock_rule *rule = alloc_rule(test, 1);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0);
+
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+
+ fill_masks(&masks, req, 1);
+ masks.layers[0].no_inherit = true;
+ landlock_unmask_layers(rule, &masks);
+
+ /* bit 0 should NOT be cleared because layer was skipped */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access, req);
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+ kfree(rule);
+}
+
+/*
+ * Verify that no_inherit on the rule is still set when the mask already
+ * has no_inherit (the skip prevents access removal but the flag propagates).
+ */
+static void test_unmask_no_inherit_both_set(struct kunit *const test)
+{
+ struct landlock_rule *rule = alloc_rule(test, 1);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0);
+
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+ rule->layers[0].flags.no_inherit = true;
+
+ fill_masks(&masks, req, 1);
+ masks.layers[0].no_inherit = true;
+ landlock_unmask_layers(rule, &masks);
+
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access, req);
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+ kfree(rule);
+}
+
+/*
+ * Verify that no_inherit on layer 1 of a multi-layer rule only affects
+ * layer 1; layer 2 still contributes normally.
+ */
+static void test_unmask_multilayer_no_inherit(struct kunit *const test)
+{
+ struct landlock_rule *rule = alloc_rule(test, 2);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0) | BIT_ULL(1);
+
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+ rule->layers[0].flags.no_inherit = true;
+
+ rule->layers[1].level = 2;
+ rule->layers[1].access = BIT_ULL(1);
+
+ fill_masks(&masks, req, 2);
+ landlock_unmask_layers(rule, &masks);
+
+ /* Layer 1: bit 0 cleared, no_inherit set */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access, BIT_ULL(1));
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+
+ /* Layer 2: bit 1 cleared, no_inherit not set */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[1].access, BIT_ULL(0));
+ KUNIT_EXPECT_FALSE(test, masks.layers[1].no_inherit);
+ kfree(rule);
+}
+
+/*
+ * Verify that when applying two rules sequentially (as happens during
+ * a path walk), no_inherit from the first rule prevents the second
+ * rule from contributing to that layer.
+ */
+static void test_unmask_no_inherit_sequential(struct kunit *const test)
+{
+ struct landlock_rule *rule1 = alloc_rule(test, 1);
+ struct landlock_rule *rule2 = alloc_rule(test, 1);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0) | BIT_ULL(1);
+
+ /* Rule 1: no_inherit on layer 1, grants access bit 0 */
+ rule1->layers[0].level = 1;
+ rule1->layers[0].access = BIT_ULL(0);
+ rule1->layers[0].flags.no_inherit = true;
+
+ /* Rule 2: also on layer 1, grants access bit 1 (ancestor rule) */
+ rule2->layers[0].level = 1;
+ rule2->layers[0].access = BIT_ULL(1);
+
+ /* Apply rule1 first (descendant), then rule2 (ancestor) */
+ fill_masks(&masks, req, 1);
+ landlock_unmask_layers(rule1, &masks);
+ landlock_unmask_layers(rule2, &masks);
+
+ /*
+ * Rule2 should be skipped because rule1 set no_inherit.
+ * bit 0 cleared by rule1, bit 1 remains because rule2 skipped.
+ */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access, BIT_ULL(1));
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+ kfree(rule1);
+ kfree(rule2);
+}
+
+/* clang-format off */
+static struct kunit_case test_cases[] = {
+ KUNIT_CASE(test_unmask_no_inherit_propagates),
+ KUNIT_CASE(test_unmask_no_inherit_skip),
+ KUNIT_CASE(test_unmask_no_inherit_both_set),
+ KUNIT_CASE(test_unmask_multilayer_no_inherit),
+ KUNIT_CASE(test_unmask_no_inherit_sequential),
+ {}
+};
+/* clang-format on */
+
+static struct kunit_suite test_suite = {
+ .name = "landlock_ruleset",
+ .test_cases = test_cases,
+};
+
+kunit_test_suite(test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
--
2.53.0
^ permalink raw reply related [flat|nested] 11+ messages in thread