]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
landlock: Allow TSYNC with LOG_SUBDOMAINS_OFF and fd=-1
authorMickaël Salaün <mic@digikod.net>
Tue, 7 Apr 2026 16:41:05 +0000 (18:41 +0200)
committerMickaël Salaün <mic@digikod.net>
Tue, 7 Apr 2026 16:51:00 +0000 (18:51 +0200)
LANDLOCK_RESTRICT_SELF_TSYNC does not allow
LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with ruleset_fd=-1, preventing
a multithreaded process from atomically propagating subdomain log muting
to all threads without creating a domain layer.  Relax the fd=-1
condition to accept TSYNC alongside LOG_SUBDOMAINS_OFF, and update the
documentation accordingly.

Add flag validation tests for all TSYNC combinations with ruleset_fd=-1,
and audit tests verifying both transition directions: muting via TSYNC
(logged to not logged) and override via TSYNC (not logged to logged).

Cc: Günther Noack <gnoack@google.com>
Cc: stable@vger.kernel.org
Fixes: 42fc7e6543f6 ("landlock: Multithreading support for landlock_restrict_self()")
Reviewed-by: Günther Noack <gnoack3000@gmail.com>
Link: https://lore.kernel.org/r/20260407164107.2012589-2-mic@digikod.net
Signed-off-by: Mickaël Salaün <mic@digikod.net>
include/uapi/linux/landlock.h
security/landlock/syscalls.c
tools/testing/selftests/landlock/audit_test.c
tools/testing/selftests/landlock/tsync_test.c

index f88fa1f68b770b8a3ee98d5a34f0f1c455647005..d37603efc273fd22c5baeebec436e49f746fd5db 100644 (file)
@@ -116,7 +116,9 @@ struct landlock_ruleset_attr {
  *     ``LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF``, this flag only affects
  *     future nested domains, not the one being created. It can also be used
  *     with a @ruleset_fd value of -1 to mute subdomain logs without creating a
- *     domain.
+ *     domain.  When combined with %LANDLOCK_RESTRICT_SELF_TSYNC and a
+ *     @ruleset_fd value of -1, this configuration is propagated to all threads
+ *     of the current process.
  *
  * The following flag supports policy enforcement in multithreaded processes:
  *
index 0d66a68677b7008cbbb3daeb0210d052b4ecc99f..a0bb664e0d315b0d69621304965148414b9aa68b 100644 (file)
@@ -512,10 +512,13 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 
        /*
         * It is allowed to set LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
-        * -1 as ruleset_fd, but no other flag must be set.
+        * -1 as ruleset_fd, optionally combined with
+        * LANDLOCK_RESTRICT_SELF_TSYNC to propagate this configuration to all
+        * threads.  No other flag must be set.
         */
        if (!(ruleset_fd == -1 &&
-             flags == LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
+             (flags & ~LANDLOCK_RESTRICT_SELF_TSYNC) ==
+                     LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
                /* Gets and checks the ruleset. */
                ruleset = get_ruleset_from_fd(ruleset_fd, FMODE_CAN_READ);
                if (IS_ERR(ruleset))
@@ -537,9 +540,10 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
 
        /*
         * The only case when a ruleset may not be set is if
-        * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set and ruleset_fd is -1.
-        * We could optimize this case by not calling commit_creds() if this flag
-        * was already set, but it is not worth the complexity.
+        * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF is set (optionally with
+        * LANDLOCK_RESTRICT_SELF_TSYNC) and ruleset_fd is -1.  We could
+        * optimize this case by not calling commit_creds() if this flag was
+        * already set, but it is not worth the complexity.
         */
        if (ruleset) {
                /*
index 20099b8667e754683e9b6f964cfa10c29dd3ef22..897596cd7c80e57c64cb532d8b776f0a45c5d951 100644 (file)
@@ -162,6 +162,7 @@ TEST_F(audit, layers)
 struct thread_data {
        pid_t parent_pid;
        int ruleset_fd, pipe_child, pipe_parent;
+       bool mute_subdomains;
 };
 
 static void *thread_audit_test(void *arg)
@@ -367,6 +368,238 @@ TEST_F(audit, log_subdomains_off_fork)
        EXPECT_EQ(0, close(ruleset_fd));
 }
 
+/*
+ * Thread function: runs two rounds of (create domain, trigger denial, signal
+ * back), waiting for the main thread before each round.  When mute_subdomains
+ * is set, phase 1 also mutes subdomain logs via the fd=-1 path before creating
+ * the domain.  The ruleset_fd is kept open across both rounds so each
+ * restrict_self call stacks a new domain layer.
+ */
+static void *thread_sandbox_deny_twice(void *arg)
+{
+       const struct thread_data *data = (struct thread_data *)arg;
+       uintptr_t err = 0;
+       char buffer;
+
+       /* Phase 1: optionally mutes, creates a domain, and triggers a denial. */
+       if (read(data->pipe_parent, &buffer, 1) != 1) {
+               err = 1;
+               goto out;
+       }
+
+       if (data->mute_subdomains &&
+           landlock_restrict_self(-1,
+                                  LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF)) {
+               err = 2;
+               goto out;
+       }
+
+       if (landlock_restrict_self(data->ruleset_fd, 0)) {
+               err = 3;
+               goto out;
+       }
+
+       if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
+               err = 4;
+               goto out;
+       }
+
+       if (write(data->pipe_child, ".", 1) != 1) {
+               err = 5;
+               goto out;
+       }
+
+       /* Phase 2: stacks another domain and triggers a denial. */
+       if (read(data->pipe_parent, &buffer, 1) != 1) {
+               err = 6;
+               goto out;
+       }
+
+       if (landlock_restrict_self(data->ruleset_fd, 0)) {
+               err = 7;
+               goto out;
+       }
+
+       if (kill(data->parent_pid, 0) != -1 || errno != EPERM) {
+               err = 8;
+               goto out;
+       }
+
+       if (write(data->pipe_child, ".", 1) != 1) {
+               err = 9;
+               goto out;
+       }
+
+out:
+       close(data->ruleset_fd);
+       close(data->pipe_child);
+       close(data->pipe_parent);
+       return (void *)err;
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF with
+ * LANDLOCK_RESTRICT_SELF_TSYNC and ruleset_fd=-1 propagates log_subdomains_off
+ * to a sibling thread, suppressing audit logging on domains it subsequently
+ * creates.
+ *
+ * Phase 1 (before TSYNC) acts as an inline baseline: the sibling creates a
+ * domain and triggers a denial that IS logged.
+ *
+ * Phase 2 (after TSYNC) verifies suppression: the sibling stacks another domain
+ * and triggers a denial that is NOT logged.
+ */
+TEST_F(audit, log_subdomains_off_tsync)
+{
+       const struct landlock_ruleset_attr ruleset_attr = {
+               .scoped = LANDLOCK_SCOPE_SIGNAL,
+       };
+       struct audit_records records;
+       struct thread_data child_data = {};
+       int pipe_child[2], pipe_parent[2];
+       char buffer;
+       pthread_t thread;
+       void *thread_ret;
+
+       child_data.parent_pid = getppid();
+       ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
+       child_data.pipe_child = pipe_child[1];
+       ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
+       child_data.pipe_parent = pipe_parent[0];
+       child_data.ruleset_fd =
+               landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+       ASSERT_LE(0, child_data.ruleset_fd);
+
+       ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+       /* Creates the sibling thread. */
+       ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
+                                   &child_data));
+
+       /*
+        * Phase 1: the sibling creates a domain and triggers a denial before
+        * any log muting.  This proves the audit path works.
+        */
+       ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+       ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+       /* The denial must be logged. */
+       EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
+                                       child_data.parent_pid, NULL));
+
+       /* Drains any remaining records (e.g. domain allocation). */
+       EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+
+       /*
+        * Mutes subdomain logs and propagates to the sibling thread via TSYNC,
+        * without creating a domain.
+        */
+       ASSERT_EQ(0, landlock_restrict_self(
+                            -1, LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+                                        LANDLOCK_RESTRICT_SELF_TSYNC));
+
+       /*
+        * Phase 2: the sibling stacks another domain and triggers a denial.
+        * Because log_subdomains_off was propagated via TSYNC, the new domain
+        * has log_status=LANDLOCK_LOG_DISABLED.
+        */
+       ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+       ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+       /* No denial record should appear. */
+       EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
+                                             child_data.parent_pid, NULL));
+
+       EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+       EXPECT_EQ(0, records.access);
+
+       EXPECT_EQ(0, close(pipe_child[0]));
+       EXPECT_EQ(0, close(pipe_parent[1]));
+       ASSERT_EQ(0, pthread_join(thread, &thread_ret));
+       EXPECT_EQ(NULL, thread_ret);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_TSYNC without
+ * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF overrides a sibling thread's
+ * log_subdomains_off, re-enabling audit logging on domains the sibling
+ * subsequently creates.
+ *
+ * Phase 1: the sibling sets log_subdomains_off, creates a muted domain, and
+ * triggers a denial that is NOT logged.
+ *
+ * Phase 2 (after TSYNC without LOG_SUBDOMAINS_OFF): the sibling stacks another
+ * domain and triggers a denial that IS logged, proving the muting was
+ * overridden.
+ */
+TEST_F(audit, tsync_override_log_subdomains_off)
+{
+       const struct landlock_ruleset_attr ruleset_attr = {
+               .scoped = LANDLOCK_SCOPE_SIGNAL,
+       };
+       struct audit_records records;
+       struct thread_data child_data = {};
+       int pipe_child[2], pipe_parent[2];
+       char buffer;
+       pthread_t thread;
+       void *thread_ret;
+
+       child_data.parent_pid = getppid();
+       ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
+       child_data.pipe_child = pipe_child[1];
+       ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
+       child_data.pipe_parent = pipe_parent[0];
+       child_data.ruleset_fd =
+               landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+       ASSERT_LE(0, child_data.ruleset_fd);
+
+       ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+       child_data.mute_subdomains = true;
+
+       /* Creates the sibling thread. */
+       ASSERT_EQ(0, pthread_create(&thread, NULL, thread_sandbox_deny_twice,
+                                   &child_data));
+
+       /*
+        * Phase 1: the sibling mutes subdomain logs, creates a domain, and
+        * triggers a denial.  The denial must not be logged.
+        */
+       ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+       ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+       EXPECT_EQ(-EAGAIN, matches_log_signal(_metadata, self->audit_fd,
+                                             child_data.parent_pid, NULL));
+
+       /* Drains any remaining records. */
+       EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+       EXPECT_EQ(0, records.access);
+
+       /*
+        * Overrides the sibling's log_subdomains_off by calling TSYNC without
+        * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF.
+        */
+       ASSERT_EQ(0, landlock_restrict_self(child_data.ruleset_fd,
+                                           LANDLOCK_RESTRICT_SELF_TSYNC));
+
+       /*
+        * Phase 2: the sibling stacks another domain and triggers a denial.
+        * Because TSYNC replaced its log_subdomains_off with 0, the new domain
+        * has log_status=LANDLOCK_LOG_PENDING.
+        */
+       ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+       ASSERT_EQ(1, read(pipe_child[0], &buffer, 1));
+
+       /* The denial must be logged. */
+       EXPECT_EQ(0, matches_log_signal(_metadata, self->audit_fd,
+                                       child_data.parent_pid, NULL));
+
+       EXPECT_EQ(0, close(pipe_child[0]));
+       EXPECT_EQ(0, close(pipe_parent[1]));
+       ASSERT_EQ(0, pthread_join(thread, &thread_ret));
+       EXPECT_EQ(NULL, thread_ret);
+}
+
 FIXTURE(audit_flags)
 {
        struct audit_filter audit_filter;
index 2b9ad4f154f481d7f287a86a7a021b7ec9d767c2..9cf1491bbaaff42e06909c24daf2e51071b907b7 100644 (file)
@@ -247,4 +247,81 @@ TEST(tsync_interrupt)
        EXPECT_EQ(0, close(ruleset_fd));
 }
 
+/* clang-format off */
+FIXTURE(tsync_without_ruleset) {};
+/* clang-format on */
+
+FIXTURE_VARIANT(tsync_without_ruleset)
+{
+       const __u32 flags;
+       const int expected_errno;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, tsync_only) {
+       /* clang-format on */
+       .flags = LANDLOCK_RESTRICT_SELF_TSYNC,
+       .expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_same_exec_off) {
+       /* clang-format on */
+       .flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+                LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
+                LANDLOCK_RESTRICT_SELF_TSYNC,
+       .expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off_new_exec_on) {
+       /* clang-format on */
+       .flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+                LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
+                LANDLOCK_RESTRICT_SELF_TSYNC,
+       .expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, all_flags) {
+       /* clang-format on */
+       .flags = LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF |
+                LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON |
+                LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+                LANDLOCK_RESTRICT_SELF_TSYNC,
+       .expected_errno = EBADF,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tsync_without_ruleset, subdomains_off) {
+       /* clang-format on */
+       .flags = LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF |
+                LANDLOCK_RESTRICT_SELF_TSYNC,
+       .expected_errno = 0,
+};
+
+FIXTURE_SETUP(tsync_without_ruleset)
+{
+       disable_caps(_metadata);
+}
+
+FIXTURE_TEARDOWN(tsync_without_ruleset)
+{
+}
+
+TEST_F(tsync_without_ruleset, check)
+{
+       int ret;
+
+       ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0));
+
+       ret = landlock_restrict_self(-1, variant->flags);
+       if (variant->expected_errno) {
+               EXPECT_EQ(-1, ret);
+               EXPECT_EQ(variant->expected_errno, errno);
+       } else {
+               EXPECT_EQ(0, ret);
+       }
+}
+
 TEST_HARNESS_MAIN