| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Landlock tests - Enforcing the same restrictions across multiple threads |
| * |
| * Copyright © 2025 Günther Noack <gnoack3000@gmail.com> |
| */ |
| |
| #define _GNU_SOURCE |
| #include <linux/landlock.h> |
| #include <pthread.h> |
| #include <signal.h> |
| #include <sys/prctl.h> |
| |
| #include "common.h" |
| |
| /* create_ruleset - Create a simple ruleset FD common to all tests */ |
| static int create_ruleset(struct __test_metadata *const _metadata) |
| { |
| struct landlock_ruleset_attr ruleset_attr = { |
| .handled_access_fs = (LANDLOCK_ACCESS_FS_WRITE_FILE | |
| LANDLOCK_ACCESS_FS_TRUNCATE), |
| }; |
| const int ruleset_fd = |
| landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); |
| |
| ASSERT_LE(0, ruleset_fd) |
| { |
| TH_LOG("landlock_create_ruleset: %s", strerror(errno)); |
| } |
| return ruleset_fd; |
| } |
| |
| TEST(single_threaded_success) |
| { |
| const int ruleset_fd = create_ruleset(_metadata); |
| |
| disable_caps(_metadata); |
| |
| ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); |
| ASSERT_EQ(0, landlock_restrict_self(ruleset_fd, |
| LANDLOCK_RESTRICT_SELF_TSYNC)); |
| |
| EXPECT_EQ(0, close(ruleset_fd)); |
| } |
| |
| static void store_no_new_privs(void *data) |
| { |
| bool *nnp = data; |
| |
| if (!nnp) |
| return; |
| *nnp = prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0); |
| } |
| |
| static void *idle(void *data) |
| { |
| pthread_cleanup_push(store_no_new_privs, data); |
| |
| while (true) |
| sleep(1); |
| |
| pthread_cleanup_pop(1); |
| } |
| |
| TEST(multi_threaded_success) |
| { |
| pthread_t t1, t2; |
| bool no_new_privs1, no_new_privs2; |
| const int ruleset_fd = create_ruleset(_metadata); |
| |
| disable_caps(_metadata); |
| |
| ASSERT_EQ(0, pthread_create(&t1, NULL, idle, &no_new_privs1)); |
| ASSERT_EQ(0, pthread_create(&t2, NULL, idle, &no_new_privs2)); |
| |
| ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); |
| |
| EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, |
| LANDLOCK_RESTRICT_SELF_TSYNC)); |
| |
| ASSERT_EQ(0, pthread_cancel(t1)); |
| ASSERT_EQ(0, pthread_cancel(t2)); |
| ASSERT_EQ(0, pthread_join(t1, NULL)); |
| ASSERT_EQ(0, pthread_join(t2, NULL)); |
| |
| /* The no_new_privs flag was implicitly enabled on all threads. */ |
| EXPECT_TRUE(no_new_privs1); |
| EXPECT_TRUE(no_new_privs2); |
| |
| EXPECT_EQ(0, close(ruleset_fd)); |
| } |
| |
| TEST(multi_threaded_success_despite_diverging_domains) |
| { |
| pthread_t t1, t2; |
| const int ruleset_fd = create_ruleset(_metadata); |
| |
| disable_caps(_metadata); |
| |
| ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); |
| |
| ASSERT_EQ(0, pthread_create(&t1, NULL, idle, NULL)); |
| ASSERT_EQ(0, pthread_create(&t2, NULL, idle, NULL)); |
| |
| /* |
| * The main thread enforces a ruleset, |
| * thereby bringing the threads' Landlock domains out of sync. |
| */ |
| EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, 0)); |
| |
| /* Still, TSYNC succeeds, bringing the threads in sync again. */ |
| EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, |
| LANDLOCK_RESTRICT_SELF_TSYNC)); |
| |
| ASSERT_EQ(0, pthread_cancel(t1)); |
| ASSERT_EQ(0, pthread_cancel(t2)); |
| ASSERT_EQ(0, pthread_join(t1, NULL)); |
| ASSERT_EQ(0, pthread_join(t2, NULL)); |
| EXPECT_EQ(0, close(ruleset_fd)); |
| } |
| |
| struct thread_restrict_data { |
| pthread_t t; |
| int ruleset_fd; |
| int result; |
| }; |
| |
| static void *thread_restrict(void *data) |
| { |
| struct thread_restrict_data *d = data; |
| |
| d->result = landlock_restrict_self(d->ruleset_fd, |
| LANDLOCK_RESTRICT_SELF_TSYNC); |
| return NULL; |
| } |
| |
| TEST(competing_enablement) |
| { |
| const int ruleset_fd = create_ruleset(_metadata); |
| struct thread_restrict_data d[] = { |
| { .ruleset_fd = ruleset_fd }, |
| { .ruleset_fd = ruleset_fd }, |
| }; |
| |
| disable_caps(_metadata); |
| |
| ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); |
| ASSERT_EQ(0, pthread_create(&d[0].t, NULL, thread_restrict, &d[0])); |
| ASSERT_EQ(0, pthread_create(&d[1].t, NULL, thread_restrict, &d[1])); |
| |
| /* Wait for threads to finish. */ |
| ASSERT_EQ(0, pthread_join(d[0].t, NULL)); |
| ASSERT_EQ(0, pthread_join(d[1].t, NULL)); |
| |
| /* Expect that both succeeded. */ |
| EXPECT_EQ(0, d[0].result); |
| EXPECT_EQ(0, d[1].result); |
| |
| EXPECT_EQ(0, close(ruleset_fd)); |
| } |
| |
| static void signal_nop_handler(int sig) |
| { |
| } |
| |
| struct signaler_data { |
| pthread_t target; |
| volatile bool stop; |
| }; |
| |
| static void *signaler_thread(void *data) |
| { |
| struct signaler_data *sd = data; |
| |
| while (!sd->stop) |
| pthread_kill(sd->target, SIGUSR1); |
| |
| return NULL; |
| } |
| |
| /* |
| * Number of idle sibling threads. This must be large enough that even on |
| * machines with many cores, the sibling threads cannot all complete their |
| * credential preparation in a single parallel wave, otherwise the signaler |
| * thread has no window to interrupt wait_for_completion_interruptible(). |
| * 200 threads on a 64-core machine yields ~3 serialized waves, giving the |
| * tight signal loop enough time to land an interruption. |
| */ |
| #define NUM_IDLE_THREADS 200 |
| |
| /* |
| * Exercises the tsync interruption and cancellation paths in tsync.c. |
| * |
| * When a signal interrupts the calling thread while it waits for sibling |
| * threads to finish their credential preparation |
| * (wait_for_completion_interruptible in landlock_restrict_sibling_threads), |
| * the kernel sets ERESTARTNOINTR, cancels queued task works that have not |
| * started yet (cancel_tsync_works), then waits for the remaining works to |
| * finish. On the error return, syscalls.c aborts the prepared credentials. |
| * The kernel automatically restarts the syscall, so userspace sees success. |
| */ |
| TEST(tsync_interrupt) |
| { |
| size_t i; |
| pthread_t threads[NUM_IDLE_THREADS]; |
| pthread_t signaler; |
| struct signaler_data sd; |
| struct sigaction sa = {}; |
| const int ruleset_fd = create_ruleset(_metadata); |
| |
| disable_caps(_metadata); |
| |
| /* Install a no-op SIGUSR1 handler so the signal does not kill us. */ |
| sa.sa_handler = signal_nop_handler; |
| sigemptyset(&sa.sa_mask); |
| ASSERT_EQ(0, sigaction(SIGUSR1, &sa, NULL)); |
| |
| ASSERT_EQ(0, prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)); |
| |
| for (i = 0; i < NUM_IDLE_THREADS; i++) |
| ASSERT_EQ(0, pthread_create(&threads[i], NULL, idle, NULL)); |
| |
| /* |
| * Start a signaler thread that continuously sends SIGUSR1 to the |
| * calling thread. This maximizes the chance of interrupting |
| * wait_for_completion_interruptible() in the kernel's tsync path. |
| */ |
| sd.target = pthread_self(); |
| sd.stop = false; |
| ASSERT_EQ(0, pthread_create(&signaler, NULL, signaler_thread, &sd)); |
| |
| /* |
| * The syscall may be interrupted and transparently restarted by the |
| * kernel (ERESTARTNOINTR). From userspace, it should always succeed. |
| */ |
| EXPECT_EQ(0, landlock_restrict_self(ruleset_fd, |
| LANDLOCK_RESTRICT_SELF_TSYNC)); |
| |
| sd.stop = true; |
| ASSERT_EQ(0, pthread_join(signaler, NULL)); |
| |
| for (i = 0; i < NUM_IDLE_THREADS; i++) { |
| ASSERT_EQ(0, pthread_cancel(threads[i])); |
| ASSERT_EQ(0, pthread_join(threads[i], NULL)); |
| } |
| |
| 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 |