// SPDX-License-Identifier: GPL-2.0 /* * Landlock tests - Signal Scoping * * Copyright © 2024 Tahera Fahimi */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include "common.h" #include "scoped_common.h" /* This variable is used for handling several signals. */ static volatile sig_atomic_t is_signaled; /* clang-format off */ FIXTURE(scoping_signals) {}; /* clang-format on */ FIXTURE_VARIANT(scoping_signals) { int sig; }; /* clang-format off */ FIXTURE_VARIANT_ADD(scoping_signals, sigtrap) { /* clang-format on */ .sig = SIGTRAP, }; /* clang-format off */ FIXTURE_VARIANT_ADD(scoping_signals, sigurg) { /* clang-format on */ .sig = SIGURG, }; /* clang-format off */ FIXTURE_VARIANT_ADD(scoping_signals, sighup) { /* clang-format on */ .sig = SIGHUP, }; /* clang-format off */ FIXTURE_VARIANT_ADD(scoping_signals, sigtstp) { /* clang-format on */ .sig = SIGTSTP, }; FIXTURE_SETUP(scoping_signals) { drop_caps(_metadata); is_signaled = 0; } FIXTURE_TEARDOWN(scoping_signals) { } static void scope_signal_handler(int sig, siginfo_t *info, void *ucontext) { if (sig == SIGTRAP || sig == SIGURG || sig == SIGHUP || sig == SIGTSTP) is_signaled = 1; } /* * In this test, a child process sends a signal to parent before and * after getting scoped. */ TEST_F(scoping_signals, send_sig_to_parent) { int pipe_parent[2]; int status; pid_t child; pid_t parent = getpid(); struct sigaction action = { .sa_sigaction = scope_signal_handler, .sa_flags = SA_SIGINFO, }; ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); ASSERT_LE(0, sigaction(variant->sig, &action, NULL)); /* The process should not have already been signaled. */ EXPECT_EQ(0, is_signaled); child = fork(); ASSERT_LE(0, child); if (child == 0) { char buf_child; int err; EXPECT_EQ(0, close(pipe_parent[1])); /* * The child process can send signal to parent when * domain is not scoped. */ err = kill(parent, variant->sig); ASSERT_EQ(0, err); ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); EXPECT_EQ(0, close(pipe_parent[0])); create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); /* * The child process cannot send signal to the parent * anymore. */ err = kill(parent, variant->sig); ASSERT_EQ(-1, err); ASSERT_EQ(EPERM, errno); /* * No matter of the domain, a process should be able to * send a signal to itself. */ ASSERT_EQ(0, is_signaled); ASSERT_EQ(0, raise(variant->sig)); ASSERT_EQ(1, is_signaled); _exit(_metadata->exit_code); return; } EXPECT_EQ(0, close(pipe_parent[0])); /* Waits for a first signal to be received, without race condition. */ while (!is_signaled && !usleep(1)) ; ASSERT_EQ(1, is_signaled); ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); EXPECT_EQ(0, close(pipe_parent[1])); is_signaled = 0; ASSERT_EQ(child, waitpid(child, &status, 0)); if (WIFSIGNALED(status) || !WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) _metadata->exit_code = KSFT_FAIL; EXPECT_EQ(0, is_signaled); } /* clang-format off */ FIXTURE(scoped_domains) {}; /* clang-format on */ #include "scoped_base_variants.h" FIXTURE_SETUP(scoped_domains) { drop_caps(_metadata); } FIXTURE_TEARDOWN(scoped_domains) { } /* * This test ensures that a scoped process cannot send signal out of * scoped domain. */ TEST_F(scoped_domains, check_access_signal) { pid_t child; pid_t parent = getpid(); int status; bool can_signal_child, can_signal_parent; int pipe_parent[2], pipe_child[2]; char buf_parent; int err; can_signal_parent = !variant->domain_child; can_signal_child = !variant->domain_parent; if (variant->domain_both) create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); child = fork(); ASSERT_LE(0, child); if (child == 0) { char buf_child; EXPECT_EQ(0, close(pipe_child[0])); EXPECT_EQ(0, close(pipe_parent[1])); if (variant->domain_child) create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); ASSERT_EQ(1, write(pipe_child[1], ".", 1)); EXPECT_EQ(0, close(pipe_child[1])); /* Waits for the parent to send signals. */ ASSERT_EQ(1, read(pipe_parent[0], &buf_child, 1)); EXPECT_EQ(0, close(pipe_parent[0])); err = kill(parent, 0); if (can_signal_parent) { ASSERT_EQ(0, err); } else { ASSERT_EQ(-1, err); ASSERT_EQ(EPERM, errno); } /* * No matter of the domain, a process should be able to * send a signal to itself. */ ASSERT_EQ(0, raise(0)); _exit(_metadata->exit_code); return; } EXPECT_EQ(0, close(pipe_parent[0])); EXPECT_EQ(0, close(pipe_child[1])); if (variant->domain_parent) create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); ASSERT_EQ(1, read(pipe_child[0], &buf_parent, 1)); EXPECT_EQ(0, close(pipe_child[0])); err = kill(child, 0); if (can_signal_child) { ASSERT_EQ(0, err); } else { ASSERT_EQ(-1, err); ASSERT_EQ(EPERM, errno); } ASSERT_EQ(0, raise(0)); ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); EXPECT_EQ(0, close(pipe_parent[1])); ASSERT_EQ(child, waitpid(child, &status, 0)); if (WIFSIGNALED(status) || !WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) _metadata->exit_code = KSFT_FAIL; } static int thread_pipe[2]; enum thread_return { THREAD_INVALID = 0, THREAD_SUCCESS = 1, THREAD_ERROR = 2, }; void *thread_func(void *arg) { char buf; if (read(thread_pipe[0], &buf, 1) != 1) return (void *)THREAD_ERROR; return (void *)THREAD_SUCCESS; } TEST(signal_scoping_threads) { pthread_t no_sandbox_thread, scoped_thread; enum thread_return ret = THREAD_INVALID; drop_caps(_metadata); ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC)); ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_func, NULL)); /* Restricts the domain after creating the first thread. */ create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); ASSERT_EQ(EPERM, pthread_kill(no_sandbox_thread, 0)); ASSERT_EQ(1, write(thread_pipe[1], ".", 1)); ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_func, NULL)); ASSERT_EQ(0, pthread_kill(scoped_thread, 0)); ASSERT_EQ(1, write(thread_pipe[1], ".", 1)); EXPECT_EQ(0, pthread_join(no_sandbox_thread, (void **)&ret)); EXPECT_EQ(THREAD_SUCCESS, ret); EXPECT_EQ(0, pthread_join(scoped_thread, (void **)&ret)); EXPECT_EQ(THREAD_SUCCESS, ret); EXPECT_EQ(0, close(thread_pipe[0])); EXPECT_EQ(0, close(thread_pipe[1])); } const short backlog = 10; static volatile sig_atomic_t signal_received; static void handle_sigurg(int sig) { if (sig == SIGURG) signal_received = 1; else signal_received = -1; } static int setup_signal_handler(int signal) { struct sigaction sa = { .sa_handler = handle_sigurg, }; if (sigemptyset(&sa.sa_mask)) return -1; sa.sa_flags = SA_SIGINFO | SA_RESTART; return sigaction(SIGURG, &sa, NULL); } /* clang-format off */ FIXTURE(fown) {}; /* clang-format on */ enum fown_sandbox { SANDBOX_NONE, SANDBOX_BEFORE_FORK, SANDBOX_BEFORE_SETOWN, SANDBOX_AFTER_SETOWN, }; FIXTURE_VARIANT(fown) { const enum fown_sandbox sandbox_setown; }; /* clang-format off */ FIXTURE_VARIANT_ADD(fown, no_sandbox) { /* clang-format on */ .sandbox_setown = SANDBOX_NONE, }; /* clang-format off */ FIXTURE_VARIANT_ADD(fown, sandbox_before_fork) { /* clang-format on */ .sandbox_setown = SANDBOX_BEFORE_FORK, }; /* clang-format off */ FIXTURE_VARIANT_ADD(fown, sandbox_before_setown) { /* clang-format on */ .sandbox_setown = SANDBOX_BEFORE_SETOWN, }; /* clang-format off */ FIXTURE_VARIANT_ADD(fown, sandbox_after_setown) { /* clang-format on */ .sandbox_setown = SANDBOX_AFTER_SETOWN, }; FIXTURE_SETUP(fown) { drop_caps(_metadata); } FIXTURE_TEARDOWN(fown) { } /* * Sending an out of bound message will trigger the SIGURG signal * through file_send_sigiotask. */ TEST_F(fown, sigurg_socket) { int server_socket, recv_socket; struct service_fixture server_address; char buffer_parent; int status; int pipe_parent[2], pipe_child[2]; pid_t child; memset(&server_address, 0, sizeof(server_address)); set_unix_address(&server_address, 0); ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC)); if (variant->sandbox_setown == SANDBOX_BEFORE_FORK) create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); child = fork(); ASSERT_LE(0, child); if (child == 0) { int client_socket; char buffer_child; EXPECT_EQ(0, close(pipe_parent[1])); EXPECT_EQ(0, close(pipe_child[0])); ASSERT_EQ(0, setup_signal_handler(SIGURG)); client_socket = socket(AF_UNIX, SOCK_STREAM, 0); ASSERT_LE(0, client_socket); /* Waits for the parent to listen. */ ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1)); ASSERT_EQ(0, connect(client_socket, &server_address.unix_addr, server_address.unix_addr_len)); /* * Waits for the parent to accept the connection, sandbox * itself, and call fcntl(2). */ ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1)); /* May signal itself. */ ASSERT_EQ(1, send(client_socket, ".", 1, MSG_OOB)); EXPECT_EQ(0, close(client_socket)); ASSERT_EQ(1, write(pipe_child[1], ".", 1)); EXPECT_EQ(0, close(pipe_child[1])); /* Waits for the message to be received. */ ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1)); EXPECT_EQ(0, close(pipe_parent[0])); if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN) { ASSERT_EQ(0, signal_received); } else { /* * A signal is only received if fcntl(F_SETOWN) was * called before any sandboxing or if the signal * receiver is in the same domain. */ ASSERT_EQ(1, signal_received); } _exit(_metadata->exit_code); return; } EXPECT_EQ(0, close(pipe_parent[0])); EXPECT_EQ(0, close(pipe_child[1])); server_socket = socket(AF_UNIX, SOCK_STREAM, 0); ASSERT_LE(0, server_socket); ASSERT_EQ(0, bind(server_socket, &server_address.unix_addr, server_address.unix_addr_len)); ASSERT_EQ(0, listen(server_socket, backlog)); ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); recv_socket = accept(server_socket, NULL, NULL); ASSERT_LE(0, recv_socket); if (variant->sandbox_setown == SANDBOX_BEFORE_SETOWN) create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); /* * Sets the child to receive SIGURG for MSG_OOB. This uncommon use is * a valid attack scenario which also simplifies this test. */ ASSERT_EQ(0, fcntl(recv_socket, F_SETOWN, child)); if (variant->sandbox_setown == SANDBOX_AFTER_SETOWN) create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); /* Waits for the child to send MSG_OOB. */ ASSERT_EQ(1, read(pipe_child[0], &buffer_parent, 1)); EXPECT_EQ(0, close(pipe_child[0])); ASSERT_EQ(1, recv(recv_socket, &buffer_parent, 1, MSG_OOB)); EXPECT_EQ(0, close(recv_socket)); EXPECT_EQ(0, close(server_socket)); ASSERT_EQ(1, write(pipe_parent[1], ".", 1)); EXPECT_EQ(0, close(pipe_parent[1])); ASSERT_EQ(child, waitpid(child, &status, 0)); if (WIFSIGNALED(status) || !WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) _metadata->exit_code = KSFT_FAIL; } TEST_HARNESS_MAIN