* [PATCH v2 00/17] Landlock tracepoints
@ 2026-04-06 14:36 Mickaël Salaün
2026-04-06 14:36 ` [PATCH v2 01/17] landlock: Prepare ruleset and domain type split Mickaël Salaün
` (16 more replies)
0 siblings, 17 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:36 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Hi,
This series adds 13 tracepoints that cover the full Landlock lifecycle,
from ruleset creation to domain destruction. They can be used directly
via /sys/kernel/tracing/events/landlock/* or attached by eBPF programs
for richer introspection.
Patches 1-4 refactor Landlock internals: they split struct
landlock_domain from struct landlock_ruleset and move denial logging
into a common framework shared by audit and tracing. Patch 5 adds
__print_untrusted_str() to the tracing core. Patches 6-9 add
lifecycle tracepoints: ruleset creation and destruction, rule addition
for filesystem and network, domain enforcement and destruction, and
per-rule access checks. Patch 10 sets audit_net.sk for socket access
checks. Patches 11-12 add denial tracepoints for filesystem, network,
and scope operations. Patches 13-16 add selftests and patch 17 adds
documentation.
Each rule type has a dedicated tracepoint with strongly-typed fields
(dev/ino for filesystem, port for network), following the same approach
as the audit logs.
This feature is useful to troubleshoot policy issues and should limit
the need for custom debugging kernel code when developing new Landlock
features.
Landlock already has audit support for logging denied access requests,
which is useful to identify security issues or sandbox misconfiguration.
However, audit might not be enough to debug Landlock policies. The
main difference with audit events is that traces are disabled by
default, can be very verbose, and can be filtered according to process
and Landlock properties (e.g. domain ID).
As for audit, tracing may expose sensitive information about all
sandboxed processes on the system, and must only be accessible to the
system administrator. For unprivileged monitoring scoped to a single
sandbox (e.g., interactive permission prompts), Tingmao Wang's
"Landlock supervise" RFC [1] proposes a dedicated userspace API. The
infrastructure changes in this series (the domain type split, the
denial framework, and the tracepoint consistency guarantees) benefit
that approach.
I will release a companion tool that leverages these tracepoints to
monitor Landlock events in real time.
This series applies on top of my next branch [2].
Changes since RFC v1:
https://lore.kernel.org/r/20250523165741.693976-1-mic@digikod.net
- New patches 1-4: split struct landlock_domain from struct
landlock_ruleset; split denial logging from audit into common
framework with CONFIG_SECURITY_LANDLOCK_LOG.
- Patch 5 (was v1 3/5): removed WARN_ON() (pointed out by Steven
Rostedt).
- New patch 6: added create_ruleset and free_ruleset tracepoints
(split from the v1 add_rule_fs tracepoint patch).
- Patch 7 (was v1 4/5): added add_rule_net tracepoint, used
ruleset Landlock ID instead of kernel pointer, added version
field to struct landlock_ruleset, differentiated d_absolute_path()
error cases (suggested by Tingmao Wang), moved
DEFINE_FREE(__putname) to include/linux/fs.h (noticed by Tingmao
Wang).
- New patch 8: added restrict_self and free_domain tracepoints.
- Patch 9 (was v1 5/5): merged find-rule consolidation, added
check_rule_net tracepoint.
- New patch 10: split audit_net.sk fix with Fixes: tag.
- New patches 11-12: added denial tracepoints for filesystem,
network, ptrace, and scope operations.
- New patches 13-17: split selftests into per-feature commits with
documentation.
Regards,
Mickaël Salaün (17):
landlock: Prepare ruleset and domain type split
landlock: Move domain query functions to domain.c
landlock: Split struct landlock_domain from struct landlock_ruleset
landlock: Split denial logging from audit into common framework
tracing: Add __print_untrusted_str()
landlock: Add create_ruleset and free_ruleset tracepoints
landlock: Add landlock_add_rule_fs and landlock_add_rule_net
tracepoints
landlock: Add restrict_self and free_domain tracepoints
landlock: Add tracepoints for rule checking
landlock: Set audit_net.sk for socket access checks
landlock: Add landlock_deny_access_fs and landlock_deny_access_net
landlock: Add tracepoints for ptrace and scope denials
selftests/landlock: Add trace event test infrastructure and tests
selftests/landlock: Add filesystem tracepoint tests
selftests/landlock: Add network tracepoint tests
selftests/landlock: Add scope and ptrace tracepoint tests
landlock: Document tracepoints
Documentation/admin-guide/LSM/landlock.rst | 210 ++-
Documentation/security/landlock.rst | 35 +-
Documentation/trace/events-landlock.rst | 160 +++
Documentation/trace/index.rst | 1 +
Documentation/userspace-api/landlock.rst | 11 +-
MAINTAINERS | 1 +
include/linux/fs.h | 1 +
include/linux/trace_events.h | 2 +
include/trace/events/landlock.h | 574 ++++++++
include/trace/stages/stage3_trace_output.h | 4 +
include/trace/stages/stage7_class_define.h | 1 +
kernel/trace/trace_output.c | 41 +
security/landlock/Kconfig | 5 +
security/landlock/Makefile | 10 +-
security/landlock/access.h | 4 +-
security/landlock/cred.c | 6 +-
security/landlock/cred.h | 29 +-
security/landlock/domain.c | 445 ++++++-
security/landlock/domain.h | 148 ++-
security/landlock/fs.c | 201 ++-
security/landlock/fs.h | 30 +
security/landlock/id.h | 6 +-
security/landlock/{audit.c => log.c} | 261 +++-
security/landlock/{audit.h => log.h} | 25 +-
security/landlock/net.c | 40 +-
security/landlock/ruleset.c | 528 ++------
security/landlock/ruleset.h | 237 ++--
security/landlock/syscalls.c | 36 +-
security/landlock/task.c | 22 +-
tools/testing/selftests/landlock/audit.h | 35 +-
tools/testing/selftests/landlock/audit_test.c | 187 +++
tools/testing/selftests/landlock/common.h | 47 +
tools/testing/selftests/landlock/config | 2 +
tools/testing/selftests/landlock/fs_test.c | 218 +++
tools/testing/selftests/landlock/net_test.c | 547 +++++++-
.../testing/selftests/landlock/ptrace_test.c | 164 +++
.../landlock/scoped_abstract_unix_test.c | 195 +++
.../selftests/landlock/scoped_signal_test.c | 150 +++
tools/testing/selftests/landlock/trace.h | 640 +++++++++
.../selftests/landlock/trace_fs_test.c | 390 ++++++
tools/testing/selftests/landlock/trace_test.c | 1168 +++++++++++++++++
tools/testing/selftests/landlock/true.c | 10 +
42 files changed, 5991 insertions(+), 836 deletions(-)
create mode 100644 Documentation/trace/events-landlock.rst
create mode 100644 include/trace/events/landlock.h
rename security/landlock/{audit.c => log.c} (73%)
rename security/landlock/{audit.h => log.h} (74%)
create mode 100644 tools/testing/selftests/landlock/trace.h
create mode 100644 tools/testing/selftests/landlock/trace_fs_test.c
create mode 100644 tools/testing/selftests/landlock/trace_test.c
base-commit: 8c6a27e02bc55ab110d1828610048b19f903aaec
--
2.53.0
^ permalink raw reply [flat|nested] 20+ messages in thread
* [PATCH v2 01/17] landlock: Prepare ruleset and domain type split
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
@ 2026-04-06 14:36 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 02/17] landlock: Move domain query functions to domain.c Mickaël Salaün
` (15 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:36 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Rulesets and domains serve fundamentally different purposes: a ruleset
is mutable and user-facing, created by landlock_create_ruleset(), while
a domain is immutable after construction and enforced on tasks via
landlock_restrict_self(). Today both are represented by struct
landlock_ruleset, which conflates mutable and immutable state in a
single type: the lock field is unused by domains, the hierarchy field
is unused by rulesets, and lifecycle functions must handle both cases.
Prepare for a clean type split by introducing two new structures and
the helpers needed to construct domains from a separate compilation
unit:
- struct landlock_rules: holds the red-black tree roots and the rule
count. This storage type is shared by both rulesets and domains.
This decouples rule storage from the domain API; the backing data
structure could be changed independently (e.g. to a hash table,
cf. [1]).
- struct landlock_domain: the immutable domain enforced on tasks. It
has no lock field because its rules and access masks are immutable
once construction is complete. The name reflects the role, not the
internal data structure, to decouple the API from the
implementation.
Embed struct landlock_rules in struct landlock_ruleset, replacing the
individual root_inode, root_net_port, and num_rules fields. All field
accesses are updated mechanically.
Add landlock_get_rule_root() as a static inline helper in the header,
enabling constant propagation when the key type is known at compile
time. Extract landlock_free_rules() so that free_domain() can reuse
the rule-freeing logic without duplicating it.
Add domain lifecycle functions: landlock_get_domain(),
landlock_put_domain(), and landlock_put_domain_deferred(). Move
domain.o from landlock-$(CONFIG_AUDIT) to landlock-y because these
lifecycle functions are needed unconditionally, not just for audit
logging.
No behavioral change. The new types and lifecycle functions are not
yet used by any caller.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Link: https://lore.kernel.org/r/20250523165741.693976-1-mic@digikod.net [1]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
security/landlock/Makefile | 6 +--
security/landlock/domain.c | 35 ++++++++++++++++
security/landlock/domain.h | 69 +++++++++++++++++++++++++++++++
security/landlock/ruleset.c | 71 ++++++++++++++++----------------
security/landlock/ruleset.h | 81 +++++++++++++++++++++++++++----------
5 files changed, 201 insertions(+), 61 deletions(-)
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index ffa7646d99f3..23e13644916f 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -8,11 +8,11 @@ landlock-y := \
cred.o \
task.o \
fs.o \
- tsync.o
+ tsync.o \
+ domain.o
landlock-$(CONFIG_INET) += net.o
landlock-$(CONFIG_AUDIT) += \
id.o \
- audit.o \
- domain.o
+ audit.o
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 06b6bd845060..378d86974ffb 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -15,14 +15,49 @@
#include <linux/mm.h>
#include <linux/path.h>
#include <linux/pid.h>
+#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/signal.h>
+#include <linux/slab.h>
#include <linux/uidgid.h>
+#include <linux/workqueue.h>
#include "access.h"
#include "common.h"
#include "domain.h"
#include "id.h"
+#include "ruleset.h"
+
+static void free_domain(struct landlock_domain *const domain)
+{
+ might_sleep();
+ landlock_free_rules(&domain->rules);
+ landlock_put_hierarchy(domain->hierarchy);
+ kfree(domain);
+}
+
+void landlock_put_domain(struct landlock_domain *const domain)
+{
+ might_sleep();
+ if (domain && refcount_dec_and_test(&domain->usage))
+ free_domain(domain);
+}
+
+static void free_domain_work(struct work_struct *const work)
+{
+ struct landlock_domain *domain;
+
+ domain = container_of(work, struct landlock_domain, work_free);
+ free_domain(domain);
+}
+
+void landlock_put_domain_deferred(struct landlock_domain *const domain)
+{
+ if (domain && refcount_dec_and_test(&domain->usage)) {
+ INIT_WORK(&domain->work_free, free_domain_work);
+ schedule_work(&domain->work_free);
+ }
+}
#ifdef CONFIG_AUDIT
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index a9d57db0120d..66333b6122a9 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -10,6 +10,7 @@
#ifndef _SECURITY_LANDLOCK_DOMAIN_H
#define _SECURITY_LANDLOCK_DOMAIN_H
+#include <linux/cleanup.h>
#include <linux/limits.h>
#include <linux/mm.h>
#include <linux/path.h>
@@ -17,9 +18,11 @@
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/slab.h>
+#include <linux/workqueue.h>
#include "access.h"
#include "audit.h"
+#include "ruleset.h"
enum landlock_log_status {
LANDLOCK_LOG_PENDING = 0,
@@ -170,4 +173,70 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
}
}
+/**
+ * struct landlock_domain - Immutable Landlock domain
+ *
+ * A domain is created from a ruleset by landlock_merge_ruleset() and enforced
+ * on a task. Once created, its rules and access masks are immutable. Unlike
+ * &struct landlock_ruleset, a domain has no lock field.
+ */
+struct landlock_domain {
+ /**
+ * @rules: Red-black tree storage for rules.
+ */
+ struct landlock_rules rules;
+ /**
+ * @hierarchy: Enables hierarchy identification even when a parent
+ * domain vanishes. This is needed for the ptrace and scope
+ * restrictions.
+ */
+ struct landlock_hierarchy *hierarchy;
+ union {
+ /**
+ * @work_free: Enables to free a domain within a lockless
+ * section. This is only used by landlock_put_domain_deferred()
+ * when @usage reaches zero. The fields @usage, @num_layers and
+ * @access_masks are then unused.
+ */
+ struct work_struct work_free;
+ struct {
+ /**
+ * @usage: Number of credentials referencing this
+ * domain.
+ */
+ refcount_t usage;
+ /**
+ * @num_layers: Number of layers that are used in this
+ * domain. This enables to check that all the layers
+ * allow an access request.
+ */
+ u32 num_layers;
+ /**
+ * @access_masks: Contains the subset of filesystem and
+ * network actions that are restricted by a domain. A
+ * domain saves all layers of merged rulesets in a stack
+ * (FAM), starting from the first layer to the last one.
+ * These layers are used when merging rulesets, for user
+ * space backward compatibility (i.e. future-proof), and
+ * to properly handle merged rulesets without
+ * overlapping access rights. These layers are set once
+ * and never changed for the lifetime of the domain.
+ */
+ struct access_masks access_masks[];
+ };
+ };
+};
+
+void landlock_put_domain(struct landlock_domain *const domain);
+void landlock_put_domain_deferred(struct landlock_domain *const domain);
+
+DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
+ if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+
+static inline void landlock_get_domain(struct landlock_domain *const domain)
+{
+ if (domain)
+ refcount_inc(&domain->usage);
+}
+
#endif /* _SECURITY_LANDLOCK_DOMAIN_H */
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 181df7736bb9..a6835011af2b 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -38,16 +38,16 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
return ERR_PTR(-ENOMEM);
refcount_set(&new_ruleset->usage, 1);
mutex_init(&new_ruleset->lock);
- new_ruleset->root_inode = RB_ROOT;
+ new_ruleset->rules.root_inode = RB_ROOT;
#if IS_ENABLED(CONFIG_INET)
- new_ruleset->root_net_port = RB_ROOT;
+ new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
new_ruleset->num_layers = num_layers;
/*
* hierarchy = NULL
- * num_rules = 0
+ * rules.num_rules = 0
* access_masks[] = 0
*/
return new_ruleset;
@@ -147,19 +147,7 @@ create_rule(const struct landlock_id id,
static struct rb_root *get_root(struct landlock_ruleset *const ruleset,
const enum landlock_key_type key_type)
{
- switch (key_type) {
- case LANDLOCK_KEY_INODE:
- return &ruleset->root_inode;
-
-#if IS_ENABLED(CONFIG_INET)
- case LANDLOCK_KEY_NET_PORT:
- return &ruleset->root_net_port;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- default:
- WARN_ON_ONCE(1);
- return ERR_PTR(-EINVAL);
- }
+ return landlock_get_rule_root(&ruleset->rules, key_type);
}
static void free_rule(struct landlock_rule *const rule,
@@ -175,19 +163,24 @@ static void free_rule(struct landlock_rule *const rule,
static void build_check_ruleset(void)
{
- const struct landlock_ruleset ruleset = {
+ const struct landlock_rules rules = {
.num_rules = ~0,
+ };
+ const struct landlock_ruleset ruleset = {
.num_layers = ~0,
};
- BUILD_BUG_ON(ruleset.num_rules < LANDLOCK_MAX_NUM_RULES);
+ BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
BUILD_BUG_ON(ruleset.num_layers < LANDLOCK_MAX_NUM_LAYERS);
}
/**
- * insert_rule - Create and insert a rule in a ruleset
+ * insert_rule - Create and insert a rule in a rule set
*
- * @ruleset: The ruleset to be updated.
+ * @rules: The rule storage to be updated. The caller is responsible for
+ * any required locking. For rulesets, this means holding
+ * landlock_ruleset.lock. For domains under construction, no lock is
+ * needed because the domain is not yet visible to other tasks.
* @id: The ID to build the new rule with. The underlying kernel object, if
* any, must be held by the caller.
* @layers: One or multiple layers to be copied into the new rule.
@@ -195,16 +188,16 @@ static void build_check_ruleset(void)
*
* When user space requests to add a new rule to a ruleset, @layers only
* contains one entry and this entry is not assigned to any level. In this
- * case, the new rule will extend @ruleset, similarly to a boolean OR between
+ * case, the new rule will extend @rules, similarly to a boolean OR between
* access rights.
*
* When merging a ruleset in a domain, or copying a domain, @layers will be
- * added to @ruleset as new constraints, similarly to a boolean AND between
- * access rights.
+ * added to @rules as new constraints, similarly to a boolean AND between access
+ * rights.
*
* Return: 0 on success, -errno on failure.
*/
-static int insert_rule(struct landlock_ruleset *const ruleset,
+static int insert_rule(struct landlock_rules *const rules,
const struct landlock_id id,
const struct landlock_layer (*layers)[],
const size_t num_layers)
@@ -215,14 +208,13 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
struct rb_root *root;
might_sleep();
- lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!layers))
return -ENOENT;
if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object))
return -ENOENT;
- root = get_root(ruleset, id.type);
+ root = landlock_get_rule_root(rules, id.type);
if (IS_ERR(root))
return PTR_ERR(root);
@@ -248,7 +240,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
if ((*layers)[0].level == 0) {
/*
* Extends access rights when the request comes from
- * landlock_add_rule(2), i.e. @ruleset is not a domain.
+ * landlock_add_rule(2), i.e. contained by a ruleset.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
return -EINVAL;
@@ -276,14 +268,14 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
/* There is no match for @id. */
build_check_ruleset();
- if (ruleset->num_rules >= LANDLOCK_MAX_NUM_RULES)
+ if (rules->num_rules >= LANDLOCK_MAX_NUM_RULES)
return -E2BIG;
new_rule = create_rule(id, layers, num_layers, NULL);
if (IS_ERR(new_rule))
return PTR_ERR(new_rule);
rb_link_node(&new_rule->node, parent_node, walker_node);
rb_insert_color(&new_rule->node, root);
- ruleset->num_rules++;
+ rules->num_rules++;
return 0;
}
@@ -314,7 +306,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
} };
build_check_layer();
- return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers));
+ lockdep_assert_held(&ruleset->lock);
+ return insert_rule(&ruleset->rules, id, &layers, ARRAY_SIZE(layers));
}
static int merge_tree(struct landlock_ruleset *const dst,
@@ -352,7 +345,7 @@ static int merge_tree(struct landlock_ruleset *const dst,
layers[0].access = walker_rule->layers[0].access;
- err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
+ err = insert_rule(&dst->rules, id, &layers, ARRAY_SIZE(layers));
if (err)
return err;
}
@@ -426,7 +419,7 @@ static int inherit_tree(struct landlock_ruleset *const parent,
.type = key_type,
};
- err = insert_rule(child, id, &walker_rule->layers,
+ err = insert_rule(&child->rules, id, &walker_rule->layers,
walker_rule->num_layers);
if (err)
return err;
@@ -480,21 +473,26 @@ static int inherit_ruleset(struct landlock_ruleset *const parent,
return err;
}
-static void free_ruleset(struct landlock_ruleset *const ruleset)
+void landlock_free_rules(struct landlock_rules *const rules)
{
struct landlock_rule *freeme, *next;
might_sleep();
- rbtree_postorder_for_each_entry_safe(freeme, next, &ruleset->root_inode,
+ rbtree_postorder_for_each_entry_safe(freeme, next, &rules->root_inode,
node)
free_rule(freeme, LANDLOCK_KEY_INODE);
#if IS_ENABLED(CONFIG_INET)
rbtree_postorder_for_each_entry_safe(freeme, next,
- &ruleset->root_net_port, node)
+ &rules->root_net_port, node)
free_rule(freeme, LANDLOCK_KEY_NET_PORT);
#endif /* IS_ENABLED(CONFIG_INET) */
+}
+static void free_ruleset(struct landlock_ruleset *const ruleset)
+{
+ might_sleep();
+ landlock_free_rules(&ruleset->rules);
landlock_put_hierarchy(ruleset->hierarchy);
kfree(ruleset);
}
@@ -594,7 +592,8 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset,
const struct rb_root *root;
const struct rb_node *node;
- root = get_root((struct landlock_ruleset *)ruleset, id.type);
+ root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+ id.type);
if (IS_ERR(root))
return NULL;
node = root->rb_node;
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 889f4b30301a..e7875a8b15df 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -57,13 +57,12 @@ union landlock_key {
*/
enum landlock_key_type {
/**
- * @LANDLOCK_KEY_INODE: Type of &landlock_ruleset.root_inode's node
- * keys.
+ * @LANDLOCK_KEY_INODE: Type of &landlock_rules.root_inode's node keys.
*/
LANDLOCK_KEY_INODE = 1,
/**
- * @LANDLOCK_KEY_NET_PORT: Type of &landlock_ruleset.root_net_port's
- * node keys.
+ * @LANDLOCK_KEY_NET_PORT: Type of &landlock_rules.root_net_port's node
+ * keys.
*/
LANDLOCK_KEY_NET_PORT,
};
@@ -111,30 +110,44 @@ struct landlock_rule {
};
/**
- * struct landlock_ruleset - Landlock ruleset
+ * struct landlock_rules - Red-black tree storage for Landlock rules
*
- * This data structure must contain unique entries, be updatable, and quick to
- * match an object.
+ * This structure holds the rule trees shared by both rulesets and domains.
*/
-struct landlock_ruleset {
+struct landlock_rules {
/**
* @root_inode: Root of a red-black tree containing &struct
- * landlock_rule nodes with inode object. Once a ruleset is tied to a
- * process (i.e. as a domain), this tree is immutable until @usage
- * reaches zero.
+ * landlock_rule nodes with inode object. Immutable for domains.
*/
struct rb_root root_inode;
#if IS_ENABLED(CONFIG_INET)
/**
* @root_net_port: Root of a red-black tree containing &struct
- * landlock_rule nodes with network port. Once a ruleset is tied to a
- * process (i.e. as a domain), this tree is immutable until @usage
- * reaches zero.
+ * landlock_rule nodes with network port. Immutable for domains.
*/
struct rb_root root_net_port;
#endif /* IS_ENABLED(CONFIG_INET) */
+ /**
+ * @num_rules: Number of non-overlapping (i.e. not for the same object)
+ * rules in this tree storage.
+ */
+ u32 num_rules;
+};
+
+/**
+ * struct landlock_ruleset - Landlock ruleset
+ *
+ * This data structure must contain unique entries, be updatable, and quick to
+ * match an object.
+ */
+struct landlock_ruleset {
+ /**
+ * @rules: Red-black tree storage for rules.
+ */
+ struct landlock_rules rules;
+
/**
* @hierarchy: Enables hierarchy identification even when a parent
* domain vanishes. This is needed for the ptrace protection.
@@ -144,9 +157,9 @@ struct landlock_ruleset {
/**
* @work_free: Enables to free a ruleset within a lockless
* section. This is only used by
- * landlock_put_ruleset_deferred() when @usage reaches zero.
- * The fields @lock, @usage, @num_rules, @num_layers and
- * @access_masks are then unused.
+ * landlock_put_ruleset_deferred() when @usage reaches zero. The
+ * fields @lock, @usage, @num_layers and @access_masks are then
+ * unused.
*/
struct work_struct work_free;
struct {
@@ -160,11 +173,6 @@ struct landlock_ruleset {
* descriptors referencing this ruleset.
*/
refcount_t usage;
- /**
- * @num_rules: Number of non-overlapping (i.e. not for
- * the same object) rules in this ruleset.
- */
- u32 num_rules;
/**
* @num_layers: Number of layers that are used in this
* ruleset. This enables to check that all the layers
@@ -204,6 +212,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access);
+void landlock_free_rules(struct landlock_rules *const rules);
+
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset);
@@ -212,6 +222,33 @@ const struct landlock_rule *
landlock_find_rule(const struct landlock_ruleset *const ruleset,
const struct landlock_id id);
+/**
+ * landlock_get_rule_root - Get the root of a rule tree by key type
+ *
+ * @rules: The rules storage to look up.
+ * @key_type: The type of key to select the tree for.
+ *
+ * Return: A pointer to the rb_root, or ERR_PTR(-EINVAL) on unknown type.
+ */
+static inline struct rb_root *
+landlock_get_rule_root(struct landlock_rules *const rules,
+ const enum landlock_key_type key_type)
+{
+ switch (key_type) {
+ case LANDLOCK_KEY_INODE:
+ return &rules->root_inode;
+
+#if IS_ENABLED(CONFIG_INET)
+ case LANDLOCK_KEY_NET_PORT:
+ return &rules->root_net_port;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ default:
+ WARN_ON_ONCE(1);
+ return ERR_PTR(-EINVAL);
+ }
+}
+
static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
{
if (ruleset)
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 02/17] landlock: Move domain query functions to domain.c
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
2026-04-06 14:36 ` [PATCH v2 01/17] landlock: Prepare ruleset and domain type split Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 03/17] landlock: Split struct landlock_domain from struct landlock_ruleset Mickaël Salaün
` (14 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Grouping domain-specific code in one compilation unit reduces coupling
between domain and ruleset implementations.
Move the access-check functions that only operate on domains:
- landlock_find_rule() (from ruleset.c to domain.c)
- landlock_unmask_layers() (from ruleset.c to domain.c)
- landlock_init_layer_masks() (from ruleset.c to domain.c)
- landlock_union_access_masks() (from ruleset.h to domain.h)
These functions are called during the pathwalk and network access checks
to evaluate whether a domain grants the requested access. They do not
modify the domain or its rules.
The merge and inherit chain (merge_tree, merge_ruleset, inherit_tree,
inherit_ruleset, landlock_merge_ruleset) stays in ruleset.c for now
because it calls the static create_ruleset() allocator. A following
commit moves it when the domain type switch eliminates the dependency on
create_ruleset().
Expand the landlock_unmask_layers() comment to document the per-layer
composition semantics.
No behavioral change. Function signatures are unchanged; only
mechanical adjustments for the struct landlock_rules embedding
introduced by the previous commit.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
security/landlock/domain.c | 150 ++++++++++++++++++++++++++++++++++++
security/landlock/domain.h | 38 +++++++++
security/landlock/net.c | 1 +
security/landlock/ruleset.c | 135 --------------------------------
security/landlock/ruleset.h | 38 ---------
5 files changed, 189 insertions(+), 173 deletions(-)
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 378d86974ffb..cb79edf5df02 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -10,11 +10,17 @@
#include <kunit/test.h>
#include <linux/bitops.h>
#include <linux/bits.h>
+#include <linux/cleanup.h>
#include <linux/cred.h>
+#include <linux/err.h>
#include <linux/file.h>
+#include <linux/lockdep.h>
#include <linux/mm.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
#include <linux/path.h>
#include <linux/pid.h>
+#include <linux/rbtree.h>
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/signal.h>
@@ -26,6 +32,8 @@
#include "common.h"
#include "domain.h"
#include "id.h"
+#include "limits.h"
+#include "object.h"
#include "ruleset.h"
static void free_domain(struct landlock_domain *const domain)
@@ -59,6 +67,148 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
}
}
+/* The returned access has the same lifetime as @ruleset. */
+const struct landlock_rule *
+landlock_find_rule(const struct landlock_ruleset *const ruleset,
+ const struct landlock_id id)
+{
+ const struct rb_root *root;
+ const struct rb_node *node;
+
+ root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+ id.type);
+ if (IS_ERR(root))
+ return NULL;
+ node = root->rb_node;
+
+ while (node) {
+ struct landlock_rule *this =
+ rb_entry(node, struct landlock_rule, node);
+
+ if (this->key.data == id.key.data)
+ return this;
+ if (this->key.data < id.key.data)
+ node = node->rb_right;
+ else
+ node = node->rb_left;
+ }
+ return NULL;
+}
+
+/**
+ * landlock_unmask_layers - Remove the access rights in @masks which are
+ * granted in @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).
+ *
+ * @rule: A rule that grants a set of access rights for each layer.
+ * @masks: A matrix of unfulfilled access rights for each layer.
+ *
+ * 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)
+{
+ if (!masks)
+ return true;
+ if (!rule)
+ return false;
+
+ /*
+ * An access is granted if, for each policy layer, at least one rule
+ * encountered on the pathwalk grants the requested access, regardless
+ * of its position in the layer stack. We must then check the remaining
+ * layers for each inode, from the first added layer to the last one.
+ * When there are multiple requested accesses, for each policy layer,
+ * the full set of requested accesses may not be granted by only one
+ * rule, but by the union (binary OR) of multiple rules. For example,
+ * /a/b <execute> + /a <read> grants /a/b <execute + read>.
+ *
+ * This function is called once per matching rule during the pathwalk,
+ * progressively clearing bits in @masks. The overall access decision
+ * is: access is granted iff FOR-ALL layers l, masks->access[l] == 0.
+ * When two independent mechanisms can each grant access within a layer
+ * (e.g. a path rule OR a scope exception), the composition must
+ * evaluate per-layer: FOR-ALL l (A(l) OR B(l)), not (FOR-ALL l A(l)) OR
+ * (FOR-ALL l B(l)), to prevent bypass when different layers grant via
+ * different mechanisms.
+ */
+ for (size_t i = 0; i < rule->num_layers; i++) {
+ const struct landlock_layer *const layer = &rule->layers[i];
+
+ /* Clear the bits where the layer in the rule grants access. */
+ masks->access[layer->level - 1] &= ~layer->access;
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) {
+ if (masks->access[i])
+ return false;
+ }
+ return true;
+}
+
+typedef access_mask_t
+get_access_mask_t(const struct landlock_ruleset *const ruleset,
+ const u16 layer_level);
+
+/**
+ * landlock_init_layer_masks - Initialize layer masks from an access request
+ *
+ * Populates @masks such that for each access right in @access_request, the bits
+ * for all the layers are set where this access right is handled.
+ *
+ * @domain: The domain that defines the current restrictions.
+ * @access_request: The requested access rights to check.
+ * @masks: Layer access masks to populate.
+ * @key_type: The key type to switch between access masks of different types.
+ *
+ * Return: An access mask where each access right bit is set which is handled in
+ * any of the active layers in @domain.
+ */
+access_mask_t
+landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+ const access_mask_t access_request,
+ struct layer_access_masks *const masks,
+ const enum landlock_key_type key_type)
+{
+ access_mask_t handled_accesses = 0;
+ get_access_mask_t *get_access_mask;
+
+ switch (key_type) {
+ case LANDLOCK_KEY_INODE:
+ get_access_mask = landlock_get_fs_access_mask;
+ break;
+
+#if IS_ENABLED(CONFIG_INET)
+ case LANDLOCK_KEY_NET_PORT:
+ get_access_mask = landlock_get_net_access_mask;
+ break;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ default:
+ WARN_ON_ONCE(1);
+ return 0;
+ }
+
+ /* An empty access request can happen because of O_WRONLY | O_RDWR. */
+ if (!access_request)
+ return 0;
+
+ for (size_t i = 0; i < domain->num_layers; i++) {
+ const access_mask_t handled = get_access_mask(domain, i);
+
+ masks->access[i] = access_request & handled;
+ handled_accesses |= masks->access[i];
+ }
+ for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->access); i++)
+ masks->access[i] = 0;
+
+ return handled_accesses;
+}
+
#ifdef CONFIG_AUDIT
/**
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 66333b6122a9..afa97011ecd2 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -227,12 +227,50 @@ struct landlock_domain {
};
};
+/**
+ * landlock_union_access_masks - Return all access rights handled in the
+ * domain
+ *
+ * @domain: Landlock ruleset (used as a domain)
+ *
+ * Return: An access_masks result of the OR of all the domain's access masks.
+ */
+static inline struct access_masks
+landlock_union_access_masks(const struct landlock_ruleset *const domain)
+{
+ union access_masks_all matches = {};
+ size_t layer_level;
+
+ for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
+ union access_masks_all layer = {
+ .masks = domain->access_masks[layer_level],
+ };
+
+ matches.all |= layer.all;
+ }
+
+ return matches.masks;
+}
+
void landlock_put_domain(struct landlock_domain *const domain);
void landlock_put_domain_deferred(struct landlock_domain *const domain);
DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+const struct landlock_rule *
+landlock_find_rule(const struct landlock_ruleset *const ruleset,
+ const struct landlock_id id);
+
+bool landlock_unmask_layers(const struct landlock_rule *const rule,
+ struct layer_access_masks *masks);
+
+access_mask_t
+landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+ const access_mask_t access_request,
+ struct layer_access_masks *masks,
+ const enum landlock_key_type key_type);
+
static inline void landlock_get_domain(struct landlock_domain *const domain)
{
if (domain)
diff --git a/security/landlock/net.c b/security/landlock/net.c
index c368649985c5..34a72a4f833d 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -15,6 +15,7 @@
#include "audit.h"
#include "common.h"
#include "cred.h"
+#include "domain.h"
#include "limits.h"
#include "net.h"
#include "ruleset.h"
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index a6835011af2b..0cf31a7e4c7b 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -581,138 +581,3 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
return no_free_ptr(new_dom);
}
-
-/*
- * The returned access has the same lifetime as @ruleset.
- */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
- const struct landlock_id id)
-{
- const struct rb_root *root;
- const struct rb_node *node;
-
- root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
- id.type);
- if (IS_ERR(root))
- return NULL;
- node = root->rb_node;
-
- while (node) {
- struct landlock_rule *this =
- rb_entry(node, struct landlock_rule, node);
-
- if (this->key.data == id.key.data)
- return this;
- if (this->key.data < id.key.data)
- node = node->rb_right;
- else
- node = node->rb_left;
- }
- return NULL;
-}
-
-/**
- * landlock_unmask_layers - Remove the access rights in @masks
- * which are granted in @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).
- *
- * @rule: A rule that grants a set of access rights for each layer
- * @masks: A matrix of unfulfilled access rights for each layer
- *
- * 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)
-{
- if (!masks)
- return true;
- if (!rule)
- return false;
-
- /*
- * An access is granted if, for each policy layer, at least one rule
- * encountered on the pathwalk grants the requested access,
- * regardless of its position in the layer stack. We must then check
- * the remaining layers for each inode, from the first added layer to
- * the last one. When there is multiple requested accesses, for each
- * policy layer, the full set of requested accesses may not be granted
- * by only one rule, but by the union (binary OR) of multiple rules.
- * E.g. /a/b <execute> + /a <read> => /a/b <execute + read>
- */
- for (size_t i = 0; i < rule->num_layers; i++) {
- const struct landlock_layer *const layer = &rule->layers[i];
-
- /* Clear the bits where the layer in the rule grants access. */
- masks->access[layer->level - 1] &= ~layer->access;
- }
-
- for (size_t i = 0; i < ARRAY_SIZE(masks->access); i++) {
- if (masks->access[i])
- return false;
- }
- return true;
-}
-
-typedef access_mask_t
-get_access_mask_t(const struct landlock_ruleset *const ruleset,
- const u16 layer_level);
-
-/**
- * landlock_init_layer_masks - Initialize layer masks from an access request
- *
- * Populates @masks such that for each access right in @access_request,
- * the bits for all the layers are set where this access right is handled.
- *
- * @domain: The domain that defines the current restrictions.
- * @access_request: The requested access rights to check.
- * @masks: Layer access masks to populate.
- * @key_type: The key type to switch between access masks of different types.
- *
- * Return: An access mask where each access right bit is set which is handled
- * in any of the active layers in @domain.
- */
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
- const access_mask_t access_request,
- struct layer_access_masks *const masks,
- const enum landlock_key_type key_type)
-{
- access_mask_t handled_accesses = 0;
- get_access_mask_t *get_access_mask;
-
- switch (key_type) {
- case LANDLOCK_KEY_INODE:
- get_access_mask = landlock_get_fs_access_mask;
- break;
-
-#if IS_ENABLED(CONFIG_INET)
- case LANDLOCK_KEY_NET_PORT:
- get_access_mask = landlock_get_net_access_mask;
- break;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- default:
- WARN_ON_ONCE(1);
- return 0;
- }
-
- /* An empty access request can happen because of O_WRONLY | O_RDWR. */
- if (!access_request)
- return 0;
-
- for (size_t i = 0; i < domain->num_layers; i++) {
- const access_mask_t handled = get_access_mask(domain, i);
-
- masks->access[i] = access_request & handled;
- handled_accesses |= masks->access[i];
- }
- for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->access); i++)
- masks->access[i] = 0;
-
- return handled_accesses;
-}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index e7875a8b15df..1d3a9c36eb74 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -218,10 +218,6 @@ struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset);
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
- const struct landlock_id id);
-
/**
* landlock_get_rule_root - Get the root of a rule tree by key type
*
@@ -255,31 +251,6 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
refcount_inc(&ruleset->usage);
}
-/**
- * landlock_union_access_masks - Return all access rights handled in the
- * domain
- *
- * @domain: Landlock ruleset (used as a domain)
- *
- * Return: An access_masks result of the OR of all the domain's access masks.
- */
-static inline struct access_masks
-landlock_union_access_masks(const struct landlock_ruleset *const domain)
-{
- union access_masks_all matches = {};
- size_t layer_level;
-
- for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
- union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
- };
-
- matches.all |= layer.all;
- }
-
- return matches.masks;
-}
-
static inline void
landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
const access_mask_t fs_access_mask,
@@ -338,13 +309,4 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
return ruleset->access_masks[layer_level].scope;
}
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_access_masks *masks);
-
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
- const access_mask_t access_request,
- struct layer_access_masks *masks,
- const enum landlock_key_type key_type);
-
#endif /* _SECURITY_LANDLOCK_RULESET_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 03/17] landlock: Split struct landlock_domain from struct landlock_ruleset
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
2026-04-06 14:36 ` [PATCH v2 01/17] landlock: Prepare ruleset and domain type split Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 02/17] landlock: Move domain query functions to domain.c Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 04/17] landlock: Split denial logging from audit into common framework Mickaël Salaün
` (13 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Switch all domain users to the new struct landlock_domain type
introduced by a previous commit. This eliminates the conflation between
mutable rulesets and immutable domains.
Change the credential domain field to struct landlock_domain *, and
update all consumer functions. Move the merge and inherit chain from
ruleset.c to domain.c; landlock_merge_ruleset() now returns struct
landlock_domain * and uses create_domain(). Lock assertions on the
destination are removed because domains have no lock.
Rename the per-layer FAM from access_masks to layers, and the single
ruleset field from access_masks to layer, to prepare for future
per-layer extensions beyond handled-access bitfields.
Clean up struct landlock_ruleset by removing domain-only fields
(hierarchy, work_free, num_layers) and replacing the layers[] FAM with a
single struct access_masks layer field.
Break the circular include between audit.h and cred.h by replacing the
cred.h include in audit.h with forward declarations.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
security/landlock/access.h | 4 +-
security/landlock/audit.c | 12 +-
security/landlock/audit.h | 4 +-
security/landlock/cred.c | 6 +-
security/landlock/cred.h | 21 ++-
security/landlock/domain.c | 252 ++++++++++++++++++++++++++-
security/landlock/domain.h | 43 ++++-
security/landlock/fs.c | 28 ++-
security/landlock/net.c | 3 +-
security/landlock/ruleset.c | 329 ++++-------------------------------
security/landlock/ruleset.h | 129 ++------------
security/landlock/syscalls.c | 10 +-
security/landlock/task.c | 20 +--
13 files changed, 386 insertions(+), 475 deletions(-)
diff --git a/security/landlock/access.h b/security/landlock/access.h
index c19d5bc13944..76ab447dfcf7 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -19,8 +19,8 @@
/*
* All access rights that are denied by default whether they are handled or not
- * by a ruleset/layer. This must be ORed with all ruleset->access_masks[]
- * entries when we need to get the absolute handled access masks, see
+ * by a ruleset/layer. This must be ORed with all domain->layers[] entries when
+ * we need to get the absolute handled access masks, see
* landlock_upgrade_handled_access_masks().
*/
/* clang-format off */
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 8d0edf94037d..75438b3cc887 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -135,7 +135,7 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
}
static struct landlock_hierarchy *
-get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
+get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
{
struct landlock_hierarchy *hierarchy = domain->hierarchy;
ssize_t i;
@@ -168,7 +168,7 @@ static void test_get_hierarchy(struct kunit *const test)
.parent = &dom1_hierarchy,
.id = 30,
};
- struct landlock_ruleset dom2 = {
+ struct landlock_domain dom2 = {
.hierarchy = &dom2_hierarchy,
.num_layers = 3,
};
@@ -182,7 +182,7 @@ static void test_get_hierarchy(struct kunit *const test)
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
/* Get the youngest layer that denied the access_request. */
-static size_t get_denied_layer(const struct landlock_ruleset *const domain,
+static size_t get_denied_layer(const struct landlock_domain *const domain,
access_mask_t *const access_request,
const struct layer_access_masks *masks)
{
@@ -202,7 +202,7 @@ static size_t get_denied_layer(const struct landlock_ruleset *const domain,
static void test_get_denied_layer(struct kunit *const test)
{
- const struct landlock_ruleset dom = {
+ const struct landlock_domain dom = {
.num_layers = 5,
};
const struct layer_access_masks masks = {
@@ -440,8 +440,8 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
* Only domains which previously appeared in the audit logs are logged again.
* This is useful to know when a domain will never show again in the audit log.
*
- * Called in a work queue scheduled by landlock_put_ruleset_deferred() called
- * by hook_cred_free().
+ * Called in a work queue scheduled by landlock_put_domain_deferred() called by
+ * hook_cred_free().
*/
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
{
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 56778331b58c..50452a791656 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -12,7 +12,9 @@
#include <linux/lsm_audit.h>
#include "access.h"
-#include "cred.h"
+
+struct landlock_cred_security;
+struct landlock_hierarchy;
enum landlock_request_type {
LANDLOCK_REQUEST_PTRACE = 1,
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index cc419de75cd6..58b544993db4 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -22,7 +22,7 @@ static void hook_cred_transfer(struct cred *const new,
const struct landlock_cred_security *const old_llcred =
landlock_cred(old);
- landlock_get_ruleset(old_llcred->domain);
+ landlock_get_domain(old_llcred->domain);
*landlock_cred(new) = *old_llcred;
}
@@ -35,10 +35,10 @@ static int hook_cred_prepare(struct cred *const new,
static void hook_cred_free(struct cred *const cred)
{
- struct landlock_ruleset *const dom = landlock_cred(cred)->domain;
+ struct landlock_domain *const dom = landlock_cred(cred)->domain;
if (dom)
- landlock_put_ruleset_deferred(dom);
+ landlock_put_domain_deferred(dom);
}
#ifdef CONFIG_AUDIT
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index f287c56b5fd4..c42b0d3ecec8 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -16,6 +16,7 @@
#include <linux/rcupdate.h>
#include "access.h"
+#include "domain.h"
#include "limits.h"
#include "ruleset.h"
#include "setup.h"
@@ -31,9 +32,9 @@
*/
struct landlock_cred_security {
/**
- * @domain: Immutable ruleset enforced on a task.
+ * @domain: Immutable domain enforced on a task.
*/
- struct landlock_ruleset *domain;
+ struct landlock_domain *domain;
#ifdef CONFIG_AUDIT
/**
@@ -70,22 +71,20 @@ landlock_cred(const struct cred *cred)
static inline void landlock_cred_copy(struct landlock_cred_security *dst,
const struct landlock_cred_security *src)
{
- landlock_put_ruleset(dst->domain);
+ landlock_put_domain(dst->domain);
*dst = *src;
- landlock_get_ruleset(src->domain);
+ landlock_get_domain(src->domain);
}
-static inline struct landlock_ruleset *landlock_get_current_domain(void)
+static inline struct landlock_domain *landlock_get_current_domain(void)
{
return landlock_cred(current_cred())->domain;
}
-/*
- * The call needs to come from an RCU read-side critical section.
- */
-static inline const struct landlock_ruleset *
+/* The call needs to come from an RCU read-side critical section. */
+static inline const struct landlock_domain *
landlock_get_task_domain(const struct task_struct *const task)
{
return landlock_cred(__task_cred(task))->domain;
@@ -126,7 +125,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
const union access_masks_all masks_all = {
.masks = masks,
};
- const struct landlock_ruleset *domain;
+ const struct landlock_domain *domain;
ssize_t layer_level;
if (!cred)
@@ -139,7 +138,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
for (layer_level = domain->num_layers - 1; layer_level >= 0;
layer_level--) {
union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
+ .masks = domain->layers[layer_level],
};
if (layer.all & masks_all.all) {
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index cb79edf5df02..317fd94d3ccd 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -36,6 +36,36 @@
#include "object.h"
#include "ruleset.h"
+static void build_check_domain(void)
+{
+ const struct landlock_domain domain = {
+ .num_layers = ~0,
+ };
+
+ BUILD_BUG_ON(domain.num_layers < LANDLOCK_MAX_NUM_LAYERS);
+}
+
+static struct landlock_domain *create_domain(const u32 num_layers)
+{
+ struct landlock_domain *new_domain;
+
+ build_check_domain();
+ new_domain = kzalloc_flex(*new_domain, layers, num_layers,
+ GFP_KERNEL_ACCOUNT);
+ if (!new_domain)
+ return ERR_PTR(-ENOMEM);
+
+ refcount_set(&new_domain->usage, 1);
+ new_domain->rules.root_inode = RB_ROOT;
+
+#if IS_ENABLED(CONFIG_INET)
+ new_domain->rules.root_net_port = RB_ROOT;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ new_domain->num_layers = num_layers;
+ return new_domain;
+}
+
static void free_domain(struct landlock_domain *const domain)
{
might_sleep();
@@ -67,15 +97,15 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
}
}
-/* The returned access has the same lifetime as @ruleset. */
+/* The returned access has the same lifetime as @domain. */
const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
+landlock_find_rule(const struct landlock_domain *const domain,
const struct landlock_id id)
{
const struct rb_root *root;
const struct rb_node *node;
- root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+ root = landlock_get_rule_root((struct landlock_rules *)&domain->rules,
id.type);
if (IS_ERR(root))
return NULL;
@@ -151,7 +181,7 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
}
typedef access_mask_t
-get_access_mask_t(const struct landlock_ruleset *const ruleset,
+get_access_mask_t(const struct landlock_domain *const domain,
const u16 layer_level);
/**
@@ -169,7 +199,7 @@ get_access_mask_t(const struct landlock_ruleset *const ruleset,
* any of the active layers in @domain.
*/
access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+landlock_init_layer_masks(const struct landlock_domain *const domain,
const access_mask_t access_request,
struct layer_access_masks *const masks,
const enum landlock_key_type key_type)
@@ -209,6 +239,218 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
return handled_accesses;
}
+static int merge_tree(struct landlock_domain *const dst,
+ struct landlock_ruleset *const src,
+ const enum landlock_key_type key_type)
+{
+ struct landlock_rule *walker_rule, *next_rule;
+ struct rb_root *src_root;
+ int err = 0;
+
+ might_sleep();
+ lockdep_assert_held(&src->lock);
+
+ src_root = landlock_get_rule_root(&src->rules, key_type);
+ if (IS_ERR(src_root))
+ return PTR_ERR(src_root);
+
+ /* Merges the @src tree. */
+ rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
+ node) {
+ struct landlock_layer layers[] = { {
+ .level = dst->num_layers,
+ } };
+ const struct landlock_id id = {
+ .key = walker_rule->key,
+ .type = key_type,
+ };
+
+ if (WARN_ON_ONCE(walker_rule->num_layers != 1))
+ return -EINVAL;
+
+ if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
+ return -EINVAL;
+
+ layers[0].access = walker_rule->layers[0].access;
+
+ err = landlock_rule_insert(&dst->rules, id, &layers,
+ ARRAY_SIZE(layers));
+ if (err)
+ return err;
+ }
+ return err;
+}
+
+static int merge_ruleset(struct landlock_domain *const dst,
+ struct landlock_ruleset *const src)
+{
+ int err = 0;
+
+ might_sleep();
+ /* Should already be checked by landlock_merge_ruleset() */
+ if (WARN_ON_ONCE(!src))
+ return 0;
+ /* Only merge into a domain. */
+ if (WARN_ON_ONCE(!dst || !dst->hierarchy))
+ return -EINVAL;
+
+ mutex_lock(&src->lock);
+
+ /* Stacks the new layer. */
+ if (WARN_ON_ONCE(dst->num_layers < 1)) {
+ err = -EINVAL;
+ goto out_unlock;
+ }
+ dst->layers[dst->num_layers - 1] =
+ landlock_upgrade_handled_access_masks(src->layer);
+
+ /* Merges the @src inode tree. */
+ err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
+ if (err)
+ goto out_unlock;
+
+#if IS_ENABLED(CONFIG_INET)
+ /* Merges the @src network port tree. */
+ err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
+ if (err)
+ goto out_unlock;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+out_unlock:
+ mutex_unlock(&src->lock);
+ return err;
+}
+
+static int inherit_tree(struct landlock_domain *const parent,
+ struct landlock_domain *const child,
+ const enum landlock_key_type key_type)
+{
+ struct landlock_rule *walker_rule, *next_rule;
+ struct rb_root *parent_root;
+ int err = 0;
+
+ might_sleep();
+
+ parent_root = landlock_get_rule_root(
+ (struct landlock_rules *)&parent->rules, key_type);
+ if (IS_ERR(parent_root))
+ return PTR_ERR(parent_root);
+
+ /* Copies the @parent inode or network tree. */
+ rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
+ parent_root, node) {
+ const struct landlock_id id = {
+ .key = walker_rule->key,
+ .type = key_type,
+ };
+
+ err = landlock_rule_insert(&child->rules, id,
+ &walker_rule->layers,
+ walker_rule->num_layers);
+ if (err)
+ return err;
+ }
+ return err;
+}
+
+static int inherit_ruleset(struct landlock_domain *const parent,
+ struct landlock_domain *const child)
+{
+ int err = 0;
+
+ might_sleep();
+ if (!parent)
+ return 0;
+
+ /* Copies the @parent inode tree. */
+ err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
+ if (err)
+ return err;
+
+#if IS_ENABLED(CONFIG_INET)
+ /* Copies the @parent network port tree. */
+ err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
+ if (err)
+ return err;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ if (WARN_ON_ONCE(child->num_layers <= parent->num_layers))
+ return -EINVAL;
+
+ /* Copies the parent layer stack and leaves a space for the new layer. */
+ memcpy(child->layers, parent->layers,
+ flex_array_size(parent, layers, parent->num_layers));
+
+ if (WARN_ON_ONCE(!parent->hierarchy))
+ return -EINVAL;
+
+ landlock_get_hierarchy(parent->hierarchy);
+ child->hierarchy->parent = parent->hierarchy;
+
+ return 0;
+}
+
+/**
+ * landlock_merge_ruleset - Merge a ruleset with a domain
+ *
+ * @parent: Parent domain.
+ * @ruleset: New ruleset to be merged.
+ *
+ * The current task is requesting to be restricted. The subjective credentials
+ * must not be in an overridden state. cf. landlock_init_hierarchy_log().
+ *
+ * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on
+ * failure. If @parent is NULL, the new domain duplicates @ruleset.
+ */
+struct landlock_domain *
+landlock_merge_ruleset(struct landlock_domain *const parent,
+ struct landlock_ruleset *const ruleset)
+{
+ struct landlock_domain *new_dom __free(landlock_put_domain) = NULL;
+ u32 num_layers;
+ int err;
+
+ might_sleep();
+ if (WARN_ON_ONCE(!ruleset))
+ return ERR_PTR(-EINVAL);
+
+ if (parent) {
+ if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
+ return ERR_PTR(-E2BIG);
+ num_layers = parent->num_layers + 1;
+ } else {
+ num_layers = 1;
+ }
+
+ /* Creates a new domain... */
+ new_dom = create_domain(num_layers);
+ if (IS_ERR(new_dom))
+ return new_dom;
+
+ new_dom->hierarchy =
+ kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
+ if (!new_dom->hierarchy)
+ return ERR_PTR(-ENOMEM);
+
+ refcount_set(&new_dom->hierarchy->usage, 1);
+
+ /* ...as a child of @parent... */
+ err = inherit_ruleset(parent, new_dom);
+ if (err)
+ return ERR_PTR(err);
+
+ /* ...and including @ruleset. */
+ err = merge_ruleset(new_dom, ruleset);
+ if (err)
+ return ERR_PTR(err);
+
+ err = landlock_init_hierarchy_log(new_dom->hierarchy);
+ if (err)
+ return ERR_PTR(err);
+
+ return no_free_ptr(new_dom);
+}
+
#ifdef CONFIG_AUDIT
/**
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index afa97011ecd2..df11cb7d4f2b 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -196,7 +196,7 @@ struct landlock_domain {
* @work_free: Enables to free a domain within a lockless
* section. This is only used by landlock_put_domain_deferred()
* when @usage reaches zero. The fields @usage, @num_layers and
- * @access_masks are then unused.
+ * @layers are then unused.
*/
struct work_struct work_free;
struct {
@@ -212,7 +212,7 @@ struct landlock_domain {
*/
u32 num_layers;
/**
- * @access_masks: Contains the subset of filesystem and
+ * @layers: Contains the subset of filesystem and
* network actions that are restricted by a domain. A
* domain saves all layers of merged rulesets in a stack
* (FAM), starting from the first layer to the last one.
@@ -222,28 +222,51 @@ struct landlock_domain {
* overlapping access rights. These layers are set once
* and never changed for the lifetime of the domain.
*/
- struct access_masks access_masks[];
+ struct access_masks layers[];
};
};
};
+static inline access_mask_t
+landlock_get_fs_access_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ /* Handles all initially denied by default access rights. */
+ return domain->layers[layer_level].fs |
+ _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
+}
+
+static inline access_mask_t
+landlock_get_net_access_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ return domain->layers[layer_level].net;
+}
+
+static inline access_mask_t
+landlock_get_scope_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ return domain->layers[layer_level].scope;
+}
+
/**
* landlock_union_access_masks - Return all access rights handled in the
* domain
*
- * @domain: Landlock ruleset (used as a domain)
+ * @domain: Landlock domain
*
* Return: An access_masks result of the OR of all the domain's access masks.
*/
static inline struct access_masks
-landlock_union_access_masks(const struct landlock_ruleset *const domain)
+landlock_union_access_masks(const struct landlock_domain *const domain)
{
union access_masks_all matches = {};
size_t layer_level;
for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
+ .masks = domain->layers[layer_level],
};
matches.all |= layer.all;
@@ -258,15 +281,19 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain);
DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+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_ruleset *const ruleset,
+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);
access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+landlock_init_layer_masks(const struct landlock_domain *const domain,
const access_mask_t access_request,
struct layer_access_masks *masks,
const enum landlock_key_type key_type);
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index c1ecfe239032..3ef453fc14a6 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -336,12 +336,10 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
if (!d_is_dir(path->dentry) &&
!access_mask_subset(access_rights, ACCESS_FILE))
return -EINVAL;
- if (WARN_ON_ONCE(ruleset->num_layers != 1))
- return -EINVAL;
-
/* Transforms relative access rights to absolute ones. */
- access_rights |= LANDLOCK_MASK_ACCESS_FS &
- ~landlock_get_fs_access_mask(ruleset, 0);
+ access_rights |=
+ LANDLOCK_MASK_ACCESS_FS &
+ ~(ruleset->layer.fs | _LANDLOCK_ACCESS_FS_INITIALLY_DENIED);
id.key.object = get_inode_object(d_backing_inode(path->dentry));
if (IS_ERR(id.key.object))
return PTR_ERR(id.key.object);
@@ -364,7 +362,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
* Returns NULL if no rule is found or if @dentry is negative.
*/
static const struct landlock_rule *
-find_rule(const struct landlock_ruleset *const domain,
+find_rule(const struct landlock_domain *const domain,
const struct dentry *const dentry)
{
const struct landlock_rule *rule;
@@ -740,7 +738,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
* Return: True if the access request is granted, false otherwise.
*/
static bool
-is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
+is_access_to_paths_allowed(const struct landlock_domain *const domain,
const struct path *const path,
const access_mask_t access_request_parent1,
struct layer_access_masks *layer_masks_parent1,
@@ -1026,7 +1024,7 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
* Return: True if all the domain access rights are allowed for @dir, false if
* the walk reached @mnt_root.
*/
-static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
+static bool collect_domain_accesses(const struct landlock_domain *const domain,
const struct dentry *const mnt_root,
struct dentry *dir,
struct layer_access_masks *layer_masks_dom)
@@ -1578,8 +1576,8 @@ static int hook_path_truncate(const struct path *const path)
* @masks: Layer access masks to unmask
* @access: Access bits that control scoping
*/
-static void unmask_scoped_access(const struct landlock_ruleset *const client,
- const struct landlock_ruleset *const server,
+static void unmask_scoped_access(const struct landlock_domain *const client,
+ const struct landlock_domain *const server,
struct layer_access_masks *const masks,
const access_mask_t access)
{
@@ -1633,7 +1631,7 @@ static void unmask_scoped_access(const struct landlock_ruleset *const client,
static int hook_unix_find(const struct path *const path, struct sock *other,
int flags)
{
- const struct landlock_ruleset *dom_other;
+ const struct landlock_domain *dom_other;
const struct landlock_cred_security *subject;
struct layer_access_masks layer_masks;
struct landlock_request request = {};
@@ -1914,7 +1912,7 @@ static bool control_current_fowner(struct fown_struct *const fown)
static void hook_file_set_fowner(struct file *file)
{
- struct landlock_ruleset *prev_dom;
+ struct landlock_domain *prev_dom;
struct landlock_cred_security fown_subject = {};
size_t fown_layer = 0;
@@ -1926,7 +1924,7 @@ static void hook_file_set_fowner(struct file *file)
landlock_get_applicable_subject(
current_cred(), signal_scope, &fown_layer);
if (new_subject) {
- landlock_get_ruleset(new_subject->domain);
+ landlock_get_domain(new_subject->domain);
fown_subject = *new_subject;
}
}
@@ -1938,12 +1936,12 @@ static void hook_file_set_fowner(struct file *file)
#endif /* CONFIG_AUDIT*/
/* May be called in an RCU read-side critical section. */
- landlock_put_ruleset_deferred(prev_dom);
+ landlock_put_domain_deferred(prev_dom);
}
static void hook_file_free_security(struct file *file)
{
- landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain);
+ landlock_put_domain_deferred(landlock_file(file)->fown_subject.domain);
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 34a72a4f833d..de108b3277bc 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -32,8 +32,7 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
/* Transforms relative access rights to absolute ones. */
- access_rights |= LANDLOCK_MASK_ACCESS_NET &
- ~landlock_get_net_access_mask(ruleset, 0);
+ access_rights |= LANDLOCK_MASK_ACCESS_NET & ~ruleset->layer.net;
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights);
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 0cf31a7e4c7b..c220e0f9cf5f 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -20,22 +20,27 @@
#include <linux/refcount.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
-#include <linux/workqueue.h>
#include "access.h"
-#include "domain.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
-static struct landlock_ruleset *create_ruleset(const u32 num_layers)
+struct landlock_ruleset *
+landlock_create_ruleset(const access_mask_t fs_access_mask,
+ const access_mask_t net_access_mask,
+ const access_mask_t scope_mask)
{
struct landlock_ruleset *new_ruleset;
- new_ruleset = kzalloc_flex(*new_ruleset, access_masks, num_layers,
- GFP_KERNEL_ACCOUNT);
+ /* Informs about useless ruleset. */
+ if (!fs_access_mask && !net_access_mask && !scope_mask)
+ return ERR_PTR(-ENOMSG);
+
+ new_ruleset = kzalloc(sizeof(*new_ruleset), GFP_KERNEL_ACCOUNT);
if (!new_ruleset)
return ERR_PTR(-ENOMEM);
+
refcount_set(&new_ruleset->usage, 1);
mutex_init(&new_ruleset->lock);
new_ruleset->rules.root_inode = RB_ROOT;
@@ -44,34 +49,21 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
- new_ruleset->num_layers = num_layers;
- /*
- * hierarchy = NULL
- * rules.num_rules = 0
- * access_masks[] = 0
- */
- return new_ruleset;
-}
-
-struct landlock_ruleset *
-landlock_create_ruleset(const access_mask_t fs_access_mask,
- const access_mask_t net_access_mask,
- const access_mask_t scope_mask)
-{
- struct landlock_ruleset *new_ruleset;
-
- /* Informs about useless ruleset. */
- if (!fs_access_mask && !net_access_mask && !scope_mask)
- return ERR_PTR(-ENOMSG);
- new_ruleset = create_ruleset(1);
- if (IS_ERR(new_ruleset))
- return new_ruleset;
- if (fs_access_mask)
- landlock_add_fs_access_mask(new_ruleset, fs_access_mask, 0);
- if (net_access_mask)
- landlock_add_net_access_mask(new_ruleset, net_access_mask, 0);
- if (scope_mask)
- landlock_add_scope_mask(new_ruleset, scope_mask, 0);
+ /* Should already be checked in sys_landlock_create_ruleset(). */
+ if (fs_access_mask) {
+ WARN_ON_ONCE(fs_access_mask !=
+ (fs_access_mask & LANDLOCK_MASK_ACCESS_FS));
+ new_ruleset->layer.fs |= fs_access_mask;
+ }
+ if (net_access_mask) {
+ WARN_ON_ONCE(net_access_mask !=
+ (net_access_mask & LANDLOCK_MASK_ACCESS_NET));
+ new_ruleset->layer.net |= net_access_mask;
+ }
+ if (scope_mask) {
+ WARN_ON_ONCE(scope_mask != (scope_mask & LANDLOCK_MASK_SCOPE));
+ new_ruleset->layer.scope |= scope_mask;
+ }
return new_ruleset;
}
@@ -128,7 +120,7 @@ create_rule(const struct landlock_id id,
return ERR_PTR(-ENOMEM);
RB_CLEAR_NODE(&new_rule->node);
if (is_object_pointer(id.type)) {
- /* This should have been caught by insert_rule(). */
+ /* This should have been caught by landlock_rule_insert(). */
WARN_ON_ONCE(!id.key.object);
landlock_get_object(id.key.object);
}
@@ -144,12 +136,6 @@ create_rule(const struct landlock_id id,
return new_rule;
}
-static struct rb_root *get_root(struct landlock_ruleset *const ruleset,
- const enum landlock_key_type key_type)
-{
- return landlock_get_rule_root(&ruleset->rules, key_type);
-}
-
static void free_rule(struct landlock_rule *const rule,
const enum landlock_key_type key_type)
{
@@ -166,16 +152,12 @@ static void build_check_ruleset(void)
const struct landlock_rules rules = {
.num_rules = ~0,
};
- const struct landlock_ruleset ruleset = {
- .num_layers = ~0,
- };
BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
- BUILD_BUG_ON(ruleset.num_layers < LANDLOCK_MAX_NUM_LAYERS);
}
/**
- * insert_rule - Create and insert a rule in a rule set
+ * landlock_rule_insert - Create and insert a rule in a rule set
*
* @rules: The rule storage to be updated. The caller is responsible for
* any required locking. For rulesets, this means holding
@@ -197,10 +179,10 @@ static void build_check_ruleset(void)
*
* Return: 0 on success, -errno on failure.
*/
-static int insert_rule(struct landlock_rules *const rules,
- const struct landlock_id id,
- const struct landlock_layer (*layers)[],
- const size_t num_layers)
+int landlock_rule_insert(struct landlock_rules *const rules,
+ 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;
@@ -240,7 +222,7 @@ static int insert_rule(struct landlock_rules *const rules,
if ((*layers)[0].level == 0) {
/*
* Extends access rights when the request comes from
- * landlock_add_rule(2), i.e. contained by a ruleset.
+ * landlock_add_rule(2), i.e. @rules is not a domain.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
return -EINVAL;
@@ -301,176 +283,14 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
{
struct landlock_layer layers[] = { {
.access = access,
- /* When @level is zero, insert_rule() extends @ruleset. */
+ /* When @level is zero, landlock_rule_insert() extends @ruleset. */
.level = 0,
} };
build_check_layer();
lockdep_assert_held(&ruleset->lock);
- return insert_rule(&ruleset->rules, id, &layers, ARRAY_SIZE(layers));
-}
-
-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 rb_root *src_root;
- int err = 0;
-
- might_sleep();
- lockdep_assert_held(&dst->lock);
- lockdep_assert_held(&src->lock);
-
- src_root = get_root(src, key_type);
- if (IS_ERR(src_root))
- return PTR_ERR(src_root);
-
- /* Merges the @src tree. */
- rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
- node) {
- struct landlock_layer layers[] = { {
- .level = dst->num_layers,
- } };
- const struct landlock_id id = {
- .key = walker_rule->key,
- .type = key_type,
- };
-
- if (WARN_ON_ONCE(walker_rule->num_layers != 1))
- return -EINVAL;
-
- if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
- return -EINVAL;
-
- layers[0].access = walker_rule->layers[0].access;
-
- err = insert_rule(&dst->rules, id, &layers, ARRAY_SIZE(layers));
- if (err)
- return err;
- }
- return err;
-}
-
-static int merge_ruleset(struct landlock_ruleset *const dst,
- struct landlock_ruleset *const src)
-{
- int err = 0;
-
- might_sleep();
- /* Should already be checked by landlock_merge_ruleset() */
- if (WARN_ON_ONCE(!src))
- return 0;
- /* Only merge into a domain. */
- if (WARN_ON_ONCE(!dst || !dst->hierarchy))
- return -EINVAL;
-
- /* Locks @dst first because we are its only owner. */
- mutex_lock(&dst->lock);
- mutex_lock_nested(&src->lock, SINGLE_DEPTH_NESTING);
-
- /* Stacks the new layer. */
- if (WARN_ON_ONCE(src->num_layers != 1 || dst->num_layers < 1)) {
- err = -EINVAL;
- goto out_unlock;
- }
- dst->access_masks[dst->num_layers - 1] =
- landlock_upgrade_handled_access_masks(src->access_masks[0]);
-
- /* Merges the @src inode tree. */
- err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
- if (err)
- goto out_unlock;
-
-#if IS_ENABLED(CONFIG_INET)
- /* Merges the @src network port tree. */
- err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
- if (err)
- goto out_unlock;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
-out_unlock:
- mutex_unlock(&src->lock);
- mutex_unlock(&dst->lock);
- return err;
-}
-
-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 rb_root *parent_root;
- int err = 0;
-
- might_sleep();
- lockdep_assert_held(&parent->lock);
- lockdep_assert_held(&child->lock);
-
- parent_root = get_root(parent, key_type);
- if (IS_ERR(parent_root))
- return PTR_ERR(parent_root);
-
- /* Copies the @parent inode or network tree. */
- rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
- parent_root, node) {
- const struct landlock_id id = {
- .key = walker_rule->key,
- .type = key_type,
- };
-
- err = insert_rule(&child->rules, id, &walker_rule->layers,
- walker_rule->num_layers);
- if (err)
- return err;
- }
- return err;
-}
-
-static int inherit_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const child)
-{
- int err = 0;
-
- might_sleep();
- if (!parent)
- return 0;
-
- /* Locks @child first because we are its only owner. */
- mutex_lock(&child->lock);
- mutex_lock_nested(&parent->lock, SINGLE_DEPTH_NESTING);
-
- /* Copies the @parent inode tree. */
- err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
- if (err)
- goto out_unlock;
-
-#if IS_ENABLED(CONFIG_INET)
- /* Copies the @parent network port tree. */
- err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
- if (err)
- goto out_unlock;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- if (WARN_ON_ONCE(child->num_layers <= parent->num_layers)) {
- err = -EINVAL;
- goto out_unlock;
- }
- /* Copies the parent layer stack and leaves a space for the new layer. */
- memcpy(child->access_masks, parent->access_masks,
- flex_array_size(parent, access_masks, parent->num_layers));
-
- if (WARN_ON_ONCE(!parent->hierarchy)) {
- err = -EINVAL;
- goto out_unlock;
- }
- landlock_get_hierarchy(parent->hierarchy);
- child->hierarchy->parent = parent->hierarchy;
-
-out_unlock:
- mutex_unlock(&parent->lock);
- mutex_unlock(&child->lock);
- return err;
+ return landlock_rule_insert(&ruleset->rules, id, &layers,
+ ARRAY_SIZE(layers));
}
void landlock_free_rules(struct landlock_rules *const rules)
@@ -493,7 +313,6 @@ static void free_ruleset(struct landlock_ruleset *const ruleset)
{
might_sleep();
landlock_free_rules(&ruleset->rules);
- landlock_put_hierarchy(ruleset->hierarchy);
kfree(ruleset);
}
@@ -503,81 +322,3 @@ void landlock_put_ruleset(struct landlock_ruleset *const ruleset)
if (ruleset && refcount_dec_and_test(&ruleset->usage))
free_ruleset(ruleset);
}
-
-static void free_ruleset_work(struct work_struct *const work)
-{
- struct landlock_ruleset *ruleset;
-
- ruleset = container_of(work, struct landlock_ruleset, work_free);
- free_ruleset(ruleset);
-}
-
-/* Only called by hook_cred_free(). */
-void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
-{
- if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
- INIT_WORK(&ruleset->work_free, free_ruleset_work);
- schedule_work(&ruleset->work_free);
- }
-}
-
-/**
- * landlock_merge_ruleset - Merge a ruleset with a domain
- *
- * @parent: Parent domain.
- * @ruleset: New ruleset to be merged.
- *
- * The current task is requesting to be restricted. The subjective credentials
- * must not be in an overridden state. cf. landlock_init_hierarchy_log().
- *
- * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR()
- * on failure. If @parent is NULL, the new domain duplicates @ruleset.
- */
-struct landlock_ruleset *
-landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset)
-{
- struct landlock_ruleset *new_dom __free(landlock_put_ruleset) = NULL;
- u32 num_layers;
- int err;
-
- might_sleep();
- if (WARN_ON_ONCE(!ruleset || parent == ruleset))
- return ERR_PTR(-EINVAL);
-
- if (parent) {
- if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
- return ERR_PTR(-E2BIG);
- num_layers = parent->num_layers + 1;
- } else {
- num_layers = 1;
- }
-
- /* Creates a new domain... */
- new_dom = create_ruleset(num_layers);
- if (IS_ERR(new_dom))
- return new_dom;
-
- new_dom->hierarchy =
- kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
- if (!new_dom->hierarchy)
- return ERR_PTR(-ENOMEM);
-
- refcount_set(&new_dom->hierarchy->usage, 1);
-
- /* ...as a child of @parent... */
- err = inherit_ruleset(parent, new_dom);
- if (err)
- return ERR_PTR(err);
-
- /* ...and including @ruleset. */
- err = merge_ruleset(new_dom, ruleset);
- if (err)
- return ERR_PTR(err);
-
- err = landlock_init_hierarchy_log(new_dom->hierarchy);
- if (err)
- return ERR_PTR(err);
-
- return no_free_ptr(new_dom);
-}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 1d3a9c36eb74..bf127ff7496e 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -14,14 +14,11 @@
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
-#include <linux/workqueue.h>
#include "access.h"
#include "limits.h"
#include "object.h"
-struct landlock_hierarchy;
-
/**
* struct landlock_layer - Access rights for a given layer
*/
@@ -147,54 +144,20 @@ struct landlock_ruleset {
* @rules: Red-black tree storage for rules.
*/
struct landlock_rules rules;
-
/**
- * @hierarchy: Enables hierarchy identification even when a parent
- * domain vanishes. This is needed for the ptrace protection.
+ * @lock: Protects against concurrent modifications of @rules, if @usage
+ * is greater than zero.
+ */
+ struct mutex lock;
+ /**
+ * @usage: Number of file descriptors referencing this ruleset.
*/
- struct landlock_hierarchy *hierarchy;
- union {
- /**
- * @work_free: Enables to free a ruleset within a lockless
- * section. This is only used by
- * landlock_put_ruleset_deferred() when @usage reaches zero. The
- * fields @lock, @usage, @num_layers and @access_masks are then
- * unused.
- */
- struct work_struct work_free;
- struct {
- /**
- * @lock: Protects against concurrent modifications of
- * @root, if @usage is greater than zero.
- */
- struct mutex lock;
- /**
- * @usage: Number of processes (i.e. domains) or file
- * descriptors referencing this ruleset.
- */
- refcount_t usage;
- /**
- * @num_layers: Number of layers that are used in this
- * ruleset. This enables to check that all the layers
- * allow an access request. A value of 0 identifies a
- * non-merged ruleset (i.e. not a domain).
- */
- u32 num_layers;
- /**
- * @access_masks: Contains the subset of filesystem and
- * network actions that are restricted by a ruleset.
- * A domain saves all layers of merged rulesets in a
- * stack (FAM), starting from the first layer to the
- * last one. These layers are used when merging
- * rulesets, for user space backward compatibility
- * (i.e. future-proof), and to properly handle merged
- * rulesets without overlapping access rights. These
- * layers are set once and never changed for the
- * lifetime of the ruleset.
- */
- struct access_masks access_masks[];
- };
- };
+ refcount_t usage;
+ /**
+ * @layer: Contains the subset of filesystem and network actions that
+ * are handled by this ruleset.
+ */
+ struct access_masks layer;
};
struct landlock_ruleset *
@@ -203,7 +166,6 @@ landlock_create_ruleset(const access_mask_t access_mask_fs,
const access_mask_t scope_mask);
void landlock_put_ruleset(struct landlock_ruleset *const ruleset);
-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))
@@ -212,11 +174,12 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access);
-void landlock_free_rules(struct landlock_rules *const rules);
+int landlock_rule_insert(struct landlock_rules *const rules,
+ const struct landlock_id id,
+ const struct landlock_layer (*layers)[],
+ const size_t num_layers);
-struct landlock_ruleset *
-landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset);
+void landlock_free_rules(struct landlock_rules *const rules);
/**
* landlock_get_rule_root - Get the root of a rule tree by key type
@@ -251,62 +214,4 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
refcount_inc(&ruleset->usage);
}
-static inline void
-landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t fs_access_mask,
- const u16 layer_level)
-{
- access_mask_t fs_mask = fs_access_mask & LANDLOCK_MASK_ACCESS_FS;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(fs_access_mask != fs_mask);
- ruleset->access_masks[layer_level].fs |= fs_mask;
-}
-
-static inline void
-landlock_add_net_access_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t net_access_mask,
- const u16 layer_level)
-{
- access_mask_t net_mask = net_access_mask & LANDLOCK_MASK_ACCESS_NET;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(net_access_mask != net_mask);
- ruleset->access_masks[layer_level].net |= net_mask;
-}
-
-static inline void
-landlock_add_scope_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t scope_mask, const u16 layer_level)
-{
- access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(scope_mask != mask);
- ruleset->access_masks[layer_level].scope |= mask;
-}
-
-static inline access_mask_t
-landlock_get_fs_access_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- /* Handles all initially denied by default access rights. */
- return ruleset->access_masks[layer_level].fs |
- _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
-}
-
-static inline access_mask_t
-landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- return ruleset->access_masks[layer_level].net;
-}
-
-static inline access_mask_t
-landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- return ruleset->access_masks[layer_level].scope;
-}
-
#endif /* _SECURITY_LANDLOCK_RULESET_H */
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index accfd2e5a0cd..73ccc32d0afd 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -283,8 +283,6 @@ static struct landlock_ruleset *get_ruleset_from_fd(const int fd,
if (!(fd_file(ruleset_f)->f_mode & mode))
return ERR_PTR(-EPERM);
ruleset = fd_file(ruleset_f)->private_data;
- if (WARN_ON_ONCE(ruleset->num_layers != 1))
- return ERR_PTR(-EINVAL);
landlock_get_ruleset(ruleset);
return ruleset;
}
@@ -341,7 +339,7 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
- mask = ruleset->access_masks[0].fs;
+ mask = ruleset->layer.fs;
if ((path_beneath_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -377,7 +375,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
- mask = landlock_get_net_access_mask(ruleset, 0);
+ mask = ruleset->layer.net;
if ((net_port_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -556,7 +554,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
* manipulating the current credentials because they are
* dedicated per thread.
*/
- struct landlock_ruleset *const new_dom =
+ struct landlock_domain *const new_dom =
landlock_merge_ruleset(new_llcred->domain, ruleset);
if (IS_ERR(new_dom)) {
abort_creds(new_cred);
@@ -571,7 +569,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
#endif /* CONFIG_AUDIT */
/* Replaces the old (prepared) domain. */
- landlock_put_ruleset(new_llcred->domain);
+ landlock_put_domain(new_llcred->domain);
new_llcred->domain = new_dom;
#ifdef CONFIG_AUDIT
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 6d46042132ce..2e7ee62958b2 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -41,8 +41,8 @@
* Return: True if @parent is an ancestor of or equal to @child, false
* otherwise.
*/
-static bool domain_scope_le(const struct landlock_ruleset *const parent,
- const struct landlock_ruleset *const child)
+static bool domain_scope_le(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
{
const struct landlock_hierarchy *walker;
@@ -63,8 +63,8 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent,
return false;
}
-static int domain_ptrace(const struct landlock_ruleset *const parent,
- const struct landlock_ruleset *const child)
+static int domain_ptrace(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
{
if (domain_scope_le(parent, child))
return 0;
@@ -97,7 +97,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
scoped_guard(rcu)
{
- const struct landlock_ruleset *const child_dom =
+ const struct landlock_domain *const child_dom =
landlock_get_task_domain(child);
err = domain_ptrace(parent_subject->domain, child_dom);
}
@@ -136,7 +136,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
static int hook_ptrace_traceme(struct task_struct *const parent)
{
const struct landlock_cred_security *parent_subject;
- const struct landlock_ruleset *child_dom;
+ const struct landlock_domain *child_dom;
int err;
child_dom = landlock_get_current_domain();
@@ -177,8 +177,8 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
* Return: True if @server is in a different domain from @client and @client
* is scoped to access @server (i.e. access should be denied), false otherwise.
*/
-static bool domain_is_scoped(const struct landlock_ruleset *const client,
- const struct landlock_ruleset *const server,
+static bool domain_is_scoped(const struct landlock_domain *const client,
+ const struct landlock_domain *const server,
access_mask_t scope)
{
int client_layer, server_layer;
@@ -237,9 +237,9 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
}
static bool sock_is_scoped(struct sock *const other,
- const struct landlock_ruleset *const domain)
+ const struct landlock_domain *const domain)
{
- const struct landlock_ruleset *dom_other;
+ const struct landlock_domain *dom_other;
/* The credentials will not change. */
lockdep_assert_held(&unix_sk(other)->lock);
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 04/17] landlock: Split denial logging from audit into common framework
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (2 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 03/17] landlock: Split struct landlock_domain from struct landlock_ruleset Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 05/17] tracing: Add __print_untrusted_str() Mickaël Salaün
` (12 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Tracepoint emission requires the denial framework (layer identification,
request validation) without depending on CONFIG_AUDIT. Separate the
denial logging infrastructure from the audit-specific code by
introducing a common log framework.
Create CONFIG_SECURITY_LANDLOCK_LOG, automatically selected when either
CONFIG_AUDIT or CONFIG_TRACEPOINTS is enabled. The CONFIG_TRACEPOINTS
dependency is added proactively alongside the audit-to-log
generalization; a following commit adds the first tracepoint consumer.
Rename audit.c to log.c and create log.h with the request types and
struct landlock_request moved from audit.h. Rename the
landlock_log_drop_domain() function to landlock_log_free_domain() to
match the landlock_free_domain tracepoint introduced in a following
commit.
The landlock_log_denial() declaration in log.h remains under
CONFIG_AUDIT in this patch; the guard is widened to
CONFIG_SECURITY_LANDLOCK_LOG in a following commit that adds the first
tracepoint consumer.
Move id.o from CONFIG_AUDIT to CONFIG_SECURITY_LANDLOCK_LOG so that
domain and ruleset IDs are available for tracing without audit support.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
security/landlock/Kconfig | 5 ++
security/landlock/Makefile | 6 +-
security/landlock/cred.h | 8 ++-
security/landlock/domain.c | 6 +-
security/landlock/domain.h | 16 +++--
security/landlock/fs.c | 11 ++--
security/landlock/{audit.c => log.c} | 88 +++++++++++++++++-----------
security/landlock/{audit.h => log.h} | 12 ++--
security/landlock/net.c | 2 +-
security/landlock/task.c | 2 +-
10 files changed, 96 insertions(+), 60 deletions(-)
rename security/landlock/{audit.c => log.c} (95%)
rename security/landlock/{audit.h => log.h} (86%)
diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig
index 3f1493402052..7aeac29160e8 100644
--- a/security/landlock/Kconfig
+++ b/security/landlock/Kconfig
@@ -21,6 +21,11 @@ config SECURITY_LANDLOCK
you should also prepend "landlock," to the content of CONFIG_LSM to
enable Landlock at boot time.
+config SECURITY_LANDLOCK_LOG
+ bool
+ depends on SECURITY_LANDLOCK
+ default y if AUDIT || TRACEPOINTS
+
config SECURITY_LANDLOCK_KUNIT_TEST
bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS
depends on KUNIT=y
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index 23e13644916f..101440da7bcd 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -13,6 +13,6 @@ landlock-y := \
landlock-$(CONFIG_INET) += net.o
-landlock-$(CONFIG_AUDIT) += \
- id.o \
- audit.o
+landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
+ log.o \
+ id.o
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index c42b0d3ecec8..38299db6efa2 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -36,13 +36,15 @@ struct landlock_cred_security {
*/
struct landlock_domain *domain;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @domain_exec: Bitmask identifying the domain layers that were enforced by
* the current task's executed file (i.e. no new execve(2) since
* landlock_restrict_self(2)).
*/
u16 domain_exec;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+#ifdef CONFIG_AUDIT
/**
* @log_subdomains_off: Set if the domain descendants's log_status should be
* set to %LANDLOCK_LOG_DISABLED. This is not a landlock_hierarchy
@@ -53,14 +55,14 @@ struct landlock_cred_security {
#endif /* CONFIG_AUDIT */
} __packed;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/* Makes sure all layer executions can be stored. */
static_assert(BITS_PER_TYPE(typeof_member(struct landlock_cred_security,
domain_exec)) >=
LANDLOCK_MAX_NUM_LAYERS);
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline struct landlock_cred_security *
landlock_cred(const struct cred *cred)
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 317fd94d3ccd..0dfd53ae9dd7 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -451,7 +451,7 @@ landlock_merge_ruleset(struct landlock_domain *const parent,
return no_free_ptr(new_dom);
}
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* get_current_exe - Get the current's executable path, if any
@@ -561,6 +561,10 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
return 0;
}
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+#ifdef CONFIG_AUDIT
+
static deny_masks_t
get_layer_deny_mask(const access_mask_t all_existing_optional_access,
const unsigned long access_bit, const size_t layer)
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index df11cb7d4f2b..56f54efb65d1 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -21,7 +21,7 @@
#include <linux/workqueue.h>
#include "access.h"
-#include "audit.h"
+#include "log.h"
#include "ruleset.h"
enum landlock_log_status {
@@ -87,7 +87,7 @@ struct landlock_hierarchy {
*/
refcount_t usage;
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
/**
* @log_status: Whether this domain should be logged or not. Because
* concurrent log entries may be created at the same time, it is still
@@ -117,7 +117,7 @@ struct landlock_hierarchy {
* %LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON. Set to false by default.
*/
log_new_exec : 1;
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
};
#ifdef CONFIG_AUDIT
@@ -127,6 +127,10 @@ landlock_get_deny_masks(const access_mask_t all_existing_optional_access,
const access_mask_t optional_access,
const struct layer_access_masks *const masks);
+#endif /* CONFIG_AUDIT */
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+
int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy);
static inline void
@@ -139,7 +143,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
kfree(hierarchy->details);
}
-#else /* CONFIG_AUDIT */
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline int
landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
@@ -152,7 +156,7 @@ landlock_free_hierarchy_details(struct landlock_hierarchy *const hierarchy)
{
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void
landlock_get_hierarchy(struct landlock_hierarchy *const hierarchy)
@@ -166,7 +170,7 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) {
const struct landlock_hierarchy *const freeme = hierarchy;
- landlock_log_drop_domain(hierarchy);
+ landlock_log_free_domain(hierarchy);
landlock_free_hierarchy_details(hierarchy);
hierarchy = hierarchy->parent;
kfree(freeme);
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 3ef453fc14a6..a0b4d0dd261f 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -42,12 +42,12 @@
#include <uapi/linux/landlock.h>
#include "access.h"
-#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
#include "limits.h"
+#include "log.h"
#include "object.h"
#include "ruleset.h"
#include "setup.h"
@@ -918,10 +918,11 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
path_put(&walker_path);
/*
- * Check CONFIG_AUDIT to enable elision of log_request_parent* and
- * associated caller's stack variables thanks to dead code elimination.
+ * Check CONFIG_SECURITY_LANDLOCK_LOG to enable elision of
+ * log_request_parent* and associated caller's stack variables thanks to
+ * dead code elimination.
*/
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
if (!allowed_parent1 && log_request_parent1) {
log_request_parent1->type = LANDLOCK_REQUEST_FS_ACCESS;
log_request_parent1->audit.type = LSM_AUDIT_DATA_PATH;
@@ -937,7 +938,7 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
log_request_parent2->access = access_masked_parent2;
log_request_parent2->layer_masks = layer_masks_parent2;
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
return allowed_parent1 && allowed_parent2;
}
diff --git a/security/landlock/audit.c b/security/landlock/log.c
similarity index 95%
rename from security/landlock/audit.c
rename to security/landlock/log.c
index 75438b3cc887..c9b506707af0 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/log.c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
- * Landlock - Audit helpers
+ * Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
*/
@@ -13,12 +13,13 @@
#include <uapi/linux/landlock.h>
#include "access.h"
-#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "limits.h"
+#include "log.h"
#include "ruleset.h"
+#ifdef CONFIG_AUDIT
static const char *const fs_access_strings[] = {
[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute",
@@ -134,6 +135,45 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
}
+static void audit_denial(const struct landlock_cred_security *const subject,
+ const struct landlock_request *const request,
+ struct landlock_hierarchy *const youngest_denied,
+ const size_t youngest_layer,
+ const access_mask_t missing)
+{
+ struct audit_buffer *ab;
+
+ if (!audit_enabled)
+ return;
+
+ /* Checks if the current exec was restricting itself. */
+ if (subject->domain_exec & BIT(youngest_layer)) {
+ /* Ignores denials for the same execution. */
+ if (!youngest_denied->log_same_exec)
+ return;
+ } else {
+ /* Ignores denials after a new execution. */
+ if (!youngest_denied->log_new_exec)
+ return;
+ }
+
+ /* Uses consistent allocation flags wrt common_lsm_audit(). */
+ ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
+ AUDIT_LANDLOCK_ACCESS);
+ if (!ab)
+ return;
+
+ audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id);
+ log_blockers(ab, request->type, missing);
+ audit_log_lsm_data(ab, &request->audit);
+ audit_log_end(ab);
+
+ /* Logs this domain the first time it shows in log. */
+ log_domain(youngest_denied);
+}
+
+#endif /* CONFIG_AUDIT */
+
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
{
@@ -352,7 +392,7 @@ static bool is_valid_request(const struct landlock_request *const request)
}
/**
- * landlock_log_denial - Create audit records related to a denial
+ * landlock_log_denial - Log a denied access
*
* @subject: The Landlock subject's credential denying an action.
* @request: Detail of the user space request.
@@ -360,7 +400,6 @@ static bool is_valid_request(const struct landlock_request *const request)
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request)
{
- struct audit_buffer *ab;
struct landlock_hierarchy *youngest_denied;
size_t youngest_layer;
access_mask_t missing;
@@ -403,37 +442,16 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
*/
atomic64_inc(&youngest_denied->num_denials);
- if (!audit_enabled)
- return;
-
- /* Checks if the current exec was restricting itself. */
- if (subject->domain_exec & BIT(youngest_layer)) {
- /* Ignores denials for the same execution. */
- if (!youngest_denied->log_same_exec)
- return;
- } else {
- /* Ignores denials after a new execution. */
- if (!youngest_denied->log_new_exec)
- return;
- }
-
- /* Uses consistent allocation flags wrt common_lsm_audit(). */
- ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
- AUDIT_LANDLOCK_ACCESS);
- if (!ab)
- return;
-
- audit_log_format(ab, "domain=%llx blockers=", youngest_denied->id);
- log_blockers(ab, request->type, missing);
- audit_log_lsm_data(ab, &request->audit);
- audit_log_end(ab);
-
- /* Logs this domain the first time it shows in log. */
- log_domain(youngest_denied);
+#ifdef CONFIG_AUDIT
+ audit_denial(subject, request, youngest_denied, youngest_layer,
+ missing);
+#endif /* CONFIG_AUDIT */
}
+#ifdef CONFIG_AUDIT
+
/**
- * landlock_log_drop_domain - Create an audit record on domain deallocation
+ * landlock_log_free_domain - Create an audit record on domain deallocation
*
* @hierarchy: The domain's hierarchy being deallocated.
*
@@ -443,7 +461,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
* Called in a work queue scheduled by landlock_put_domain_deferred() called by
* hook_cred_free().
*/
-void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
{
struct audit_buffer *ab;
@@ -471,6 +489,8 @@ void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
audit_log_end(ab);
}
+#endif /* CONFIG_AUDIT */
+
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static struct kunit_case test_cases[] = {
@@ -483,7 +503,7 @@ static struct kunit_case test_cases[] = {
};
static struct kunit_suite test_suite = {
- .name = "landlock_audit",
+ .name = "landlock_log",
.test_cases = test_cases,
};
diff --git a/security/landlock/audit.h b/security/landlock/log.h
similarity index 86%
rename from security/landlock/audit.h
rename to security/landlock/log.h
index 50452a791656..4370fff86e45 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/log.h
@@ -1,12 +1,12 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
- * Landlock - Audit helpers
+ * Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
*/
-#ifndef _SECURITY_LANDLOCK_AUDIT_H
-#define _SECURITY_LANDLOCK_AUDIT_H
+#ifndef _SECURITY_LANDLOCK_LOG_H
+#define _SECURITY_LANDLOCK_LOG_H
#include <linux/audit.h>
#include <linux/lsm_audit.h>
@@ -54,7 +54,7 @@ struct landlock_request {
#ifdef CONFIG_AUDIT
-void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy);
+void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy);
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request);
@@ -62,7 +62,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
#else /* CONFIG_AUDIT */
static inline void
-landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
{
}
@@ -74,4 +74,4 @@ landlock_log_denial(const struct landlock_cred_security *const subject,
#endif /* CONFIG_AUDIT */
-#endif /* _SECURITY_LANDLOCK_AUDIT_H */
+#endif /* _SECURITY_LANDLOCK_LOG_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index de108b3277bc..63f1fe0ec876 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -12,11 +12,11 @@
#include <linux/socket.h>
#include <net/ipv6.h>
-#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "limits.h"
+#include "log.h"
#include "net.h"
#include "ruleset.h"
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 2e7ee62958b2..5bfbbe6107ce 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -20,11 +20,11 @@
#include <net/af_unix.h>
#include <net/sock.h>
-#include "audit.h"
#include "common.h"
#include "cred.h"
#include "domain.h"
#include "fs.h"
+#include "log.h"
#include "ruleset.h"
#include "setup.h"
#include "task.h"
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 05/17] tracing: Add __print_untrusted_str()
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (3 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 04/17] landlock: Split denial logging from audit into common framework Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 06/17] landlock: Add create_ruleset and free_ruleset tracepoints Mickaël Salaün
` (11 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Landlock tracepoints expose filesystem paths and process names
that may contain spaces, equal signs, or other characters that
break ftrace field parsing.
Add a new __print_untrusted_str() helper to safely print strings after
escaping all special characters, including common separators (space,
equal sign), quotes, and backslashes. This transforms a string from an
untrusted source (e.g. user space) to make it:
- safe to parse,
- easy to read (for simple strings),
- easy to get back the original.
Cc: Günther Noack <gnoack@google.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-4-mic@digikod.net
- Remove WARN_ON() (pointed out by Steven Rostedt).
---
include/linux/trace_events.h | 2 ++
include/trace/stages/stage3_trace_output.h | 4 +++
include/trace/stages/stage7_class_define.h | 1 +
kernel/trace/trace_output.c | 41 ++++++++++++++++++++++
4 files changed, 48 insertions(+)
diff --git a/include/linux/trace_events.h b/include/linux/trace_events.h
index 37eb2f0f3dd8..7f4325d327ee 100644
--- a/include/linux/trace_events.h
+++ b/include/linux/trace_events.h
@@ -57,6 +57,8 @@ trace_print_hex_dump_seq(struct trace_seq *p, const char *prefix_str,
int prefix_type, int rowsize, int groupsize,
const void *buf, size_t len, bool ascii);
+const char *trace_print_untrusted_str_seq(struct trace_seq *s, const char *str);
+
int trace_raw_output_prep(struct trace_iterator *iter,
struct trace_event *event);
extern __printf(2, 3)
diff --git a/include/trace/stages/stage3_trace_output.h b/include/trace/stages/stage3_trace_output.h
index fce85ea2df1c..62e98babb969 100644
--- a/include/trace/stages/stage3_trace_output.h
+++ b/include/trace/stages/stage3_trace_output.h
@@ -133,6 +133,10 @@
trace_print_hex_dump_seq(p, prefix_str, prefix_type, \
rowsize, groupsize, buf, len, ascii)
+#undef __print_untrusted_str
+#define __print_untrusted_str(str) \
+ trace_print_untrusted_str_seq(p, __get_str(str))
+
#undef __print_ns_to_secs
#define __print_ns_to_secs(value) \
({ \
diff --git a/include/trace/stages/stage7_class_define.h b/include/trace/stages/stage7_class_define.h
index fcd564a590f4..1164aacd550f 100644
--- a/include/trace/stages/stage7_class_define.h
+++ b/include/trace/stages/stage7_class_define.h
@@ -24,6 +24,7 @@
#undef __print_array
#undef __print_dynamic_array
#undef __print_hex_dump
+#undef __print_untrusted_str
#undef __get_buf
/*
diff --git a/kernel/trace/trace_output.c b/kernel/trace/trace_output.c
index 1996d7aba038..9d14c7cc654d 100644
--- a/kernel/trace/trace_output.c
+++ b/kernel/trace/trace_output.c
@@ -16,6 +16,7 @@
#include <linux/btf.h>
#include <linux/bpf.h>
#include <linux/hashtable.h>
+#include <linux/string_helpers.h>
#include "trace_output.h"
#include "trace_btf.h"
@@ -321,6 +322,46 @@ trace_print_hex_dump_seq(struct trace_seq *p, const char *prefix_str,
}
EXPORT_SYMBOL(trace_print_hex_dump_seq);
+/**
+ * trace_print_untrusted_str_seq - print a string after escaping characters
+ * @s: trace seq struct to write to
+ * @src: The string to print
+ *
+ * Prints a string to a trace seq after escaping all special characters,
+ * including common separators (space, equal sign), quotes, and backslashes.
+ * This transforms a string from an untrusted source (e.g. user space) to make
+ * it:
+ * - safe to parse,
+ * - easy to read (for simple strings),
+ * - easy to get back the original.
+ */
+const char *trace_print_untrusted_str_seq(struct trace_seq *s,
+ const char *src)
+{
+ int escaped_size;
+ char *buf;
+ size_t buf_size = seq_buf_get_buf(&s->seq, &buf);
+ const char *ret = trace_seq_buffer_ptr(s);
+
+ /* Buffer exhaustion is normal when the trace buffer is full. */
+ if (!src || buf_size == 0)
+ return NULL;
+
+ escaped_size = string_escape_mem(src, strlen(src), buf, buf_size,
+ ESCAPE_SPACE | ESCAPE_SPECIAL | ESCAPE_NAP | ESCAPE_APPEND |
+ ESCAPE_OCTAL, " ='\"\\");
+ if (unlikely(escaped_size >= buf_size)) {
+ /* We need some room for the final '\0'. */
+ seq_buf_set_overflow(&s->seq);
+ s->full = 1;
+ return NULL;
+ }
+ seq_buf_commit(&s->seq, escaped_size);
+ trace_seq_putc(s, 0);
+ return ret;
+}
+EXPORT_SYMBOL(trace_print_untrusted_str_seq);
+
int trace_raw_output_prep(struct trace_iterator *iter,
struct trace_event *trace_event)
{
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 06/17] landlock: Add create_ruleset and free_ruleset tracepoints
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (4 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 05/17] tracing: Add __print_untrusted_str() Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 07/17] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints Mickaël Salaün
` (10 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add tracepoints for ruleset lifecycle events: landlock_create_ruleset
fires from the landlock_create_ruleset() syscall handler, logging the
ruleset Landlock ID and handled access masks; landlock_free_ruleset
fires in free_ruleset() before the ruleset is freed, so eBPF programs
can access the full ruleset state via BTF.
The create_ruleset TP_PROTO takes only the ruleset pointer. The handled
access masks are read from the ruleset in TP_fast_assign rather than
passed as scalar arguments, so eBPF programs can access the full ruleset
state (rules, access masks) via BTF on a single pointer. No lock is
needed because the ruleset is not yet shared (the file descriptor has
not been installed).
Create the trace header with a DOC comment documenting the consistency
guarantees, locking conventions, TP_PROTO safety, and security
considerations shared by all Landlock tracepoints. Add
CREATE_TRACE_POINTS in log.c to generate the tracepoint implementations.
Add an id field to struct landlock_ruleset, assigned from
landlock_get_id_range() at creation time. Extend the CONFIG guard on
landlock_get_id_range() from CONFIG_AUDIT to
CONFIG_SECURITY_LANDLOCK_LOG so that IDs are available for tracing even
without audit support.
The deallocation events use the "free_" prefix (rather than "drop_")
because they fire when the object is actually freed. There is no need
for allocated/deallocated symmetry because ruleset creation happens with
the landlock_create_ruleset tracepoint.
landlock_create_ruleset tracepoint.
Unlike audit records which share a record type and need a "status="
field to distinguish allocation from deallocation, tracepoints provide
one event type per lifecycle transition, each with a type-safe TP_PROTO
matching the specific transition. This enables type-safe eBPF BTF
access and precise ftrace filtering by event name.
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:
- New patch (split from the v1 add_rule_fs tracepoint patch).
---
MAINTAINERS | 1 +
include/trace/events/landlock.h | 94 +++++++++++++++++++++++++++++++++
security/landlock/id.h | 6 +--
security/landlock/log.c | 5 ++
security/landlock/ruleset.c | 8 +++
security/landlock/ruleset.h | 9 ++++
security/landlock/syscalls.c | 5 ++
7 files changed, 125 insertions(+), 3 deletions(-)
create mode 100644 include/trace/events/landlock.h
diff --git a/MAINTAINERS b/MAINTAINERS
index c3fe46d7c4bc..51104faa3951 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14389,6 +14389,7 @@ F: Documentation/admin-guide/LSM/landlock.rst
F: Documentation/security/landlock.rst
F: Documentation/userspace-api/landlock.rst
F: fs/ioctl.c
+F: include/trace/events/landlock.h
F: include/uapi/linux/landlock.h
F: samples/landlock/
F: security/landlock/
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
new file mode 100644
index 000000000000..5e847844fbf7
--- /dev/null
+++ b/include/trace/events/landlock.h
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare
+ */
+
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM landlock
+
+#if !defined(_TRACE_LANDLOCK_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_LANDLOCK_H
+
+#include <linux/tracepoint.h>
+
+struct landlock_ruleset;
+
+/**
+ * DOC: Landlock trace events
+ *
+ * Consistency guarantee: every trace event corresponds to an operation
+ * that has irrevocably succeeded. Lifecycle events fire only after
+ * the point of no return; denial events fire only for denials that
+ * actually happen. This guarantees that eBPF programs observing the
+ * trace stream can build a faithful model of Landlock state without
+ * reconciliation logic.
+ *
+ * Mutable object pointers in TP_PROTO (e.g., struct landlock_ruleset
+ * for add_rule events) are passed while the caller holds the object's
+ * lock, so that TP_fast_assign and eBPF programs reading via BTF see a
+ * consistent snapshot. For objects that are immutable at the emission
+ * site (e.g., a domain after creation), no lock is needed.
+ *
+ * All pointer arguments in TP_PROTO are guaranteed non-NULL by the
+ * caller. eBPF programs can access these pointers via BTF for richer
+ * introspection than the TP_STRUCT__entry fields provide.
+ *
+ * TP_STRUCT__entry fields serve TP_printk display only. eBPF programs
+ * access the raw TP_PROTO arguments directly.
+ *
+ * Security: as for audit, Landlock trace events may expose sensitive
+ * information about all sandboxed processes on the system. See
+ * Documentation/admin-guide/LSM/landlock.rst for security considerations
+ * and privilege requirements.
+ */
+
+/**
+ * landlock_create_ruleset - new ruleset created
+ * @ruleset: Newly created ruleset (never NULL); not yet shared via an fd,
+ * so no lock is needed. eBPF programs can read the full ruleset
+ * state via BTF.
+ */
+TRACE_EVENT(
+ landlock_create_ruleset,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset),
+
+ TP_ARGS(ruleset),
+
+ TP_STRUCT__entry(__field(__u64, ruleset_id) __field(access_mask_t,
+ handled_fs)
+ __field(access_mask_t, handled_net)
+ __field(access_mask_t, scoped)),
+
+ TP_fast_assign(__entry->ruleset_id = ruleset->id;
+ __entry->handled_fs = ruleset->layer.fs;
+ __entry->handled_net = ruleset->layer.net;
+ __entry->scoped = ruleset->layer.scope;),
+
+ TP_printk("ruleset=%llx handled_fs=0x%x handled_net=0x%x scoped=0x%x",
+ __entry->ruleset_id, __entry->handled_fs,
+ __entry->handled_net, __entry->scoped));
+
+/**
+ * landlock_free_ruleset - Ruleset freed
+ *
+ * Emitted when a ruleset's last reference is dropped (typically when
+ * the creating process closes the ruleset file descriptor).
+ */
+TRACE_EVENT(landlock_free_ruleset,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset),
+
+ TP_ARGS(ruleset),
+
+ TP_STRUCT__entry(__field(__u64, ruleset_id)),
+
+ TP_fast_assign(__entry->ruleset_id = ruleset->id;),
+
+ TP_printk("ruleset=%llx", __entry->ruleset_id));
+
+#endif /* _TRACE_LANDLOCK_H */
+
+/* This part must be outside protection */
+#include <trace/define_trace.h>
diff --git a/security/landlock/id.h b/security/landlock/id.h
index 45dcfb9e9a8b..2a43c2b523a8 100644
--- a/security/landlock/id.h
+++ b/security/landlock/id.h
@@ -8,18 +8,18 @@
#ifndef _SECURITY_LANDLOCK_ID_H
#define _SECURITY_LANDLOCK_ID_H
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
void __init landlock_init_id(void);
u64 landlock_get_id_range(size_t number_of_ids);
-#else /* CONFIG_AUDIT */
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void __init landlock_init_id(void)
{
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* _SECURITY_LANDLOCK_ID_H */
diff --git a/security/landlock/log.c b/security/landlock/log.c
index c9b506707af0..ef79e4ed0037 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -174,6 +174,11 @@ static void audit_denial(const struct landlock_cred_security *const subject,
#endif /* CONFIG_AUDIT */
+#ifdef CONFIG_TRACEPOINTS
+#define CREATE_TRACE_POINTS
+#include <trace/events/landlock.h>
+#endif /* CONFIG_TRACEPOINTS */
+
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
{
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index c220e0f9cf5f..0d1e3dadb318 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -22,10 +22,13 @@
#include <linux/spinlock.h>
#include "access.h"
+#include "id.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
+#include <trace/events/landlock.h>
+
struct landlock_ruleset *
landlock_create_ruleset(const access_mask_t fs_access_mask,
const access_mask_t net_access_mask,
@@ -49,6 +52,10 @@ landlock_create_ruleset(const access_mask_t fs_access_mask,
new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ new_ruleset->id = landlock_get_id_range(1);
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
/* Should already be checked in sys_landlock_create_ruleset(). */
if (fs_access_mask) {
WARN_ON_ONCE(fs_access_mask !=
@@ -312,6 +319,7 @@ void landlock_free_rules(struct landlock_rules *const rules)
static void free_ruleset(struct landlock_ruleset *const ruleset)
{
might_sleep();
+ trace_landlock_free_ruleset(ruleset);
landlock_free_rules(&ruleset->rules);
kfree(ruleset);
}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index bf127ff7496e..0d60e7fb8ff2 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare
*/
#ifndef _SECURITY_LANDLOCK_RULESET_H
@@ -153,6 +154,14 @@ struct landlock_ruleset {
* @usage: Number of file descriptors referencing this ruleset.
*/
refcount_t usage;
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ /**
+ * @id: Unique identifier for this ruleset, used for tracing.
+ */
+ u64 id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
/**
* @layer: Contains the subset of filesystem and network actions that
* are handled by this ruleset.
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 73ccc32d0afd..b18e83e457c2 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -38,6 +38,8 @@
#include "setup.h"
#include "tsync.h"
+#include <trace/events/landlock.h>
+
static bool is_initialized(void)
{
if (likely(landlock_initialized))
@@ -256,6 +258,9 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
if (IS_ERR(ruleset))
return PTR_ERR(ruleset);
+ /* Ruleset is not yet shared (FD not installed), no lock needed. */
+ trace_landlock_create_ruleset(ruleset);
+
/* Creates anonymous FD referring to the ruleset. */
ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
ruleset, O_RDWR | O_CLOEXEC);
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 07/17] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (5 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 06/17] landlock: Add create_ruleset and free_ruleset tracepoints Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 08/17] landlock: Add restrict_self and free_domain tracepoints Mickaël Salaün
` (9 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add tracepoints for Landlock rule addition: landlock_add_rule_fs for
filesystem rules and landlock_add_rule_net for network rules. These
enable eBPF programs and ftrace consumers to correlate filesystem
objects and network ports with their rulesets.
Both tracepoints include lockdep_assert_held(&ruleset->lock) in
TP_fast_assign to enforce that the ruleset lock is held during emission.
This guarantees that eBPF programs reading the ruleset via BTF see a
consistent version and the rule just inserted.
Add a version field to struct landlock_ruleset, incremented under the
ruleset lock on each rule insertion. The version fills the existing
4-byte hole between usage and id (no struct size increase). Add a
static assertion to ensure the version type can hold
LANDLOCK_MAX_NUM_RULES.
For filesystem rules, resolve the absolute path via
resolve_path_for_trace() which uses d_absolute_path(). Unlike d_path()
(used by audit), d_absolute_path() produces namespace-independent paths
that do not depend on the tracer's chroot state. This makes trace
output deterministic regardless of mount namespace configuration.
Differentiate error cases: "<too_long>" for -ENAMETOOLONG and
"<unreachable>" for anonymous files or detached mounts.
Add DEFINE_FREE(__putname) to include/linux/fs.h alongside the
__getname()/__putname() definitions.
Cc: Christian Brauner <brauner@kernel.org>
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-5-mic@digikod.net
- Added landlock_add_rule_net tracepoint for network rules.
- Dropped key=inode:0x%lx from add_rule_fs printk, using dev/ino
instead.
- Used ruleset Landlock ID instead of kernel pointer in printk.
- Differentiated d_absolute_path() error cases (suggested by
Tingmao Wang).
- Moved DEFINE_FREE(__putname) to include/linux/fs.h (noticed by
Tingmao Wang).
- Added version field to struct landlock_ruleset.
- Added version to add_rule trace events (format:
ruleset=<id>.<version>).
- Added d_absolute_path() vs d_path() rationale to commit message.
---
include/linux/fs.h | 1 +
include/trace/events/landlock.h | 93 ++++++++++++++++++++++++++++++---
security/landlock/fs.c | 19 +++++++
security/landlock/fs.h | 30 +++++++++++
security/landlock/net.c | 12 +++++
security/landlock/ruleset.c | 21 +++++++-
security/landlock/ruleset.h | 6 +++
7 files changed, 172 insertions(+), 10 deletions(-)
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 8b3dd145b25e..3849382fad4a 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2562,6 +2562,7 @@ extern void __init vfs_caches_init(void);
#define __getname() kmalloc(PATH_MAX, GFP_KERNEL)
#define __putname(name) kfree(name)
+DEFINE_FREE(__putname, char *, if (_T) __putname(_T))
void emergency_thaw_all(void);
extern int sync_filesystem(struct super_block *);
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 5e847844fbf7..f1e96c447b97 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -13,6 +13,7 @@
#include <linux/tracepoint.h>
struct landlock_ruleset;
+struct path;
/**
* DOC: Landlock trace events
@@ -41,6 +42,10 @@ struct landlock_ruleset;
* information about all sandboxed processes on the system. See
* Documentation/admin-guide/LSM/landlock.rst for security considerations
* and privilege requirements.
+ *
+ * Network port fields use __u64 in host endianness, matching the
+ * landlock_net_port_attr.port UAPI convention. Callers convert from
+ * network byte order before emitting the event.
*/
/**
@@ -56,19 +61,20 @@ TRACE_EVENT(
TP_ARGS(ruleset),
- TP_STRUCT__entry(__field(__u64, ruleset_id) __field(access_mask_t,
- handled_fs)
+ TP_STRUCT__entry(__field(__u64, ruleset_id) __field(
+ __u32, ruleset_version) __field(access_mask_t, handled_fs)
__field(access_mask_t, handled_net)
__field(access_mask_t, scoped)),
TP_fast_assign(__entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
__entry->handled_fs = ruleset->layer.fs;
__entry->handled_net = ruleset->layer.net;
__entry->scoped = ruleset->layer.scope;),
- TP_printk("ruleset=%llx handled_fs=0x%x handled_net=0x%x scoped=0x%x",
- __entry->ruleset_id, __entry->handled_fs,
- __entry->handled_net, __entry->scoped));
+ TP_printk("ruleset=%llx.%u handled_fs=0x%x handled_net=0x%x scoped=0x%x",
+ __entry->ruleset_id, __entry->ruleset_version,
+ __entry->handled_fs, __entry->handled_net, __entry->scoped));
/**
* landlock_free_ruleset - Ruleset freed
@@ -82,12 +88,83 @@ TRACE_EVENT(landlock_free_ruleset,
TP_ARGS(ruleset),
- TP_STRUCT__entry(__field(__u64, ruleset_id)),
+ TP_STRUCT__entry(__field(__u64, ruleset_id)
+ __field(__u32, ruleset_version)),
+
+ TP_fast_assign(__entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;),
+
+ TP_printk("ruleset=%llx.%u", __entry->ruleset_id,
+ __entry->ruleset_version));
+
+/**
+ * landlock_add_rule_fs - filesystem rule added to a ruleset
+ * @ruleset: Source ruleset (never NULL)
+ * @access_rights: Allowed access mask for this rule
+ * @path: Filesystem path for the rule (never NULL)
+ * @pathname: Resolved absolute path string (never NULL; error placeholder
+ * on resolution failure)
+ */
+TRACE_EVENT(
+ landlock_add_rule_fs,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset,
+ access_mask_t access_rights, const struct path *path,
+ const char *pathname),
+
+ TP_ARGS(ruleset, access_rights, path, pathname),
+
+ TP_STRUCT__entry(__field(__u64, ruleset_id) __field(__u32,
+ ruleset_version)
+ __field(access_mask_t, access_rights)
+ __field(dev_t, dev) __field(ino_t, ino)
+ __string(pathname, pathname)),
+
+ TP_fast_assign(lockdep_assert_held(&ruleset->lock);
+ __entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
+ __entry->access_rights = access_rights;
+ __entry->dev = path->dentry->d_sb->s_dev;
+ /*
+ * The inode number may not be the user-visible one,
+ * but it will be the same used by audit.
+ */
+ __entry->ino = d_backing_inode(path->dentry)->i_ino;
+ __assign_str(pathname);),
+
+ TP_printk("ruleset=%llx.%u access_rights=0x%x dev=%u:%u ino=%lu path=%s",
+ __entry->ruleset_id, __entry->ruleset_version,
+ __entry->access_rights, MAJOR(__entry->dev),
+ MINOR(__entry->dev), __entry->ino,
+ __print_untrusted_str(pathname)));
+
+/**
+ * landlock_add_rule_net - network port rule added to a ruleset
+ * @ruleset: Source ruleset (never NULL)
+ * @port: Network port number in host endianness
+ * @access_rights: Allowed access mask for this rule
+ */
+TRACE_EVENT(landlock_add_rule_net,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset, __u64 port,
+ access_mask_t access_rights),
+
+ TP_ARGS(ruleset, port, access_rights),
- TP_fast_assign(__entry->ruleset_id = ruleset->id;),
+ TP_STRUCT__entry(__field(__u64, ruleset_id) __field(__u32,
+ ruleset_version)
+ __field(access_mask_t, access_rights)
+ __field(__u64, port)),
- TP_printk("ruleset=%llx", __entry->ruleset_id));
+ TP_fast_assign(lockdep_assert_held(&ruleset->lock);
+ __entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
+ __entry->access_rights = access_rights;
+ __entry->port = port;),
+ TP_printk("ruleset=%llx.%u access_rights=0x%x port=%llu",
+ __entry->ruleset_id, __entry->ruleset_version,
+ __entry->access_rights, __entry->port));
#endif /* _TRACE_LANDLOCK_H */
/* This part must be outside protection */
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index a0b4d0dd261f..f627ecc537a5 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -52,6 +52,8 @@
#include "ruleset.h"
#include "setup.h"
+#include <trace/events/landlock.h>
+
/* Underlying object management */
static void release_inode(struct landlock_object *const object)
@@ -345,7 +347,24 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
return PTR_ERR(id.key.object);
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights);
+
+ /*
+ * Emit after the rule insertion succeeds, so every event corresponds
+ * to a rule that is actually in the ruleset. The ruleset lock is
+ * still held for BTF consistency (enforced by lockdep_assert_held
+ * in TP_fast_assign).
+ */
+ if (!err && trace_landlock_add_rule_fs_enabled()) {
+ char *buffer __free(__putname) = __getname();
+ const char *pathname =
+ buffer ? resolve_path_for_trace(path, buffer) :
+ "<no_mem>";
+
+ trace_landlock_add_rule_fs(ruleset, access_rights, path,
+ pathname);
+ }
mutex_unlock(&ruleset->lock);
+
/*
* No need to check for an error because landlock_insert_rule()
* increments the refcount for the new object if needed.
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index bf9948941f2f..cc54133ae33d 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -11,6 +11,7 @@
#define _SECURITY_LANDLOCK_FS_H
#include <linux/build_bug.h>
+#include <linux/cleanup.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
@@ -128,4 +129,33 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
const struct path *const path,
access_mask_t access_hierarchy);
+/**
+ * resolve_path_for_trace - Resolve a path for tracepoint display
+ *
+ * @path: The path to resolve.
+ * @buf: A buffer of at least PATH_MAX bytes for the resolved path.
+ *
+ * Uses d_absolute_path() to produce a namespace-independent absolute path,
+ * unlike d_path() which resolves relative to the process's chroot. This
+ * ensures trace output is deterministic regardless of the tracer's mount
+ * namespace.
+ *
+ * Return: A pointer into @buf with the resolved path, or an error string
+ * ("<too_long>", "<unreachable>").
+ */
+static inline const char *resolve_path_for_trace(const struct path *path,
+ char *buf)
+{
+ const char *p;
+
+ p = d_absolute_path(path, buf, PATH_MAX);
+ if (!IS_ERR_OR_NULL(p))
+ return p;
+
+ if (PTR_ERR(p) == -ENAMETOOLONG)
+ return "<too_long>";
+
+ return "<unreachable>";
+}
+
#endif /* _SECURITY_LANDLOCK_FS_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 63f1fe0ec876..1e893123e787 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -20,6 +20,8 @@
#include "net.h"
#include "ruleset.h"
+#include <trace/events/landlock.h>
+
int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
const u16 port, access_mask_t access_rights)
{
@@ -36,6 +38,16 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights);
+
+ /*
+ * Emit after the rule insertion succeeds, so every event corresponds
+ * to a rule that is actually in the ruleset. The ruleset lock is
+ * still held for BTF consistency (enforced by lockdep_assert_held
+ * in TP_fast_assign).
+ */
+ if (!err)
+ trace_landlock_add_rule_net(ruleset, port, access_rights);
+
mutex_unlock(&ruleset->lock);
return err;
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 0d1e3dadb318..4bd997b58058 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare
*/
#include <linux/bits.h>
@@ -159,8 +160,16 @@ static void build_check_ruleset(void)
const struct landlock_rules rules = {
.num_rules = ~0,
};
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ const struct landlock_ruleset ruleset = {
+ .version = ~0,
+ };
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ BUILD_BUG_ON(ruleset.version < LANDLOCK_MAX_NUM_RULES);
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
/**
@@ -293,11 +302,19 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
/* When @level is zero, landlock_rule_insert() extends @ruleset. */
.level = 0,
} };
+ int err;
build_check_layer();
lockdep_assert_held(&ruleset->lock);
- return landlock_rule_insert(&ruleset->rules, id, &layers,
- ARRAY_SIZE(layers));
+ err = landlock_rule_insert(&ruleset->rules, id, &layers,
+ ARRAY_SIZE(layers));
+
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (!err)
+ ruleset->version++;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
+ return err;
}
void landlock_free_rules(struct landlock_rules *const rules)
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 0d60e7fb8ff2..aa489ca9d450 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -156,6 +156,12 @@ struct landlock_ruleset {
refcount_t usage;
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ /**
+ * @version: Monotonic counter incremented on each rule insertion. Used
+ * by tracepoints to correlate a domain with the exact ruleset state it
+ * was created from. Protected by @lock.
+ */
+ u32 version;
/**
* @id: Unique identifier for this ruleset, used for tracing.
*/
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 08/17] landlock: Add restrict_self and free_domain tracepoints
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (6 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 07/17] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 09/17] landlock: Add tracepoints for rule checking Mickaël Salaün
` (8 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add a tracepoint for sandbox enforcement, emitted from the
landlock_restrict_self() syscall handler after the new domain is
created. This logs both the source ruleset ID (with its version at the
time of the merge) and the new domain ID, enabling trace consumers to
correlate add_rule events (which use the ruleset ID) with check_rule
events (which use the domain ID).
The TP_PROTO takes only the ruleset and domain pointers. The ruleset
version and parent domain ID are computed in TP_fast_assign from these
pointers rather than passed as scalar arguments. This lets eBPF
programs access the full ruleset and domain state via BTF on just two
pointers. TP_fast_assign includes lockdep_assert_held(&ruleset->lock)
to enforce that the caller holds the ruleset lock during emission,
ensuring eBPF programs see a consistent ruleset->version via BTF.
Move the ruleset lock acquisition from landlock_merge_ruleset() to the
caller so the lock is held across the merge, TSYNC, and tracepoint
emission. The tracepoint fires only after all fallible operations
(including TSYNC) have succeeded, so every event corresponds to a domain
that is actually installed.
The flags-only restrict_self path (ruleset_fd == -1) does not create a
domain and does not emit this event. restrict_self flags that affect
logging (log_same_exec, log_new_exec) are accessible via BTF on
domain->hierarchy.
Add a landlock_free_domain tracepoint that fires when a domain's
hierarchy node is freed. The hierarchy node is the lifecycle boundary
because it represents the domain's identity and outlives the domain's
access masks, which may still be active in descendant domains.
Domain freeing is asynchronous: it happens in a workqueue because the
credential free path runs in RCU callback context where the teardown
chain's sleeping operations (iput, audit_log_start, put_pid) are
forbidden.
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:
- New patch.
---
include/trace/events/landlock.h | 69 +++++++++++++++++++++++++++++++++
security/landlock/domain.c | 20 +++++-----
security/landlock/log.c | 5 +++
security/landlock/syscalls.c | 23 ++++++++++-
4 files changed, 105 insertions(+), 12 deletions(-)
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index f1e96c447b97..533aea6152e1 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -12,6 +12,8 @@
#include <linux/tracepoint.h>
+struct landlock_domain;
+struct landlock_hierarchy;
struct landlock_ruleset;
struct path;
@@ -165,6 +167,73 @@ TRACE_EVENT(landlock_add_rule_net,
TP_printk("ruleset=%llx.%u access_rights=0x%x port=%llu",
__entry->ruleset_id, __entry->ruleset_version,
__entry->access_rights, __entry->port));
+
+/**
+ * landlock_restrict_self - new domain created from landlock_restrict_self()
+ * @ruleset: Source ruleset frozen into the domain (never NULL); caller
+ * holds ruleset->lock for BTF consistency. eBPF programs can
+ * read the full ruleset state via BTF (rules, version, access
+ * masks).
+ * @domain: Newly created domain (never NULL, immutable after creation).
+ * eBPF programs can navigate domain->hierarchy->parent for the
+ * parent domain chain.
+ *
+ * Emitted after the domain is successfully installed (including TSYNC
+ * if requested). The flags-only restrict_self path (ruleset_fd == -1)
+ * does not create a domain and does not emit this event. Restrict_self
+ * flags that affect logging (log_same_exec, log_new_exec) are accessible
+ * via BTF on domain->hierarchy.
+ */
+TRACE_EVENT(landlock_restrict_self,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset,
+ const struct landlock_domain *domain),
+
+ TP_ARGS(ruleset, domain),
+
+ TP_STRUCT__entry(__field(__u64, ruleset_id)
+ __field(__u32, ruleset_version)
+ __field(__u64, domain_id)
+ __field(__u64, parent_id)),
+
+ TP_fast_assign(
+ lockdep_assert_held(&ruleset->lock);
+ __entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
+ __entry->domain_id = domain->hierarchy->id;
+ __entry->parent_id = domain->hierarchy->parent ?
+ domain->hierarchy->parent->id :
+ 0;),
+
+ TP_printk("ruleset=%llx.%u domain=%llx parent=%llx",
+ __entry->ruleset_id, __entry->ruleset_version,
+ __entry->domain_id, __entry->parent_id));
+
+/**
+ * landlock_free_domain - domain freed
+ * @hierarchy: Hierarchy node being freed (never NULL); eBPF can read
+ * hierarchy->details (creator identity), hierarchy->parent
+ * (domain chain), and hierarchy->log_status via BTF
+ *
+ * Emitted when the domain's last reference is dropped, either
+ * asynchronously from a kworker (via landlock_put_domain_deferred) or
+ * synchronously from the calling task (via landlock_put_domain).
+ */
+TRACE_EVENT(landlock_free_domain,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy),
+
+ TP_ARGS(hierarchy),
+
+ TP_STRUCT__entry(__field(__u64, domain_id) __field(__u64, denials)),
+
+ TP_fast_assign(
+ __entry->domain_id = hierarchy->id;
+ __entry->denials = atomic64_read(&hierarchy->num_denials);),
+
+ TP_printk("domain=%llx denials=%llu", __entry->domain_id,
+ __entry->denials));
+
#endif /* _TRACE_LANDLOCK_H */
/* This part must be outside protection */
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 0dfd53ae9dd7..45ee7ec87957 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -294,31 +294,28 @@ static int merge_ruleset(struct landlock_domain *const dst,
if (WARN_ON_ONCE(!dst || !dst->hierarchy))
return -EINVAL;
- mutex_lock(&src->lock);
+ lockdep_assert_held(&src->lock);
/* Stacks the new layer. */
- if (WARN_ON_ONCE(dst->num_layers < 1)) {
- err = -EINVAL;
- goto out_unlock;
- }
+ if (WARN_ON_ONCE(dst->num_layers < 1))
+ return -EINVAL;
+
dst->layers[dst->num_layers - 1] =
landlock_upgrade_handled_access_masks(src->layer);
/* Merges the @src inode tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
if (err)
- goto out_unlock;
+ return err;
#if IS_ENABLED(CONFIG_INET)
/* Merges the @src network port tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
if (err)
- goto out_unlock;
+ return err;
#endif /* IS_ENABLED(CONFIG_INET) */
-out_unlock:
- mutex_unlock(&src->lock);
- return err;
+ return 0;
}
static int inherit_tree(struct landlock_domain *const parent,
@@ -399,6 +396,8 @@ static int inherit_ruleset(struct landlock_domain *const parent,
* The current task is requesting to be restricted. The subjective credentials
* must not be in an overridden state. cf. landlock_init_hierarchy_log().
*
+ * The caller must hold @ruleset->lock.
+ *
* Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on
* failure. If @parent is NULL, the new domain duplicates @ruleset.
*/
@@ -411,6 +410,7 @@ landlock_merge_ruleset(struct landlock_domain *const parent,
int err;
might_sleep();
+ lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!ruleset))
return ERR_PTR(-EINVAL);
diff --git a/security/landlock/log.c b/security/landlock/log.c
index ef79e4ed0037..ab4f982f8184 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -174,9 +174,12 @@ static void audit_denial(const struct landlock_cred_security *const subject,
#endif /* CONFIG_AUDIT */
+#include <trace/events/landlock.h>
+
#ifdef CONFIG_TRACEPOINTS
#define CREATE_TRACE_POINTS
#include <trace/events/landlock.h>
+#undef CREATE_TRACE_POINTS
#endif /* CONFIG_TRACEPOINTS */
static struct landlock_hierarchy *
@@ -473,6 +476,8 @@ void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
if (WARN_ON_ONCE(!hierarchy))
return;
+ trace_landlock_free_domain(hierarchy);
+
if (!audit_enabled)
return;
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index b18e83e457c2..93999749d80e 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -491,6 +491,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
flags)
{
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
+ struct landlock_domain *new_dom = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
@@ -558,10 +559,15 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
* There is no possible race condition while copying and
* manipulating the current credentials because they are
* dedicated per thread.
+ *
+ * Holds @ruleset->lock across the merge and tracepoint
+ * emission so that the tracepoint reads the exact
+ * ruleset version frozen into the new domain.
*/
- struct landlock_domain *const new_dom =
- landlock_merge_ruleset(new_llcred->domain, ruleset);
+ mutex_lock(&ruleset->lock);
+ new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
if (IS_ERR(new_dom)) {
+ mutex_unlock(&ruleset->lock);
abort_creds(new_cred);
return PTR_ERR(new_dom);
}
@@ -586,10 +592,23 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
const int err = landlock_restrict_sibling_threads(
current_cred(), new_cred);
if (err) {
+ if (ruleset)
+ mutex_unlock(&ruleset->lock);
abort_creds(new_cred);
return err;
}
}
+ /*
+ * Emit after all fallible operations (including TSYNC) have
+ * succeeded, so every event corresponds to an installed domain.
+ * The ruleset lock is still held for BTF consistency (enforced
+ * by lockdep_assert_held in TP_fast_assign).
+ */
+ if (new_dom)
+ trace_landlock_restrict_self(ruleset, new_dom);
+
+ if (ruleset)
+ mutex_unlock(&ruleset->lock);
return commit_creds(new_cred);
}
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 09/17] landlock: Add tracepoints for rule checking
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (7 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 08/17] landlock: Add restrict_self and free_domain tracepoints Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 10/17] landlock: Set audit_net.sk for socket access checks Mickaël Salaün
` (7 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
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
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 10/17] landlock: Set audit_net.sk for socket access checks
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (8 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 09/17] landlock: Add tracepoints for rule checking Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 11/17] landlock: Add landlock_deny_access_fs and landlock_deny_access_net Mickaël Salaün
` (6 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel, stable
Set audit_net.sk in current_check_access_socket() to provide the socket
object to audit_log_lsm_data(). This makes Landlock consistent with
AppArmor, which always sets .sk for socket operations, and with
SELinux's generic socket permission checks.
The socket's local and foreign address information (laddr, lport, faddr,
fport) is logged by the shared lsm_audit.c infrastructure when the
socket has bound or connected state. Fields with zero values are
suppressed by print_ipv4_addr()/print_ipv6_addr(), so the audit output
is unchanged for the common case of bind denials on unbound sockets.
For connect denials after a prior bind, the bound local address (laddr,
lport) appears before the existing sockaddr fields (daddr, dest).
No existing fields are removed or reordered, and the new field names
(laddr, lport, faddr, fport) are standard audit fields already emitted
by other LSMs through the same lsm_audit.c code path.
Add net_bind and net_connect audit tests. The net_bind test verifies
basic net denial auditing. The net_connect test binds to an allowed
port, then connects to a denied port, and verifies that the audit record
includes laddr/lport from the socket state.
Fixes: 9f74411a40ce ("landlock: Log TCP bind and connect denials")
Cc: stable@vger.kernel.org
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
security/landlock/net.c | 1 +
tools/testing/selftests/landlock/audit_test.c | 187 ++++++++++++++++++
2 files changed, 188 insertions(+)
diff --git a/security/landlock/net.c b/security/landlock/net.c
index a2aefc7967a1..d8bc9e0d012a 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -225,6 +225,7 @@ static int current_check_access_socket(struct socket *const sock,
return 0;
audit_net.family = address->sa_family;
+ audit_net.sk = sock->sk;
landlock_log_denial(subject,
&(struct landlock_request){
.type = LANDLOCK_REQUEST_NET_ACCESS,
diff --git a/tools/testing/selftests/landlock/audit_test.c b/tools/testing/selftests/landlock/audit_test.c
index da0bfd06391e..65dfb272c825 100644
--- a/tools/testing/selftests/landlock/audit_test.c
+++ b/tools/testing/selftests/landlock/audit_test.c
@@ -6,14 +6,17 @@
*/
#define _GNU_SOURCE
+#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <linux/landlock.h>
+#include <netinet/in.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/prctl.h>
+#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -160,6 +163,190 @@ TEST_F(audit, layers)
EXPECT_EQ(0, close(ruleset_fd));
}
+static int matches_log_net_bind(struct __test_metadata *const _metadata,
+ int audit_fd, __u16 port, __u64 *domain_id)
+{
+ /*
+ * The socket is unbound at bind() time, so laddr/lport/faddr/fport from
+ * the socket object are zero and not printed. Only the sockaddr fields
+ * (src) appear.
+ */
+ static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ " blockers=net\\.bind_tcp src=%u$";
+ char log_match[sizeof(log_template) + 10];
+
+ snprintf(log_match, sizeof(log_match), log_template, port);
+ return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
+ domain_id);
+}
+
+/*
+ * Verifies that network denial audit records include enriched socket
+ * information (laddr/lport/faddr/fport) from the socket object.
+ */
+TEST_F(audit, net_bind)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ struct landlock_net_port_attr net_port = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .port = 1024,
+ };
+ int status, ruleset_fd;
+ pid_t child;
+ __u64 denial_dom = 1;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Allow port 1024 only. */
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &net_port, 0));
+
+ EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(1025),
+ .sin_addr.s_addr = htonl(INADDR_ANY),
+ };
+ int sock_fd;
+
+ EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
+ close(ruleset_fd);
+
+ /* Bind to port 1025 (not allowed). */
+ sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ ASSERT_LE(0, sock_fd);
+ EXPECT_EQ(-1, bind(sock_fd, (struct sockaddr *)&addr,
+ sizeof(addr)));
+ EXPECT_EQ(EACCES, errno);
+ close(sock_fd);
+
+ /* Verify audit record with enriched socket info. */
+ EXPECT_EQ(0, matches_log_net_bind(_metadata, self->audit_fd,
+ 1025, &denial_dom));
+ EXPECT_NE(denial_dom, 1);
+ EXPECT_NE(denial_dom, 0);
+
+ _exit(_metadata->exit_code);
+ return;
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ if (WIFSIGNALED(status) || !WIFEXITED(status) ||
+ WEXITSTATUS(status) != EXIT_SUCCESS)
+ _metadata->exit_code = KSFT_FAIL;
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
+static int matches_log_net_connect(struct __test_metadata *const _metadata,
+ int audit_fd, __u16 denied_port,
+ __u16 bound_port, __u64 *domain_id)
+{
+ /*
+ * After bind(), the socket has local address state. The audit record
+ * should include laddr/lport from the socket (via audit_net.sk) and
+ * daddr/dest from the connect sockaddr.
+ */
+ static const char log_template[] = REGEX_LANDLOCK_PREFIX
+ " blockers=net\\.connect_tcp"
+ " laddr=127\\.0\\.0\\.1 lport=%u"
+ " daddr=127\\.0\\.0\\.1 dest=%u$";
+ char log_match[sizeof(log_template) + 20];
+
+ snprintf(log_match, sizeof(log_match), log_template, bound_port,
+ denied_port);
+ return audit_match_record(audit_fd, AUDIT_LANDLOCK_ACCESS, log_match,
+ domain_id);
+}
+
+/*
+ * Verifies that network denial audit records for connect include enriched
+ * socket information (laddr/lport) from the socket object after a prior bind.
+ * This complements net_bind which tests the unbound case.
+ */
+TEST_F(audit, net_connect)
+{
+ const struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ };
+ struct landlock_net_port_attr net_port;
+ int status, ruleset_fd;
+ pid_t child;
+ __u64 denial_dom = 1;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Allow bind to port 1024 and connect to port 1024. */
+ net_port.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP;
+ net_port.port = 1024;
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &net_port, 0));
+
+ EXPECT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ struct sockaddr_in bind_addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(1024),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ struct sockaddr_in conn_addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(1025),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ int sock_fd, optval = 1;
+
+ EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0));
+ close(ruleset_fd);
+
+ sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ ASSERT_LE(0, sock_fd);
+ ASSERT_EQ(0, setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR,
+ &optval, sizeof(optval)));
+
+ /* Bind to allowed port 1024 (succeeds). */
+ ASSERT_EQ(0, bind(sock_fd, (struct sockaddr *)&bind_addr,
+ sizeof(bind_addr)));
+
+ /* Connect to denied port 1025 (fails). */
+ EXPECT_EQ(-1, connect(sock_fd, (struct sockaddr *)&conn_addr,
+ sizeof(conn_addr)));
+ EXPECT_EQ(EACCES, errno);
+ close(sock_fd);
+
+ /* Verify audit record with laddr/lport from bound socket. */
+ EXPECT_EQ(0, matches_log_net_connect(_metadata, self->audit_fd,
+ 1025, 1024, &denial_dom));
+ EXPECT_NE(denial_dom, 1);
+ EXPECT_NE(denial_dom, 0);
+
+ _exit(_metadata->exit_code);
+ return;
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ if (WIFSIGNALED(status) || !WIFEXITED(status) ||
+ WEXITSTATUS(status) != EXIT_SUCCESS)
+ _metadata->exit_code = KSFT_FAIL;
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
struct thread_data {
pid_t parent_pid;
int ruleset_fd, pipe_child, pipe_parent;
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 11/17] landlock: Add landlock_deny_access_fs and landlock_deny_access_net
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (9 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 10/17] landlock: Set audit_net.sk for socket access checks Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials Mickaël Salaün
` (5 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add per-type tracepoints emitted from landlock_log_denial() when an
access is denied: landlock_deny_access_fs for filesystem denials and
landlock_deny_access_net for network denials.
The events use the "deny_" prefix (rather than "check_") to make clear
that they fire only on denial, not on every access check.
These complement the check_rule tracepoints by showing the final denial
verdict, including the denial-by-absence case (when no rule matches
along the pathwalk, no check_rule events fire, but the deny_access event
makes the denial explicit).
Trace events fire unconditionally, independent of audit configuration
and user-specified log flags (LANDLOCK_LOG_DISABLED). The user's
"disable logging" intent applies to audit records, not to kernel
tracing. The LANDLOCK_LOG_DISABLED check is moved into the
audit-specific path; num_denials and trace emission execute regardless.
The deny_access events pass the denying hierarchy node (const struct
landlock_hierarchy *hierarchy) in TP_PROTO, not the task's current
domain. The domain_id entry field shows the ID of the specific
hierarchy node that blocked the access, matching audit record semantics.
This differs from check_rule events which pass the task's current domain
(needed for the dynamic per-layer array sizing).
The same_exec field is passed in TP_PROTO because it is computed from
the credential bitmask, not derivable from the hierarchy pointer alone.
The events include same_exec, log_same_exec, and log_new_exec fields for
stateless ftrace filtering that replicates audit's suppression logic.
The denial field is named "blockers" (matching the audit record field)
rather than "blocked", to enable consistent field-name correlation
between audit and trace output.
Network denial sport and dport fields use __u64 host-endian, matching
the landlock_net_port_attr.port UAPI convention. The caller converts
from the lsm_network_audit __be16 fields via ntohs() before emitting
the event.
The filesystem path is resolved via d_absolute_path() (the same helper
used by landlock_add_rule_fs), producing namespace-independent absolute
paths. Audit uses d_path() which resolves relative to the process's
chroot; the difference is documented but acceptable for tracepoints
which are designed for deterministic output regardless of the tracer's
namespace state. Device numbers use numeric major:minor format (unlike
audit's string s_id) for machine parseability.
For FS_CHANGE_TOPOLOGY hooks that provide only a dentry, the path is
resolved via dentry_path_raw() instead of d_absolute_path().
The denial tracepoint allocates PATH_MAX bytes from the heap via
__getname() for path resolution. This cost is only paid when a tracer
is attached.
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:
- New patch.
---
include/trace/events/landlock.h | 100 +++++++++++++++++++++
security/landlock/log.c | 149 ++++++++++++++++++++++++++------
security/landlock/log.h | 9 +-
3 files changed, 227 insertions(+), 31 deletions(-)
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index e7bb8fa802bf..1afab091efba 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -18,6 +18,7 @@ struct landlock_hierarchy;
struct landlock_rule;
struct landlock_ruleset;
struct path;
+struct sock;
/**
* DOC: Landlock trace events
@@ -50,6 +51,15 @@ struct path;
* Network port fields use __u64 in host endianness, matching the
* landlock_net_port_attr.port UAPI convention. Callers convert from
* network byte order before emitting the event.
+ *
+ * Field ordering convention for denial events: domain ID, same_exec,
+ * log_same_exec, log_new_exec, then blockers (deny_access events only),
+ * then type-specific object identification fields, then variable-length
+ * fields.
+ *
+ * The deny_access denial events include same_exec and log_same_exec /
+ * log_new_exec fields so that both stateless (ftrace filter) and stateful
+ * (eBPF) consumers can replicate the audit subsystem's filtering logic.
*/
/**
@@ -333,6 +343,96 @@ TRACE_EVENT(landlock_check_rule_net,
__entry->port,
__print_dynamic_array(layers, sizeof(access_mask_t))));
+/**
+ * landlock_deny_access_fs - filesystem access denied
+ * @hierarchy: Hierarchy node that blocked the access (never NULL).
+ * Identifies the specific domain in the hierarchy whose
+ * rules caused the denial. eBPF can read hierarchy->id,
+ * hierarchy->log_same_exec, hierarchy->log_new_exec, and
+ * walk hierarchy->parent for the domain chain.
+ * @same_exec: Whether the current task is the same executable that
+ * called landlock_restrict_self() for the denying hierarchy
+ * node. Computed from the credential bitmask, not derivable
+ * from the hierarchy alone.
+ * @blockers: Access mask that was blocked
+ * @path: Filesystem path that was denied (never NULL)
+ * @pathname: Resolved absolute path string (never NULL)
+ */
+TRACE_EVENT(
+ landlock_deny_access_fs,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ access_mask_t blockers, const struct path *path,
+ const char *pathname),
+
+ TP_ARGS(hierarchy, same_exec, blockers, path, pathname),
+
+ TP_STRUCT__entry(
+ __field(__u64, domain_id) __field(bool, same_exec)
+ __field(u32, log_same_exec) __field(u32, log_new_exec)
+ __field(access_mask_t, blockers)
+ __field(dev_t, dev) __field(ino_t, ino)
+ __string(pathname, pathname)),
+
+ TP_fast_assign(__entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->log_same_exec = hierarchy->log_same_exec;
+ __entry->log_new_exec = hierarchy->log_new_exec;
+ __entry->blockers = blockers;
+ __entry->dev = path->dentry->d_sb->s_dev;
+ __entry->ino = d_backing_inode(path->dentry)->i_ino;
+ __assign_str(pathname);),
+
+ TP_printk(
+ "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u blockers=0x%x dev=%u:%u ino=%lu path=%s",
+ __entry->domain_id, __entry->same_exec, __entry->log_same_exec,
+ __entry->log_new_exec, __entry->blockers, MAJOR(__entry->dev),
+ MINOR(__entry->dev), __entry->ino,
+ __print_untrusted_str(pathname)));
+
+/**
+ * landlock_deny_access_net - network access denied
+ * @hierarchy: Hierarchy node that blocked the access (never NULL)
+ * @same_exec: Whether the current task is the same executable that
+ * called landlock_restrict_self() for the denying hierarchy
+ * node
+ * @blockers: Access mask that was blocked
+ * @sk: Socket object (never NULL); eBPF can read socket family, state,
+ * local/remote addresses, and options via BTF
+ * @sport: Source port in host endianness (non-zero for bind denials,
+ * zero for connect denials)
+ * @dport: Destination port in host endianness (non-zero for connect
+ * denials, zero for bind denials)
+ */
+TRACE_EVENT(
+ landlock_deny_access_net,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ access_mask_t blockers, const struct sock *sk, __u64 sport,
+ __u64 dport),
+
+ TP_ARGS(hierarchy, same_exec, blockers, sk, sport, dport),
+
+ TP_STRUCT__entry(
+ __field(__u64, domain_id) __field(bool, same_exec)
+ __field(u32, log_same_exec) __field(u32, log_new_exec)
+ __field(access_mask_t, blockers)
+ __field(__u64, sport)
+ __field(__u64, dport)),
+
+ TP_fast_assign(__entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->log_same_exec = hierarchy->log_same_exec;
+ __entry->log_new_exec = hierarchy->log_new_exec;
+ __entry->blockers = blockers; __entry->sport = sport;
+ __entry->dport = dport;),
+
+ TP_printk(
+ "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u blockers=0x%x sport=%llu dport=%llu",
+ __entry->domain_id, __entry->same_exec, __entry->log_same_exec,
+ __entry->log_new_exec, __entry->blockers, __entry->sport,
+ __entry->dport));
+
#endif /* _TRACE_LANDLOCK_H */
/* This part must be outside protection */
diff --git a/security/landlock/log.c b/security/landlock/log.c
index ab4f982f8184..c81cb7c1c448 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -3,6 +3,7 @@
* Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare
*/
#include <kunit/test.h>
@@ -143,6 +144,9 @@ static void audit_denial(const struct landlock_cred_security *const subject,
{
struct audit_buffer *ab;
+ if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
+ return;
+
if (!audit_enabled)
return;
@@ -172,6 +176,16 @@ static void audit_denial(const struct landlock_cred_security *const subject,
log_domain(youngest_denied);
}
+#else /* CONFIG_AUDIT */
+
+static inline void
+audit_denial(const struct landlock_cred_security *const subject,
+ const struct landlock_request *const request,
+ struct landlock_hierarchy *const youngest_denied,
+ const size_t youngest_layer, const access_mask_t missing)
+{
+}
+
#endif /* CONFIG_AUDIT */
#include <trace/events/landlock.h>
@@ -180,6 +194,86 @@ static void audit_denial(const struct landlock_cred_security *const subject,
#define CREATE_TRACE_POINTS
#include <trace/events/landlock.h>
#undef CREATE_TRACE_POINTS
+
+#include "fs.h"
+
+static void trace_denial(const struct landlock_cred_security *const subject,
+ const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const size_t youngest_layer,
+ const access_mask_t missing)
+{
+ const bool same_exec = !!(subject->domain_exec & BIT(youngest_layer));
+
+ switch (request->type) {
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
+ if (trace_landlock_deny_access_fs_enabled()) {
+ char *buf __free(__putname) = __getname();
+ const char *pathname;
+ const struct path *path;
+
+ /*
+ * FS_CHANGE_TOPOLOGY uses either LSM_AUDIT_DATA_PATH or
+ * LSM_AUDIT_DATA_DENTRY depending on the hook. For the
+ * dentry case, build a path on the stack with the real
+ * dentry so TP_fast_assign can extract dev and ino.
+ * The mnt field is unused by TP_fast_assign.
+ */
+ if (request->audit.type == LSM_AUDIT_DATA_DENTRY) {
+ struct path dentry_path = {
+ .dentry = request->audit.u.dentry,
+ };
+
+ path = &dentry_path;
+ pathname =
+ buf ? dentry_path_raw(
+ request->audit.u.dentry,
+ buf, PATH_MAX) :
+ "<no_mem>";
+ if (IS_ERR(pathname))
+ pathname = "<unreachable>";
+
+ trace_landlock_deny_access_fs(youngest_denied,
+ same_exec,
+ missing, path,
+ pathname);
+ } else {
+ path = &request->audit.u.path;
+ pathname = buf ? resolve_path_for_trace(path,
+ buf) :
+ "<no_mem>";
+
+ trace_landlock_deny_access_fs(youngest_denied,
+ same_exec,
+ missing, path,
+ pathname);
+ }
+ }
+ break;
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ if (trace_landlock_deny_access_net_enabled())
+ trace_landlock_deny_access_net(
+ youngest_denied, same_exec, missing,
+ request->audit.u.net->sk,
+ ntohs(request->audit.u.net->sport),
+ ntohs(request->audit.u.net->dport));
+ break;
+ default:
+ break;
+ }
+}
+
+#else /* CONFIG_TRACEPOINTS */
+
+static inline void
+trace_denial(const struct landlock_cred_security *const subject,
+ const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const size_t youngest_layer, const access_mask_t missing)
+{
+}
+
#endif /* CONFIG_TRACEPOINTS */
static struct landlock_hierarchy *
@@ -439,9 +533,6 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
get_hierarchy(subject->domain, youngest_layer);
}
- if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
- return;
-
/*
* Consistently keeps track of the number of denied access requests
* even if audit is currently disabled, or if audit rules currently
@@ -450,45 +541,25 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
*/
atomic64_inc(&youngest_denied->num_denials);
-#ifdef CONFIG_AUDIT
+ trace_denial(subject, request, youngest_denied, youngest_layer,
+ missing);
audit_denial(subject, request, youngest_denied, youngest_layer,
missing);
-#endif /* CONFIG_AUDIT */
}
#ifdef CONFIG_AUDIT
-/**
- * landlock_log_free_domain - Create an audit record on domain deallocation
- *
- * @hierarchy: The domain's hierarchy being deallocated.
- *
- * Only domains which previously appeared in the audit logs are logged again.
- * This is useful to know when a domain will never show again in the audit log.
- *
- * Called in a work queue scheduled by landlock_put_domain_deferred() called by
- * hook_cred_free().
- */
-void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
+static void audit_drop_domain(const struct landlock_hierarchy *const hierarchy)
{
struct audit_buffer *ab;
- if (WARN_ON_ONCE(!hierarchy))
- return;
-
- trace_landlock_free_domain(hierarchy);
-
if (!audit_enabled)
return;
- /* Ignores domains that were not logged. */
+ /* Ignores domains that were not logged. */
if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_RECORDED)
return;
- /*
- * If logging of domain allocation succeeded, warns about failure to log
- * domain deallocation to highlight unbalanced domain lifetime logs.
- */
ab = audit_log_start(audit_context(), GFP_KERNEL,
AUDIT_LANDLOCK_DOMAIN);
if (!ab)
@@ -499,8 +570,32 @@ void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
audit_log_end(ab);
}
+#else /* CONFIG_AUDIT */
+
+static inline void
+audit_drop_domain(const struct landlock_hierarchy *const hierarchy)
+{
+}
+
#endif /* CONFIG_AUDIT */
+/**
+ * landlock_log_free_domain - Log domain deallocation
+ *
+ * @hierarchy: The domain's hierarchy being deallocated.
+ *
+ * Called from landlock_put_domain_deferred() (via a work queue scheduled by
+ * hook_cred_free()) or directly from landlock_put_domain().
+ */
+void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+ if (WARN_ON_ONCE(!hierarchy))
+ return;
+
+ trace_landlock_free_domain(hierarchy);
+ audit_drop_domain(hierarchy);
+}
+
#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
static struct kunit_case test_cases[] = {
diff --git a/security/landlock/log.h b/security/landlock/log.h
index 4370fff86e45..5615a776c29a 100644
--- a/security/landlock/log.h
+++ b/security/landlock/log.h
@@ -3,6 +3,7 @@
* Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare
*/
#ifndef _SECURITY_LANDLOCK_LOG_H
@@ -28,7 +29,7 @@ enum landlock_request_type {
/*
* We should be careful to only use a variable of this type for
* landlock_log_denial(). This way, the compiler can remove it entirely if
- * CONFIG_AUDIT is not set.
+ * CONFIG_SECURITY_LANDLOCK_LOG is not set.
*/
struct landlock_request {
/* Mandatory fields. */
@@ -52,14 +53,14 @@ struct landlock_request {
deny_masks_t deny_masks;
};
-#ifdef CONFIG_AUDIT
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy);
void landlock_log_denial(const struct landlock_cred_security *const subject,
const struct landlock_request *const request);
-#else /* CONFIG_AUDIT */
+#else /* CONFIG_SECURITY_LANDLOCK_LOG */
static inline void
landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
@@ -72,6 +73,6 @@ landlock_log_denial(const struct landlock_cred_security *const subject,
{
}
-#endif /* CONFIG_AUDIT */
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
#endif /* _SECURITY_LANDLOCK_LOG_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (10 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 11/17] landlock: Add landlock_deny_access_fs and landlock_deny_access_net Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 15:01 ` Steven Rostedt
2026-04-06 14:37 ` [PATCH v2 13/17] selftests/landlock: Add trace event test infrastructure and tests Mickaël Salaün
` (4 subsequent siblings)
16 siblings, 1 reply; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Scope and ptrace denials follow a different code path (domain hierarchy
check) than access-right denials, requiring dedicated tracepoints with
type-specific TP_PROTO arguments.
Complete the tracepoint coverage for all Landlock denial types by adding
tracepoints for ptrace and scope-based denials:
- landlock_deny_ptrace: emitted when ptrace access is denied due to
domain hierarchy mismatch.
- landlock_deny_scope_signal: emitted when signal delivery is denied by
LANDLOCK_SCOPE_SIGNAL.
- landlock_deny_scope_abstract_unix_socket: emitted when abstract unix
socket access is denied by LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET.
TP_PROTO passes the raw kernel object (struct task_struct or struct
sock) for eBPF BTF access. String fields (comm, sun_path) use
__print_untrusted_str() because they contain untrusted input.
Unlike deny_access_fs and deny_access_net which include a blockers field
showing which specific access rights were denied, these events omit
blockers because each event corresponds to exactly one denial type
identified by the event name itself (e.g., landlock_deny_ptrace can only
mean a ptrace denial). A blockers field is always zero since
scope and ptrace denials do not use access-right bitmasks.
Audit records use generic field names (opid, ocomm) for the target
process, while tracepoints use role-specific names (tracee_pid,
target_pid, peer_pid). The tracepoint naming is more descriptive
because trace events are strongly typed and tied to the semantics of each
event, while the audit log format is generic.
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:
- New patch.
---
include/trace/events/landlock.h | 135 ++++++++++++++++++++++++++++++++
security/landlock/log.c | 20 +++++
2 files changed, 155 insertions(+)
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 1afab091efba..9f96c9897f44 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -11,6 +11,7 @@
#define _TRACE_LANDLOCK_H
#include <linux/tracepoint.h>
+#include <net/af_unix.h>
struct dentry;
struct landlock_domain;
@@ -19,6 +20,7 @@ struct landlock_rule;
struct landlock_ruleset;
struct path;
struct sock;
+struct task_struct;
/**
* DOC: Landlock trace events
@@ -433,6 +435,139 @@ TRACE_EVENT(
__entry->log_new_exec, __entry->blockers, __entry->sport,
__entry->dport));
+/**
+ * landlock_deny_ptrace - ptrace access denied
+ * @hierarchy: Hierarchy node that blocked the access (never NULL)
+ * @same_exec: Whether the current task is the same executable that called
+ * landlock_restrict_self() for the denying hierarchy node
+ * @tracee: Target task (never NULL); eBPF can read pid, comm, cred,
+ * namespaces, and cgroup via BTF
+ */
+TRACE_EVENT(
+ landlock_deny_ptrace,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ const struct task_struct *tracee),
+
+ TP_ARGS(hierarchy, same_exec, tracee),
+
+ TP_STRUCT__entry(
+ __field(__u64, domain_id) __field(bool, same_exec)
+ __field(u32, log_same_exec) __field(u32, log_new_exec)
+ __field(pid_t, tracee_pid)
+ __string(tracee_comm, tracee->comm)),
+
+ TP_fast_assign(__entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->log_same_exec = hierarchy->log_same_exec;
+ __entry->log_new_exec = hierarchy->log_new_exec;
+ __entry->tracee_pid =
+ task_tgid_nr((struct task_struct *)tracee);
+ __assign_str(tracee_comm);),
+
+ TP_printk(
+ "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u tracee_pid=%d comm=%s",
+ __entry->domain_id, __entry->same_exec, __entry->log_same_exec,
+ __entry->log_new_exec, __entry->tracee_pid,
+ __print_untrusted_str(tracee_comm)));
+
+/**
+ * landlock_deny_scope_signal - signal delivery denied by
+ * LANDLOCK_SCOPE_SIGNAL
+ * @hierarchy: Hierarchy node that blocked the access (never NULL)
+ * @same_exec: Whether the current task is the same executable that called
+ * landlock_restrict_self() for the denying hierarchy node
+ * @target: Signal target task (never NULL); eBPF can read pid, comm, cred,
+ * namespaces, and cgroup via BTF
+ */
+TRACE_EVENT(
+ landlock_deny_scope_signal,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ const struct task_struct *target),
+
+ TP_ARGS(hierarchy, same_exec, target),
+
+ TP_STRUCT__entry(
+ __field(__u64, domain_id) __field(bool, same_exec)
+ __field(u32, log_same_exec) __field(u32, log_new_exec)
+ __field(pid_t, target_pid)
+ __string(target_comm, target->comm)),
+
+ TP_fast_assign(__entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->log_same_exec = hierarchy->log_same_exec;
+ __entry->log_new_exec = hierarchy->log_new_exec;
+ __entry->target_pid =
+ task_tgid_nr((struct task_struct *)target);
+ __assign_str(target_comm);),
+
+ TP_printk(
+ "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u target_pid=%d comm=%s",
+ __entry->domain_id, __entry->same_exec, __entry->log_same_exec,
+ __entry->log_new_exec, __entry->target_pid,
+ __print_untrusted_str(target_comm)));
+
+/**
+ * landlock_deny_scope_abstract_unix_socket - abstract unix socket access
+ * denied by LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
+ * @hierarchy: Hierarchy node that blocked the access (never NULL)
+ * @same_exec: Whether the current task is the same executable that called
+ * landlock_restrict_self() for the denying hierarchy node
+ * @peer: Peer socket (never NULL); eBPF can read sk_peer_pid,
+ * sk_peer_cred, socket type, and protocol via BTF
+ */
+TRACE_EVENT(
+ landlock_deny_scope_abstract_unix_socket,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ const struct sock *peer),
+
+ TP_ARGS(hierarchy, same_exec, peer),
+
+ TP_STRUCT__entry(
+ __field(__u64, domain_id) __field(bool, same_exec)
+ __field(u32, log_same_exec) __field(u32, log_new_exec)
+ __field(pid_t, peer_pid)
+ /*
+ * Abstract socket names are untrusted binary data from
+ * user space. Use __string_len because abstract names
+ * are not NUL-terminated; their length is determined by
+ * addr->len.
+ */
+ __string_len(sun_path,
+ unix_sk(peer)->addr ?
+ unix_sk(peer)->addr->name->sun_path + 1 :
+ "",
+ unix_sk(peer)->addr ?
+ unix_sk(peer)->addr->len -
+ offsetof(struct sockaddr_un,
+ sun_path) -
+ 1 :
+ 0)),
+
+ TP_fast_assign(struct pid *peer_pid;
+
+ __entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->log_same_exec = hierarchy->log_same_exec;
+ __entry->log_new_exec = hierarchy->log_new_exec;
+ /*
+ * READ_ONCE prevents compiler double-read. The value
+ * is stable because unix_state_lock(peer) is held by
+ * the caller (hook_unix_stream_connect or
+ * hook_unix_may_send).
+ */
+ peer_pid = READ_ONCE(peer->sk_peer_pid);
+ __entry->peer_pid = peer_pid ? pid_nr(peer_pid) : 0;
+ __assign_str(sun_path);),
+
+ TP_printk(
+ "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u peer_pid=%d sun_path=%s",
+ __entry->domain_id, __entry->same_exec, __entry->log_same_exec,
+ __entry->log_new_exec, __entry->peer_pid,
+ __print_untrusted_str(sun_path)));
+
#endif /* _TRACE_LANDLOCK_H */
/* This part must be outside protection */
diff --git a/security/landlock/log.c b/security/landlock/log.c
index c81cb7c1c448..a2f61aed81ff 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -11,6 +11,9 @@
#include <linux/bitops.h>
#include <linux/lsm_audit.h>
#include <linux/pid.h>
+#include <linux/sched.h>
+#include <linux/slab.h>
+#include <net/sock.h>
#include <uapi/linux/landlock.h>
#include "access.h"
@@ -259,6 +262,23 @@ static void trace_denial(const struct landlock_cred_security *const subject,
ntohs(request->audit.u.net->sport),
ntohs(request->audit.u.net->dport));
break;
+ case LANDLOCK_REQUEST_PTRACE:
+ if (trace_landlock_deny_ptrace_enabled())
+ trace_landlock_deny_ptrace(youngest_denied, same_exec,
+ request->audit.u.tsk);
+ break;
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ if (trace_landlock_deny_scope_signal_enabled())
+ trace_landlock_deny_scope_signal(youngest_denied,
+ same_exec,
+ request->audit.u.tsk);
+ break;
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ if (trace_landlock_deny_scope_abstract_unix_socket_enabled())
+ trace_landlock_deny_scope_abstract_unix_socket(
+ youngest_denied, same_exec,
+ request->audit.u.net->sk);
+ break;
default:
break;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 13/17] selftests/landlock: Add trace event test infrastructure and tests
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (11 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 14/17] selftests/landlock: Add filesystem tracepoint tests Mickaël Salaün
` (3 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add tracefs test infrastructure in trace.h: helpers for mounting
tracefs, enabling/disabling events, reading the trace buffer, counting
regex matches, and extracting field values. Add per-event regex
patterns for matching trace lines.
The TRACE_PREFIX macro matches the ftrace trace-file line format with
either the expected task name (truncated to TASK_COMM_LEN - 1) or
"<...>" (for evicted comm cache entries). All regex patterns are
anchored with ^ and $, verify every TP_printk field, and use no
unescaped dot characters.
Extend the existing true helper to open its working directory before
exiting, which triggers a read_dir denial when executed inside a
sandbox. The exec-based tests use this to verify same_exec=0 and log
flag behavior after exec.
Add trace_test.c with the trace fixture (setup enables all available
events with a PID filter, teardown disables and clears) and lifecycle
and API tests: no_trace_when_disabled, create_ruleset, ruleset_version,
restrict_self, restrict_self_nested, restrict_self_invalid,
add_rule_invalid_fd, add_rule_net_fields, free_domain,
free_ruleset_on_close.
Add denial field and log flag tests: deny_access_fs_fields,
same_exec_before_exec, same_exec_after_exec, log_flags_same_exec_off,
log_flags_new_exec_on, log_flags_subdomains_off,
non_audit_visible_denial_counting.
Move regex_escape() from audit.h to common.h for shared use by both
audit and trace tests.
Enable CONFIG_FTRACE_SYSCALLS alongside CONFIG_FTRACE in the selftest
config because CONFIG_FTRACE alone only enables the tracer menu without
activating any tracer. CONFIG_FTRACE_SYSCALLS is the lightest tracer
option that selects GENERIC_TRACER, TRACING, and TRACEPOINTS, which are
required for tracefs and Landlock trace events. Both UML and x86_64
provide the required HAVE_SYSCALL_TRACEPOINTS. When CONFIG_FTRACE is
disabled, CONFIG_FTRACE_SYSCALLS is gated by the FTRACE menu and cannot
be set, so TRACEPOINTS is correctly disabled.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
tools/testing/selftests/landlock/audit.h | 35 +-
tools/testing/selftests/landlock/common.h | 47 +
tools/testing/selftests/landlock/config | 2 +
tools/testing/selftests/landlock/trace.h | 640 +++++++++
tools/testing/selftests/landlock/trace_test.c | 1168 +++++++++++++++++
tools/testing/selftests/landlock/true.c | 10 +
6 files changed, 1868 insertions(+), 34 deletions(-)
create mode 100644 tools/testing/selftests/landlock/trace.h
create mode 100644 tools/testing/selftests/landlock/trace_test.c
diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h
index 834005b2b0f0..84bb8f34bc83 100644
--- a/tools/testing/selftests/landlock/audit.h
+++ b/tools/testing/selftests/landlock/audit.h
@@ -206,40 +206,7 @@ static int audit_set_status(int fd, __u32 key, __u32 val)
return audit_request(fd, &msg, NULL);
}
-/* Returns a pointer to the last filled character of @dst, which is `\0`. */
-static __maybe_unused char *regex_escape(const char *const src, char *dst,
- size_t dst_size)
-{
- char *d = dst;
-
- for (const char *s = src; *s; s++) {
- switch (*s) {
- case '$':
- case '*':
- case '.':
- case '[':
- case '\\':
- case ']':
- case '^':
- if (d >= dst + dst_size - 2)
- return (char *)-ENOMEM;
-
- *d++ = '\\';
- *d++ = *s;
- break;
- default:
- if (d >= dst + dst_size - 1)
- return (char *)-ENOMEM;
-
- *d++ = *s;
- }
- }
- if (d >= dst + dst_size - 1)
- return (char *)-ENOMEM;
-
- *d = '\0';
- return d;
-}
+/* regex_escape() is defined in common.h */
/*
* @domain_id: The domain ID extracted from the audit message (if the first part
diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h
index 90551650299c..dfc0df543e56 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -251,3 +251,50 @@ static void __maybe_unused set_unix_address(struct service_fixture *const srv,
srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
srv->unix_addr.sun_path[0] = '\0';
}
+
+/**
+ * regex_escape - Escape BRE metacharacters in a string
+ *
+ * @src: Source string to escape.
+ * @dst: Destination buffer for the escaped string.
+ * @dst_size: Size of the destination buffer.
+ *
+ * Escapes characters that have special meaning in POSIX Basic Regular
+ * Expressions: $ * . [ \ ] ^
+ *
+ * Returns a pointer to the NUL terminator in @dst (cursor-style API for
+ * chaining), or (char *)-ENOMEM if the buffer is too small.
+ */
+static __maybe_unused char *regex_escape(const char *const src, char *dst,
+ size_t dst_size)
+{
+ char *d = dst;
+
+ for (const char *s = src; *s; s++) {
+ switch (*s) {
+ case '$':
+ case '*':
+ case '.':
+ case '[':
+ case '\\':
+ case ']':
+ case '^':
+ if (d >= dst + dst_size - 2)
+ return (char *)-ENOMEM;
+
+ *d++ = '\\';
+ *d++ = *s;
+ break;
+ default:
+ if (d >= dst + dst_size - 1)
+ return (char *)-ENOMEM;
+
+ *d++ = *s;
+ }
+ }
+ if (d >= dst + dst_size - 1)
+ return (char *)-ENOMEM;
+
+ *d = '\0';
+ return d;
+}
diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config
index 8fe9b461b1fd..acfa31670c44 100644
--- a/tools/testing/selftests/landlock/config
+++ b/tools/testing/selftests/landlock/config
@@ -2,6 +2,8 @@ CONFIG_AF_UNIX_OOB=y
CONFIG_AUDIT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
+CONFIG_FTRACE=y
+CONFIG_FTRACE_SYSCALLS=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_KEYS=y
diff --git a/tools/testing/selftests/landlock/trace.h b/tools/testing/selftests/landlock/trace.h
new file mode 100644
index 000000000000..d8a4eb0906f0
--- /dev/null
+++ b/tools/testing/selftests/landlock/trace.h
@@ -0,0 +1,640 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Landlock trace test helpers
+ *
+ * Copyright © 2026 Cloudflare
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+
+#define TRACEFS_ROOT "/sys/kernel/tracing"
+#define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock"
+#define TRACEFS_CREATE_RULESET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
+#define TRACEFS_RESTRICT_SELF_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_restrict_self/enable"
+#define TRACEFS_ADD_RULE_FS_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
+#define TRACEFS_ADD_RULE_NET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable"
+#define TRACEFS_CHECK_RULE_FS_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable"
+#define TRACEFS_CHECK_RULE_NET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable"
+#define TRACEFS_DENY_ACCESS_FS_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
+#define TRACEFS_DENY_ACCESS_NET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
+#define TRACEFS_DENY_PTRACE_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
+#define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable"
+#define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \
+ TRACEFS_LANDLOCK_DIR \
+ "/landlock_deny_scope_abstract_unix_socket/enable"
+#define TRACEFS_FREE_DOMAIN_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable"
+#define TRACEFS_FREE_RULESET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable"
+#define TRACEFS_TRACE TRACEFS_ROOT "/trace"
+#define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid"
+#define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork"
+
+#define TRACE_BUFFER_SIZE (64 * 1024)
+
+/*
+ * Trace line prefix: matches the ftrace "trace" file format. Format: "
+ * <task>-<pid> [<cpu>] <flags> <timestamp>: "
+ *
+ * The task parameter must be a string literal truncated to 15 chars
+ * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The
+ * pattern accepts either the expected task name or "<...>" because the ftrace
+ * comm cache may evict short-lived processes (e.g., forked children that exit
+ * before the trace buffer is read).
+ *
+ * No unescaped '.' in any REGEX macro; literal dots use '\\.'.
+ */
+/* clang-format off */
+#define TRACE_PREFIX(task) \
+ "^ *\\(<\\.\\.\\.>" \
+ "\\|" task "\\)" \
+ "-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: "
+
+/*
+ * Task name for events emitted by kworker threads (e.g., free_domain fires from
+ * a work queue, not from the test process).
+ */
+#define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+"
+
+#define REGEX_ADD_RULE_FS(task) \
+ TRACE_PREFIX(task) \
+ "landlock_add_rule_fs: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+ "access_rights=0x[0-9a-f]\\+ " \
+ "dev=[0-9]\\+:[0-9]\\+ " \
+ "ino=[0-9]\\+ " \
+ "path=[^ ]\\+$"
+
+#define REGEX_ADD_RULE_NET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_add_rule_net: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+ "access_rights=0x[0-9a-f]\\+ " \
+ "port=[0-9]\\+$"
+
+#define REGEX_CREATE_RULESET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_create_ruleset: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+ "handled_fs=0x[0-9a-f]\\+ " \
+ "handled_net=0x[0-9a-f]\\+ " \
+ "scoped=0x[0-9a-f]\\+$"
+
+#define REGEX_RESTRICT_SELF(task) \
+ TRACE_PREFIX(task) \
+ "landlock_restrict_self: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+ "domain=[0-9a-f]\\+ " \
+ "parent=[0-9a-f]\\+$"
+
+#define REGEX_CHECK_RULE_FS(task) \
+ TRACE_PREFIX(task) \
+ "landlock_check_rule_fs: " \
+ "domain=[0-9a-f]\\+ " \
+ "request=0x[0-9a-f]\\+ " \
+ "dev=[0-9]\\+:[0-9]\\+ " \
+ "ino=[0-9]\\+ " \
+ "allowed={[0-9a-fx, ]*}$"
+
+#define REGEX_CHECK_RULE_NET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_check_rule_net: " \
+ "domain=[0-9a-f]\\+ " \
+ "request=0x[0-9a-f]\\+ " \
+ "port=[0-9]\\+ " \
+ "allowed={[0-9a-fx, ]*}$"
+
+#define REGEX_DENY_ACCESS_FS(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_access_fs: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "log_same_exec=[01] " \
+ "log_new_exec=[01] " \
+ "blockers=0x[0-9a-f]\\+ " \
+ "dev=[0-9]\\+:[0-9]\\+ " \
+ "ino=[0-9]\\+ " \
+ "path=[^ ]*$"
+
+#define REGEX_DENY_ACCESS_NET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_access_net: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "log_same_exec=[01] " \
+ "log_new_exec=[01] " \
+ "blockers=0x[0-9a-f]\\+ " \
+ "sport=[0-9]\\+ " \
+ "dport=[0-9]\\+$"
+
+#define REGEX_DENY_PTRACE(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_ptrace: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "log_same_exec=[01] " \
+ "log_new_exec=[01] " \
+ "tracee_pid=[0-9]\\+ " \
+ "comm=[^ ]*$"
+
+#define REGEX_DENY_SCOPE_SIGNAL(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_scope_signal: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "log_same_exec=[01] " \
+ "log_new_exec=[01] " \
+ "target_pid=[0-9]\\+ " \
+ "comm=[^ ]*$"
+
+#define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_scope_abstract_unix_socket: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "log_same_exec=[01] " \
+ "log_new_exec=[01] " \
+ "peer_pid=[0-9]\\+ " \
+ "sun_path=[^ ]*$"
+
+#define REGEX_FREE_DOMAIN(task) \
+ TRACE_PREFIX(task) \
+ "landlock_free_domain: " \
+ "domain=[0-9a-f]\\+ " \
+ "denials=[0-9]\\+$"
+
+#define REGEX_FREE_RULESET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_free_ruleset: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
+/* clang-format on */
+
+static int __maybe_unused tracefs_write(const char *path, const char *value)
+{
+ int fd;
+ ssize_t ret;
+ size_t len = strlen(value);
+
+ fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+
+ ret = write(fd, value, len);
+ close(fd);
+ if (ret < 0)
+ return -errno;
+ if ((size_t)ret != len)
+ return -EIO;
+
+ return 0;
+}
+
+static int __maybe_unused tracefs_write_int(const char *path, int value)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%d", value);
+ return tracefs_write(path, buf);
+}
+
+static int __maybe_unused tracefs_setup(void)
+{
+ struct stat st;
+
+ /* Mount tracefs if not already mounted. */
+ if (stat(TRACEFS_ROOT, &st) != 0) {
+ int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL);
+
+ if (ret)
+ return -errno;
+ }
+
+ /* Verify landlock events are available. */
+ if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0)
+ return -ENOENT;
+
+ return 0;
+}
+
+/*
+ * Set up PID-based event filtering so only events from the current process and
+ * its children are recorded. This is analogous to audit's AUDIT_EXE filter: it
+ * prevents events from unrelated processes from polluting the trace buffer.
+ */
+static int __maybe_unused tracefs_set_pid_filter(pid_t pid)
+{
+ int ret;
+
+ /* Enable event-fork so children inherit the PID filter. */
+ ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1");
+ if (ret)
+ return ret;
+
+ return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid);
+}
+
+/* Clear the PID filter to stop filtering by PID. */
+static int __maybe_unused tracefs_clear_pid_filter(void)
+{
+ return tracefs_write(TRACEFS_SET_EVENT_PID, "");
+}
+
+static int __maybe_unused tracefs_enable_event(const char *enable_path,
+ bool enable)
+{
+ return tracefs_write(enable_path, enable ? "1" : "0");
+}
+
+static int __maybe_unused tracefs_clear(void)
+{
+ return tracefs_write(TRACEFS_TRACE, "");
+}
+
+/*
+ * Reads the trace buffer content into a newly allocated buffer. The caller is
+ * responsible for freeing the returned buffer. Returns NULL on error.
+ */
+static char __maybe_unused *tracefs_read_trace(void)
+{
+ char *buf;
+ int fd;
+ ssize_t total = 0, ret;
+
+ buf = malloc(TRACE_BUFFER_SIZE);
+ if (!buf)
+ return NULL;
+
+ fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ free(buf);
+ return NULL;
+ }
+
+ while (total < TRACE_BUFFER_SIZE - 1) {
+ ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total);
+ if (ret <= 0)
+ break;
+ total += ret;
+ }
+ close(fd);
+ buf[total] = '\0';
+ return buf;
+}
+
+/* Counts the number of lines in @buf matching the basic regex @pattern. */
+static int __maybe_unused tracefs_count_matches(const char *buf,
+ const char *pattern)
+{
+ regex_t regex;
+ int count = 0;
+ const char *line, *end;
+
+ if (regcomp(®ex, pattern, 0) != 0)
+ return -EINVAL;
+
+ line = buf;
+ while (*line) {
+ end = strchr(line, '\n');
+ if (!end)
+ end = line + strlen(line);
+
+ /* Create a temporary null-terminated line. */
+ size_t len = end - line;
+ char *tmp = malloc(len + 1);
+
+ if (tmp) {
+ memcpy(tmp, line, len);
+ tmp[len] = '\0';
+ if (regexec(®ex, tmp, 0, NULL, 0) == 0)
+ count++;
+ free(tmp);
+ }
+
+ if (*end == '\n')
+ line = end + 1;
+ else
+ break;
+ }
+
+ regfree(®ex);
+ return count;
+}
+
+/*
+ * Extracts the value of a named field from a trace line in @buf. Searches for
+ * the first line matching @line_pattern, then extracts the value after
+ * "@field_name=" into @out. Stops at space or newline.
+ *
+ * Returns 0 on success, -ENOENT if no match.
+ */
+static int __maybe_unused tracefs_extract_field(const char *buf,
+ const char *line_pattern,
+ const char *field_name,
+ char *out, size_t out_size)
+{
+ regex_t regex;
+ const char *line, *end;
+
+ if (regcomp(®ex, line_pattern, 0) != 0)
+ return -EINVAL;
+
+ line = buf;
+ while (*line) {
+ end = strchr(line, '\n');
+ if (!end)
+ end = line + strlen(line);
+
+ size_t len = end - line;
+ char *tmp = malloc(len + 1);
+
+ if (tmp) {
+ const char *field, *val_start;
+ size_t field_len, val_len;
+
+ memcpy(tmp, line, len);
+ tmp[len] = '\0';
+
+ if (regexec(®ex, tmp, 0, NULL, 0) != 0) {
+ free(tmp);
+ goto next;
+ }
+
+ /*
+ * Find "field_name=" in the line, ensuring a word
+ * boundary before the field name to avoid substring
+ * matches (e.g., "port" in "sport").
+ */
+ field_len = strlen(field_name);
+ field = tmp;
+ while ((field = strstr(field, field_name))) {
+ if (field[field_len] == '=' &&
+ (field == tmp || field[-1] == ' '))
+ break;
+ field++;
+ }
+ if (!field) {
+ free(tmp);
+ regfree(®ex);
+ return -ENOENT;
+ }
+
+ val_start = field + field_len + 1;
+ val_len = 0;
+ while (val_start[val_len] &&
+ val_start[val_len] != ' ' &&
+ val_start[val_len] != '\n')
+ val_len++;
+
+ if (val_len >= out_size)
+ val_len = out_size - 1;
+ memcpy(out, val_start, val_len);
+ out[val_len] = '\0';
+
+ free(tmp);
+ regfree(®ex);
+ return 0;
+ }
+next:
+ if (*end == '\n')
+ line = end + 1;
+ else
+ break;
+ }
+
+ regfree(®ex);
+ return -ENOENT;
+}
+
+/*
+ * Common fixture setup for trace tests. Mounts tracefs if needed and
+ * sets a PID filter. The caller must create a mount namespace first
+ * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate
+ * tracefs state.
+ *
+ * Returns 0 on success, -errno on failure (caller should SKIP).
+ */
+static int __maybe_unused tracefs_fixture_setup(void)
+{
+ int ret;
+
+ ret = tracefs_setup();
+ if (ret)
+ return ret;
+
+ return tracefs_set_pid_filter(getpid());
+}
+
+static void __maybe_unused tracefs_fixture_teardown(void)
+{
+ tracefs_clear_pid_filter();
+}
+
+/*
+ * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then
+ * drops the capability. Returns the value from @func, or -EPERM if the
+ * capability manipulation fails.
+ */
+static int __maybe_unused tracefs_priv_call(int (*func)(void))
+{
+ const cap_value_t admin = CAP_SYS_ADMIN;
+ cap_t cap_p;
+ int ret;
+
+ cap_p = cap_get_proc();
+ if (!cap_p)
+ return -EPERM;
+
+ if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
+ cap_set_proc(cap_p)) {
+ cap_free(cap_p);
+ return -EPERM;
+ }
+
+ ret = func();
+
+ cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
+ cap_set_proc(cap_p);
+ cap_free(cap_p);
+ return ret;
+}
+
+/* Read the trace buffer with elevated privileges. Returns NULL on failure. */
+static char __maybe_unused *tracefs_read_buf(void)
+{
+ /* Cannot use tracefs_priv_call() because the return type is char *. */
+ cap_t cap_p;
+ char *buf;
+ const cap_value_t admin = CAP_SYS_ADMIN;
+
+ cap_p = cap_get_proc();
+ if (!cap_p)
+ return NULL;
+
+ if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
+ cap_set_proc(cap_p)) {
+ cap_free(cap_p);
+ return NULL;
+ }
+
+ buf = tracefs_read_trace();
+
+ cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
+ cap_set_proc(cap_p);
+ cap_free(cap_p);
+ return buf;
+}
+
+/* Clear the trace buffer with elevated privileges. Returns 0 on success. */
+static int __maybe_unused tracefs_clear_buf(void)
+{
+ return tracefs_priv_call(tracefs_clear);
+}
+
+/*
+ * Forks a child that creates a Landlock sandbox and performs an FS access. The
+ * parent waits for the child, then reads the trace buffer.
+ *
+ * Requires common.h and wrappers.h to be included before trace.h.
+ */
+static void __maybe_unused sandbox_child_fs_access(
+ struct __test_metadata *const _metadata, const char *rule_path,
+ __u64 handled_access, __u64 allowed_access, const char *access_path)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = handled_access,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = allowed_access,
+ };
+ int ruleset_fd, fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0)) {
+ close(path_beneath.parent_fd);
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+}
+
+/*
+ * Forks a child that creates a Landlock sandbox allowing execute+read_dir for
+ * /usr and execute-only for ".", then execs ./true. The true binary opens "."
+ * on startup, triggering a read_dir denial with same_exec=0. The parent waits
+ * for the child to exit.
+ */
+static void __maybe_unused sandbox_child_exec_true(
+ struct __test_metadata *const _metadata, __u32 restrict_flags)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
+ LANDLOCK_ACCESS_FS_EXECUTE,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd >= 0) {
+ landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+ }
+
+ path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
+ path_beneath.parent_fd =
+ open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd >= 0) {
+ landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+ }
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, restrict_flags))
+ _exit(1);
+ close(ruleset_fd);
+
+ execl("./true", "./true", NULL);
+ _exit(1);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+}
diff --git a/tools/testing/selftests/landlock/trace_test.c b/tools/testing/selftests/landlock/trace_test.c
new file mode 100644
index 000000000000..0256383489fe
--- /dev/null
+++ b/tools/testing/selftests/landlock/trace_test.c
@@ -0,0 +1,1168 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Landlock tests - Tracepoints
+ *
+ * Copyright © 2026 Cloudflare
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/landlock.h>
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "trace_test"
+
+/* clang-format off */
+FIXTURE(trace) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_RESTRICT_SELF_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ /* Disables landlock events and clears PID filter. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_RESTRICT_SELF_ENABLE, false);
+ tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false);
+ tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false);
+ tracefs_clear_pid_filter();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * The mount namespace is cleaned up automatically when the test process
+ * (harness child) exits.
+ */
+}
+
+/*
+ * Verifies that no trace events are emitted when the tracepoints are disabled.
+ */
+TEST_F(trace, no_trace_when_disabled)
+{
+ char *buf;
+
+ /* Disable all landlock events. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_RESTRICT_SELF_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE,
+ false));
+ ASSERT_EQ(0, tracefs_enable_event(
+ TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
+ false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * Trigger both allowed and denied accesses to verify neither check_rule
+ * nor check_access events fire when disabled.
+ */
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+ /* Read trace buffer and verify no landlock events at all. */
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(0, tracefs_count_matches(buf, "landlock_"))
+ {
+ TH_LOG("Expected 0 landlock events when disabled\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_create_ruleset emits a trace event with the correct
+ * handled access masks.
+ */
+TEST_F(trace, create_ruleset)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ int ruleset_fd;
+ char *buf, *dot;
+ char field[64];
+ char expected[32];
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(1,
+ tracefs_count_matches(buf, REGEX_CREATE_RULESET(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 create_ruleset event\n%s", buf);
+ }
+
+ /* Verify handled_fs matches what we requested. */
+ snprintf(expected, sizeof(expected), "0x%x",
+ (unsigned int)LANDLOCK_ACCESS_FS_READ_FILE);
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "handled_fs", field, sizeof(field)));
+ EXPECT_STREQ(expected, field);
+
+ /* Verify handled_net matches. */
+ snprintf(expected, sizeof(expected), "0x%x",
+ (unsigned int)LANDLOCK_ACCESS_NET_BIND_TCP);
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "handled_net", field, sizeof(field)));
+ EXPECT_STREQ(expected, field);
+
+ /* Verify version is 0 at creation (no rules added yet). */
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "ruleset", field, sizeof(field)));
+ /* Format is <hex>.<dec>; version is after the dot. */
+ dot = strchr(field, '.');
+ ASSERT_NE(0, !!dot);
+ EXPECT_STREQ("0", dot + 1);
+
+ free(buf);
+}
+
+/*
+ * Verifies that the ruleset version increments with each add_rule call and that
+ * restrict_self records the correct version.
+ */
+TEST_F(trace, ruleset_version)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ const char *dot;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ /* First rule: version becomes 1. */
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ /* Second rule: version becomes 2. */
+ path_beneath.parent_fd =
+ open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Verify create_ruleset has version=0. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "ruleset", field, sizeof(field)));
+ dot = strchr(field, '.');
+ ASSERT_NE(0, !!dot);
+ EXPECT_STREQ("0", dot + 1);
+
+ /* Verify 2 add_rule_fs events were emitted. */
+ EXPECT_EQ(2, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
+ {
+ TH_LOG("Expected 2 add_rule_fs events\n%s", buf);
+ }
+
+ /*
+ * Verify restrict_self records version=2 (after 2 add_rule calls). The
+ * ruleset field format is <hex_id>.<dec_version>.
+ */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_RESTRICT_SELF(TRACE_TASK),
+ "ruleset", field, sizeof(field)));
+ dot = strchr(field, '.');
+ ASSERT_NE(0, !!dot);
+ EXPECT_STREQ("2", dot + 1);
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_restrict_self emits a trace event linking the ruleset
+ * ID to the new domain ID.
+ */
+TEST_F(trace, restrict_self)
+{
+ pid_t pid;
+ int status, check_count;
+ char *buf;
+ char ruleset_id[64], domain_id[64], check_domain[64];
+
+ /* Clear before the sandboxed child. */
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ /* Trigger a check_rule to verify domain_id correlation. */
+ fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Verify restrict_self event exists. */
+ EXPECT_EQ(1,
+ tracefs_count_matches(buf, REGEX_RESTRICT_SELF(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 restrict_self event\n%s", buf);
+ }
+
+ /* Extract the domain ID from restrict_self. */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_RESTRICT_SELF(TRACE_TASK),
+ "domain", domain_id,
+ sizeof(domain_id)));
+
+ /* Extract the ruleset ID from restrict_self. */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_RESTRICT_SELF(TRACE_TASK),
+ "ruleset", ruleset_id,
+ sizeof(ruleset_id)));
+
+ /* Verify domain ID is non-zero. */
+ EXPECT_NE(0, strcmp(domain_id, "0"));
+
+ /* Verify parent=0 (first restriction, no prior domain). */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_RESTRICT_SELF(TRACE_TASK),
+ "parent", ruleset_id,
+ sizeof(ruleset_id)));
+ EXPECT_STREQ("0", ruleset_id);
+
+ /*
+ * Verify the same domain ID appears in the check_rule event, confirming
+ * end-to-end correlation.
+ */
+ check_count =
+ tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+ ASSERT_LE(1, check_count)
+ {
+ TH_LOG("Expected check_rule_fs events\n%s", buf);
+ }
+
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "domain", check_domain,
+ sizeof(check_domain)));
+ EXPECT_STREQ(domain_id, check_domain);
+
+ free(buf);
+}
+
+/*
+ * Verifies that nested landlock_restrict_self calls produce trace events with
+ * correct parent domain IDs: the second restrict_self's parent should be the
+ * first domain's ID.
+ */
+TEST_F(trace, restrict_self_nested)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ const char *after_first;
+ char first_domain[64], first_parent[64], second_parent[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+
+ /* First restriction. */
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ /* Second restriction (nested). */
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Should have 2 restrict_self events. */
+ EXPECT_EQ(2,
+ tracefs_count_matches(buf, REGEX_RESTRICT_SELF(TRACE_TASK)))
+ {
+ TH_LOG("Expected 2 restrict_self events\n%s", buf);
+ }
+
+ /*
+ * Extract domain and parent from each restrict_self event. The first
+ * event (parent=0) is the outer domain; the second (parent!=0) is the
+ * nested domain whose parent should match the first domain's ID.
+ */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_RESTRICT_SELF(TRACE_TASK),
+ "domain", first_domain,
+ sizeof(first_domain)));
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_RESTRICT_SELF(TRACE_TASK),
+ "parent", first_parent,
+ sizeof(first_parent)));
+ EXPECT_STREQ("0", first_parent);
+
+ /*
+ * Find the second restrict_self by scanning past the first.
+ * tracefs_extract_field returns the first match, so search in the
+ * buffer after the first event.
+ *
+ * Skip past the first restrict_self line. tracefs_extract_field
+ * matches the first line that matches the regex, so passing the
+ * buffer after the first matching line gives us the second
+ * event.
+ */
+ after_first = strstr(buf, "landlock_restrict_self:");
+ ASSERT_NE(NULL, after_first);
+ after_first = strchr(after_first, '\n');
+ ASSERT_NE(NULL, after_first);
+
+ ASSERT_EQ(0, tracefs_extract_field(
+ after_first + 1, REGEX_RESTRICT_SELF(TRACE_TASK),
+ "parent", second_parent, sizeof(second_parent)));
+
+ /* The second domain's parent should be the first domain's ID. */
+ EXPECT_STREQ(first_domain, second_parent);
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_add_rule does not emit a trace event when the syscall
+ * fails (e.g., invalid ruleset fd).
+ */
+TEST_F(trace, add_rule_invalid_fd)
+{
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
+ };
+ char *buf;
+
+ path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_LE(0, path_beneath.parent_fd);
+
+ /* Invalid ruleset fd (-1). */
+ ASSERT_EQ(-1, landlock_add_rule(-1, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0));
+ ASSERT_EQ(0, close(path_beneath.parent_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
+ {
+ TH_LOG("No add_rule_fs event expected on invalid fd\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_restrict_self does not emit a trace event when the
+ * syscall fails (e.g., invalid ruleset fd or unknown flags).
+ */
+TEST_F(trace, restrict_self_invalid)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+ char *buf;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Clear the trace buffer after create_ruleset event. */
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Invalid fd. */
+ ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
+
+ /* Unknown flags. */
+ ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, -1));
+
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(0,
+ tracefs_count_matches(buf, REGEX_RESTRICT_SELF(TRACE_TASK)))
+ {
+ TH_LOG("No restrict_self event expected on error\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * Verifies that trace_landlock_free_domain fires when a domain is deallocated,
+ * with the correct denials count.
+ */
+TEST_F(trace, free_domain)
+{
+ char *buf;
+ int count;
+ char denials_field[32];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /*
+ * The domain is freed via a work queue (kworker), so the free_domain
+ * trace event is emitted from a different PID. Clear the PID filter
+ * BEFORE the child exits, so the kworker event passes the filter when
+ * it fires.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_clear_pid_filter();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+ /*
+ * Wait for the deferred deallocation work to run. The domain is freed
+ * asynchronously from a kworker; poll until the event appears or a
+ * timeout is reached.
+ */
+ for (int retry = 0; retry < 10; retry++) {
+ /* TODO: Improve */
+ usleep(100000);
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ buf = tracefs_read_trace();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf,
+ REGEX_FREE_DOMAIN(KWORKER_TASK));
+ if (count >= 1)
+ break;
+ free(buf);
+ buf = NULL;
+ }
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ ASSERT_NE(NULL, buf);
+ EXPECT_LE(1, count)
+ {
+ TH_LOG("Expected free_domain event, got %d\n%s", count, buf);
+ }
+
+ /* Verify denials count matches the single denial we triggered. */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
+ "denials", denials_field,
+ sizeof(denials_field)));
+ EXPECT_STREQ("1", denials_field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that deny_access_fs includes the enriched fields: same_exec,
+ * log_same_exec, log_new_exec.
+ */
+TEST_F(trace, deny_access_fs_fields)
+{
+ char *buf;
+ char field_buf[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Trigger a denial: rule for /usr, access /tmp. */
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Verify the enriched fields are present and have valid values. */
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), "same_exec",
+ field_buf, sizeof(field_buf)));
+ /* Child is the same exec that restricted itself. */
+ EXPECT_STREQ("1", field_buf);
+
+ /* Default: log_same_exec=1 (not disabled). */
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_same_exec", field_buf, sizeof(field_buf)));
+ EXPECT_STREQ("1", field_buf);
+
+ /* Default: log_new_exec=0 (not enabled). */
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_new_exec", field_buf, sizeof(field_buf)));
+ EXPECT_STREQ("0", field_buf);
+
+ free(buf);
+}
+
+/*
+ * Verifies that same_exec is 1 (true) for denials from the same executable that
+ * called landlock_restrict_self().
+ */
+TEST_F(trace, same_exec_before_exec)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, dir_fd;
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ /* No rules: all read_dir access is denied. */
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ /* Trigger denial without exec (same executable). */
+ dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (dir_fd >= 0)
+ close(dir_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Should have at least one deny_access_fs denial. */
+ EXPECT_LE(1,
+ tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
+
+ /* Verify same_exec=1 (same executable, no exec). */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "same_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ /* Verify default log flags. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_same_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_new_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that same_exec is 0 (false) for denials from a process that has
+ * exec'd a new binary after landlock_restrict_self(). The sandboxed child
+ * exec's true which opens "." and triggers a read_dir denial. Also verifies
+ * the default log flags (log_same_exec=1, log_new_exec=0) and covers the
+ * "trace-only" visibility condition: same_exec=0 AND log_new_exec=0 means audit
+ * suppresses the denial, but trace still fires.
+ */
+TEST_F(trace, same_exec_after_exec)
+{
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ sandbox_child_exec_true(_metadata, 0);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
+
+ /* Verify same_exec=0 (different executable after exec). */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "same_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ /* Default log flags should still be the same. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "log_same_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "log_new_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF disables log_same_exec
+ * in the trace event.
+ */
+TEST_F(trace, log_flags_same_exec_off)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, dir_fd;
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(
+ ruleset_fd,
+ LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF))
+ _exit(1);
+ close(ruleset_fd);
+
+ dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (dir_fd >= 0)
+ close(dir_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1,
+ tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_same_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_new_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON enables log_new_exec in
+ * the trace event. The child exec's true so that the denial comes from a new
+ * executable (same_exec=0).
+ */
+TEST_F(trace, log_flags_new_exec_on)
+{
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ sandbox_child_exec_true(_metadata,
+ LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
+
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "same_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "log_same_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "log_new_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that denials suppressed by audit log flags are still counted in
+ * num_denials. The child restricts itself with default flags (log_same_exec=1,
+ * log_new_exec=0), then execs true which attempts to read a denied directory.
+ * After exec, same_exec=0 and log_new_exec=0, so audit suppresses the denial.
+ * But the trace event fires unconditionally and free_domain must report the
+ * correct denials count.
+ */
+TEST_F(trace, non_audit_visible_denial_counting)
+{
+ char *buf = NULL;
+ char denials_field[32];
+ int count;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, tracefs_clear());
+ tracefs_clear_pid_filter();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ sandbox_child_exec_true(_metadata, 0);
+
+ /* Wait for free_domain event with retry. */
+ for (int retry = 0; retry < 10; retry++) {
+ usleep(100000);
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ buf = tracefs_read_trace();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ if (!buf)
+ break;
+
+ count = tracefs_count_matches(buf,
+ REGEX_FREE_DOMAIN(KWORKER_TASK));
+ if (count >= 1)
+ break;
+ free(buf);
+ buf = NULL;
+ }
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * The denial happened after exec (same_exec=0), so audit would suppress
+ * it. But num_denials counts all denials regardless.
+ */
+ ASSERT_NE(NULL, buf)
+ {
+ TH_LOG("free_domain event not found after 10 retries");
+ }
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
+ "denials", denials_field,
+ sizeof(denials_field)));
+ EXPECT_STREQ("1", denials_field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_add_rule_net emits a trace event with the correct port
+ * and allowed access mask fields.
+ */
+TEST_F(trace, add_rule_net_fields)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ struct landlock_net_port_attr net_port = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .port = 8080,
+ };
+ int ruleset_fd;
+ char *buf;
+ char field[64], expected[32];
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &net_port, 0));
+ close(ruleset_fd);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_ADD_RULE_NET(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 add_rule_net event\n%s", buf);
+ }
+
+ /*
+ * Verify the port is in host endianness, matching the UAPI
+ * convention (landlock_net_port_attr.port). On little-endian,
+ * htons(8080) is 36895, so this comparison catches byte-order
+ * bugs.
+ */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
+ "port", field, sizeof(field)));
+ EXPECT_STREQ("8080", field);
+ /*
+ * The allowed mask is the absolute value after transformation:
+ * the user-requested BIND_TCP plus all unhandled access rights
+ * (CONNECT_TCP is unhandled because the ruleset only handles
+ * BIND_TCP).
+ */
+ snprintf(expected, sizeof(expected), "0x%x",
+ (unsigned int)(LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP));
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
+ "access_rights", field, sizeof(field)));
+ EXPECT_STREQ(expected, field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF disables audit
+ * logging for child domains but trace events still fire. The parent creates a
+ * domain with LOG_SUBDOMAINS_OFF, then the child creates a sub-domain and
+ * triggers a denial. The trace event should fire (tracing is unconditional)
+ * with log_same_exec=1 and log_new_exec=0 (the child's default flags).
+ */
+TEST_F(trace, log_flags_subdomains_off)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int parent_fd, child_fd, dir_fd;
+
+ /* Parent domain with LOG_SUBDOMAINS_OFF. */
+ parent_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (parent_fd < 0)
+ _exit(1);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(
+ parent_fd,
+ LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF))
+ _exit(1);
+ close(parent_fd);
+
+ /* Child sub-domain with default flags. */
+ child_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (child_fd < 0)
+ _exit(1);
+
+ if (landlock_restrict_self(child_fd, 0))
+ _exit(1);
+ close(child_fd);
+
+ /* Trigger a denial from the child domain. */
+ dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (dir_fd >= 0)
+ close(dir_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /*
+ * Trace fires unconditionally even though audit is disabled for the
+ * child domain (parent had LOG_SUBDOMAINS_OFF).
+ */
+ EXPECT_LE(1,
+ tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)))
+ {
+ TH_LOG("Expected deny_access_fs event despite "
+ "LOG_SUBDOMAINS_OFF\n%s",
+ buf);
+ }
+
+ /* The child domain's own flags: log_same_exec=1 (default). */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_same_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "log_new_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ free(buf);
+}
+
+/* Verifies that landlock_free_ruleset fires when a ruleset FD is closed. */
+TEST_F(trace, free_ruleset_on_close)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+ char *buf;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Closing the FD should trigger free_ruleset. */
+ close(ruleset_fd);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_FREE_RULESET(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 free_ruleset event\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * The following tests are intentionally elided because the underlying kernel
+ * mechanisms are already validated by audit tests:
+ *
+ * - Domain ID monotonicity: validated by audit_test.c:layers. The same
+ * landlock_get_id_range() function serves both audit and trace.
+ *
+ * - Domain deallocation order (LIFO): validated by audit_test.c:layers. Trace
+ * events fire from the same free_domain_work() code path.
+ *
+ * - Max-layer stacking (16 domains): validated by audit_test.c:layers.
+ *
+ * - IPv6 network tests: IPv6 hook dispatch uses the same
+ * current_check_access_socket() as IPv4, validated by net_test.c:audit tests.
+ *
+ * - Per-access-right full matrix (all 16 FS rights): hook dispatch is validated
+ * by fs_test.c:audit tests. Trace tests verify representative samples to
+ * ensure bitmask encoding is correct.
+ *
+ * - Combined log flag variants (e.g., LOG_SUBDOMAINS_OFF + LOG_NEW_EXEC_ON):
+ * individual flag tests above cover each flag's effect on trace fields. Flag
+ * combination logic is validated by audit_test.c:audit_flags tests.
+ *
+ * - fs.refer multi-record denials and fs.change_topology (mount):
+ * trace_denial() uses the same code path for all FS request types. The
+ * DENTRY union member fix (C1) is validated by the deny_access_fs_fields
+ * test. Audit tests in fs_test.c cover refer and mount denial specifics.
+ *
+ * - Ptrace TRACEME direction: the tracepoint fires from the same
+ * hook_ptrace_access_check() for both ATTACH and TRACEME. Audit tests in
+ * ptrace_test.c cover both directions.
+ *
+ * - check_rule_net field verification: the tracepoint uses the same
+ * landlock_unmask_layers() as check_rule_fs, just with a different key type.
+ * The FS path is validated by trace_fs_test.c tests.
+ */
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/true.c b/tools/testing/selftests/landlock/true.c
index 3f9ccbf52783..1e39b664512d 100644
--- a/tools/testing/selftests/landlock/true.c
+++ b/tools/testing/selftests/landlock/true.c
@@ -1,5 +1,15 @@
// SPDX-License-Identifier: GPL-2.0
+/*
+ * Minimal helper for Landlock selftests. Opens its own working directory
+ * before exiting, which may trigger access denials depending on the sandbox
+ * configuration.
+ */
+
+#include <fcntl.h>
+#include <unistd.h>
+
int main(void)
{
+ close(open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
return 0;
}
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 14/17] selftests/landlock: Add filesystem tracepoint tests
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (12 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 13/17] selftests/landlock: Add trace event test infrastructure and tests Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 15/17] selftests/landlock: Add network " Mickaël Salaün
` (2 subsequent siblings)
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add filesystem-specific trace tests in a dedicated test file, following
the same pattern as audit tests which live alongside the functional
tests for each subsystem.
Tests in trace_fs_test.c verify that:
- landlock_add_rule_fs events fire with correct path and fields,
- landlock_check_rule_fs events fire when rules match during pathwalk
and do not fire for unhandled access types,
- landlock_deny_access_fs events fire on denied accesses,
- nested domains produce both check_rule and deny_access events,
- no trace events fire without a Landlock sandbox (unsandboxed
baseline).
Add trace_layout1 fixture tests in fs_test.c for field verification
(check_rule_fs_fields) and multi-rule pathwalk
(check_rule_fs_multiple_rules) that reuse the layout1 filesystem
hierarchy.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
tools/testing/selftests/landlock/fs_test.c | 218 ++++++++++
.../selftests/landlock/trace_fs_test.c | 390 ++++++++++++++++++
2 files changed, 608 insertions(+)
create mode 100644 tools/testing/selftests/landlock/trace_fs_test.c
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index cdb47fc1fc0a..8f1ab43a07a0 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -44,6 +44,9 @@
#include "audit.h"
#include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "fs_test"
#ifndef renameat2
int renameat2(int olddirfd, const char *oldpath, int newdirfd,
@@ -7764,4 +7767,219 @@ TEST_F(audit_layout1, mount)
EXPECT_EQ(1, records.domain);
}
+/* clang-format off */
+FIXTURE(trace_layout1) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_layout1)
+{
+ struct stat st;
+
+ /*
+ * Check tracefs availability before creating the layout, following the
+ * layout3_fs pattern: skip before any layout creation to avoid leaving
+ * stale TMP_DIR on skip.
+ */
+ if (stat(TRACEFS_LANDLOCK_DIR, &st)) {
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ /* Isolate tracefs state (PID filter, event enables). */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ prepare_layout(_metadata);
+ create_layout1(_metadata);
+
+ set_cap(_metadata, CAP_DAC_OVERRIDE);
+ ASSERT_EQ(0, tracefs_fixture_setup());
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+ clear_cap(_metadata, CAP_DAC_OVERRIDE);
+}
+
+FIXTURE_TEARDOWN_PARENT(trace_layout1)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_DAC_OVERRIDE);
+ tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
+ tracefs_clear_pid_filter();
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_DAC_OVERRIDE);
+
+ remove_layout1(_metadata);
+ cleanup_layout(_metadata);
+}
+
+/*
+ * Verifies that check_rule_fs events include correct field values: domain, dev,
+ * ino, request, and allowed. All values are verified against stat() of the
+ * rule path on a deterministic tmpfs layout.
+ */
+TEST_F(trace_layout1, check_rule_fs_fields)
+{
+ struct stat dir_stat;
+ char expected_dev[32];
+ char expected_ino[32];
+ char expected_req[32];
+ char *buf;
+ char field[64];
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ ASSERT_EQ(0, stat(dir_s1d1, &dir_stat));
+ snprintf(expected_dev, sizeof(expected_dev), "%u:%u",
+ major(dir_stat.st_dev), minor(dir_stat.st_dev));
+ snprintf(expected_ino, sizeof(expected_ino), "%lu", dir_stat.st_ino);
+ snprintf(expected_req, sizeof(expected_req), "0x%x",
+ (unsigned int)LANDLOCK_ACCESS_FS_READ_DIR);
+
+ set_cap(_metadata, CAP_DAC_OVERRIDE);
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_DAC_OVERRIDE);
+
+ sandbox_child_fs_access(_metadata, dir_s1d1,
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, dir_s1d1);
+
+ set_cap(_metadata, CAP_DAC_OVERRIDE);
+ buf = tracefs_read_trace();
+ clear_cap(_metadata, CAP_DAC_OVERRIDE);
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(1,
+ tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 check_rule_fs event\n%s", buf);
+ }
+
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "dev", field, sizeof(field)));
+ EXPECT_STREQ(expected_dev, field)
+ {
+ TH_LOG("Expected dev=%s, got %s", expected_dev, field);
+ }
+
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "ino", field, sizeof(field)));
+ EXPECT_STREQ(expected_ino, field)
+ {
+ TH_LOG("Expected ino=%s, got %s", expected_ino, field);
+ }
+
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "request", field, sizeof(field)));
+ EXPECT_STREQ(expected_req, field)
+ {
+ TH_LOG("Expected request=%s, got %s", expected_req, field);
+ }
+
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "allowed", field, sizeof(field)));
+ EXPECT_EQ('{', field[0])
+ {
+ TH_LOG("Expected allowed={...}, got %s", field);
+ }
+
+ free(buf);
+}
+
+/*
+ * Verifies check_rule_fs behavior with multiple rules. With rules at s1d1 and
+ * s1d2 (a child of s1d1), accessing s1d2 produces only 1 event because the
+ * pathwalk short-circuits after the first rule fully unmasks the single layer.
+ */
+TEST_F(trace_layout1, check_rule_fs_multiple_rules)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ int count;
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ set_cap(_metadata, CAP_DAC_OVERRIDE);
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_DAC_OVERRIDE);
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, fd;
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open(dir_s1d1, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0))
+ _exit(1);
+ close(path_beneath.parent_fd);
+
+ path_beneath.parent_fd =
+ open(dir_s1d2, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0))
+ _exit(1);
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ fd = open(dir_s1d2, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ set_cap(_metadata, CAP_DAC_OVERRIDE);
+ buf = tracefs_read_trace();
+ clear_cap(_metadata, CAP_DAC_OVERRIDE);
+ ASSERT_NE(NULL, buf);
+
+ /*
+ * Only 1 check_rule_fs event: the rule on dir_s1d2 fully unmasked the
+ * single layer, so the pathwalk short-circuits before reaching the
+ * dir_s1d1 rule.
+ */
+ count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+ EXPECT_EQ(1, count)
+ {
+ TH_LOG("Expected 1 check_rule_fs event, got %d\n%s", count,
+ buf);
+ }
+
+ free(buf);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/trace_fs_test.c b/tools/testing/selftests/landlock/trace_fs_test.c
new file mode 100644
index 000000000000..60ed63aea049
--- /dev/null
+++ b/tools/testing/selftests/landlock/trace_fs_test.c
@@ -0,0 +1,390 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Landlock tests - Filesystem tracepoints
+ *
+ * Copyright © 2026 Cloudflare
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/landlock.h>
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "trace_fs_test"
+
+/* clang-format off */
+FIXTURE(trace_fs) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_fs)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_fs)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Baseline: verifies that without Landlock, the operation succeeds and no
+ * check_rule or deny_access trace events fire.
+ */
+TEST_F(trace_fs, unsandboxed)
+{
+ char *buf;
+ int count, status, fd;
+ pid_t pid;
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ /*
+ * No sandbox: verify that a normal FS access does not produce
+ * Landlock trace events.
+ */
+ fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+ EXPECT_EQ(0, count);
+ count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
+ EXPECT_EQ(0, count);
+
+ free(buf);
+}
+
+/*
+ * Verifies that adding a filesystem rule emits a landlock_add_rule_fs trace
+ * event with the expected path and field values: ruleset ID is non-zero,
+ * access_rights is non-zero, and path matches.
+ */
+TEST_F(trace_fs, add_rule_fs)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_WRITE_FILE |
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
+ };
+ char *buf, field_buf[64];
+ int ruleset_fd, count;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_LE(0, path_beneath.parent_fd);
+
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0));
+ ASSERT_EQ(0, close(path_beneath.parent_fd));
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK));
+ EXPECT_EQ(1, count)
+ {
+ TH_LOG("Expected 1 add_rule_fs event, got %d\n%s", count, buf);
+ }
+
+ /* Ruleset ID should be non-zero. */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
+ "ruleset", field_buf,
+ sizeof(field_buf)));
+ EXPECT_STRNE("0", field_buf);
+
+ /* Access rights should be non-zero. */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
+ "access_rights", field_buf,
+ sizeof(field_buf)));
+ EXPECT_STRNE("0x0", field_buf);
+
+ /* Path should be /usr. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_ADD_RULE_FS(TRACE_TASK),
+ "path", field_buf, sizeof(field_buf)));
+ EXPECT_STREQ("/usr", field_buf);
+
+ free(buf);
+}
+
+/*
+ * Verifies that an allowed access emits check_rule events (rule matched during
+ * pathwalk) but does NOT emit deny_access events (no denial).
+ */
+TEST_F(trace_fs, allowed_access)
+{
+ char *buf, field_buf[64];
+ int count;
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Rule allows READ_DIR for /usr, access /usr which is allowed. */
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/usr");
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+ EXPECT_LE(1, count);
+
+ /* Single-layer allowed array: {0x<mask>}. */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "allowed", field_buf,
+ sizeof(field_buf)));
+ EXPECT_EQ('{', field_buf[0]);
+
+ count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
+ EXPECT_EQ(0, count);
+
+ free(buf);
+}
+
+/*
+ * Verifies that accessing a path whose access type is not in the handled set
+ * does not emit landlock_check_rule events. The ruleset handles READ_FILE,
+ * but the directory open checks READ_DIR which is unhandled; Landlock has no
+ * opinion and no rule evaluation occurs.
+ */
+TEST_F(trace_fs, check_rule_unhandled)
+{
+ char *buf;
+ int count;
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Handles READ_FILE only; READ_DIR is unhandled. */
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_FILE,
+ LANDLOCK_ACCESS_FS_READ_FILE, "/tmp");
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* No check_rule events because READ_DIR is not in the handled set. */
+ count = tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+ EXPECT_EQ(0, count);
+
+ free(buf);
+}
+
+/*
+ * Verifies that nested domains (child sandboxed under a parent domain) emit
+ * check_rule events from both layers and produce a deny_access event when the
+ * inner domain's rule does not cover the access.
+ */
+TEST_F(trace_fs, check_rule_nested)
+{
+ char *buf, field_buf[64], *comma;
+ size_t first_len, second_len;
+ int count_rule, count_access, status;
+ pid_t pid;
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, fd;
+
+ /* First layer: allow /usr. */
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0)) {
+ close(path_beneath.parent_fd);
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ /* Second layer: also allow /usr. */
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0)) {
+ close(path_beneath.parent_fd);
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(path_beneath.parent_fd);
+
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ /* Access /usr which is allowed by both layers. */
+ fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+
+ /* Access /tmp which has no rule in either layer. */
+ fd = open("/tmp", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count_rule =
+ tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+ EXPECT_LE(1, count_rule);
+
+ /*
+ * Both layers have the same rule, so the allowed array must
+ * have two identical entries: {0x<mask>,0x<mask>}.
+ */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "allowed", field_buf,
+ sizeof(field_buf)));
+ comma = strchr(field_buf, ',');
+ EXPECT_NE(0, !!comma);
+ if (comma) {
+ /*
+ * Verify both entries are identical: compare the
+ * substring before the comma with the substring after
+ * it (stripping the braces).
+ */
+ first_len = comma - field_buf - 1;
+ second_len = strlen(comma + 1) - 1;
+ EXPECT_EQ(first_len, second_len);
+ EXPECT_EQ(0, strncmp(field_buf + 1, comma + 1, first_len));
+ }
+
+ count_access =
+ tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
+ EXPECT_LE(1, count_access);
+
+ free(buf);
+}
+
+/*
+ * Verifies that a denied FS access emits a landlock_deny_access_fs trace event
+ * with the blocked access and path.
+ */
+TEST_F(trace_fs, deny_access_fs_denied)
+{
+ char *buf;
+ int count;
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /*
+ * Rule allows READ_DIR for /usr, but access /tmp which has no rule.
+ * READ_DIR access to /tmp is denied by absence and should emit a
+ * deny_access_fs event.
+ */
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK));
+ EXPECT_LE(1, count);
+
+ free(buf);
+}
+
+TEST_HARNESS_MAIN
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 15/17] selftests/landlock: Add network tracepoint tests
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (13 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 14/17] selftests/landlock: Add filesystem tracepoint tests Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 16/17] selftests/landlock: Add scope and ptrace " Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 17/17] landlock: Document tracepoints Mickaël Salaün
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add trace tests for the landlock_deny_access_net tracepoint: denied
bind, allowed bind (no event), denied connect, bind field verification,
connect-after-bind field verification, and an unsandboxed baseline.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
tools/testing/selftests/landlock/net_test.c | 547 +++++++++++++++++++-
1 file changed, 546 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/landlock/net_test.c b/tools/testing/selftests/landlock/net_test.c
index 4c528154ea92..4fe41425995c 100644
--- a/tools/testing/selftests/landlock/net_test.c
+++ b/tools/testing/selftests/landlock/net_test.c
@@ -10,11 +10,12 @@
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
-#include <linux/landlock.h>
#include <linux/in.h>
+#include <linux/landlock.h>
#include <sched.h>
#include <stdint.h>
#include <string.h>
+#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/syscall.h>
@@ -22,6 +23,9 @@
#include "audit.h"
#include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "net_test"
const short sock_port_start = (1 << 10);
@@ -2026,4 +2030,545 @@ TEST_F(audit, connect)
EXPECT_EQ(0, close(sock_fd));
}
+/* Trace tests */
+
+/* clang-format off */
+FIXTURE(trace_net) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_net)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_net)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Baseline: verifies that without Landlock, the bind succeeds and no
+ * deny_access_net trace event fires.
+ */
+/* clang-format off */
+FIXTURE_VARIANT(trace_net)
+{
+ /* clang-format on */
+ bool sandbox;
+ int bind_port_offset; /* 0 = allowed port, 1 = denied port */
+ int expect_denied;
+};
+
+/* Unsandboxed: no Landlock, bind should succeed with no events. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_net, unsandboxed) {
+ /* clang-format on */
+ .sandbox = false,
+ .bind_port_offset = 0,
+ .expect_denied = 0,
+};
+
+/* Denied: sandboxed, bind to port not in ruleset. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_net, bind_denied) {
+ /* clang-format on */
+ .sandbox = true,
+ .bind_port_offset = 1,
+ .expect_denied = 1,
+};
+
+/* Allowed: sandboxed, bind to port in ruleset. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_net, bind_allowed) {
+ /* clang-format on */
+ .sandbox = true,
+ .bind_port_offset = 0,
+ .expect_denied = 0,
+};
+
+TEST_F(trace_net, deny_access_net_bind)
+{
+ char *buf;
+ int count, status;
+ pid_t child;
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ int sock_fd;
+
+ if (variant->sandbox) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net =
+ LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ struct landlock_net_port_attr port_attr = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .port = sock_port_start,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(
+ &ruleset_attr, sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ if (landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_NET_PORT,
+ &port_attr, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+ }
+
+ sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock_fd < 0)
+ _exit(1);
+
+ addr.sin_port =
+ htons(sock_port_start + variant->bind_port_offset);
+ if (variant->expect_denied) {
+ /* Bind should be denied. */
+ if (bind(sock_fd, (struct sockaddr *)&addr,
+ sizeof(addr)) == 0) {
+ close(sock_fd);
+ _exit(2);
+ }
+ if (errno != EACCES) {
+ close(sock_fd);
+ _exit(3);
+ }
+ } else {
+ /* Bind should succeed. */
+ if (bind(sock_fd, (struct sockaddr *)&addr,
+ sizeof(addr))) {
+ close(sock_fd);
+ _exit(2);
+ }
+ }
+ close(sock_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK));
+ if (variant->expect_denied) {
+ EXPECT_LE(variant->expect_denied, count)
+ {
+ TH_LOG("Expected deny_access_net event, got %d\n%s",
+ count, buf);
+ }
+ } else {
+ EXPECT_EQ(0, count)
+ {
+ TH_LOG("Expected 0 deny_access_net events, "
+ "got %d\n%s",
+ count, buf);
+ }
+ }
+
+ free(buf);
+}
+
+/* Connect and field-check tests use a separate fixture without variants. */
+
+/* clang-format off */
+FIXTURE(trace_net_connect) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_net_connect)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_net_connect)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/*
+ * Verifies that a denied connect emits a deny_access_net trace event with
+ * sport=0 and dport=<denied_port>.
+ */
+TEST_F(trace_net_connect, deny_access_net_connect_denied)
+{
+ pid_t child;
+ int status;
+ char *buf;
+ char field[64], expected[16];
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ };
+ struct landlock_net_port_attr port_attr = {
+ .allowed_access = LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ .port = sock_port_start,
+ };
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ int ruleset_fd, sock_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &port_attr, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ /* Connect to denied port. */
+ sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock_fd < 0)
+ _exit(1);
+
+ addr.sin_port = htons(sock_port_start + 1);
+ if (connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) ==
+ 0) {
+ close(sock_fd);
+ _exit(2);
+ }
+ if (errno != EACCES) {
+ close(sock_fd);
+ _exit(3);
+ }
+ close(sock_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1, tracefs_count_matches(buf,
+ REGEX_DENY_ACCESS_NET(TRACE_TASK)));
+
+ /*
+ * Verify dport is the denied port and sport is 0. The port
+ * value must be in host endianness, matching the UAPI convention
+ * (landlock_net_port_attr.port). On little-endian,
+ * htons(sock_port_start + 1) would produce a different decimal
+ * value, so this comparison also catches byte-order bugs.
+ */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+ "sport", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+ "dport", field, sizeof(field)));
+ snprintf(expected, sizeof(expected), "%llu",
+ (unsigned long long)(sock_port_start + 1));
+ EXPECT_STREQ(expected, field);
+
+ free(buf);
+}
+
+/* Verifies that a denied bind emits sport=<port> dport=0. */
+TEST_F(trace_net_connect, deny_access_net_bind_fields)
+{
+ pid_t child;
+ int status;
+ char *buf;
+ char field[64], expected[16];
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ struct landlock_net_port_attr port_attr = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .port = sock_port_start,
+ };
+ struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ int ruleset_fd, sock_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &port_attr, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ /* Bind to denied port. */
+ sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock_fd < 0)
+ _exit(1);
+
+ addr.sin_port = htons(sock_port_start + 1);
+ if (bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)) ==
+ 0) {
+ close(sock_fd);
+ _exit(2);
+ }
+ if (errno != EACCES) {
+ close(sock_fd);
+ _exit(3);
+ }
+ close(sock_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1, tracefs_count_matches(buf,
+ REGEX_DENY_ACCESS_NET(TRACE_TASK)));
+
+ /* Verify sport is the denied port and dport is 0. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+ "dport", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+ "sport", field, sizeof(field)));
+ snprintf(expected, sizeof(expected), "%llu",
+ (unsigned long long)(sock_port_start + 1));
+ EXPECT_STREQ(expected, field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that a denied connect after a successful bind shows sport=0 and
+ * dport=<denied_port>. The bind succeeds (allowed port), then the connect is
+ * denied. sport=0 because the denied operation is connect, not bind.
+ */
+TEST_F(trace_net_connect, deny_access_net_connect_after_bind)
+{
+ pid_t child;
+ int status;
+ char *buf;
+ char field[64], expected[16];
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ };
+ struct landlock_net_port_attr port_attr;
+ struct sockaddr_in bind_addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(sock_port_start),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ struct sockaddr_in conn_addr = {
+ .sin_family = AF_INET,
+ .sin_port = htons(sock_port_start + 1),
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ };
+ int ruleset_fd, sock_fd, optval = 1;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ /* Allow bind and connect on sock_port_start only. */
+ port_attr.allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP |
+ LANDLOCK_ACCESS_NET_CONNECT_TCP;
+ port_attr.port = sock_port_start;
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &port_attr, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sock_fd < 0)
+ _exit(1);
+ setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &optval,
+ sizeof(optval));
+
+ /* Bind to allowed port (succeeds, no trace event). */
+ if (bind(sock_fd, (struct sockaddr *)&bind_addr,
+ sizeof(bind_addr))) {
+ close(sock_fd);
+ _exit(1);
+ }
+
+ /* Connect to denied port (fails, emits trace event). */
+ if (connect(sock_fd, (struct sockaddr *)&conn_addr,
+ sizeof(conn_addr)) == 0) {
+ close(sock_fd);
+ _exit(2);
+ }
+ if (errno != EACCES) {
+ close(sock_fd);
+ _exit(3);
+ }
+ close(sock_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1, tracefs_count_matches(buf,
+ REGEX_DENY_ACCESS_NET(TRACE_TASK)));
+
+ /*
+ * The denied operation is connect, so sport=0 and dport=<denied_port>,
+ * regardless of the prior bind.
+ */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+ "sport", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_NET(TRACE_TASK),
+ "dport", field, sizeof(field)));
+ snprintf(expected, sizeof(expected), "%llu",
+ (unsigned long long)(sock_port_start + 1));
+ EXPECT_STREQ(expected, field);
+
+ free(buf);
+}
+
+/*
+ * IPv6 network trace tests are intentionally elided. IPv6 hook dispatch uses
+ * the same current_check_access_socket() code path as IPv4, validated by the
+ * audit tests in this file. The trace events use the same blockers/sport/dport
+ * fields regardless of address family.
+ */
+
TEST_HARNESS_MAIN
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 16/17] selftests/landlock: Add scope and ptrace tracepoint tests
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (14 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 15/17] selftests/landlock: Add network " Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 17/17] landlock: Document tracepoints Mickaël Salaün
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add trace tests for the landlock_deny_ptrace,
landlock_deny_scope_signal, and landlock_deny_scope_abstract_unix_socket
tracepoints, following the audit test pattern of placing tests alongside
the functional tests for each subsystem.
The ptrace trace test verifies that the landlock_deny_ptrace event fires
when a sandboxed child attempts to ptrace an unsandboxed parent. The
signal and unix socket tests verify the corresponding scope tracepoints
fire on denied operations.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
.../testing/selftests/landlock/ptrace_test.c | 164 +++++++++++++++
.../landlock/scoped_abstract_unix_test.c | 195 ++++++++++++++++++
.../selftests/landlock/scoped_signal_test.c | 150 ++++++++++++++
3 files changed, 509 insertions(+)
diff --git a/tools/testing/selftests/landlock/ptrace_test.c b/tools/testing/selftests/landlock/ptrace_test.c
index 1b6c8b53bf33..a72035d1c27b 100644
--- a/tools/testing/selftests/landlock/ptrace_test.c
+++ b/tools/testing/selftests/landlock/ptrace_test.c
@@ -11,7 +11,9 @@
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
+#include <sched.h>
#include <signal.h>
+#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/types.h>
@@ -20,6 +22,7 @@
#include "audit.h"
#include "common.h"
+#include "trace.h"
/* Copied from security/yama/yama_lsm.c */
#define YAMA_SCOPE_DISABLED 0
@@ -429,4 +432,165 @@ TEST_F(audit, trace)
EXPECT_EQ(0, records.domain);
}
+/* Trace tests */
+
+/* clang-format off */
+FIXTURE(trace_ptrace) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_ptrace)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_ptrace)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false);
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/* clang-format off */
+FIXTURE_VARIANT(trace_ptrace)
+{
+ /* clang-format on */
+ bool sandbox;
+ int expect_denied;
+};
+
+/* Denied: sandboxed child ptraces unsandboxed parent. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_ptrace, denied) {
+ /* clang-format on */
+ .sandbox = true,
+ .expect_denied = 1,
+};
+
+/* Allowed: unsandboxed child uses PTRACE_TRACEME. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_ptrace, allowed) {
+ /* clang-format on */
+ .sandbox = false,
+ .expect_denied = 0,
+};
+
+TEST_F(trace_ptrace, deny_ptrace)
+{
+ char *buf, field[64], expected_pid[16];
+ int count, status;
+ pid_t child, parent;
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ parent = getpid();
+
+ /*
+ * Set a known comm so the denied variant can verify both the trace
+ * line task name and the comm= field.
+ */
+ prctl(PR_SET_NAME, "ll_trace_test");
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ if (variant->sandbox) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .scoped = LANDLOCK_SCOPE_SIGNAL,
+ };
+ int ruleset_fd;
+
+ /*
+ * Any scope creates a domain. Ptrace denial
+ * checks domain ancestry, not specific flags.
+ */
+ ruleset_fd = landlock_create_ruleset(
+ &ruleset_attr, sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ /* PTRACE_ATTACH on unsandboxed parent: denied. */
+ if (ptrace(PTRACE_ATTACH, parent, NULL, NULL) == 0) {
+ ptrace(PTRACE_DETACH, parent, NULL, NULL);
+ _exit(2);
+ }
+ if (errno != EPERM)
+ _exit(3);
+ } else {
+ /* No sandbox: ptrace should succeed. */
+ if (ptrace(PTRACE_TRACEME) != 0)
+ _exit(1);
+ }
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf, REGEX_DENY_PTRACE("ll_trace_test"));
+ if (variant->expect_denied) {
+ EXPECT_LE(variant->expect_denied, count)
+ {
+ TH_LOG("Expected deny_ptrace event, got %d\n%s", count,
+ buf);
+ }
+
+ /* Verify tracee_pid is the parent's TGID. */
+ snprintf(expected_pid, sizeof(expected_pid), "%d", parent);
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PTRACE("ll_trace_test"),
+ "tracee_pid", field, sizeof(field)));
+ EXPECT_STREQ(expected_pid, field);
+
+ /* Verify comm matches prctl(PR_SET_NAME). */
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_PTRACE("ll_trace_test"),
+ "comm", field, sizeof(field)));
+ EXPECT_STREQ("ll_trace_test", field);
+ } else {
+ EXPECT_EQ(0, count)
+ {
+ TH_LOG("Expected 0 deny_ptrace events, got %d\n%s",
+ count, buf);
+ }
+ }
+
+ free(buf);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
index c47491d2d1c1..444df8ead1bf 100644
--- a/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
+++ b/tools/testing/selftests/landlock/scoped_abstract_unix_test.c
@@ -12,6 +12,7 @@
#include <sched.h>
#include <signal.h>
#include <stddef.h>
+#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
@@ -23,6 +24,9 @@
#include "audit.h"
#include "common.h"
#include "scoped_common.h"
+#include "trace.h"
+
+#define TRACE_TASK "scoped_abstract"
/* Number of pending connections queue to be hold. */
const short backlog = 10;
@@ -1145,4 +1149,195 @@ TEST(self_connect)
_metadata->exit_code = KSFT_FAIL;
}
+/* Trace tests */
+
+/* clang-format off */
+FIXTURE(trace_unix) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_unix)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0, tracefs_enable_event(
+ TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
+ true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_unix)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
+ false);
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/* clang-format off */
+FIXTURE_VARIANT(trace_unix)
+{
+ /* clang-format on */
+ bool sandbox;
+ int expect_denied;
+};
+
+/* Denied: sandboxed child connects to unsandboxed parent's socket. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_unix, denied) {
+ /* clang-format on */
+ .sandbox = true,
+ .expect_denied = 1,
+};
+
+/* Allowed: unsandboxed child connects. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_unix, allowed) {
+ /* clang-format on */
+ .sandbox = false,
+ .expect_denied = 0,
+};
+
+TEST_F(trace_unix, deny_scope_unix)
+{
+ struct sockaddr_un addr = {
+ .sun_family = AF_UNIX,
+ };
+ char *buf, field[128], expected_path[64], expected_pid[16];
+ int server_fd, client_fd, count, status;
+ pid_t child;
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ /* Create an abstract unix socket server in the parent. */
+ server_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ ASSERT_LE(0, server_fd);
+
+ addr.sun_path[0] = '\0';
+ snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1,
+ "landlock_trace_test_%d", getpid());
+
+ ASSERT_EQ(0, bind(server_fd, (struct sockaddr *)&addr,
+ offsetof(struct sockaddr_un, sun_path) + 1 +
+ strlen(addr.sun_path + 1)));
+ ASSERT_EQ(0, listen(server_fd, 1));
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ if (variant->sandbox) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .scoped = LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(
+ &ruleset_attr, sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+ }
+
+ client_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (client_fd < 0)
+ _exit(1);
+
+ if (variant->sandbox) {
+ /* Connect should be denied. */
+ if (connect(client_fd, (struct sockaddr *)&addr,
+ offsetof(struct sockaddr_un, sun_path) + 1 +
+ strlen(addr.sun_path + 1)) == 0) {
+ close(client_fd);
+ _exit(2);
+ }
+ if (errno != EPERM) {
+ close(client_fd);
+ _exit(3);
+ }
+ } else {
+ /* No sandbox: connect should succeed. */
+ if (connect(client_fd, (struct sockaddr *)&addr,
+ offsetof(struct sockaddr_un, sun_path) + 1 +
+ strlen(addr.sun_path + 1)) != 0) {
+ close(client_fd);
+ _exit(2);
+ }
+ }
+ close(client_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+ close(server_fd);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(
+ buf, REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(TRACE_TASK));
+ if (variant->expect_denied) {
+ EXPECT_LE(variant->expect_denied, count)
+ {
+ TH_LOG("Expected deny_scope_abstract_unix_socket "
+ "event, got %d\n%s",
+ count, buf);
+ }
+
+ /* Verify sun_path (trace skips the leading NUL). */
+ snprintf(expected_path, sizeof(expected_path),
+ "landlock_trace_test_%d", getpid());
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf,
+ REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(
+ TRACE_TASK),
+ "sun_path", field, sizeof(field)));
+ EXPECT_STREQ(expected_path, field);
+
+ /* Verify peer_pid is the parent's PID. */
+ snprintf(expected_pid, sizeof(expected_pid), "%d", getpid());
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf,
+ REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(
+ TRACE_TASK),
+ "peer_pid", field, sizeof(field)));
+ EXPECT_STREQ(expected_pid, field);
+ } else {
+ EXPECT_EQ(0, count)
+ {
+ TH_LOG("Expected 0 deny_scope events, got %d\n%s",
+ count, buf);
+ }
+ }
+
+ free(buf);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c
index d8bf33417619..811dc4b9358d 100644
--- a/tools/testing/selftests/landlock/scoped_signal_test.c
+++ b/tools/testing/selftests/landlock/scoped_signal_test.c
@@ -10,7 +10,9 @@
#include <fcntl.h>
#include <linux/landlock.h>
#include <pthread.h>
+#include <sched.h>
#include <signal.h>
+#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
@@ -18,6 +20,9 @@
#include "common.h"
#include "scoped_common.h"
+#include "trace.h"
+
+#define TRACE_TASK "scoped_signal_t"
/* This variable is used for handling several signals. */
static volatile sig_atomic_t is_signaled;
@@ -559,4 +564,149 @@ TEST_F(fown, sigurg_socket)
_metadata->exit_code = KSFT_FAIL;
}
+/* Trace tests */
+
+/* clang-format off */
+FIXTURE(trace_signal) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace_signal)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace_signal)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, false);
+ tracefs_fixture_teardown();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+/* clang-format off */
+FIXTURE_VARIANT(trace_signal)
+{
+ /* clang-format on */
+ bool sandbox;
+ int expect_denied;
+};
+
+/* Denied: sandboxed child signals unsandboxed parent. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_signal, denied) {
+ /* clang-format on */
+ .sandbox = true,
+ .expect_denied = 1,
+};
+
+/* Allowed: unsandboxed child signals unsandboxed parent. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(trace_signal, allowed) {
+ /* clang-format on */
+ .sandbox = false,
+ .expect_denied = 0,
+};
+
+TEST_F(trace_signal, deny_scope_signal)
+{
+ char *buf, field[64], expected_pid[16];
+ int count, status;
+ pid_t child;
+
+ if (!self->tracefs_ok)
+ SKIP(return, "tracefs not available");
+
+ child = fork();
+ ASSERT_LE(0, child);
+
+ if (child == 0) {
+ if (variant->sandbox) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .scoped = LANDLOCK_SCOPE_SIGNAL,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(
+ &ruleset_attr, sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+ }
+
+ if (variant->sandbox) {
+ /* Signal to unsandboxed parent should be denied. */
+ if (kill(getppid(), 0) == 0)
+ _exit(2);
+ if (errno != EPERM)
+ _exit(3);
+ } else {
+ /* No sandbox: kill should succeed. */
+ if (kill(getppid(), 0) != 0)
+ _exit(1);
+ }
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK));
+ if (variant->expect_denied) {
+ EXPECT_LE(variant->expect_denied, count)
+ {
+ TH_LOG("Expected deny_scope_signal event, got %d\n%s",
+ count, buf);
+ }
+
+ /* Verify target_pid is the parent's PID. */
+ snprintf(expected_pid, sizeof(expected_pid), "%d", getpid());
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK),
+ "target_pid", field, sizeof(field)));
+ EXPECT_STREQ(expected_pid, field);
+ } else {
+ EXPECT_EQ(0, count)
+ {
+ TH_LOG("Expected 0 deny_scope_signal events, "
+ "got %d\n%s",
+ count, buf);
+ }
+ }
+
+ free(buf);
+}
+
TEST_HARNESS_MAIN
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* [PATCH v2 17/17] landlock: Document tracepoints
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
` (15 preceding siblings ...)
2026-04-06 14:37 ` [PATCH v2 16/17] selftests/landlock: Add scope and ptrace " Mickaël Salaün
@ 2026-04-06 14:37 ` Mickaël Salaün
16 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-06 14:37 UTC (permalink / raw)
To: Christian Brauner, Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Jann Horn, Jeff Xu, Justin Suess,
Kees Cook, Masami Hiramatsu, Mathieu Desnoyers, Matthieu Buffet,
Mikhail Ivanov, Tingmao Wang, kernel-team, linux-fsdevel,
linux-security-module, linux-trace-kernel
Add tracepoint documentation to the kernel security documentation.
Describe the complete lifecycle of trace events (create, deny, free),
the enriched denial fields (same_exec, log_same_exec, log_new_exec), and
the design for both stateful (eBPF) and stateless (ftrace) consumers.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v1:
- New patch.
---
Documentation/admin-guide/LSM/landlock.rst | 210 ++++++++++++++++++++-
Documentation/security/landlock.rst | 35 +++-
Documentation/trace/events-landlock.rst | 160 ++++++++++++++++
Documentation/trace/index.rst | 1 +
Documentation/userspace-api/landlock.rst | 11 +-
5 files changed, 412 insertions(+), 5 deletions(-)
create mode 100644 Documentation/trace/events-landlock.rst
diff --git a/Documentation/admin-guide/LSM/landlock.rst b/Documentation/admin-guide/LSM/landlock.rst
index 9923874e2156..cad5845b6ec7 100644
--- a/Documentation/admin-guide/LSM/landlock.rst
+++ b/Documentation/admin-guide/LSM/landlock.rst
@@ -1,12 +1,13 @@
.. SPDX-License-Identifier: GPL-2.0
.. Copyright © 2025 Microsoft Corporation
+.. Copyright © 2026 Cloudflare
================================
Landlock: system-wide management
================================
:Author: Mickaël Salaün
-:Date: January 2026
+:Date: April 2026
Landlock can leverage the audit framework to log events.
@@ -176,11 +177,218 @@ filters to limit noise with two complementary ways:
programs,
- or with audit rules (see :manpage:`auditctl(8)`).
+Tracepoints
+===========
+
+Landlock also provides tracepoints as an alternative to audit for
+debugging and observability. Tracepoints fire unconditionally,
+independent of audit configuration, ``audit_enabled``, and domain log
+flags. This makes them suitable for always-on monitoring with eBPF or
+for ad-hoc debugging with ``trace-pipe``. See
+:doc:`/trace/events-landlock` for the complete event reference.
+
+Enabling tracepoints
+--------------------
+
+Enable individual Landlock tracepoints via tracefs::
+
+ # Enable filesystem denial tracing:
+ echo 1 > /sys/kernel/tracing/events/landlock/landlock_deny_access_fs/enable
+
+ # Enable all Landlock events:
+ echo 1 > /sys/kernel/tracing/events/landlock/enable
+
+ # Read the trace output:
+ cat /sys/kernel/tracing/trace_pipe
+
+Available events
+----------------
+
+**Policy setup events:**
+
+- ``landlock_create_ruleset`` -- emitted when a ruleset is created.
+ Fields: ``ruleset`` (ID and version), ``handled_fs``, ``handled_net``,
+ ``scoped``.
+
+- ``landlock_add_rule_fs``, ``landlock_add_rule_net`` -- emitted when a
+ rule is added. Fields: ``ruleset`` (ID and version),
+ ``access_rights`` (access mask),
+ target identifier (``dev:ino`` and ``path`` for FS, ``port`` for net).
+
+- ``landlock_restrict_self`` -- emitted when a task restricts itself.
+ Fields: ``ruleset`` (ID and version), ``domain`` (new domain ID),
+ ``parent`` (parent domain ID or 0).
+
+**Access check events (hot path):**
+
+- ``landlock_check_rule_fs``, ``landlock_check_rule_net`` -- emitted
+ when a rule matches during an access check. Fires for every matching
+ rule in the pathwalk, regardless of the final outcome (allowed or
+ denied).
+
+**Denial events:**
+
+- ``landlock_deny_access_fs``, ``landlock_deny_access_net`` -- emitted
+ when a filesystem or network access is denied.
+- ``landlock_deny_ptrace``, ``landlock_deny_scope_signal``,
+ ``landlock_deny_scope_abstract_unix_socket`` -- emitted when a scope
+ check denies access.
+
+ Common fields include:
+
+ - ``domain`` -- the denying domain's ID
+ - ``blockers`` -- the denied access rights (bitmask,
+ ``deny_access_fs`` and ``deny_access_net`` only)
+ - ``same_exec`` -- whether the task is the same executable that
+ called ``landlock_restrict_self()`` for the denying domain
+ - ``log_same_exec``, ``log_new_exec`` -- the domain's configured log
+ flags (useful for filtering expected denials)
+ - Type-specific fields: ``path`` (FS), ``sport``/``dport`` (net),
+ ``tracee_pid``/``comm`` (ptrace), ``target_pid``/``comm`` (signal),
+ ``peer_pid``/``sun_path`` (abstract unix socket)
+
+**Lifecycle events:**
+
+- ``landlock_free_domain`` -- emitted when a domain is deallocated.
+ Fields: ``domain`` (ID), ``denials`` (total denial count).
+- ``landlock_free_ruleset`` -- emitted when a ruleset is freed.
+ Fields: ``ruleset`` (ID and version).
+
+Event samples
+-------------
+
+A sandboxed program tries to read ``/etc/passwd`` with only ``/tmp``
+writable::
+
+ $ echo 1 > /sys/kernel/tracing/events/landlock/enable
+ $ LL_FS_RO=/ LL_FS_RW=/tmp ./sandboxer cat /etc/passwd &
+ $ cat /sys/kernel/tracing/trace_pipe
+ sandboxer-286 landlock_create_ruleset: ruleset=10b556c58.0 handled_fs=0xdfff handled_net=0x0 scoped=0x0
+ sandboxer-286 landlock_restrict_self: ruleset=10b556c58.3 domain=10b556c61 parent=0
+ cat-287 landlock_deny_access_fs: domain=10b556c61 same_exec=0 log_same_exec=1 log_new_exec=0 blockers=0x4 dev=254:2 ino=143821 path=/etc/passwd
+ kworker/0:1-12 landlock_free_domain: domain=10b556c61 denials=1
+
+Unlike audit, tracepoints fire for all denials regardless of the
+domain's log flags. This means ``deny_access_*`` events appear even
+when ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF`` would suppress the
+corresponding audit record.
+
+Filtering with ftrace
+---------------------
+
+Use ftrace filter expressions to select specific events::
+
+ # Only show denials that audit would also log:
+ echo 'same_exec == 1 && log_same_exec == 1 || same_exec == 0 && log_new_exec == 1' > \
+ /sys/kernel/tracing/events/landlock/landlock_deny_access_fs/filter
+
+Using eBPF
+----------
+
+eBPF programs can attach to Landlock tracepoints to build custom
+monitoring. A stateful eBPF program observes the full event stream and
+maintains per-domain state in BPF maps:
+
+1. On ``landlock_restrict_self``: record the domain ID, parent, flags.
+2. On ``landlock_deny_access_*``: look up the domain, decide whether
+ to count, alert, or ignore the denial based on custom policy.
+3. On ``landlock_free_domain``: clean up the per-domain state, log
+ final statistics.
+
+This approach requires no kernel modification and no Landlock-specific
+BPF helpers. The Landlock IDs serve as correlation keys across events.
+
+.. _landlock_observability:
+
+When to use tracing vs audit
+-----------------------------
+
+Audit and tracing both help diagnose Landlock policy issues:
+
+**Audit** records denied accesses with the blockers, domain, and object
+identification (path, port). Audit is the standard Linux mechanism for
+security events, with a stable record format that is well established
+and already supported by log management systems, SIEM platforms, and EDR
+solutions. Audit is always active (when ``CONFIG_AUDIT`` is set),
+filtered by log flags to reduce noise in production, and designed for
+long-term security monitoring and compliance.
+
+**Tracing** provides deeper introspection for policy debugging. In
+addition to denied accesses, trace events cover the complete lifecycle
+of Landlock objects (rulesets, domains) and intermediate rule matching
+during access checks. Trace events are disabled by default (zero
+overhead) and fire unconditionally, regardless of log flags. eBPF
+programs attached to trace events can access the full kernel context
+(ruleset rules, domain hierarchy, process credentials) via BTF, enabling
+richer analysis than the flat fields in audit records. For example, an
+eBPF-based live monitoring tool can correlate creation, rule-addition,
+and denial events to build a real-time view of all active Landlock
+domains and their policies. However, BTF-based access depends on
+internal kernel struct layouts which have no stability guarantee. CO-RE
+(Compile Once, Run Everywhere) provides best-effort field relocation.
+The ftrace printk format is also not a stable ABI, but is
+self-describing via the per-event ``format`` file, allowing tools to
+adapt dynamically.
+
+Observability guarantees and limitations
+-----------------------------------------
+
+Both audit records and trace events are emitted for every denied access,
+with these exceptions:
+
+- **Log flags** (audit only): ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``,
+ ``LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON``, and
+ ``LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF`` control which denials
+ generate audit records. Trace events fire regardless of these flags.
+
+- **NOAUDIT hooks**: Some LSM hooks suppress logging for speculative
+ permission probes (e.g., reading ``/proc/<pid>/status`` uses
+ ``PTRACE_MODE_NOAUDIT``). When NOAUDIT is set, neither audit records
+ nor trace events are emitted, and the denial is not counted in
+ ``denials``. The denial is still enforced. This avoids performance
+ overhead and noise from speculative probes that test permissions
+ without performing an actual access.
+
+- **Audit rate limiting**: The audit subsystem may silently drop records
+ when the audit queue is full. Trace events are not rate-limited.
+
+- **Tracepoint disabled**: When a trace event is disabled (the default
+ state), the tracepoint is a no-op with zero overhead.
+
+When both audit and tracing are active, every logged denial produces both
+an audit record (subject to log flags) and a trace event. The
+``denials`` count in ``free_domain`` events reflects the total number of
+logged denials, which may be lower than the actual number of enforced
+denials due to NOAUDIT hooks.
+
+.. _landlock_observability_security:
+
+Observability security considerations
+---------------------------------------
+
+Both audit records and trace events expose information about all
+Landlock-sandboxed processes on the system, including filesystem paths
+being accessed, network ports, and process identities. System
+administrators must ensure that access to audit logs (controlled by the
+audit subsystem configuration) and to trace events (requiring
+``CAP_SYS_ADMIN`` or ``CAP_BPF`` + ``CAP_PERFMON``) is restricted to
+trusted users.
+
+eBPF programs attached to Landlock trace events have access to the full
+kernel context of each event (ruleset rules, domain hierarchy, process
+credentials) via BTF. This level of access is comparable to
+``CAP_SYS_ADMIN`` and must be treated accordingly.
+
+Audit logs and kernel trace events require elevated privileges and are
+system-wide; they are not designed for per-sandbox unprivileged
+monitoring.
+
Additional documentation
========================
* `Linux Audit Documentation`_
* Documentation/userspace-api/landlock.rst
+* Documentation/trace/events-landlock.rst
* Documentation/security/landlock.rst
* https://landlock.io
diff --git a/Documentation/security/landlock.rst b/Documentation/security/landlock.rst
index c5186526e76f..5ef0164fbafb 100644
--- a/Documentation/security/landlock.rst
+++ b/Documentation/security/landlock.rst
@@ -1,13 +1,14 @@
.. SPDX-License-Identifier: GPL-2.0
.. Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
.. Copyright © 2019-2020 ANSSI
+.. Copyright © 2026 Cloudflare
==================================
Landlock LSM: kernel documentation
==================================
:Author: Mickaël Salaün
-:Date: March 2026
+:Date: April 2026
Landlock's goal is to create scoped access-control (i.e. sandboxing). To
harden a whole system, this feature should be available to any process,
@@ -177,11 +178,43 @@ makes the reasoning much easier and helps avoid pitfalls.
.. kernel-doc:: security/landlock/domain.h
:identifiers:
+Denial logging
+==============
+
+Access denials are logged through two independent channels: audit
+records and tracepoints. Both are managed by the common denial
+framework in ``log.c``, compiled under ``CONFIG_SECURITY_LANDLOCK_LOG``
+(automatically selected by ``CONFIG_AUDIT`` or ``CONFIG_TRACEPOINTS``).
+
+Audit records respect audit configuration, domain log flags, and
+``LANDLOCK_LOG_DISABLED``. Tracepoints fire unconditionally,
+independent of audit configuration and domain log flags. The denial
+counter (``num_denials``) is always incremented regardless of logging
+configuration.
+
+See Documentation/admin-guide/LSM/landlock.rst for audit record format,
+tracepoint usage, and filtering examples.
+
+.. kernel-doc:: security/landlock/log.h
+ :identifiers:
+
+Trace events
+------------
+
+See :doc:`/trace/events-landlock` for trace event usage and format details.
+
+.. kernel-doc:: include/trace/events/landlock.h
+ :doc: Landlock trace events
+
+.. kernel-doc:: include/trace/events/landlock.h
+ :internal:
+
Additional documentation
========================
* Documentation/userspace-api/landlock.rst
* Documentation/admin-guide/LSM/landlock.rst
+* Documentation/trace/events-landlock.rst
* https://landlock.io
.. Links
diff --git a/Documentation/trace/events-landlock.rst b/Documentation/trace/events-landlock.rst
new file mode 100644
index 000000000000..802df09259ce
--- /dev/null
+++ b/Documentation/trace/events-landlock.rst
@@ -0,0 +1,160 @@
+.. SPDX-License-Identifier: GPL-2.0
+.. Copyright © 2026 Cloudflare
+
+=====================
+Landlock Trace Events
+=====================
+
+:Date: April 2026
+
+Landlock emits trace events for sandbox lifecycle operations and access
+denials. These events can be consumed by ftrace (for human-readable
+trace output and filtering) and by eBPF programs (for programmatic
+introspection via BTF).
+
+See Documentation/security/landlock.rst for Landlock kernel internals and
+Documentation/admin-guide/LSM/landlock.rst for system administration.
+
+.. warning::
+
+ Landlock trace events, like audit records, expose sensitive
+ information about all sandboxed processes on the system. See
+ :ref:`landlock_observability_security` for security considerations
+ and privilege requirements.
+
+See Documentation/userspace-api/landlock.rst for the userspace API.
+
+Event overview
+==============
+
+Landlock trace events are organized in four categories:
+
+**Syscall events** are emitted during Landlock system calls:
+
+- ``landlock_create_ruleset``: a new ruleset is created
+- ``landlock_add_rule_fs``: a filesystem rule is added to a ruleset
+- ``landlock_add_rule_net``: a network port rule is added to a ruleset
+- ``landlock_restrict_self``: a new domain is created from a ruleset
+
+**Denial events** are emitted when an access is denied:
+
+- ``landlock_deny_access_fs``: filesystem access denied
+- ``landlock_deny_access_net``: network access denied
+- ``landlock_deny_ptrace``: ptrace access denied
+- ``landlock_deny_scope_signal``: signal delivery denied
+- ``landlock_deny_scope_abstract_unix_socket``: abstract unix socket
+ access denied
+
+**Rule evaluation events** are emitted during rule matching:
+
+- ``landlock_check_rule_fs``: a filesystem rule is evaluated
+- ``landlock_check_rule_net``: a network port rule is evaluated
+
+**Lifecycle events**:
+
+- ``landlock_free_domain``: a domain is freed
+- ``landlock_free_ruleset``: a ruleset is freed
+
+Enabling events
+===============
+
+Enable all Landlock events::
+
+ echo 1 > /sys/kernel/tracing/events/landlock/enable
+
+Enable a specific event::
+
+ echo 1 > /sys/kernel/tracing/events/landlock/landlock_deny_access_fs/enable
+
+Read the trace output::
+
+ cat /sys/kernel/tracing/trace_pipe
+
+Differences from audit records
+==============================
+
+Tracepoints and audit records both log Landlock denials, but differ
+in some field formats:
+
+- **Paths**: Tracepoints use ``d_absolute_path()`` (namespace-independent
+ absolute paths). Audit uses ``d_path()`` (relative to the process's
+ chroot). Tracepoint paths are deterministic regardless of the tracer's
+ mount namespace.
+
+- **Device names**: Tracepoints use numeric ``dev=<major>:<minor>``.
+ Audit uses string ``dev="<s_id>"``. Numeric format is more precise
+ for machine parsing.
+
+- **Denied access field**: The ``deny_access_fs`` and ``deny_access_net``
+ tracepoints use the ``blockers=`` field name (same as audit).
+ Audit uses human-readable access right names (e.g.,
+ ``blockers=fs.read_file``), while tracepoints use a hex bitmask
+ (e.g., ``blockers=0x4``). Scope and ptrace tracepoints omit
+ ``blockers`` because the event name identifies the denial type.
+
+- **Scope target names**: Tracepoints use role-specific field names
+ (``tracee_pid``, ``target_pid``, ``peer_pid``) that reflect the
+ semantic of each event. Audit uses generic names (``opid``, ``ocomm``)
+ because the audit log format is not event-type-specific.
+
+- **Process name**: Scope tracepoints include ``comm=`` in the printk
+ output for stateless consumers. eBPF consumers can read ``comm``
+ directly from the task_struct via BTF. The ``comm`` value is treated
+ as untrusted input (escaped via ``__print_untrusted_str``).
+
+Ruleset versioning
+==================
+
+Syscall events include a ruleset version (``ruleset=<hex_id>.<version>``)
+that tracks the number of rules added to the ruleset. The version is
+incremented on each ``landlock_add_rule()`` call and frozen at
+``landlock_restrict_self()`` time. This enables trace consumers to
+correlate a domain with the exact set of rules it was created from.
+
+eBPF access
+===========
+
+eBPF programs attached via ``BPF_RAW_TRACEPOINT`` can access the
+tracepoint arguments directly through BTF. The arguments include both
+standard kernel objects and Landlock-internal objects:
+
+- Standard kernel objects (``struct task_struct``, ``struct sock``,
+ ``struct path``, ``struct dentry``) can be used with existing BPF
+ helpers.
+- Landlock-internal objects (``struct landlock_domain``,
+ ``struct landlock_ruleset``, ``struct landlock_rule``,
+ ``struct landlock_hierarchy``) can be read via ``BPF_CORE_READ``.
+ Internal struct layouts may change between kernel versions; use CO-RE
+ for field relocation.
+
+All pointer arguments in the tracepoint prototypes are guaranteed
+non-NULL.
+
+Audit filtering equivalence
+============================
+
+Denial events include ``same_exec``, ``log_same_exec``, and
+``log_new_exec`` fields. These allow both stateless (ftrace filter)
+and stateful (eBPF) consumers to replicate the audit subsystem's
+filtering logic::
+
+ # Show only denials that audit would also log:
+ echo 'same_exec==1 && log_same_exec==1 || same_exec==0 && log_new_exec==1' > \
+ /sys/kernel/tracing/events/landlock/landlock_deny_access_fs/filter
+
+Event reference
+===============
+
+.. kernel-doc:: include/trace/events/landlock.h
+ :doc: Landlock trace events
+
+.. kernel-doc:: include/trace/events/landlock.h
+ :internal:
+
+Additional documentation
+========================
+
+* Documentation/userspace-api/landlock.rst
+* Documentation/admin-guide/LSM/landlock.rst
+* Documentation/security/landlock.rst
+* https://landlock.io
diff --git a/Documentation/trace/index.rst b/Documentation/trace/index.rst
index 338bc4d7cfab..d60e010e042b 100644
--- a/Documentation/trace/index.rst
+++ b/Documentation/trace/index.rst
@@ -54,6 +54,7 @@ applications.
events-power
events-nmi
events-msr
+ events-landlock
events-pci
boottime-trace
histogram
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index fd8b78c31f2f..e65370212aa1 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -8,7 +8,7 @@ Landlock: unprivileged access control
=====================================
:Author: Mickaël Salaün
-:Date: March 2026
+:Date: April 2026
The goal of Landlock is to enable restriction of ambient rights (e.g. global
filesystem or network access) for a set of processes. Because Landlock
@@ -698,8 +698,12 @@ Starting with the Landlock ABI version 7, it is possible to control logging of
Landlock audit events with the ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``,
``LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON``, and
``LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF`` flags passed to
-sys_landlock_restrict_self(). See Documentation/admin-guide/LSM/landlock.rst
-for more details on audit.
+sys_landlock_restrict_self(). These flags control audit record generation.
+Landlock tracepoints are not affected by these flags and always fire when
+enabled, providing an alternative observability channel for debugging and
+monitoring. See :doc:`/admin-guide/LSM/landlock` for more details
+on audit and tracepoints, and :doc:`/trace/events-landlock` for the
+complete trace event reference.
Thread synchronization (ABI < 8)
--------------------------------
@@ -814,6 +818,7 @@ Additional documentation
========================
* Documentation/admin-guide/LSM/landlock.rst
+* Documentation/trace/events-landlock.rst
* Documentation/security/landlock.rst
* https://landlock.io
--
2.53.0
^ permalink raw reply related [flat|nested] 20+ messages in thread
* Re: [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials
2026-04-06 14:37 ` [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials Mickaël Salaün
@ 2026-04-06 15:01 ` Steven Rostedt
2026-04-07 13:00 ` Mickaël Salaün
0 siblings, 1 reply; 20+ messages in thread
From: Steven Rostedt @ 2026-04-06 15:01 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Christian Brauner, Günther Noack, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-fsdevel, linux-security-module, linux-trace-kernel
On Mon, 6 Apr 2026 16:37:10 +0200
Mickaël Salaün <mic@digikod.net> wrote:
> ---
> include/trace/events/landlock.h | 135 ++++++++++++++++++++++++++++++++
> security/landlock/log.c | 20 +++++
> 2 files changed, 155 insertions(+)
>
> diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
> index 1afab091efba..9f96c9897f44 100644
> --- a/include/trace/events/landlock.h
> +++ b/include/trace/events/landlock.h
> @@ -11,6 +11,7 @@
> #define _TRACE_LANDLOCK_H
>
> #include <linux/tracepoint.h>
> +#include <net/af_unix.h>
>
> struct dentry;
> struct landlock_domain;
> @@ -19,6 +20,7 @@ struct landlock_rule;
> struct landlock_ruleset;
> struct path;
> struct sock;
> +struct task_struct;
>
> /**
> * DOC: Landlock trace events
> @@ -433,6 +435,139 @@ TRACE_EVENT(
> __entry->log_new_exec, __entry->blockers, __entry->sport,
> __entry->dport));
>
> +/**
> + * landlock_deny_ptrace - ptrace access denied
> + * @hierarchy: Hierarchy node that blocked the access (never NULL)
> + * @same_exec: Whether the current task is the same executable that called
> + * landlock_restrict_self() for the denying hierarchy node
> + * @tracee: Target task (never NULL); eBPF can read pid, comm, cred,
> + * namespaces, and cgroup via BTF
> + */
> +TRACE_EVENT(
> + landlock_deny_ptrace,
> +
> + TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
> + const struct task_struct *tracee),
> +
> + TP_ARGS(hierarchy, same_exec, tracee),
> +
> + TP_STRUCT__entry(
> + __field(__u64, domain_id) __field(bool, same_exec)
> + __field(u32, log_same_exec) __field(u32, log_new_exec)
> + __field(pid_t, tracee_pid)
> + __string(tracee_comm, tracee->comm)),
Event formats are different than normal macro formatting. Please use the
event formatting. The above is a defined structure that is being created
for use. Keep it looking like a structure:
TP_STRUCT__entry(
__field( __u64, domain_id)
__field( bool, same_exec)
__field( u32, log_same_exec)
__field( u32, log_new_exec)
__field( pid_t, tracee_pid)
__string( tracee_comm, tracee->comm)
),
See how the above resembles:
struct entry {
__u64 domain_id;
bool same_exec;
u32 log_same_exec;
u32 log_new_exec;
pid_t tracee_pid;
string tracee_comm;
};
Because that's pretty much what the trace event TP_STRUCT__entry() is going
to do with it. (The string will obviously be something else).
This way it's also easy to spot wholes in the structure that is written
into the ring buffer. The "same_exec" being a bool followed by two u32
types, is going to cause a hole. Move it to between tracee_pid and
tracee_comm.
Please fix the other events too.
-- Steve
> +
> + TP_fast_assign(__entry->domain_id = hierarchy->id;
> + __entry->same_exec = same_exec;
> + __entry->log_same_exec = hierarchy->log_same_exec;
> + __entry->log_new_exec = hierarchy->log_new_exec;
> + __entry->tracee_pid =
> + task_tgid_nr((struct task_struct *)tracee);
> + __assign_str(tracee_comm);),
> +
> + TP_printk(
> + "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u tracee_pid=%d comm=%s",
> + __entry->domain_id, __entry->same_exec, __entry->log_same_exec,
> + __entry->log_new_exec, __entry->tracee_pid,
> + __print_untrusted_str(tracee_comm)));
> +
>
^ permalink raw reply [flat|nested] 20+ messages in thread
* Re: [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials
2026-04-06 15:01 ` Steven Rostedt
@ 2026-04-07 13:00 ` Mickaël Salaün
0 siblings, 0 replies; 20+ messages in thread
From: Mickaël Salaün @ 2026-04-07 13:00 UTC (permalink / raw)
To: Steven Rostedt
Cc: Christian Brauner, Günther Noack, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-fsdevel, linux-security-module, linux-trace-kernel
On Mon, Apr 06, 2026 at 11:01:23AM -0400, Steven Rostedt wrote:
> On Mon, 6 Apr 2026 16:37:10 +0200
> Mickaël Salaün <mic@digikod.net> wrote:
>
> > ---
> > include/trace/events/landlock.h | 135 ++++++++++++++++++++++++++++++++
> > security/landlock/log.c | 20 +++++
> > 2 files changed, 155 insertions(+)
> >
> > diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
> > index 1afab091efba..9f96c9897f44 100644
> > --- a/include/trace/events/landlock.h
> > +++ b/include/trace/events/landlock.h
> > @@ -11,6 +11,7 @@
> > #define _TRACE_LANDLOCK_H
> >
> > #include <linux/tracepoint.h>
> > +#include <net/af_unix.h>
> >
> > struct dentry;
> > struct landlock_domain;
> > @@ -19,6 +20,7 @@ struct landlock_rule;
> > struct landlock_ruleset;
> > struct path;
> > struct sock;
> > +struct task_struct;
> >
> > /**
> > * DOC: Landlock trace events
> > @@ -433,6 +435,139 @@ TRACE_EVENT(
> > __entry->log_new_exec, __entry->blockers, __entry->sport,
> > __entry->dport));
> >
> > +/**
> > + * landlock_deny_ptrace - ptrace access denied
> > + * @hierarchy: Hierarchy node that blocked the access (never NULL)
> > + * @same_exec: Whether the current task is the same executable that called
> > + * landlock_restrict_self() for the denying hierarchy node
> > + * @tracee: Target task (never NULL); eBPF can read pid, comm, cred,
> > + * namespaces, and cgroup via BTF
> > + */
> > +TRACE_EVENT(
> > + landlock_deny_ptrace,
> > +
> > + TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
> > + const struct task_struct *tracee),
> > +
> > + TP_ARGS(hierarchy, same_exec, tracee),
> > +
> > + TP_STRUCT__entry(
> > + __field(__u64, domain_id) __field(bool, same_exec)
> > + __field(u32, log_same_exec) __field(u32, log_new_exec)
> > + __field(pid_t, tracee_pid)
> > + __string(tracee_comm, tracee->comm)),
>
> Event formats are different than normal macro formatting. Please use the
> event formatting. The above is a defined structure that is being created
> for use. Keep it looking like a structure:
>
> TP_STRUCT__entry(
> __field( __u64, domain_id)
> __field( bool, same_exec)
> __field( u32, log_same_exec)
> __field( u32, log_new_exec)
> __field( pid_t, tracee_pid)
> __string( tracee_comm, tracee->comm)
> ),
I was using clang-format, but it doesn't make sense here, I'll fix it.
>
> See how the above resembles:
>
> struct entry {
> __u64 domain_id;
> bool same_exec;
> u32 log_same_exec;
> u32 log_new_exec;
> pid_t tracee_pid;
> string tracee_comm;
> };
>
> Because that's pretty much what the trace event TP_STRUCT__entry() is going
> to do with it. (The string will obviously be something else).
>
> This way it's also easy to spot wholes in the structure that is written
> into the ring buffer. The "same_exec" being a bool followed by two u32
> types, is going to cause a hole. Move it to between tracee_pid and
> tracee_comm.
Actually, the log_* field should be bool too. Anyway, is it a concern
that the ring buffer leaks (previous event) kernel memory or is the
concern mostly about avoiding wasted space and making easy to spot holes
even if it's OK?
>
> Please fix the other events too.
Sure. Thanks!
>
> -- Steve
>
>
> > +
> > + TP_fast_assign(__entry->domain_id = hierarchy->id;
> > + __entry->same_exec = same_exec;
> > + __entry->log_same_exec = hierarchy->log_same_exec;
> > + __entry->log_new_exec = hierarchy->log_new_exec;
> > + __entry->tracee_pid =
> > + task_tgid_nr((struct task_struct *)tracee);
> > + __assign_str(tracee_comm);),
> > +
> > + TP_printk(
> > + "domain=%llx same_exec=%d log_same_exec=%u log_new_exec=%u tracee_pid=%d comm=%s",
> > + __entry->domain_id, __entry->same_exec, __entry->log_same_exec,
> > + __entry->log_new_exec, __entry->tracee_pid,
> > + __print_untrusted_str(tracee_comm)));
Are you OK with this new helper?
> > +
> >
>
^ permalink raw reply [flat|nested] 20+ messages in thread
end of thread, other threads:[~2026-04-07 13:00 UTC | newest]
Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-06 14:36 [PATCH v2 00/17] Landlock tracepoints Mickaël Salaün
2026-04-06 14:36 ` [PATCH v2 01/17] landlock: Prepare ruleset and domain type split Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 02/17] landlock: Move domain query functions to domain.c Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 03/17] landlock: Split struct landlock_domain from struct landlock_ruleset Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 04/17] landlock: Split denial logging from audit into common framework Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 05/17] tracing: Add __print_untrusted_str() Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 06/17] landlock: Add create_ruleset and free_ruleset tracepoints Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 07/17] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 08/17] landlock: Add restrict_self and free_domain tracepoints Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 09/17] landlock: Add tracepoints for rule checking Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 10/17] landlock: Set audit_net.sk for socket access checks Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 11/17] landlock: Add landlock_deny_access_fs and landlock_deny_access_net Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 12/17] landlock: Add tracepoints for ptrace and scope denials Mickaël Salaün
2026-04-06 15:01 ` Steven Rostedt
2026-04-07 13:00 ` Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 13/17] selftests/landlock: Add trace event test infrastructure and tests Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 14/17] selftests/landlock: Add filesystem tracepoint tests Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 15/17] selftests/landlock: Add network " Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 16/17] selftests/landlock: Add scope and ptrace " Mickaël Salaün
2026-04-06 14:37 ` [PATCH v2 17/17] landlock: Document tracepoints Mickaël Salaün
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox