The landlock_create_domain event records that a domain was created, once, before thread-sync. It cannot tell which threads end up enforcing it: a successful landlock_restrict_self(2) with LANDLOCK_RESTRICT_SELF_TSYNC applies the domain to the caller and every eligible sibling. Creation (the operation) and enforcement (the per-thread outcome) are distinct.
Add landlock_enforce_domain(domain, complete, process_wide), emitted once per thread the domain is applied to, strictly after that thread's commit_creds(), so it fires only for a thread that is enforcing the domain, never speculatively; an aborted operation emits none. The lifecycle now reads create -> enforce* -> free. The two booleans name properties, not the implementation: - complete: marks the single event that concludes the operation. It names the outcome, the set is now enforced, not which thread finishes, which the contract leaves unspecified. - process_wide: means every eligible thread of the process is covered. It is set race-free by either establishing path, thread-sync or a single-threaded process, so complete && process_wide is the whole-process-enforced guarantee. The requesting thread and source ruleset are not repeated here: they are on create_domain (joined via domain->hierarchy->id) and on the immutable domain->hierarchy->details. Source ruleset means the ruleset_id and ruleset_version recorded on create_domain, not the ruleset object, which the caller may close before enforcement. Cc: Günther Noack <[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: - New patch. --- include/trace/events/landlock.h | 52 +++++++++++++++++++++++++++++++++ security/landlock/syscalls.c | 13 ++++++++- security/landlock/tsync.c | 15 ++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h index cb0e21a2fa1f..7f221d8fff38 100644 --- a/include/trace/events/landlock.h +++ b/include/trace/events/landlock.h @@ -289,6 +289,58 @@ TRACE_EVENT(landlock_create_domain, __entry->ruleset_id, __entry->ruleset_version) ); +/** + * landlock_enforce_domain - Domain enforced on a thread + * + * @domain: Domain now enforced on the current thread (never NULL, + * immutable; read locklessly). Correlate to + * landlock_create_domain via @domain->hierarchy->id for the + * source ruleset and requesting thread, or read + * @domain->hierarchy->details for the requesting process. + * @complete: Set on the single event that concludes the operation, after + * all its other enforcements; filter on it for one event per + * operation. + * @process_wide: The enforcement covers every eligible (non-exiting) + * thread of the process: set when the caller used + * %LANDLOCK_RESTRICT_SELF_TSYNC or the process is + * single-threaded. A lone thread whose group still + * holds a zombie leader is not counted single-threaded, + * so process_wide == 0 never proves the opposite. + * + * Emitted for each thread sys_landlock_restrict_self() enforces the + * domain on, in that thread's own context, right after its + * commit_creds(), so it fires only once the thread is irreversibly + * enforcing the domain (aborted operations emit none). Not + * balanced; every enforcement falls between the domain's + * landlock_create_domain and landlock_free_domain events. + * + * @complete == 1 && @process_wide == 1 means the whole process is + * sandboxed by @domain, durably (Landlock domains are monotonic and + * inherited on :manpage:`clone(2)`). + */ +TRACE_EVENT(landlock_enforce_domain, + + TP_PROTO(const struct landlock_domain *domain, bool complete, + bool process_wide), + + TP_ARGS(domain, complete, process_wide), + + TP_STRUCT__entry( + __field( __u64, domain_id ) + __field( bool, complete ) + __field( bool, process_wide ) + ), + + TP_fast_assign( + __entry->domain_id = domain->hierarchy->id; + __entry->complete = complete; + __entry->process_wide = process_wide; + ), + + TP_printk("domain=%llx complete=%d process_wide=%d", + __entry->domain_id, __entry->complete, __entry->process_wide) +); + /** * landlock_free_domain - Domain freed * diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index 7a1ec140cf14..bf838f65b060 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -22,6 +22,7 @@ #include <linux/mount.h> #include <linux/path.h> #include <linux/sched.h> +#include <linux/sched/signal.h> #include <linux/security.h> #include <linux/stddef.h> #include <linux/syscalls.h> @@ -539,6 +540,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, struct landlock_domain *new_dom = NULL; struct cred *new_cred; struct landlock_cred_security *new_llcred; + bool process_wide; bool __maybe_unused log_same_exec, log_new_exec, log_subdomains, prev_log_subdomains; @@ -677,5 +679,14 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, } } - return commit_creds(new_cred); + /* Whole process: thread-sync swept siblings, or single-threaded. */ + process_wide = (flags & LANDLOCK_RESTRICT_SELF_TSYNC) || + get_nr_threads(current) == 1; + commit_creds(new_cred); + + /* The caller commits last, so its event concludes the operation. */ + if (ruleset) + trace_landlock_enforce_domain(new_dom, true, process_wide); + + return 0; } diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c index c5730bbd9ed3..b7de4f9624c7 100644 --- a/security/landlock/tsync.c +++ b/security/landlock/tsync.c @@ -21,6 +21,8 @@ #include "cred.h" #include "tsync.h" +#include <trace/events/landlock.h> + /* * Shared state between multiple threads which are enforcing Landlock rulesets * in lockstep with each other. @@ -78,6 +80,8 @@ struct tsync_work { */ static void restrict_one_thread(struct tsync_shared_context *ctx) { + const struct landlock_domain *new_dom = + landlock_cred(ctx->new_cred)->domain; int err; struct cred *cred = NULL; @@ -146,6 +150,17 @@ static void restrict_one_thread(struct tsync_shared_context *ctx) commit_creds(cred); + /* + * Emitted strictly after commit_creds() and before the out: label, so + * it fires only for a thread now enforcing new_dom, and every + * non-concluding (complete == false) event happens-before the + * operation's single concluding one. Skipped on the flags-only path, + * where old_cred and new_cred carry the same domain. A sibling never + * concludes the operation and its enforcement is always process-wide. + */ + if (new_dom != landlock_cred(ctx->old_cred)->domain) + trace_landlock_enforce_domain(new_dom, false, true); + out: /* Notify the calling thread once all threads are done */ if (atomic_dec_return(&ctx->num_unfinished) == 0) -- 2.54.0
