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 v3: https://patch.msgid.link/[email protected] - Rebased onto landlock-next: emit the enforce_domain tracepoint alongside the new LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS handling in landlock_restrict_self(). - Add a no_new_privs field to landlock_enforce_domain recording the thread's no_new_privs state (task_no_new_privs() at emission, for both the caller and TSYNC-swept siblings), so consumers see whether the domain is backed by no_new_privs or by CAP_SYS_ADMIN. The raw restrict_self flags stay unexposed here (readable via the syscall-entry tracepoint). Changes since v2: - New patch. --- include/trace/events/landlock.h | 60 +++++++++++++++++++++++++++++++++ security/landlock/syscalls.c | 14 +++++++- security/landlock/tsync.c | 16 +++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h index 9f0d0436a8bb..43bf126eb449 100644 --- a/include/trace/events/landlock.h +++ b/include/trace/events/landlock.h @@ -339,6 +339,66 @@ 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. + * @no_new_privs: The enforcing thread's no_new_privs state at + * enforcement time: 1 if set (by a prior + * :manpage:`prctl(2)` %PR_SET_NO_NEW_PRIVS or by + * %LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS), 0 if the domain + * was enforced with %CAP_SYS_ADMIN instead. + * + * 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, bool no_new_privs), + + TP_ARGS(domain, complete, process_wide, no_new_privs), + + TP_STRUCT__entry( + __field( __u64, domain_id ) + __field( bool, complete ) + __field( bool, process_wide ) + __field( bool, no_new_privs ) + ), + + TP_fast_assign( + __entry->domain_id = domain->hierarchy->id; + __entry->complete = complete; + __entry->process_wide = process_wide; + __entry->no_new_privs = no_new_privs; + ), + + TP_printk("domain=%llx complete=%d process_wide=%d no_new_privs=%d", + __entry->domain_id, __entry->complete, __entry->process_wide, + __entry->no_new_privs) +); + /** * landlock_free_domain - Domain freed * diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index ba4193673c70..1d02d57f4c48 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> @@ -546,6 +547,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; @@ -690,5 +692,15 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, if (flags & LANDLOCK_RESTRICT_SELF_NO_NEW_PRIVS) task_set_no_new_privs(current); - 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, + task_no_new_privs(current)); + + return 0; } diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c index 0b71e158c3f5..cfa73a2a67ca 100644 --- a/security/landlock/tsync.c +++ b/security/landlock/tsync.c @@ -22,6 +22,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. @@ -79,6 +81,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; @@ -147,6 +151,18 @@ 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, + task_no_new_privs(current)); + out: /* Notify the calling thread once all threads are done */ if (atomic_dec_return(&ctx->num_unfinished) == 0) -- 2.54.0
