Add tracepoints for Landlock rule addition, landlock_add_rule_fs for filesystem rules and landlock_add_rule_net for network rules, so trace consumers can correlate filesystem objects and network ports with their rulesets. Both are emitted under the ruleset lock (asserted in TP_fast_assign) so an eBPF program reads the ruleset, including the rule just inserted, in a consistent snapshot.
Add a version field to struct landlock_ruleset, gated on CONFIG_TRACEPOINTS like the id field and incremented under the ruleset lock on each successful landlock_add_rule(2), including when it only extends an existing rule's access rights. It fills the existing 4-byte hole after usage, so the struct does not grow. Pairing the ruleset ID with the version lets a later restrict_self event record the exact ruleset revision merged into a domain. Resolve the filesystem rule's absolute path with d_absolute_path() rather than the d_path() audit uses: d_absolute_path() produces namespace-independent paths that do not depend on the tracer's chroot state, making trace output deterministic regardless of mount namespace configuration. Distinguish the error cases as "<too_long>" (-ENAMETOOLONG) and "<unreachable>" (anonymous files or detached mounts). Cc: Christian Brauner <[email protected]> Cc: Günther Noack <[email protected]> Cc: Justin Suess <[email protected]> Cc: Masami Hiramatsu <[email protected]> Cc: Mathieu Desnoyers <[email protected]> Cc: Steven Rostedt <[email protected]> Cc: Tingmao Wang <[email protected]> Signed-off-by: Mickaël Salaün <[email protected]> --- Changes since v2: https://patch.msgid.link/[email protected] - Order the add_rule_net tracepoint arguments access_rights before port, matching add_rule_fs. - Reformatted TP_STRUCT__entry and TP_fast_assign to kernel tracing convention (requested by Steven Rostedt). - Render access_rights as symbolic names with __print_flags(), shared with the audit blocker names, instead of raw hex. - Drop the tautological version static assertion (the counter tracks add-rule operations, not rule count) and clarify that @version is incremented on access-right extensions too. - Gate the version field and its writer on CONFIG_TRACEPOINTS (only tracing uses it) instead of CONFIG_SECURITY_LANDLOCK_LOG, matching the id field. - Adapt rule insertion to the base's quiet flag (landlock_insert_rule() flags argument). Changes since v1: https://patch.msgid.link/[email protected] - 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 | 115 +++++++++++++++++++++++++++++++- security/landlock/fs.c | 19 ++++++ security/landlock/fs.h | 30 +++++++++ security/landlock/net.c | 11 +++ security/landlock/ruleset.c | 13 +++- security/landlock/ruleset.h | 7 ++ 7 files changed, 191 insertions(+), 5 deletions(-) diff --git a/include/linux/fs.h b/include/linux/fs.h index 50ce731a2b78..925517c672f3 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.h @@ -2587,6 +2587,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 2fb717055cc8..69a75cf47f65 100644 --- a/include/trace/events/landlock.h +++ b/include/trace/events/landlock.h @@ -14,6 +14,7 @@ #include <linux/tracepoint.h> struct landlock_ruleset; +struct path; /* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */ #define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name } @@ -63,6 +64,14 @@ struct landlock_ruleset; * lockless snapshot instead: a task's comm, and the deny_access_net struct * sock (whose network hook holds no socket lock), matching how the sched * and signal trace events sample comm. + * + * Field encoding + * ~~~~~~~~~~~~~~ + * + * Fields that mirror the Landlock UAPI use the same C types and endianness + * (e.g. network ports are __u64 in host endianness, like + * landlock_net_port_attr.port). Per-event details, such as where a value + * is byte-swapped, live in the field's own kdoc. */ /** @@ -84,6 +93,7 @@ TRACE_EVENT(landlock_create_ruleset, 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 ) @@ -91,13 +101,14 @@ TRACE_EVENT(landlock_create_ruleset, TP_fast_assign( __entry->ruleset_id = ruleset->id; + __entry->ruleset_version = ruleset->version; __entry->handled_fs = ruleset->handled_masks.fs; __entry->handled_net = ruleset->handled_masks.net; __entry->scoped = ruleset->handled_masks.scope; ), - TP_printk("ruleset=%llx handled_fs=%s handled_net=%s scoped=%s", - __entry->ruleset_id, + TP_printk("ruleset=%llx.%u handled_fs=%s handled_net=%s scoped=%s", + __entry->ruleset_id, __entry->ruleset_version, __print_flags(__entry->handled_fs, "|", _LANDLOCK_ACCESS_FS_NAMES), __print_flags(__entry->handled_net, "|", _LANDLOCK_ACCESS_NET_NAMES), __print_flags(__entry->scoped, "|", _LANDLOCK_SCOPE_NAMES)) @@ -122,13 +133,111 @@ TRACE_EVENT(landlock_free_ruleset, 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: Effective access mask stored in the rule, not the raw + * sys_landlock_add_rule() argument (unhandled rights + * added). + * @path: Filesystem path for the rule (never NULL). + * @pathname: Resolved absolute path string (never NULL; error placeholder + * on resolution failure). + * + * Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so + * the reported ruleset is a stable snapshot that no concurrent writer can + * change. + */ +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=%s dev=%u:%u ino=%lu path=%s", + __entry->ruleset_id, __entry->ruleset_version, + __print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_FS_NAMES), + 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). + * @access_rights: Effective access mask stored in the rule, not the raw + * sys_landlock_add_rule() argument (unhandled rights + * added). + * @port: Network port, the landlock_net_port_attr.port UAPI value + * forwarded directly. + * + * Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so + * the reported ruleset is a stable snapshot that no concurrent writer can + * change. + */ +TRACE_EVENT(landlock_add_rule_net, + + TP_PROTO(const struct landlock_ruleset *ruleset, + access_mask_t access_rights, __u64 port), + + TP_ARGS(ruleset, access_rights, port), + + TP_STRUCT__entry( + __field( __u64, ruleset_id ) + __field( __u32, ruleset_version ) + __field( access_mask_t, access_rights ) + __field( __u64, port ) ), 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", __entry->ruleset_id) + TP_printk("ruleset=%llx.%u access_rights=%s port=%llu", + __entry->ruleset_id, __entry->ruleset_version, + __print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_NET_NAMES), + __entry->port) ); #undef _LANDLOCK_NAME_ENTRY diff --git a/security/landlock/fs.c b/security/landlock/fs.c index d39da6e9fa8c..48744a21d0a3 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) @@ -346,7 +348,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, flags); + + /* + * 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 c16f24e30bd5..4e3d7cbd7e1e 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> @@ -153,4 +154,33 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset, const struct path *const path, access_mask_t access_hierarchy, const u32 flags); +/** + * 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 e27b3ba15664..ead97fcfdcff 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, const u32 flags) @@ -37,6 +39,15 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset, mutex_lock(&ruleset->lock); err = landlock_insert_rule(ruleset, id, access_rights, flags); + + /* + * 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, access_rights, port); mutex_unlock(&ruleset->lock); return err; diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c index 3bfee53177d8..b78714047ddf 100644 --- a/security/landlock/ruleset.c +++ b/security/landlock/ruleset.c @@ -4,6 +4,7 @@ * * Copyright © 2016-2020 Mickaël Salaün <[email protected]> * Copyright © 2018-2020 ANSSI + * Copyright © 2026 Cloudflare, Inc. */ #include <linux/bits.h> @@ -306,11 +307,19 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset, .quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET), }, } }; + 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_TRACEPOINTS + if (!err) + ruleset->version++; +#endif /* CONFIG_TRACEPOINTS */ + + return err; } void landlock_free_rules(struct landlock_rules *const rules) diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h index ca1fd5f4c417..799d9b3cc205 100644 --- a/security/landlock/ruleset.h +++ b/security/landlock/ruleset.h @@ -167,6 +167,13 @@ struct landlock_ruleset { refcount_t usage; #ifdef CONFIG_TRACEPOINTS + /** + * @version: Counter incremented on each successful + * landlock_add_rule(2), including when it only extends an existing + * rule's access rights. 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.54.0
