landlock: fix SCOPE_SIGNAL bypass on the SIGIO/fowner path
Subsystems (4)
v5 cover landlock: fix SCOPE_SIGNAL bypass on the SIGIO/fowner path
This series fixes a LANDLOCK_SCOPE_SIGNAL bypass on the asynchronous SIGIO (fcntl(F_SETOWN)) delivery path, and adds regression tests.
A sandboxed process that owns a file or socket can request a signal (F_SETSIG, e.g. SIGKILL) to be delivered to a whole process group on I/O readiness (F_SETOWN(-pgid) + O_ASYNC). When it is the head of its own process group -- the default after fork() -- that group still contains the non-sandboxed process that launched it (a supervisor, a security monitor), so the sandbox can signal processes that SCOPE_SIGNAL is meant to protect from it.
Patch 1 has two parts:
- Narrow the same-thread-group exemption in control_current_fowner() so a process-group fowner always records the caller's Landlock domain; the delivery-time check in hook_file_send_sigiotask() then runs against every group member. This closes the bypass.
- Recording the domain alone over-blocks one corner: the kernel signals a process group through its members' thread-group leaders, and the leader of the registrant's own process can carry a different Landlock domain than the sibling thread that armed F_SETOWN. domain_is_scoped() would then deny that leader, even though commit 18eb75f3af40 requires same-process delivery to be allowed. hook_task_kill() avoids this by checking same_thread_group() live, per recipient; the SIGIO path delegated the whole decision to a single registration-time check that a fan-out cannot honor. So patch 1 also records the registrant's thread group next to its domain and exempts it at delivery, restoring the same-process guarantee while keeping out-of-domain group members blocked.
The direct kill() path (hook_task_kill) is unaffected.
Patch 2 adds two regression tests in scoped_signal_test.c:sigio_to_pgid_members (out-of-domain member must not be signaled) and sigio_to_pgid_self (the registrant's own process, reached through its thread-group leader, must still be signaled).
The defect was introduced by commit 18eb75f3af40 ("landlock: Always allow signals between threads of the same process") in v6.15, and is present in the stable branches that backported it (6.12.y, 6.13.y, 6.14.y). control_current_fowner() is identical across those branches.
Verified on 7.1.0-rc5 + CONFIG_SECURITY_LANDLOCK=y (same .config, only the landlock change differs across arms):
- unpatched: sigio_to_pgid_members fails (out-of-domain member signaled,
bypass), sigio_to_pgid_self passes;
- patch-1-record-only (the v4 hunk): sigio_to_pgid_members passes,
sigio_to_pgid_self fails (the registrant's own leader is over-blocked);- this series: both pass, and the landlock signal-scoping suite is 21/21.
A standalone reproducer of both invariants was also built -m32 and -m64 and run on each arm: the fix behaves identically through the i386-compat and the x86-64 native syscall paths.
v4 -> v5 (review feedback from Günther Noack):
- patch 1: also fix the same-process over-block introduced by recording the domain for a process-group fowner -- record the registrant's thread group (struct pid) in landlock_file_security and exempt it in hook_file_send_sigiotask() (task_tgid(tsk) == fown_tg), restoring the 18eb75f3af40 guarantee for the registrant's own process;
- patch 2: add sigio_to_pgid_self covering the non-leader-registrant /
pgid-includes-self case;
- drop Tested-by: Justin Suess -- patch 1 gained the delivery-time exemption
he did not test (re-test welcome);- posted as a fresh top-level thread (no In-Reply-To to the v4 review).
v4: https://lore.kernel.org/all/[email protected]/
(v1/v2 were sent to [email protected] while embargoed; not in a public archive.)
Bryam Vargas (2):
landlock: fix LANDLOCK_SCOPE_SIGNAL bypass on the SIGIO path selftests/landlock: test SCOPE_SIGNAL on the SIGIO/fowner pgid path
security/landlock/fs.c | 15 ++ security/landlock/fs.h | 10 + security/landlock/task.c | 11 ++ .../selftests/landlock/scoped_signal_test.c | 183 ++++++++++++++++++ 4 files changed, 219 insertions(+)
base-commit: 6f3ed7fec72fc8979b2a8c7219c0a9fcfc8d07b5 --
2.43.0
PATCH v5 1/2 landlock: fix LANDLOCK_SCOPE_SIGNAL bypass on the SIGIO path
LANDLOCK_SCOPE_SIGNAL must prevent a sandboxed process from signaling processes outside its Landlock domain. It can be bypassed through the asynchronous SIGIO delivery path.
A sandboxed process that owns any file or socket can arm it with fcntl(F_SETOWN, fd, -pgid), fcntl(F_SETSIG, fd, SIGKILL) and O_ASYNC, so that an I/O event makes the kernel deliver the chosen signal to the whole process group. As the head of its own process group -- the default right after fork() -- that group also holds the non-sandboxed process that launched it, e.g. a supervisor or a security monitor. The sandbox can thus kill or repeatedly signal exactly the processes SCOPE_SIGNAL is meant to protect from it.
The scope is enforced in hook_file_send_sigiotask() against the Landlock domain recorded at F_SETOWN time, not the live domain of the sender. control_current_fowner() decides whether to record that domain and skips recording it when the fowner target is in the caller's thread group -- safe only when the target is a single process sharing the caller's credentials (PIDTYPE_PID, PIDTYPE_TGID). For a process group (PIDTYPE_PGID) the target resolves to the caller itself when it is the group head, recording is skipped, and hook_file_send_sigiotask() then lets the signal fan out to the whole group unchecked.
Record the domain for every non single-process target so the scope is enforced against each group member at delivery time.
That recording is necessary but not sufficient on its own: the kernel signals a process group through its members' thread-group leaders, and the leader of the registrant's own process can carry a different Landlock domain than the sibling thread that armed the owner. domain_is_scoped() would then deny that leader, even though commit 18eb75f3af40 ("landlock:
Always allow signals between threads of the same process") requires same-process delivery to be allowed. hook_task_kill() avoids this by evaluating same_thread_group() live, per recipient; the SIGIO path instead delegates the whole decision to a single registration-time check, which a process-group fan-out cannot honor.
So also record the registrant's thread group next to its domain and exempt it at delivery: hook_file_send_sigiotask() allows the signal whenever the recipient belongs to the registrant's own process, restoring the same-process guarantee while keeping out-of-domain group members blocked. The direct kill() path (hook_task_kill) already evaluates the live domain and is unaffected.
--- security/landlock/fs.c | 15 +++++++++++++++ security/landlock/fs.h | 10 ++++++++++ security/landlock/task.c | 11 +++++++++++ 3 files changed, 36 insertions(+)
security/landlock/fs.c +15 -0
@@ -1909,6 +1909,15 @@ static bool control_current_fowner(struct fown_struct *const fown)if (!p)return true; +/* +* A process-group fowner fans the signal out to every member at +* delivery time, so record the domain for any non single-process +* target -- even when it resolves to current as the group head -- and +* let hook_file_send_sigiotask() check the live scope per recipient. +*/ +if (fown->pid_type !=PIDTYPE_PID && fown->pid_type !=PIDTYPE_TGID ) +return true; +return !same_thread_group (p, current); }
@@ -1916,6 +1925,7 @@ static void hook_file_set_fowner(struct file *file) {struct landlock_ruleset *prev_dom;struct landlock_cred_security fown_subject = {}; +struct pid *prev_tg, *fown_tg =NULL ;size_t fown_layer =0 ;if (control_current_fowner (file_f_owner (file))) {
@@ -1928,21 +1938,26 @@ static void hook_file_set_fowner(struct file *file)if (new_subject) {landlock_get_ruleset (new_subject->domain); fown_subject = *new_subject; + fown_tg =get_pid (task_tgid (current)); } } prev_dom =landlock_file (file)->fown_subject.domain; + prev_tg =landlock_file (file)->fown_tg;landlock_file (file)->fown_subject = fown_subject; +landlock_file (file)->fown_tg = fown_tg;#ifdef CONFIG_AUDIT landlock_file (file)->fown_layer = fown_layer;#endif /* CONFIG_AUDIT*/ /* May be called in an RCU read-side critical section. */ landlock_put_ruleset_deferred (prev_dom); +put_pid (prev_tg); }static void hook_file_free_security (struct file *file) { +put_pid (landlock_file (file)->fown_tg);landlock_put_ruleset_deferred (landlock_file (file)->fown_subject.domain); }
security/landlock/fs.h +10 -0
@@ -78,6 +78,16 @@ struct landlock_file_security {* euid. */ struct landlock_cred_security fown_subject; +/** +* @fown_tg: Thread group of the task that set the file owner, pinned +* while @fown_subject holds a domain. It lets +* hook_file_send_sigiotask() always allow a SIGIO delivered to the +* owner's own process -- e.g. the thread-group leader reached through a +* process-group owner -- matching the same-process exemption of +* hook_task_kill(). NULL when no domain is recorded. Protected by +* file->f_owner->lock, like @fown_subject. +*/ +struct pid *fown_tg; };#ifdef CONFIG_AUDIT
security/landlock/task.c +11 -0
@@ -411,6 +411,17 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,if (!subject->domain)return 0 ; +/* +* Always allow delivery to the file owner's own process, including a +* thread-group leader reached through a process-group owner. This +* mirrors hook_task_kill()'s same-process exemption and preserves the +* guarantee of commit 18eb75f3af40 ("landlock: Always allow signals +* between threads of the same process"), which the registration-time +* check cannot honor for a process-group target. +*/ +if (task_tgid (tsk) ==landlock_file (fown->file)->fown_tg) +return 0 ; +scoped_guard (rcu) { is_scoped = domain_is_scoped(subject->domain,
Signature · 1 line
2.43.0
PATCH v5 2/2 selftests/landlock: test SCOPE_SIGNAL on the SIGIO/fowner pgid path
In reply to Bryam VargasAdd 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 PID 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.
--- .../selftests/landlock/scoped_signal_test.c | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+)
tools/testing/selftests/landlock/scoped_signal_test.c +183 -0
@@ -559,4 +559,187 @@ 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
Signature · 1 line
2.43.0
Re: [PATCH v5 1/2] landlock: fix LANDLOCK_SCOPE_SIGNAL bypass on the SIGIO path
In reply to Bryam VargasQuoted 155 lines · Bryam Vargas
LANDLOCK_SCOPE_SIGNALmust prevent a sandboxed process from signaling processes outside its Landlock domain. It can be bypassed through the asynchronous SIGIO delivery path.A sandboxed process that owns any file or socket can arm it with
fcntl(F_SETOWN, fd, -pgid),fcntl(F_SETSIG, fd, SIGKILL)andO_ASYNC, so that an I/O event makes the kernel deliver the chosen signal to the whole process group. As the head of its own process group -- the default right afterfork()-- that group also holds the non-sandboxed process that launched it, e.g. a supervisor or a security monitor. The sandbox can thus kill or repeatedly signal exactly the processesSCOPE_SIGNALis meant to protect from it.The scope is enforced in
hook_file_send_sigiotask()against the Landlock domain recorded atF_SETOWNtime, not the live domain of the sender.control_current_fowner()decides whether to record that domain and skips recording it when the fowner target is in the caller's thread group -- safe only when the target is a single process sharing the caller's credentials (PIDTYPE_PID,PIDTYPE_TGID). For a process group (PIDTYPE_PGID) the target resolves to the caller itself when it is the group head, recording is skipped, andhook_file_send_sigiotask()then lets the signal fan out to the whole group unchecked.Record the domain for every non single-process target so the scope is enforced against each group member at delivery time.
That recording is necessary but not sufficient on its own: the kernel signals a process group through its members' thread-group leaders, and the leader of the registrant's own process can carry a different Landlock domain than the sibling thread that armed the owner.
domain_is_scoped()would then deny that leader, even though commit18eb75f3af40("landlock:
Always allow signals between threads of the same process") requires same-process delivery to be allowed.hook_task_kill()avoids this by evaluatingsame_thread_group()live, per recipient; the SIGIO path instead delegates the whole decision to a single registration-time check, which a process-group fan-out cannot honor.So also record the registrant's thread group next to its domain and exempt it at delivery:
hook_file_send_sigiotask()allows the signal whenever the recipient belongs to the registrant's own process, restoring the same-process guarantee while keeping out-of-domain group members blocked. The directkill()path (hook_task_kill) already evaluates the live domain and is unaffected.Fixes 18eb75f3af40 ("landlock: Always allow signals between threads of the same process")Signed-off-by Bryam Vargas <[email protected]>--- security/landlock/fs.c | 15 +++++++++++++++ security/landlock/fs.h | 10 ++++++++++ security/landlock/task.c | 11 +++++++++++ 3 files changed, 36 insertions(+)security/landlock/fs.c +15
-0@@ -1909,6 +1909,15 @@ static bool control_current_fowner(struct fown_struct *const fown)if (!p)return true; +/* +* A process-group fowner fans the signal out to every member at +* delivery time, so record the domain for any non single-process +* target -- even when it resolves to current as the group head -- and +* let hook_file_send_sigiotask() check the live scope per recipient. +*/ +if (fown->pid_type !=PIDTYPE_PID && fown->pid_type !=PIDTYPE_TGID ) +return true; +return !same_thread_group (p, current); }@@ -1916,6 +1925,7 @@ static void hook_file_set_fowner(struct file *file) {struct landlock_ruleset *prev_dom;struct landlock_cred_security fown_subject = {}; +struct pid *prev_tg, *fown_tg =NULL ;size_t fown_layer =0 ;if (control_current_fowner (file_f_owner (file))) {@@ -1928,21 +1938,26 @@ static void hook_file_set_fowner(struct file *file)if (new_subject) {landlock_get_ruleset (new_subject->domain); fown_subject = *new_subject; + fown_tg =get_pid (task_tgid (current)); } } prev_dom =landlock_file (file)->fown_subject.domain; + prev_tg =landlock_file (file)->fown_tg;landlock_file (file)->fown_subject = fown_subject; +landlock_file (file)->fown_tg = fown_tg;#ifdef CONFIG_AUDIT landlock_file (file)->fown_layer = fown_layer;#endif /* CONFIG_AUDIT*/ /* May be called in an RCU read-side critical section. */ landlock_put_ruleset_deferred (prev_dom); +put_pid (prev_tg); }static void hook_file_free_security (struct file *file) { +put_pid (landlock_file (file)->fown_tg);landlock_put_ruleset_deferred (landlock_file (file)->fown_subject.domain); }security/landlock/fs.h +10
-0@@ -78,6 +78,16 @@ struct landlock_file_security {* euid. */ struct landlock_cred_security fown_subject; +/** +* @fown_tg: Thread group of the task that set the file owner, pinned +* while @fown_subject holds a domain. It lets +* hook_file_send_sigiotask() always allow a SIGIO delivered to the +* owner's own process -- e.g. the thread-group leader reached through a +* process-group owner -- matching the same-process exemption of +* hook_task_kill(). NULL when no domain is recorded. Protected by +* file->f_owner->lock, like @fown_subject. +*/ +struct pid *fown_tg; };#ifdef CONFIG_AUDIT security/landlock/task.c +11
-0@@ -411,6 +411,17 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,if (!subject->domain)return 0 ; +/* +* Always allow delivery to the file owner's own process, including a +* thread-group leader reached through a process-group owner. This +* mirrors hook_task_kill()'s same-process exemption and preserves the +* guarantee of commit 18eb75f3af40 ("landlock: Always allow signals +* between threads of the same process"), which the registration-time +* check cannot honor for a process-group target. +*/ +if (task_tgid (tsk) ==landlock_file (fown->file)->fown_tg) +return 0 ; +scoped_guard (rcu) { is_scoped = domain_is_scoped(subject->domain,Signature · 1 line
2.43.0
Thank you, this looks good!
–Günther
Re: [PATCH v5 2/2] selftests/landlock: test SCOPE_SIGNAL on the SIGIO/fowner pgid path
In reply to Bryam VargasQuoted 219 lines · Bryam Vargas
Add regression tests for the
LANDLOCK_SCOPE_SIGNALhandling of the asynchronous SIGIO delivery path (fcntl(F_SETOWN)) with a process-group owner.
sigio_to_pgid_memberscovers the bypass: a sandboxed process at the head of its process group's PID hlist (the default afterfork()) armsF_SETOWN(-pgrp)+O_ASYNCand 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_selfcovers 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.
Signed-off-by Bryam Vargas <[email protected]>--- .../selftests/landlock/scoped_signal_test.c | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+)tools/testing/selftests/landlock/scoped_signal_test.c +183
-0@@ -559,4 +559,187 @@ 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 Signature · 1 line
2.43.0