| // 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 <sched.h> |
| #include <signal.h> |
| #include <sys/mount.h> |
| #include <sys/prctl.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| |
| #include "common.h" |
| #include "scoped_common.h" |
| #include "trace.h" |
| |
| #define TRACE_TASK "scoped_signal_t" |
| |
| /* 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; |
| } |
| |
| /* clang-format off */ |
| #define THREAD_INVALID ((void *)0) |
| #define THREAD_SUCCESS ((void *)1) |
| #define THREAD_ERROR ((void *)2) |
| #define THREAD_TEST_FAILED ((void *)3) |
| /* clang-format on */ |
| |
| static void *thread_sync(void *arg) |
| { |
| const int pipe_read = *(int *)arg; |
| char buf; |
| |
| if (read(pipe_read, &buf, 1) != 1) |
| return THREAD_ERROR; |
| |
| return THREAD_SUCCESS; |
| } |
| |
| TEST(signal_scoping_thread_before) |
| { |
| pthread_t no_sandbox_thread; |
| void *ret = THREAD_INVALID; |
| int thread_pipe[2]; |
| |
| drop_caps(_metadata); |
| ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC)); |
| |
| ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_sync, |
| &thread_pipe[0])); |
| |
| /* Enforces restriction after creating the thread. */ |
| create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); |
| |
| EXPECT_EQ(0, pthread_kill(no_sandbox_thread, 0)); |
| EXPECT_EQ(1, write(thread_pipe[1], ".", 1)); |
| |
| EXPECT_EQ(0, pthread_join(no_sandbox_thread, &ret)); |
| EXPECT_EQ(THREAD_SUCCESS, ret); |
| |
| EXPECT_EQ(0, close(thread_pipe[0])); |
| EXPECT_EQ(0, close(thread_pipe[1])); |
| } |
| |
| TEST(signal_scoping_thread_after) |
| { |
| pthread_t scoped_thread; |
| void *ret = THREAD_INVALID; |
| int thread_pipe[2]; |
| |
| drop_caps(_metadata); |
| ASSERT_EQ(0, pipe2(thread_pipe, O_CLOEXEC)); |
| |
| /* Enforces restriction before creating the thread. */ |
| create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); |
| |
| ASSERT_EQ(0, pthread_create(&scoped_thread, NULL, thread_sync, |
| &thread_pipe[0])); |
| |
| EXPECT_EQ(0, pthread_kill(scoped_thread, 0)); |
| EXPECT_EQ(1, write(thread_pipe[1], ".", 1)); |
| |
| EXPECT_EQ(0, pthread_join(scoped_thread, &ret)); |
| EXPECT_EQ(THREAD_SUCCESS, ret); |
| |
| EXPECT_EQ(0, close(thread_pipe[0])); |
| EXPECT_EQ(0, close(thread_pipe[1])); |
| } |
| |
| struct thread_setuid_args { |
| int pipe_read, new_uid; |
| }; |
| |
| void *thread_setuid(void *ptr) |
| { |
| const struct thread_setuid_args *arg = ptr; |
| char buf; |
| |
| if (read(arg->pipe_read, &buf, 1) != 1) |
| return THREAD_ERROR; |
| |
| /* libc's setuid() should update all thread's credentials. */ |
| if (getuid() != arg->new_uid) |
| return THREAD_TEST_FAILED; |
| |
| return THREAD_SUCCESS; |
| } |
| |
| TEST(signal_scoping_thread_setuid) |
| { |
| struct thread_setuid_args arg; |
| pthread_t no_sandbox_thread; |
| void *ret = THREAD_INVALID; |
| int pipe_parent[2]; |
| int prev_uid; |
| |
| disable_caps(_metadata); |
| |
| /* This test does not need to be run as root. */ |
| prev_uid = getuid(); |
| arg.new_uid = prev_uid + 1; |
| EXPECT_LT(0, arg.new_uid); |
| |
| ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC)); |
| arg.pipe_read = pipe_parent[0]; |
| |
| /* Capabilities must be set before creating a new thread. */ |
| set_cap(_metadata, CAP_SETUID); |
| ASSERT_EQ(0, pthread_create(&no_sandbox_thread, NULL, thread_setuid, |
| &arg)); |
| |
| /* Enforces restriction after creating the thread. */ |
| create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); |
| |
| EXPECT_NE(arg.new_uid, getuid()); |
| EXPECT_EQ(0, setuid(arg.new_uid)); |
| EXPECT_EQ(arg.new_uid, getuid()); |
| EXPECT_EQ(1, write(pipe_parent[1], ".", 1)); |
| |
| EXPECT_EQ(0, pthread_join(no_sandbox_thread, &ret)); |
| EXPECT_EQ(THREAD_SUCCESS, ret); |
| |
| clear_cap(_metadata, CAP_SETUID); |
| EXPECT_EQ(0, close(pipe_parent[0])); |
| EXPECT_EQ(0, close(pipe_parent[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); |
| } |
| |
| /* |
| * MSG_OOB might be disabled in the kernel via the CONFIG_AF_UNIX_OOB |
| * switch, so this function can be used for probing for its availability. |
| */ |
| static bool has_af_unix_oob(void) |
| { |
| bool available = false; |
| int sp[2]; |
| |
| if (socketpair(AF_UNIX, SOCK_STREAM, 0, sp) == 0) { |
| available = (send(sp[0], ".", 1, MSG_OOB) == 1); |
| close(sp[0]); |
| close(sp[1]); |
| } |
| |
| return available; |
| } |
| |
| /* 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; |
| |
| if (!has_af_unix_oob()) |
| SKIP(return, "CONFIG_AF_UNIX_OOB / MSG_OOB not available"); |
| |
| 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; |
| } |
| |
| /* |
| * Checks that LANDLOCK_SCOPE_SIGNAL is enforced on the asynchronous SIGIO |
| * delivery path (fcntl(F_SETOWN)) when the file owner is a process group. |
| * |
| * A sandboxed process sitting at the head of its process group's PID hlist (the |
| * default position right after fork()) used to escape the fcntl(F_SETOWN, |
| * -pgrp) domain recording: pid_task(pgrp, PIDTYPE_PGID) resolved to the process |
| * itself, so the same-thread-group exemption skipped recording its Landlock |
| * domain. At SIGIO time that domain was then unset and the signal fanned out |
| * to every group member, including non-sandboxed processes outside the domain. |
| */ |
| TEST(sigio_to_pgid_members) |
| { |
| int trigger[2], sync_child[2]; |
| char buf; |
| pid_t child; |
| int status, i; |
| |
| drop_caps(_metadata); |
| |
| /* |
| * Isolates the test in its own process group so the SIGIO fan-out stays |
| * bounded to this parent and the child forked below. |
| */ |
| ASSERT_EQ(0, setpgid(0, 0)); |
| |
| /* The non-sandboxed parent is the protected (out-of-domain) target. */ |
| ASSERT_EQ(0, setup_signal_handler(SIGURG)); |
| signal_received = 0; |
| |
| ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC)); |
| ASSERT_EQ(0, pipe2(sync_child, O_CLOEXEC)); |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| if (child == 0) { |
| /* |
| * The child inherits the parent's new process group and, just |
| * attached with hlist_add_head_rcu(), is now the head of the |
| * pgid hlist: this is the case that used to skip the recording. |
| */ |
| EXPECT_EQ(0, close(sync_child[0])); |
| |
| /* In-domain positive control: the child must be signaled. */ |
| ASSERT_EQ(0, setup_signal_handler(SIGURG)); |
| signal_received = 0; |
| |
| create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); |
| |
| /* Owns the SIGIO source for the whole process group. */ |
| ASSERT_EQ(0, fcntl(trigger[0], F_SETSIG, SIGURG)); |
| ASSERT_EQ(0, fcntl(trigger[0], F_SETOWN, -getpgrp())); |
| ASSERT_EQ(0, fcntl(trigger[0], F_SETFL, O_ASYNC)); |
| |
| /* Fans SIGURG out to every member of the process group. */ |
| ASSERT_EQ(1, write(trigger[1], ".", 1)); |
| |
| /* |
| * The sandboxed child is in its own domain and must always be |
| * signaled: this proves the SIGIO actually fired. |
| */ |
| for (i = 0; i < 1000 && !signal_received; i++) |
| usleep(1000); |
| EXPECT_EQ(1, signal_received); |
| |
| ASSERT_EQ(1, write(sync_child[1], ".", 1)); |
| EXPECT_EQ(0, close(sync_child[1])); |
| |
| _exit(_metadata->exit_code); |
| return; |
| } |
| EXPECT_EQ(0, close(sync_child[1])); |
| EXPECT_EQ(0, close(trigger[0])); |
| EXPECT_EQ(0, close(trigger[1])); |
| |
| /* Waits for the child to generate the SIGIO. */ |
| ASSERT_EQ(1, read(sync_child[0], &buf, 1)); |
| EXPECT_EQ(0, close(sync_child[0])); |
| |
| /* Lets a delivered-but-pending signal run our handler, if any. */ |
| for (i = 0; i < 100 && !signal_received; i++) |
| usleep(1000); |
| |
| /* |
| * SCOPE_SIGNAL must block the fan-out to this non-sandboxed parent, |
| * which is outside the child's Landlock domain. Before the fix the |
| * parent was signaled here. |
| */ |
| EXPECT_EQ(0, signal_received); |
| |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| if (WIFSIGNALED(status) || !WIFEXITED(status) || |
| WEXITSTATUS(status) != EXIT_SUCCESS) |
| _metadata->exit_code = KSFT_FAIL; |
| } |
| |
| static void *thread_setown_scoped(void *arg) |
| { |
| const int fd = *(int *)arg; |
| int ruleset_fd; |
| const struct landlock_ruleset_attr ruleset_attr = { |
| .scoped = LANDLOCK_SCOPE_SIGNAL, |
| }; |
| |
| /* Sandboxes only this non-leader thread (no thread syncing). */ |
| ruleset_fd = |
| landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); |
| if (ruleset_fd < 0) |
| return THREAD_ERROR; |
| if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) || |
| landlock_restrict_self(ruleset_fd, 0)) { |
| close(ruleset_fd); |
| return THREAD_ERROR; |
| } |
| close(ruleset_fd); |
| |
| /* Makes this process group own the SIGIO source. */ |
| if (fcntl(fd, F_SETSIG, SIGURG) || fcntl(fd, F_SETOWN, -getpgrp()) || |
| fcntl(fd, F_SETFL, O_ASYNC)) |
| return THREAD_ERROR; |
| |
| return THREAD_SUCCESS; |
| } |
| |
| /* |
| * Checks that the SIGIO fan-out is still delivered to the file owner's own |
| * process when fcntl(F_SETOWN, -pgrp) was issued from a sandboxed non-leader |
| * thread. |
| * |
| * The Landlock domain is recorded for a process-group owner (so out-of-domain |
| * members stay blocked, see sigio_to_pgid_members), but the kernel signals a |
| * process group through its members' thread-group leaders. Here the leader is |
| * not sandboxed and thus has a different domain than the registering thread, so |
| * the registration-time check cannot tell that it belongs to the owner's own |
| * process. hook_file_send_sigiotask() must recognize it through the recorded |
| * thread group and allow the delivery, matching the same-process guarantee of |
| * commit 18eb75f3af40. Without that exemption the leader is wrongly denied and |
| * never signaled. |
| */ |
| TEST(sigio_to_pgid_self) |
| { |
| int trigger[2]; |
| pthread_t thread; |
| void *ret = THREAD_INVALID; |
| int i; |
| |
| drop_caps(_metadata); |
| |
| /* Bounds the SIGIO fan-out to this process. */ |
| ASSERT_EQ(0, setpgid(0, 0)); |
| |
| /* The non-sandboxed thread-group leader is the SIGIO target. */ |
| ASSERT_EQ(0, setup_signal_handler(SIGURG)); |
| signal_received = 0; |
| |
| ASSERT_EQ(0, pipe2(trigger, O_CLOEXEC)); |
| |
| /* |
| * Registers the process-group fowner from a sibling thread that |
| * sandboxes only itself, so its domain differs from the leader's. |
| */ |
| ASSERT_EQ(0, pthread_create(&thread, NULL, thread_setown_scoped, |
| &trigger[0])); |
| ASSERT_EQ(0, pthread_join(thread, &ret)); |
| ASSERT_EQ(THREAD_SUCCESS, ret); |
| |
| /* Fans SIGURG out to the process group. */ |
| ASSERT_EQ(1, write(trigger[1], ".", 1)); |
| |
| for (i = 0; i < 1000 && !signal_received; i++) |
| usleep(1000); |
| |
| /* |
| * Same-process delivery must always be allowed, even though the owner |
| * was registered from a sandboxed sibling thread. |
| */ |
| EXPECT_EQ(1, signal_received); |
| |
| EXPECT_EQ(0, close(trigger[0])); |
| EXPECT_EQ(0, close(trigger[1])); |
| } |
| |
| /* Trace tests */ |
| |
| /* clang-format off */ |
| FIXTURE(trace_signal) { |
| /* clang-format on */ |
| int tracefs_ok; |
| }; |
| |
| FIXTURE_SETUP(trace_signal) |
| { |
| int ret; |
| |
| set_cap(_metadata, CAP_SYS_ADMIN); |
| ASSERT_EQ(0, unshare(CLONE_NEWNS)); |
| ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); |
| |
| ret = tracefs_fixture_setup(); |
| if (ret) { |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| self->tracefs_ok = 0; |
| SKIP(return, "tracefs not available"); |
| } |
| self->tracefs_ok = 1; |
| |
| ASSERT_EQ(0, |
| tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, true)); |
| ASSERT_EQ(0, tracefs_clear()); |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| } |
| |
| FIXTURE_TEARDOWN(trace_signal) |
| { |
| if (!self->tracefs_ok) |
| return; |
| |
| set_cap(_metadata, CAP_SYS_ADMIN); |
| tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, false); |
| tracefs_fixture_teardown(); |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| } |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT(trace_signal) |
| { |
| /* clang-format on */ |
| bool sandbox; |
| bool sandbox_target; |
| int expect_denied; |
| }; |
| |
| /* Denied: sandboxed child signals unsandboxed parent (target_domain=0). */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(trace_signal, denied) { |
| /* clang-format on */ |
| .sandbox = true, |
| .sandbox_target = false, |
| .expect_denied = 1, |
| }; |
| |
| /* |
| * Denied: sandboxed child signals a sandboxed parent, so the target is in a |
| * domain and target_domain= is non-zero. |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(trace_signal, denied_scoped_target) { |
| /* clang-format on */ |
| .sandbox = true, |
| .sandbox_target = true, |
| .expect_denied = 1, |
| }; |
| |
| /* Allowed: unsandboxed child signals unsandboxed parent. */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(trace_signal, allowed) { |
| /* clang-format on */ |
| .sandbox = false, |
| .sandbox_target = false, |
| .expect_denied = 0, |
| }; |
| |
| TEST_F(trace_signal, deny_scope_signal) |
| { |
| char *buf, field[64], expected_pid[16]; |
| int count, status; |
| pid_t child; |
| |
| if (!self->tracefs_ok) |
| SKIP(return, "tracefs not available"); |
| |
| /* |
| * For the non-zero target_domain case, sandbox the parent (the signal |
| * target) before forking. The child inherits that domain and adds its |
| * own scoped layer, so the signal is still denied and target_domain= |
| * names the parent's domain. |
| */ |
| if (variant->sandbox_target) |
| create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); |
| |
| child = fork(); |
| ASSERT_LE(0, child); |
| |
| if (child == 0) { |
| if (variant->sandbox) { |
| struct landlock_ruleset_attr ruleset_attr = { |
| .scoped = LANDLOCK_SCOPE_SIGNAL, |
| }; |
| int ruleset_fd; |
| |
| ruleset_fd = landlock_create_ruleset( |
| &ruleset_attr, sizeof(ruleset_attr), 0); |
| if (ruleset_fd < 0) |
| _exit(1); |
| |
| prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); |
| if (landlock_restrict_self(ruleset_fd, 0)) { |
| close(ruleset_fd); |
| _exit(1); |
| } |
| close(ruleset_fd); |
| } |
| |
| if (variant->sandbox) { |
| /* Signal to unsandboxed parent should be denied. */ |
| if (kill(getppid(), 0) == 0) |
| _exit(2); |
| if (errno != EPERM) |
| _exit(3); |
| } else { |
| /* No sandbox: kill should succeed. */ |
| if (kill(getppid(), 0) != 0) |
| _exit(1); |
| } |
| |
| _exit(0); |
| } |
| |
| ASSERT_EQ(child, waitpid(child, &status, 0)); |
| ASSERT_TRUE(WIFEXITED(status)); |
| EXPECT_EQ(0, WEXITSTATUS(status)); |
| |
| buf = tracefs_read_buf(); |
| ASSERT_NE(NULL, buf); |
| |
| count = tracefs_count_matches(buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK)); |
| if (variant->expect_denied) { |
| EXPECT_EQ(variant->expect_denied, count) |
| { |
| TH_LOG("Expected deny_scope_signal event, got %d\n%s", |
| count, buf); |
| } |
| |
| /* Verify target_pid is the parent's PID. */ |
| snprintf(expected_pid, sizeof(expected_pid), "%d", getpid()); |
| ASSERT_EQ(0, tracefs_extract_field( |
| buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK), |
| "target_pid", field, sizeof(field))); |
| EXPECT_STREQ(expected_pid, field); |
| |
| /* |
| * Verify target_domain: 0 when the target is unsandboxed, |
| * non-zero when the target is in a domain. |
| */ |
| ASSERT_EQ(0, tracefs_extract_field( |
| buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK), |
| "target_domain", field, sizeof(field))); |
| EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0) |
| { |
| TH_LOG("Unexpected target_domain=%s", field); |
| } |
| } else { |
| EXPECT_EQ(0, count) |
| { |
| TH_LOG("Expected 0 deny_scope_signal events, " |
| "got %d\n%s", |
| count, buf); |
| } |
| } |
| |
| free(buf); |
| } |
| |
| /* |
| * Trace test for the asynchronous SIGIO/SIGURG delivery path |
| * (hook_file_send_sigiotask), which reaches the same landlock_deny_scope_signal |
| * tracepoint as a synchronous kill(2) but through fcntl(F_SETOWN). |
| */ |
| |
| /* clang-format off */ |
| FIXTURE(trace_fown) { |
| /* clang-format on */ |
| int tracefs_ok; |
| }; |
| |
| FIXTURE_SETUP(trace_fown) |
| { |
| int ret; |
| |
| set_cap(_metadata, CAP_SYS_ADMIN); |
| ASSERT_EQ(0, unshare(CLONE_NEWNS)); |
| ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)); |
| |
| ret = tracefs_fixture_setup(); |
| if (ret) { |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| self->tracefs_ok = 0; |
| SKIP(return, "tracefs not available"); |
| } |
| self->tracefs_ok = 1; |
| |
| ASSERT_EQ(0, |
| tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, true)); |
| ASSERT_EQ(0, tracefs_clear()); |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| } |
| |
| FIXTURE_TEARDOWN(trace_fown) |
| { |
| if (!self->tracefs_ok) |
| return; |
| |
| set_cap(_metadata, CAP_SYS_ADMIN); |
| tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE, false); |
| tracefs_fixture_teardown(); |
| clear_cap(_metadata, CAP_SYS_ADMIN); |
| } |
| |
| /* clang-format off */ |
| FIXTURE_VARIANT(trace_fown) |
| { |
| /* clang-format on */ |
| bool sandbox; |
| bool sandbox_target; |
| int expect_denied; |
| }; |
| |
| /* |
| * Denied: a sandboxed file owner's SIGURG is delivered to an unsandboxed target |
| * process (target_domain=0). |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(trace_fown, denied) { |
| /* clang-format on */ |
| .sandbox = true, |
| .sandbox_target = false, |
| .expect_denied = 1, |
| }; |
| |
| /* |
| * Denied: the SIGURG target sandboxes itself in its own domain, so the target |
| * is in a domain and target_domain= is non-zero. |
| */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(trace_fown, denied_scoped_target) { |
| /* clang-format on */ |
| .sandbox = true, |
| .sandbox_target = true, |
| .expect_denied = 1, |
| }; |
| |
| /* Allowed: an unsandboxed file owner delivers SIGURG. */ |
| /* clang-format off */ |
| FIXTURE_VARIANT_ADD(trace_fown, allowed) { |
| /* clang-format on */ |
| .sandbox = false, |
| .sandbox_target = false, |
| .expect_denied = 0, |
| }; |
| |
| TEST_F(trace_fown, deny_scope_fown) |
| { |
| int server_socket, recv_socket; |
| struct service_fixture server_address; |
| char buffer_parent, field[64], *buf; |
| int status, count; |
| int pipe_parent[2], pipe_child[2]; |
| pid_t child; |
| |
| if (!self->tracefs_ok) |
| SKIP(return, "tracefs not available"); |
| |
| 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)); |
| |
| 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); |
| |
| /* |
| * The SIGURG target is this child; for the non-zero |
| * target_domain case it sandboxes itself in its own domain, |
| * unrelated to the file owner's domain. |
| */ |
| if (variant->sandbox_target) |
| create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL); |
| |
| /* 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(F_SETOWN). |
| */ |
| ASSERT_EQ(1, read(pipe_parent[0], &buffer_child, 1)); |
| /* Triggers the asynchronous SIGURG to this file owner. */ |
| 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])); |
| |
| _exit(0); |
| 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); |
| |
| /* |
| * The file owner is the denying subject; its domain is captured at |
| * fcntl(F_SETOWN) time, so sandbox it before setting the owner. |
| */ |
| if (variant->sandbox) |
| 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)); |
| |
| 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(child, waitpid(child, &status, 0)); |
| ASSERT_TRUE(WIFEXITED(status)); |
| EXPECT_EQ(0, WEXITSTATUS(status)); |
| |
| buf = tracefs_read_buf(); |
| ASSERT_NE(NULL, buf); |
| |
| count = tracefs_count_matches(buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK)); |
| if (variant->expect_denied) { |
| EXPECT_EQ(variant->expect_denied, count) |
| { |
| TH_LOG("Expected deny_scope_signal event, got %d\n%s", |
| count, buf); |
| } |
| |
| /* |
| * Verify target_domain: 0 when the target is unsandboxed, |
| * non-zero when the target is in a domain. |
| */ |
| ASSERT_EQ(0, tracefs_extract_field( |
| buf, REGEX_DENY_SCOPE_SIGNAL(TRACE_TASK), |
| "target_domain", field, sizeof(field))); |
| EXPECT_EQ(variant->sandbox_target, strcmp("0", field) != 0) |
| { |
| TH_LOG("Unexpected target_domain=%s", field); |
| } |
| } else { |
| EXPECT_EQ(0, count) |
| { |
| TH_LOG("Expected 0 deny_scope_signal events, " |
| "got %d\n%s", |
| count, buf); |
| } |
| } |
| |
| free(buf); |
| } |
| |
| TEST_HARNESS_MAIN |