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


Reply via email to