mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-31 07:03:28 -04:00
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:
@@ -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/
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef _LINUX_LANDLOCK_H
|
||||
#define _LINUX_LANDLOCK_H
|
||||
|
||||
#include <linux/types.h>
|
||||
#include <uapi/linux/landlock.h>
|
||||
|
||||
/*
|
||||
|
||||
143
include/trace/events/landlock.h
Normal file
143
include/trace/events/landlock.h
Normal 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 */
|
||||
@@ -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
|
||||
|
||||
@@ -18,3 +18,5 @@ landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
|
||||
log.o
|
||||
|
||||
landlock-$(CONFIG_AUDIT) += audit.o
|
||||
|
||||
landlock-$(CONFIG_TRACEPOINTS) += trace.o
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
17
security/landlock/trace.c
Normal 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>
|
||||
Reference in New Issue
Block a user