landlock: Add create_ruleset and free_ruleset tracepoints

Add the first Landlock tracepoints, for ruleset lifecycle:
landlock_create_ruleset fires from the landlock_create_ruleset() syscall
handler, and landlock_free_ruleset fires in free_ruleset() before the
ruleset is freed.

These tracepoints, and the ones added by the following commits, share a
common design.  Rather than one polymorphic event distinguished by a
status field (as audit uses a shared record type with a "status="
field), each lifecycle transition and denial type gets its own event
with a type-safe TP_PROTO, giving precise ftrace filtering by event name
and type-safe eBPF access.  TP_PROTO passes the object pointer and the
fields are read from it in TP_fast_assign, so an eBPF program reads the
full object state (rules, access masks, hierarchy) via BTF from a single
pointer rather than from the flattened TP_STRUCT__entry fields.  The
whole cost is paid only when a tracer is attached; the static branch is
not taken otherwise.  Trace fields carry the bare access-right and scope
names (read_file), reusing the audit name tables; audit prepends the
category (fs.read_file), which the trace event name already conveys.
The trace header's DOC comment documents the consistency and locking
guarantees these events share.

create_ruleset needs no lock because the ruleset is not yet shared (its
file descriptor is not yet installed).  The deallocation events use the
"free_" prefix, not "drop_", because they fire when the object is
actually freed.

Add trace.c, built for CONFIG_TRACEPOINTS, which defines
CREATE_TRACE_POINTS, and extend CONFIG_SECURITY_LANDLOCK_LOG to also be
selected by CONFIG_TRACEPOINTS so the common log framework is available
to a tracepoints-only build.

Add an id field to struct landlock_ruleset, gated on CONFIG_TRACEPOINTS
and assigned from landlock_get_id_range() at creation.  Only the
tracepoints consume it (audit identifies domains, not rulesets), so it
does not exist in an audit-only build.  The Landlock ID is a stable u64
that names the ruleset across the trace stream and uses the same scheme
as audit, so a ruleset can be correlated between trace and audit
records.

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>
Link: https://patch.msgid.link/20260811094338.288094-8-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
Mickaël Salaün
2026-08-11 11:43:21 +02:00
parent bc62ec6034
commit b4540a72be
9 changed files with 193 additions and 1 deletions

View File

@@ -14600,6 +14600,7 @@ F: Documentation/security/landlock.rst
F: Documentation/userspace-api/landlock.rst
F: fs/ioctl.c
F: include/linux/landlock.h
F: include/trace/events/landlock.h
F: include/uapi/linux/landlock.h
F: samples/landlock/
F: security/landlock/

View File

@@ -9,6 +9,7 @@
#ifndef _LINUX_LANDLOCK_H
#define _LINUX_LANDLOCK_H
#include <linux/types.h>
#include <uapi/linux/landlock.h>
/*

View File

@@ -0,0 +1,143 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright © 2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#undef TRACE_SYSTEM
#define TRACE_SYSTEM landlock
#if !defined(_TRACE_LANDLOCK_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_LANDLOCK_H
#include <linux/landlock.h>
#include <linux/tracepoint.h>
struct landlock_ruleset;
/* clang-format off */
/* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
#define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
/**
* DOC: Landlock trace events
*
* These guarantees and constraints hold for every Landlock tracepoint.
* A new tracepoint must uphold them, and an eBPF consumer can rely on
* them.
*
* Lifecycle consistency
* ~~~~~~~~~~~~~~~~~~~~~~
*
* Lifecycle events are balanced: a creation event always has a matching
* deallocation event and vice versa, so an eBPF program can model object
* lifetimes from the trace stream without reconciliation logic. A creation
* event fires while the object is still private to the calling thread
* (landlock_create_ruleset fires before the ruleset's file descriptor is
* installed, so it cannot race a concurrent :manpage:`close(2)`); if fd
* installation later fails and the ruleset is freed, free_ruleset still
* fires, keeping the pair balanced. The domain pair (create_domain and
* free_domain) is balanced the same way: create_domain fires when the
* domain is created (under the ruleset lock, before thread-sync), and
* free_domain fires when it is freed. A rare thread-sync failure aborts
* the just-created domain, which then emits both events (its creation, then
* an immediate free). Denial events fire only for denials that actually
* happen.
*
* Pointer access
* ~~~~~~~~~~~~~~
*
* All pointer arguments in TP_PROTO are guaranteed non-NULL by the
* caller, but pointers reached through them may still be NULL (e.g.,
* hierarchy->parent at a root domain) and must be checked. eBPF programs
* read these pointers via BTF for richer introspection than the
* TP_STRUCT__entry fields, which serve TP_printk display only.
*
* Mutable object pointers are passed while the caller holds the object's
* lock, so TP_fast_assign and a BTF reader see the exact object the event
* reports, a snapshot no concurrent writer can change: add_rule holds the
* modified ruleset's lock, and create_domain holds the ruleset lock across
* the emission (before the thread-sync wait) so the inspected ruleset is
* the one merged into the domain. Objects immutable at the emission site
* (a domain after creation, a hierarchy at its last reference) need no
* lock. A few values that no held lock protects are a best-effort
* 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.
*/
/**
* landlock_create_ruleset - New ruleset created
*
* @ruleset: Newly created ruleset (never NULL); not yet shared via an fd,
* so no lock is needed.
*
* Emitted by sys_landlock_create_ruleset() while the new ruleset is still
* private to the calling thread, before its file descriptor is installed,
* so it cannot race a concurrent :manpage:`close(2)`. Balanced by a
* matching landlock_free_ruleset event.
*/
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->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,
__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))
);
/**
* landlock_free_ruleset - Ruleset freed
*
* @ruleset: Ruleset being freed (never NULL); at its last reference, so no
* lock is needed.
*
* Emitted when a ruleset's last reference is dropped (typically when
* the creating process closes the ruleset file descriptor). Fires even
* when file-descriptor installation failed after creation, keeping the
* create/free pair balanced.
*/
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)
);
#undef _LANDLOCK_NAME_ENTRY
#endif /* _TRACE_LANDLOCK_H */
/* This part must be outside protection */
#include <trace/define_trace.h>
/* clang-format on */

View File

@@ -24,7 +24,7 @@ config SECURITY_LANDLOCK
config SECURITY_LANDLOCK_LOG
bool
depends on SECURITY_LANDLOCK
default y if AUDIT
default y if AUDIT || TRACEPOINTS
config SECURITY_LANDLOCK_KUNIT_TEST
bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS

View File

@@ -18,3 +18,5 @@ landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
log.o
landlock-$(CONFIG_AUDIT) += audit.o
landlock-$(CONFIG_TRACEPOINTS) += trace.o

View File

@@ -23,10 +23,13 @@
#include <uapi/linux/landlock.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,
@@ -50,6 +53,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_TRACEPOINTS
new_ruleset->id = landlock_get_id_range(1);
#endif /* CONFIG_TRACEPOINTS */
/* Should already be checked in landlock_create_ruleset(). */
if (fs_access_mask) {
const access_mask_t mask = fs_access_mask &
@@ -325,6 +332,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);
}

View File

@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
* Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_RULESET_H
@@ -164,6 +165,14 @@ struct landlock_ruleset {
* @usage: Number of file descriptors referencing this ruleset.
*/
refcount_t usage;
#ifdef CONFIG_TRACEPOINTS
/**
* @id: Unique identifier for this ruleset, used for tracing.
*/
u64 id;
#endif /* CONFIG_TRACEPOINTS */
/**
* @quiet_masks: Stores the quiet flags for an unmerged ruleset. For a
* merged domain, this is stored in each layer's struct

View File

@@ -38,6 +38,8 @@
#include "setup.h"
#include "tsync.h"
#include <trace/events/landlock.h>
static bool is_initialized(void)
{
if (likely(landlock_initialized))
@@ -281,6 +283,15 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
/*
* Emits before anon_inode_getfd() installs the file descriptor, while
* the ruleset is still private to this thread: no lock is needed, and
* the event cannot race a concurrent close() freeing the ruleset under
* the tracepoint's BTF read. This is the last point at which the
* ruleset is guaranteed alive and unshared.
*/
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);

17
security/landlock/trace.c Normal file
View File

@@ -0,0 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Landlock - Tracepoint helpers
*
* Copyright © 2025 Microsoft Corporation
* Copyright © 2026 Cloudflare, Inc.
*/
#include "ruleset.h"
/*
* Generates the tracepoint definitions in this translation unit. The trace
* event header dereferences the traced objects in TP_fast_assign, so the full
* struct definitions (e.g. ruleset.h) must be included before it.
*/
#define CREATE_TRACE_POINTS
#include <trace/events/landlock.h>