| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Landlock tests - Signal Scoping |
| * |
| * Copyright © 2024 Tahera Fahimi <fahimitahera@gmail.com> |
| */ |
| |
| #define _GNU_SOURCE |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <linux/landlock.h> |
| #include <pthread.h> |
| #include <signal.h> |
| #include <sys/prctl.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| |
| #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 |