766 lines
19 KiB
C
766 lines
19 KiB
C
// 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;
|
|
}
|
|
|
|
/* 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]));
|
|
}
|
|
|
|
TEST_HARNESS_MAIN
|