[PATCH v2 09/17] landlock: Add tracepoints for rule checking

Mickaël Salaün
2026-04-06T14:37:38Z

Merge landlock_find_rule() into landlock_unmask_layers() to consolidate
rule finding into unmask checking. landlock_unmask_layers() now takes a
landlock_id and the domain instead of a rule pointer.

This enables us to not deal with Landlock rule pointers outside of the
domain implementation, to avoid two calls, and to get all required
information available to landlock_unmask_layers().

Use the per-type tracepoint wrappers unmask_layers_fs() and
unmask_layers_net() to emit tracepoints recording which rules matched
and what access masks were fulfilled.

Setting allowed_parent2 to true for non-dom-check requests when
get_inode_id() returns false preserves the pre-refactoring behavior: a
negative dentry (no backing inode) has no matching rule, so the access
is allowed at this path component. Before the refactoring,
landlock_unmask_layers() with a NULL rule produced this result as a side
effect; now the caller must set it explicitly.

The check_rule tracepoints add up to 80 bytes of stack in the access
check path (dynamic layers array in TP_STRUCT__entry). This cost is
only paid when a tracer is attached; the static branch is not taken
otherwise.

Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---

Changes since v1:
https://lore.kernel.org/r/20250523165741.693976-6-mic@digikod.net
- Merged find-rule consolidation (v1 2/5) into this patch.
- Added check_rule_net tracepoint for network rules.
- Added get_inode_id() helper with rcu_access_pointer().
- Added allowed_parent2 behavioral fix.
---
include/trace/events/landlock.h | 99 ++++++++++++++++++++++
security/landlock/domain.c | 32 ++++---
security/landlock/domain.h | 10 +--
security/landlock/fs.c | 145 +++++++++++++++++++++++---------
security/landlock/net.c | 21 ++++-
5 files changed, 246 insertions(+), 61 deletions(-)

diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 533aea6152e1..e7bb8fa802bf 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -12,8 +12,10 @@

#include <linux/tracepoint.h>

+struct dentry;
struct landlock_domain;
struct landlock_hierarchy;
+struct landlock_rule;
struct landlock_ruleset;
struct path;

@@ -234,6 +236,103 @@ TRACE_EVENT(landlock_free_domain,
TP_printk("domain=%llx denials=%llu", __entry->domain_id,
__entry->denials));

+/**
+ * landlock_check_rule_fs - filesystem rule evaluated during access check
+ * @domain: Enforcing domain (never NULL)
+ * @dentry: Filesystem dentry being checked (never NULL)
+ * @access_request: Access mask being requested
+ * @rule: Matching rule with per-layer access masks (never NULL)
+ *
+ * Emitted for each rule that matches during a filesystem access check.
+ * The layers array shows the allowed access mask at each domain layer.
+ */
+TRACE_EVENT(landlock_check_rule_fs,
+
+ TP_PROTO(const struct landlock_domain *domain,
+ const struct dentry *dentry, access_mask_t access_request,
+ const struct landlock_rule *rule),
+
+ TP_ARGS(domain, dentry, access_request, rule),
+
+ TP_STRUCT__entry(__field(__u64, domain_id) __field(
+ access_mask_t,
+ access_request) __field(dev_t, dev) __field(ino_t, ino)
+ __dynamic_array(access_mask_t, layers,
+ domain->num_layers)),
+
+ TP_fast_assign(__entry->domain_id = domain->hierarchy->id;
+ __entry->access_request = access_request;
+ __entry->dev = dentry->d_sb->s_dev;
+ __entry->ino = d_backing_inode(dentry)->i_ino;
+
+ for (size_t level = 1, i = 0;
+ level <= __get_dynamic_array_len(layers) /
+ sizeof(access_mask_t);
+ level++) {
+ access_mask_t allowed;
+
+ if (i < rule->num_layers &&
+ level == rule->layers[i].level) {
+ allowed = rule->layers[i].access;
+ i++;
+ } else {
+ allowed = 0;
+ }
+ ((access_mask_t *)__get_dynamic_array(
+ layers))[level - 1] = allowed;
+ }),
+
+ TP_printk("domain=%llx request=0x%x dev=%u:%u ino=%lu allowed=%s",
+ __entry->domain_id, __entry->access_request,
+ MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
+ __print_dynamic_array(layers, sizeof(access_mask_t))));
+
+/**
+ * landlock_check_rule_net - network port rule evaluated during access check
+ * @domain: Enforcing domain (never NULL)
+ * @port: Network port being checked (host endianness)
+ * @access_request: Access mask being requested
+ * @rule: Matching rule with per-layer access masks (never NULL)
+ */
+TRACE_EVENT(landlock_check_rule_net,
+
+ TP_PROTO(const struct landlock_domain *domain, __u64 port,
+ access_mask_t access_request,
+ const struct landlock_rule *rule),
+
+ TP_ARGS(domain, port, access_request, rule),
+
+ TP_STRUCT__entry(__field(__u64, domain_id) __field(
+ access_mask_t, access_request) __field(__u64, port)
+ __dynamic_array(access_mask_t, layers,
+ domain->num_layers)),
+
+ TP_fast_assign(__entry->domain_id = domain->hierarchy->id;
+ __entry->access_request = access_request;
+ __entry->port = port;
+
+ for (size_t level = 1, i = 0;
+ level <= __get_dynamic_array_len(layers) /
+ sizeof(access_mask_t);
+ level++) {
+ access_mask_t allowed;
+
+ if (i < rule->num_layers &&
+ level == rule->layers[i].level) {
+ allowed = rule->layers[i].access;
+ i++;
+ } else {
+ allowed = 0;
+ }
+ ((access_mask_t *)__get_dynamic_array(
+ layers))[level - 1] = allowed;
+ }),
+
+ TP_printk("domain=%llx request=0x%x port=%llu allowed=%s",
+ __entry->domain_id, __entry->access_request,
+ __entry->port,
+ __print_dynamic_array(layers, sizeof(access_mask_t))));
+
#endif /* _TRACE_LANDLOCK_H */

/* This part must be outside protection */
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 45ee7ec87957..e8d82b8a14a3 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -98,9 +98,9 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
}

/* The returned access has the same lifetime as @domain. */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
- const struct landlock_id id)
+static const struct landlock_rule *
+find_rule(const struct landlock_domain *const domain,
+ const struct landlock_id id)
{
const struct rb_root *root;
const struct rb_node *node;
@@ -127,26 +127,38 @@ landlock_find_rule(const struct landlock_domain *const domain,

/**
* landlock_unmask_layers - Remove the access rights in @masks which are
- * granted in @rule
+ * granted by a matching rule
*
- * Updates the set of (per-layer) unfulfilled access rights @masks so that all
- * the access rights granted in @rule are removed from it (because they are now
- * fulfilled).
+ * Looks up the rule matching @id in @domain, then updates the set of
+ * (per-layer) unfulfilled access rights @masks so that all the access rights
+ * granted by that rule are removed (because they are now fulfilled).
*
- * @rule: A rule that grants a set of access rights for each layer.
+ * @domain: The Landlock domain to search for a matching rule.
+ * @id: Identifier for the rule target (e.g. inode, port).
* @masks: A matrix of unfulfilled access rights for each layer.
+ * @matched_rule: Optional output for the matched rule (for tracing); set to
+ * the matching rule when non-NULL, unchanged otherwise.
*
* Return: True if the request is allowed (i.e. the access rights granted all
* remaining unfulfilled access rights and masks has no leftover set bits).
*/
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_access_masks *masks)
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_access_masks *masks,
+ const struct landlock_rule **matched_rule)
{
+ const struct landlock_rule *rule;
+
if (!masks)
return true;
+
+ rule = find_rule(domain, id);
if (!rule)
return false;

+ if (matched_rule)
+ *matched_rule = rule;
+
/*
* An access is granted if, for each policy layer, at least one rule
* encountered on the pathwalk grants the requested access, regardless
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 56f54efb65d1..35abae29677c 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -289,12 +289,10 @@ struct landlock_domain *
landlock_merge_ruleset(struct landlock_domain *const parent,
struct landlock_ruleset *const ruleset);

-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
- const struct landlock_id id);
-
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_access_masks *masks);
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_access_masks *masks,
+ const struct landlock_rule **matched_rule);

access_mask_t
landlock_init_layer_masks(const struct landlock_domain *const domain,
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index f627ecc537a5..fe211656f6d9 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -375,31 +375,55 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,

/* Access-control management */

-/*
- * The lifetime of the returned rule is tied to @domain.
+/**
+ * get_inode_id - Look up the Landlock object for a dentry
+ * @dentry: The dentry to look up.
+ * @id: Filled with the inode's Landlock object pointer on success.
+ *
+ * Extracts the Landlock object pointer from @dentry's inode security blob and
+ * stores it in @id for use as a rule-tree lookup key.
+ *
+ * When this returns false (negative dentry or no Landlock object), no rule can
+ * match this inode, so landlock_unmask_layers() need not be called. Callers
+ * that gate landlock_unmask_layers() on this function must handle the NULL
+ * @masks case independently, since the !masks-returns-true early-return in
+ * landlock_unmask_layers() will not be reached. See the allowed_parent2
+ * initialization in is_access_to_paths_allowed().
*
- * Returns NULL if no rule is found or if @dentry is negative.
+ * Return: True if a Landlock object exists for @dentry, false otherwise.
*/
-static const struct landlock_rule *
-find_rule(const struct landlock_domain *const domain,
- const struct dentry *const dentry)
+static bool get_inode_id(const struct dentry *const dentry,
+ struct landlock_id *id)
{
- const struct landlock_rule *rule;
- const struct inode *inode;
- struct landlock_id id = {
- .type = LANDLOCK_KEY_INODE,
- };
-
/* Ignores nonexistent leafs. */
if (d_is_negative(dentry))
- return NULL;
+ return false;

- inode = d_backing_inode(dentry);
- rcu_read_lock();
- id.key.object = rcu_dereference(landlock_inode(inode)->object);
- rule = landlock_find_rule(domain, id);
- rcu_read_unlock();
- return rule;
+ /*
+ * rcu_access_pointer() is sufficient: the pointer is used only
+ * as a numeric comparison key for rule lookup, not dereferenced.
+ * The object cannot be freed while the domain exists because the
+ * domain's rule tree holds its own reference to it.
+ */
+ id->key.object = rcu_access_pointer(
+ landlock_inode(d_backing_inode(dentry))->object);
+ return !!id->key.object;
+}
+
+static bool unmask_layers_fs(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ const access_mask_t access_request,
+ struct layer_access_masks *masks,
+ const struct dentry *const dentry)
+{
+ const struct landlock_rule *rule = NULL;
+ bool ret;
+
+ ret = landlock_unmask_layers(domain, id, masks, &rule);
+ if (rule)
+ trace_landlock_check_rule_fs(domain, dentry, access_request,
+ rule);
+ return ret;
}

/*
@@ -771,6 +795,9 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
child1_is_directory = true, child2_is_directory = true;
struct path walker_path;
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
access_mask_t access_masked_parent1, access_masked_parent2;
struct layer_access_masks _layer_masks_child1, _layer_masks_child2;
struct layer_access_masks *layer_masks_child1 = NULL,
@@ -810,24 +837,46 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
/* For a simple request, only check for requested accesses. */
access_masked_parent1 = access_request_parent1;
access_masked_parent2 = access_request_parent2;
+ /*
+ * Simple requests have no parent2 to check, so parent2 is
+ * trivially allowed. This must be set explicitly because the
+ * get_inode_id() gate in the pathwalk loop may prevent
+ * landlock_unmask_layers() from being called (which would
+ * otherwise return true for NULL masks as a side effect).
+ */
+ allowed_parent2 = true;
is_dom_check = false;
}

if (unlikely(dentry_child1)) {
- if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- &_layer_masks_child1,
- LANDLOCK_KEY_INODE))
- landlock_unmask_layers(find_rule(domain, dentry_child1),
- &_layer_masks_child1);
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ access_mask_t handled;
+
+ handled = landlock_init_layer_masks(domain,
+ LANDLOCK_MASK_ACCESS_FS,
+ &_layer_masks_child1,
+ LANDLOCK_KEY_INODE);
+ if (handled && get_inode_id(dentry_child1, &id))
+ unmask_layers_fs(domain, id, handled,
+ &_layer_masks_child1, dentry_child1);
layer_masks_child1 = &_layer_masks_child1;
child1_is_directory = d_is_dir(dentry_child1);
}
if (unlikely(dentry_child2)) {
- if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- &_layer_masks_child2,
- LANDLOCK_KEY_INODE))
- landlock_unmask_layers(find_rule(domain, dentry_child2),
- &_layer_masks_child2);
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ access_mask_t handled;
+
+ handled = landlock_init_layer_masks(domain,
+ LANDLOCK_MASK_ACCESS_FS,
+ &_layer_masks_child2,
+ LANDLOCK_KEY_INODE);
+ if (handled && get_inode_id(dentry_child2, &id))
+ unmask_layers_fs(domain, id, handled,
+ &_layer_masks_child2, dentry_child2);
layer_masks_child2 = &_layer_masks_child2;
child2_is_directory = d_is_dir(dentry_child2);
}
@@ -839,8 +888,6 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
* restriction.
*/
while (true) {
- const struct landlock_rule *rule;
-
/*
* If at least all accesses allowed on the destination are
* already allowed on the source, respectively if there is at
@@ -881,13 +928,20 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
break;
}

- rule = find_rule(domain, walker_path.dentry);
- allowed_parent1 =
- allowed_parent1 ||
- landlock_unmask_layers(rule, layer_masks_parent1);
- allowed_parent2 =
- allowed_parent2 ||
- landlock_unmask_layers(rule, layer_masks_parent2);
+ if (get_inode_id(walker_path.dentry, &id)) {
+ allowed_parent1 =
+ allowed_parent1 ||
+ unmask_layers_fs(domain, id,
+ access_masked_parent1,
+ layer_masks_parent1,
+ walker_path.dentry);
+ allowed_parent2 =
+ allowed_parent2 ||
+ unmask_layers_fs(domain, id,
+ access_masked_parent2,
+ layer_masks_parent2,
+ walker_path.dentry);
+ }

/* Stops when a rule from each layer grants access. */
if (allowed_parent1 && allowed_parent2)
@@ -1050,23 +1104,30 @@ static bool collect_domain_accesses(const struct landlock_domain *const domain,
struct layer_access_masks *layer_masks_dom)
{
bool ret = false;
+ access_mask_t access_masked_dom;

if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom))
return true;
if (is_nouser_or_private(dir))
return true;

- if (!landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- layer_masks_dom, LANDLOCK_KEY_INODE))
+ access_masked_dom =
+ landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
+ layer_masks_dom, LANDLOCK_KEY_INODE);
+ if (!access_masked_dom)
return true;

dget(dir);
while (true) {
struct dentry *parent_dentry;
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };

/* Gets all layers allowing all domain accesses. */
- if (landlock_unmask_layers(find_rule(domain, dir),
- layer_masks_dom)) {
+ if (get_inode_id(dir, &id) &&
+ unmask_layers_fs(domain, id, access_masked_dom,
+ layer_masks_dom, dir)) {
/*
* Stops when all handled accesses are allowed by at
* least one rule in each layer.
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 1e893123e787..a2aefc7967a1 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -53,6 +53,22 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
return err;
}

+static bool unmask_layers_net(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_access_masks *masks,
+ access_mask_t access_request)
+{
+ const struct landlock_rule *rule = NULL;
+ bool ret;
+
+ ret = landlock_unmask_layers(domain, id, masks, &rule);
+ if (rule)
+ trace_landlock_check_rule_net(
+ domain, ntohs((__force __be16)id.key.data),
+ access_request, rule);
+ return ret;
+}
+
static int current_check_access_socket(struct socket *const sock,
struct sockaddr *const address,
const int addrlen,
@@ -60,7 +76,6 @@ static int current_check_access_socket(struct socket *const sock,
{
__be16 port;
struct layer_access_masks layer_masks = {};
- const struct landlock_rule *rule;
struct landlock_id id = {
.type = LANDLOCK_KEY_NET_PORT,
};
@@ -199,14 +214,14 @@ static int current_check_access_socket(struct socket *const sock,
id.key.data = (__force uintptr_t)port;
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));

- rule = landlock_find_rule(subject->domain, id);
access_request = landlock_init_layer_masks(subject->domain,
access_request, &layer_masks,
LANDLOCK_KEY_NET_PORT);
if (!access_request)
return 0;

- if (landlock_unmask_layers(rule, &layer_masks))
+ if (unmask_layers_net(subject->domain, id, &layer_masks,
+ access_request))
return 0;

audit_net.family = address->sa_family;
--
2.53.0