mirror of
https://github.com/torvalds/linux.git
synced 2026-07-27 09:36:22 +02:00
selftests/landlock: Test SCOPE_SIGNAL on the SIGIO/fowner pgid path
Add regression tests for the LANDLOCK_SCOPE_SIGNAL handling of the asynchronous SIGIO delivery path (fcntl(F_SETOWN)) with a process-group owner. sigio_to_pgid_members covers the bypass: a sandboxed process at the head of its process group's PGID hlist (the default after fork()) arms F_SETOWN(-pgrp) + O_ASYNC and triggers the fan-out; the in-domain owner must be signaled (proving the trigger fired) while the non-sandboxed member of the group, outside the domain, must not. sigio_to_pgid_self covers the same-process guarantee: the owner is registered from a sandboxed non-leader thread, whose domain differs from the thread-group leader the kernel signals for a process-group owner. That leader belongs to the owner's own process and must still be signaled. Without the fix the first test sees the out-of-domain member signaled and the second sees the owner's own leader denied. Cc: stable@vger.kernel.org Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me> Reviewed-by: Günther Noack <gnoack3000@gmail.com> Link: https://patch.msgid.link/43370e89f7a896a583bf33d1cd171d02630e61bf.1780614610.git.hexlabsecurity@proton.me [mic: Fix comment] Signed-off-by: Mickaël Salaün <mic@digikod.net>
This commit is contained in:
parent
4b80320ca7
commit
76579d09be
|
|
@ -559,4 +559,186 @@ TEST_F(fown, sigurg_socket)
|
|||
_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 (void *)THREAD_ERROR;
|
||||
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ||
|
||||
landlock_restrict_self(ruleset_fd, 0)) {
|
||||
close(ruleset_fd);
|
||||
return (void *)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 (void *)THREAD_ERROR;
|
||||
|
||||
return (void *)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;
|
||||
enum thread_return 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, (void **)&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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user