* [RFC PATCH 0/2] Landlock signal scope and TIOCSIG
@ 2026-09-13 22:19 Christopher Lusk
2026-09-13 22:19 ` [RFC PATCH 1/2] tty: mediate TIOCSIG through task_kill LSM hooks Christopher Lusk
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Christopher Lusk @ 2026-09-13 22:19 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, Oleg Nesterov, Jiri Slaby, Shuah Khan,
Tahera Fahimi, Paul Moore, Casey Schaufler, John Johansen,
linux-security-module, linux-kernel, linux-serial,
linux-kselftest
Landlock documents LANDLOCK_SCOPE_SIGNAL as limiting signal delivery to
processes in the same or a nested Landlock domain. A retained PTY master
can currently use TIOCSIG to deliver SIGINT, SIGQUIT, or SIGTSTP to an
out-of-domain slave foreground process group because the privileged TTY
signal path never reaches security_task_kill().
This RFC asks two questions before proposing a final interface.
First, should this be classified as SCOPE_SIGNAL under-enforcement, or as
part of Landlock's documented inherited-TTY limitation? The "Current
limitations / IOCTL support" section says that IOCTL_DEV does not affect
pre-existing descriptors, names TIOCSTI and TIOCLINUX, and recommends
closing inherited TTY descriptors. That text discusses the filesystem
IOCTL_DEV right rather than SCOPE_SIGNAL, and unlike the two named ioctls,
TIOCSIG is not CAP_SYS_ADMIN-gated. Commit 4b80320ca7ed fixed the same
effect-level class for SIGIO rather than treating the retained signal
source as exempt.
Second, if this is a bug, should TIOCSIG use the existing task_kill hook as
patch 1 demonstrates, or should it gain a dedicated TTY-signal hook which
Landlock can implement without changing other LSM policies? The prototype
is atomic with process-group delivery and behaviorally narrow to TIOCSIG,
but calling task_kill means SELinux, Smack, AppArmor, BPF LSM programs, and
future implementations also mediate this operation. The series does not
claim that cross-LSM policy change is settled.
The demonstrated generic impact is low:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:L = 3.8. Scope is changed
because the effect reaches a process outside the sandbox authority, but
the primitive is limited to three job-control signals and no independent
integrity impact has been reproduced.
Patch 1 is the behaviorally validated proof-of-concept fix. Patch 2 is a
minimal regression test; further test polishing should follow the chosen
interface direction.
Validation used the same userspace image against the affected and patched
kernels. Across three boots per image and 32 iterations per cell:
affected: 96/96 cross-domain TIOCSIG deliveries
patched: 96/96 cross-domain TIOCSIG denials
both: 96/96 unconfined deliveries
96/96 same-domain deliveries
96/96 scoped direct-kill denials
The regression test separately fails on the affected image and passes on
the patched image, with exactly one TAP test executed in each run.
No external report or patch has been sent before this RFC. Guidance on
both classification and hook direction would be appreciated.
Christopher Lusk (2):
tty: mediate TIOCSIG through task_kill LSM hooks
selftests/landlock: cover TIOCSIG signal scoping
drivers/tty/pty.c | 8 +-
include/linux/sched/signal.h | 1 +
kernel/signal.c | 30 +++-
.../selftests/landlock/scoped_signal_test.c | 142 ++++++++++++++++++
4 files changed, 177 insertions(+), 4 deletions(-)
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [RFC PATCH 1/2] tty: mediate TIOCSIG through task_kill LSM hooks
2026-09-13 22:19 [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Christopher Lusk
@ 2026-09-13 22:19 ` Christopher Lusk
2026-09-13 23:49 ` Christopher Lusk
2026-09-13 22:19 ` [RFC PATCH 2/2] selftests/landlock: cover TIOCSIG signal scoping Christopher Lusk
2026-09-14 9:34 ` [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Günther Noack
2 siblings, 1 reply; 7+ messages in thread
From: Christopher Lusk @ 2026-09-13 22:19 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, Oleg Nesterov, Jiri Slaby, Shuah Khan,
Tahera Fahimi, Paul Moore, Casey Schaufler, John Johansen,
linux-security-module, linux-kernel, linux-serial,
linux-kselftest
TIOCSIG lets a PTY master holder send SIGINT, SIGQUIT, or SIGTSTP to
the slave's foreground process group. pty_signal() currently uses
kill_pgrp(..., priv=1), which represents the signal as SEND_SIG_PRIV.
check_kill_permission() consequently returns before security_task_kill().
This leaves the operation outside every task_kill LSM policy. In
particular, a task restricted with LANDLOCK_SCOPE_SIGNAL can use a
retained PTY master to signal an out-of-domain foreground process group.
Add a kill_pgrp_lsm() variant selected only by pty_signal(). It invokes
security_task_kill() for each process-group member immediately before
delivery while tasklist_lock remains held. This preserves the existing
per-recipient and partial-success semantics without a separate pre-check
race. Ordinary privileged process-group signals continue to use the
unchanged kill_pgrp() path.
This is an RFC because the policy boundary is not settled. Landlock's
IOCTL documentation warns that pre-existing TTY file descriptors remain
dangerous, while LANDLOCK_SCOPE_SIGNAL separately promises to restrict
signals to processes outside the domain hierarchy. The proposed helper
also makes SELinux, Smack, AppArmor, and other task_kill LSMs mediate
TIOCSIG for the first time. Maintainer guidance is requested on whether
this should instead use a dedicated, opt-in TTY signal hook.
Tested on x86-64 QEMU with a held-constant four-cell effect oracle.
Across three boots per image, the unpatched kernel
delivered 96/96 cross-domain scoped TIOCSIG attempts; the patched kernel
denied 96/96. Both images delivered 96/96 unconfined and 96/96
same-domain TIOCSIG controls, and denied 96/96 scoped direct-kill
anchors. There were no indeterminate cases or kernel diagnostics.
Fixes: 54a6e6bbf3be ("landlock: Add signal scoping")
Link: https://lore.kernel.org/r/56bffc24f3d0d08b45a686a48e99766b0a0821fa.1780614610.git.hexlabsecurity@proton.me
Assisted-by: Claude:claude-opus-4-8
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Christopher Lusk <clusk@northecho.dev>
---
drivers/tty/pty.c | 8 ++++++--
include/linux/sched/signal.h | 1 +
kernel/signal.c | 30 ++++++++++++++++++++++++++++--
3 files changed, 35 insertions(+), 4 deletions(-)
diff --git a/drivers/tty/pty.c b/drivers/tty/pty.c
index cc7f7091ed9a..8f5eea156ce4 100644
--- a/drivers/tty/pty.c
+++ b/drivers/tty/pty.c
@@ -187,6 +187,7 @@ static int pty_get_pktmode(struct tty_struct *tty, int __user *arg)
/* Send a signal to the slave */
static int pty_signal(struct tty_struct *tty, int sig)
{
+ int ret = 0;
struct pid *pgrp;
if (sig != SIGINT && sig != SIGQUIT && sig != SIGTSTP)
@@ -195,10 +196,13 @@ static int pty_signal(struct tty_struct *tty, int sig)
if (tty->link) {
pgrp = tty_get_pgrp(tty->link);
if (pgrp)
- kill_pgrp(pgrp, sig, 1);
+ ret = kill_pgrp_lsm(pgrp, sig, 1);
put_pid(pgrp);
}
- return 0;
+ /* Preserve the historical success result for an empty process group. */
+ if (ret == -ESRCH)
+ return 0;
+ return ret;
}
static void pty_flush_buffer(struct tty_struct *tty)
diff --git a/include/linux/sched/signal.h b/include/linux/sched/signal.h
index 584ae88b435e..7d6aee7256a3 100644
--- a/include/linux/sched/signal.h
+++ b/include/linux/sched/signal.h
@@ -337,6 +337,7 @@ extern int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid);
extern int kill_pid_usb_asyncio(int sig, int errno, sigval_t addr, struct pid *,
const struct cred *);
extern int kill_pgrp(struct pid *pid, int sig, int priv);
+int kill_pgrp_lsm(struct pid *pid, int sig, int priv);
extern int kill_pid(struct pid *pid, int sig, int priv);
extern __must_check bool do_notify_parent(struct task_struct *, int);
extern void __wake_up_parent(struct task_struct *p, struct task_struct *parent);
diff --git a/kernel/signal.c b/kernel/signal.c
index a5e15bf09d31..758393b7257d 100644
--- a/kernel/signal.c
+++ b/kernel/signal.c
@@ -1426,13 +1426,22 @@ int group_send_sig_info(int sig, struct kernel_siginfo *info,
* control characters do (^C, ^Z etc)
* - the caller must hold at least a readlock on tasklist_lock
*/
-int __kill_pgrp_info(int sig, struct kernel_siginfo *info, struct pid *pgrp)
+static int __kill_pgrp_info_filtered(int sig, struct kernel_siginfo *info,
+ struct pid *pgrp, bool check_lsm)
{
struct task_struct *p = NULL;
int ret = -ESRCH;
do_each_pid_task(pgrp, PIDTYPE_PGID, p) {
- int err = group_send_sig_info(sig, info, p, PIDTYPE_PGID);
+ int err = 0;
+
+ if (check_lsm) {
+ rcu_read_lock();
+ err = security_task_kill(p, info, sig, NULL);
+ rcu_read_unlock();
+ }
+ if (!err)
+ err = group_send_sig_info(sig, info, p, PIDTYPE_PGID);
/*
* If group_send_sig_info() succeeds at least once ret
* becomes 0 and after that the code below has no effect.
@@ -1446,6 +1455,11 @@ int __kill_pgrp_info(int sig, struct kernel_siginfo *info, struct pid *pgrp)
return ret;
}
+int __kill_pgrp_info(int sig, struct kernel_siginfo *info, struct pid *pgrp)
+{
+ return __kill_pgrp_info_filtered(sig, info, pgrp, false);
+}
+
static int kill_pid_info_type(int sig, struct kernel_siginfo *info,
struct pid *pid, enum pid_type type)
{
@@ -1886,6 +1900,18 @@ int kill_pgrp(struct pid *pid, int sig, int priv)
}
EXPORT_SYMBOL(kill_pgrp);
+int kill_pgrp_lsm(struct pid *pid, int sig, int priv)
+{
+ int ret;
+
+ read_lock(&tasklist_lock);
+ ret = __kill_pgrp_info_filtered(sig, __si_special(priv), pid, true);
+ read_unlock(&tasklist_lock);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(kill_pgrp_lsm);
+
int kill_pid(struct pid *pid, int sig, int priv)
{
return kill_pid_info(sig, __si_special(priv), pid);
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [RFC PATCH 2/2] selftests/landlock: cover TIOCSIG signal scoping
2026-09-13 22:19 [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Christopher Lusk
2026-09-13 22:19 ` [RFC PATCH 1/2] tty: mediate TIOCSIG through task_kill LSM hooks Christopher Lusk
@ 2026-09-13 22:19 ` Christopher Lusk
2026-09-14 9:34 ` [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Günther Noack
2 siblings, 0 replies; 7+ messages in thread
From: Christopher Lusk @ 2026-09-13 22:19 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, Oleg Nesterov, Jiri Slaby, Shuah Khan,
Tahera Fahimi, Paul Moore, Casey Schaufler, John Johansen,
linux-security-module, linux-kernel, linux-serial,
linux-kselftest
Add a focused regression test for a sandboxed PTY master holder using
TIOCSIG to signal an out-of-domain slave foreground process group.
The test observes both the ioctl result and the target's signal-handler
effect. It fails on the unpatched base because TIOCSIG succeeds and
SIGTSTP is delivered. It passes with the preceding RFC prototype because
the ioctl fails with EPERM and the target observes no signal.
The identical test binary and initramfs were booted against both kernels
under QEMU. TAP reported one failing test on the affected image and one
passing test on the patched image.
Assisted-by: Claude:claude-opus-4-8
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Christopher Lusk <clusk@northecho.dev>
---
.../selftests/landlock/scoped_signal_test.c | 142 ++++++++++++++++++
1 file changed, 142 insertions(+)
diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c
index 259cdcc8aa5c..9e1dbcfa07c2 100644
--- a/tools/testing/selftests/landlock/scoped_signal_test.c
+++ b/tools/testing/selftests/landlock/scoped_signal_test.c
@@ -12,6 +12,8 @@
#include <pthread.h>
#include <sched.h>
#include <signal.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/types.h>
@@ -681,6 +683,146 @@ TEST(sigio_to_pgid_members)
_metadata->exit_code = KSFT_FAIL;
}
+struct tiocsig_result {
+ int ret;
+ int error;
+};
+
+static void handle_tiocsig(int sig)
+{
+ if (sig == SIGTSTP)
+ signal_received = 1;
+}
+
+static int setup_tiocsig_handler(void)
+{
+ struct sigaction action = {
+ .sa_handler = handle_tiocsig,
+ .sa_flags = SA_RESTART,
+ };
+
+ if (sigemptyset(&action.sa_mask))
+ return -1;
+ return sigaction(SIGTSTP, &action, NULL);
+}
+
+static int create_pty_master(char *const slave_path,
+ const size_t slave_path_size)
+{
+ int master_fd, pty_number, unlock = 0;
+
+ master_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY | O_CLOEXEC);
+ if (master_fd < 0)
+ return -1;
+ if (ioctl(master_fd, TIOCSPTLCK, &unlock) < 0 ||
+ ioctl(master_fd, TIOCGPTN, &pty_number) < 0) {
+ const int saved_errno = errno;
+
+ close(master_fd);
+ errno = saved_errno;
+ return -1;
+ }
+ if (snprintf(slave_path, slave_path_size, "/dev/pts/%d", pty_number) >=
+ (int)slave_path_size) {
+ close(master_fd);
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+ return master_fd;
+}
+
+/*
+ * Checks that TIOCSIG cannot bypass LANDLOCK_SCOPE_SIGNAL when a sandboxed
+ * holder of a PTY master targets an out-of-domain foreground process group.
+ */
+TEST(tiocsig_to_foreground_pgrp)
+{
+ struct tiocsig_result result = {};
+ char slave_path[64], byte;
+ int ready[2], release[2], effect[2], report[2];
+ int master_fd, status, target_effect = -1;
+ pid_t attacker, target;
+
+ drop_caps(_metadata);
+ master_fd = create_pty_master(slave_path, sizeof(slave_path));
+ if (master_fd < 0 && errno == ENOENT)
+ SKIP(return, "Unix98 PTY not available");
+ ASSERT_LE(0, master_fd);
+ ASSERT_EQ(0, pipe2(ready, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(release, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(effect, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(report, O_CLOEXEC));
+
+ target = fork();
+ ASSERT_LE(0, target);
+ if (target == 0) {
+ int slave_fd;
+
+ EXPECT_EQ(0, close(master_fd));
+ EXPECT_EQ(0, close(ready[0]));
+ EXPECT_EQ(0, close(release[1]));
+ EXPECT_EQ(0, close(effect[0]));
+ EXPECT_EQ(0, close(report[0]));
+ EXPECT_EQ(0, close(report[1]));
+ ASSERT_LE(0, setsid());
+ slave_fd = open(slave_path, O_RDWR | O_CLOEXEC);
+ ASSERT_LE(0, slave_fd);
+ ASSERT_NE(SIG_ERR, signal(SIGTTOU, SIG_IGN));
+ ASSERT_EQ(0, setup_tiocsig_handler());
+ signal_received = 0;
+ ASSERT_EQ(0, tcsetpgrp(slave_fd, getpgrp()));
+ ASSERT_EQ(1, write(ready[1], ".", 1));
+ ASSERT_EQ(1, read(release[0], &byte, 1));
+ target_effect = signal_received;
+ ASSERT_EQ((ssize_t)sizeof(target_effect),
+ write(effect[1], &target_effect,
+ sizeof(target_effect)));
+ EXPECT_EQ(0, close(slave_fd));
+ _exit(_metadata->exit_code);
+ return;
+ }
+ EXPECT_EQ(0, close(ready[1]));
+ EXPECT_EQ(0, close(release[0]));
+ EXPECT_EQ(0, close(effect[1]));
+ ASSERT_EQ(1, read(ready[0], &byte, 1));
+
+ attacker = fork();
+ ASSERT_LE(0, attacker);
+ if (attacker == 0) {
+ EXPECT_EQ(0, close(ready[0]));
+ EXPECT_EQ(0, close(release[1]));
+ EXPECT_EQ(0, close(effect[0]));
+ EXPECT_EQ(0, close(report[0]));
+ create_scoped_domain(_metadata, LANDLOCK_SCOPE_SIGNAL);
+ errno = 0;
+ result.ret = ioctl(master_fd, TIOCSIG, SIGTSTP);
+ result.error = errno;
+ ASSERT_EQ((ssize_t)sizeof(result),
+ write(report[1], &result, sizeof(result)));
+ _exit(_metadata->exit_code);
+ return;
+ }
+ EXPECT_EQ(0, close(report[1]));
+ ASSERT_EQ((ssize_t)sizeof(result),
+ read(report[0], &result, sizeof(result)));
+ ASSERT_EQ(attacker, waitpid(attacker, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ /* Release the target only after the signal has either fired or failed. */
+ ASSERT_EQ(1, write(release[1], ".", 1));
+ ASSERT_EQ((ssize_t)sizeof(target_effect),
+ read(effect[0], &target_effect, sizeof(target_effect)));
+ ASSERT_EQ(target, waitpid(target, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ EXPECT_EQ(-1, result.ret);
+ EXPECT_EQ(EPERM, result.error);
+ EXPECT_EQ(0, target_effect);
+ EXPECT_EQ(0, close(master_fd));
+}
+
static void *thread_setown_scoped(void *arg)
{
const int fd = *(int *)arg;
--
2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC PATCH 1/2] tty: mediate TIOCSIG through task_kill LSM hooks
2026-09-13 22:19 ` [RFC PATCH 1/2] tty: mediate TIOCSIG through task_kill LSM hooks Christopher Lusk
@ 2026-09-13 23:49 ` Christopher Lusk
0 siblings, 0 replies; 7+ messages in thread
From: Christopher Lusk @ 2026-09-13 23:49 UTC (permalink / raw)
To: Mickaël Salaün
Cc: Günther Noack, Oleg Nesterov, Jiri Slaby, Shuah Khan,
Tahera Fahimi, Paul Moore, Casey Schaufler, John Johansen,
linux-security-module, linux-kernel, linux-serial,
linux-kselftest
The bot's finding is correct, and this is a regression this patch
introduces, so I want to flag it and the v2 direction up front.
kill_pgrp_lsm() calls security_task_kill() with __si_special(priv), i.e.
SEND_SIG_PRIV ((void *)1). On the ordinary paths,
check_kill_permission() returns early via si_fromuser() before the hook,
so security_task_kill() never receives that sentinel. This patch reaches
the hook directly and breaks that invariant. The in-tree C LSMs do not
dereference info, but task_kill is a trusted BPF LSM hook. An attached BPF
LSM program is therefore allowed to dereference info and can fault when it
receives (void *)1.
v2 will synthesize a valid kernel_siginfo for the LSM check
(user-attributed, since TIOCSIG is user-triggered) and keep the privileged
delivery unchanged, so the hook always sees a valid pointer.
This does not change the two questions the RFC is really asking (whether
TIOCSIG should be mediated at all, and task_kill versus a dedicated TTY
signal hook). If the answer is a dedicated hook, the siginfo concern goes
away with it.
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC PATCH 0/2] Landlock signal scope and TIOCSIG
2026-09-13 22:19 [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Christopher Lusk
2026-09-13 22:19 ` [RFC PATCH 1/2] tty: mediate TIOCSIG through task_kill LSM hooks Christopher Lusk
2026-09-13 22:19 ` [RFC PATCH 2/2] selftests/landlock: cover TIOCSIG signal scoping Christopher Lusk
@ 2026-09-14 9:34 ` Günther Noack
2026-09-14 13:40 ` Christopher Lusk
2 siblings, 1 reply; 7+ messages in thread
From: Günther Noack @ 2026-09-14 9:34 UTC (permalink / raw)
To: Christopher Lusk
Cc: Mickaël Salaün, Günther Noack, Oleg Nesterov,
Jiri Slaby, Shuah Khan, Tahera Fahimi, Paul Moore,
Casey Schaufler, John Johansen, linux-security-module,
linux-kernel, linux-serial, linux-kselftest
Hello Christopher!
On Sun, Sep 13, 2026 at 06:19:56PM -0400, Christopher Lusk wrote:
> Landlock documents LANDLOCK_SCOPE_SIGNAL as limiting signal delivery to
> processes in the same or a nested Landlock domain. A retained PTY master
> can currently use TIOCSIG to deliver SIGINT, SIGQUIT, or SIGTSTP to an
> out-of-domain slave foreground process group because the privileged TTY
> signal path never reaches security_task_kill().
>
> This RFC asks two questions before proposing a final interface.
>
> First, should this be classified as SCOPE_SIGNAL under-enforcement, or as
> part of Landlock's documented inherited-TTY limitation? The "Current
> limitations / IOCTL support" section says that IOCTL_DEV does not affect
> pre-existing descriptors, names TIOCSTI and TIOCLINUX, and recommends
> closing inherited TTY descriptors. That text discusses the filesystem
> IOCTL_DEV right rather than SCOPE_SIGNAL, and unlike the two named ioctls,
> TIOCSIG is not CAP_SYS_ADMIN-gated. Commit 4b80320ca7ed fixed the same
> effect-level class for SIGIO rather than treating the retained signal
> source as exempt.
>
> Second, if this is a bug, should TIOCSIG use the existing task_kill hook as
> patch 1 demonstrates, or should it gain a dedicated TTY-signal hook which
> Landlock can implement without changing other LSM policies? The prototype
> is atomic with process-group delivery and behaviorally narrow to TIOCSIG,
> but calling task_kill means SELinux, Smack, AppArmor, BPF LSM programs, and
> future implementations also mediate this operation. The series does not
> claim that cross-LSM policy change is settled.
Thank you for bringing this up; I was not aware of this code path and
researched it a bit.
Let me try to paraphrase the issue to make sure I understand:
1. A process creates a PTY device and acquires the PTY master FD.
2. The process then restricts itself into a signal-scoped Landlock domain.
3. Processes outside of the domain are attached to the terminal
4. Through the PTY master FD, the master process emulates a terminal
to the attached processes. One of the commands it can issue is
TIOCSIG, allowing the PTY master process to send SIGINT ("Ctrl-C"),
SIGQUIT ("Ctrl-\") or SIGTSTP ("Ctrl-Z") to TTY-attached processes,
which may live *outside* the Landlock domain. (source:
pty_signal() in drivers/tty/pty.c)
TIOCSIG was introduced in 2010 in Linux [1] and in 1989 in BSD (quoted
in the Linux patch). According to the patch, it is only required in a
special terminal mode where the mapping of signals is disabled.
In more normal operation modes, the terminal interprets these keyboard
shortcuts sent as characters. This is implemented in
n_tty_receive_char_special() when you write() the characters '\x03'
(Ctrl-C), '\x1c' (Ctrl-\) or '\x1a' (Ctrl-Z) to the PTY master FD.
(Your proposed patch does not fix this either, even though it sends
the same signals.)
Additionally, PTYs can send:
* SIGWINCH (when you do ioctl(TIOCSWINSZ) on the master FD)
* SIGHUP and SIGCONT
[1] https://lore.kernel.org/all/E1OR73h-0004VN-JE@lirone.symas.net/
In summary:
* Sending signals to attached processes is a very normal way how PTYs
interact with the attached processes, and TIOCSIG is not the only
cause for it.
* Processes are attached to a PTY because they were started on that
PTY or they have voluntarily attached to it.
With these two points in mind, I am leaning towards treating access to
the PTY master FD as a "capability" whose acquisition can already be
adequately restricted with existing Landlock controls. Like
socketpair(), which Landlock also don't restrict, the creation of a
new PTY always returns a new master and client side TTY FD and it
feels to me more effective to control who attaches to these than to
control the TTY-internal communication protocols itself.
Maybe the way to think about this is to say that it is the *TTY
driver* which is sending these signals in response to the PTY master
FD receiving a TIOCSIG or having a Ctrl-C written to it. This is
similar to a SSH or Telnet daemon which can also trigger signals for
the attached processes on the other end of the terminal by sending the
right commands over the wire, and I also don't see an issue with that,
because in the same way as here, the client programs have voluntarily
attached to the TTY. 🤔
Does that seem reasonable? I am happy to be corrected if this
analysis is wrong.
If you agree, I think the best path forward might be to document it
more clearly that TTY interactions are not part of the SCOPE_SIGNAL
guarantees.
–Günther
> The demonstrated generic impact is low:
> CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:N/A:L = 3.8. Scope is changed
> because the effect reaches a process outside the sandbox authority, but
> the primitive is limited to three job-control signals and no independent
> integrity impact has been reproduced.
>
> Patch 1 is the behaviorally validated proof-of-concept fix. Patch 2 is a
> minimal regression test; further test polishing should follow the chosen
> interface direction.
>
> Validation used the same userspace image against the affected and patched
> kernels. Across three boots per image and 32 iterations per cell:
>
> affected: 96/96 cross-domain TIOCSIG deliveries
> patched: 96/96 cross-domain TIOCSIG denials
> both: 96/96 unconfined deliveries
> 96/96 same-domain deliveries
> 96/96 scoped direct-kill denials
>
> The regression test separately fails on the affected image and passes on
> the patched image, with exactly one TAP test executed in each run.
>
> No external report or patch has been sent before this RFC. Guidance on
> both classification and hook direction would be appreciated.
>
> Christopher Lusk (2):
> tty: mediate TIOCSIG through task_kill LSM hooks
> selftests/landlock: cover TIOCSIG signal scoping
>
> drivers/tty/pty.c | 8 +-
> include/linux/sched/signal.h | 1 +
> kernel/signal.c | 30 +++-
> .../selftests/landlock/scoped_signal_test.c | 142 ++++++++++++++++++
> 4 files changed, 177 insertions(+), 4 deletions(-)
>
> --
> 2.55.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC PATCH 0/2] Landlock signal scope and TIOCSIG
2026-09-14 9:34 ` [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Günther Noack
@ 2026-09-14 13:40 ` Christopher Lusk
2026-09-14 17:13 ` Günther Noack
0 siblings, 1 reply; 7+ messages in thread
From: Christopher Lusk @ 2026-09-14 13:40 UTC (permalink / raw)
To: Günther Noack
Cc: Mickaël Salaün, Günther Noack, Oleg Nesterov,
Jiri Slaby, Shuah Khan, Tahera Fahimi, Paul Moore,
Casey Schaufler, John Johansen, linux-security-module,
linux-kernel, linux-serial, linux-kselftest
Hello Günther,
Thanks for digging into this so carefully, and for the clear write-up.
Your capability framing convinces me. Controlling who may attach to (or
open a master for) the PTY is the right layer, and the master FD is best
thought of as the capability, the same way socketpair() is.
You are also right that the series is incomplete as a fix: the same three
signals arrive through the n_tty control-character path (Ctrl-C / Ctrl-\ /
Ctrl-Z) that my patch does not touch, and PTYs additionally raise SIGWINCH,
SIGHUP and SIGCONT. That reinforces your point rather than mine. Chasing
individual signal-delivery paths inside the TTY layer is the wrong layer,
and a per-ioctl hook would only paper over one entry into a mechanism that
is working as designed.
One question, mostly so I have the line right in my own notes rather than
to relitigate: how do you see this relative to the SIGIO/fowner path that
4b80320ca7ed brought under SCOPE_SIGNAL? My read of the distinction is
that in the SIGIO case the sandboxed process unilaterally selects the target
by arming the owner, whereas here the recipients have voluntarily attached
to the terminal and the TTY driver delivers job-control signals over that
attachment. If that is the intended boundary, it is a clean one, and I am
happy to treat TTY-driven signals as outside the guarantee.
If it is useful, I would be glad to send a small documentation patch making
that explicit: a note in the SCOPE_SIGNAL / IPC-scoping section of
landlock.rst that TTY-driver signal delivery (TIOCSIG and the
control-character path) is not mediated by SCOPE_SIGNAL, with the practical
guidance to control PTY attachment instead. I will drop the task_kill
approach.
Thanks again for the thorough look.
Christopher
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC PATCH 0/2] Landlock signal scope and TIOCSIG
2026-09-14 13:40 ` Christopher Lusk
@ 2026-09-14 17:13 ` Günther Noack
0 siblings, 0 replies; 7+ messages in thread
From: Günther Noack @ 2026-09-14 17:13 UTC (permalink / raw)
To: Christopher Lusk
Cc: Mickaël Salaün, Günther Noack, Oleg Nesterov,
Jiri Slaby, Shuah Khan, Tahera Fahimi, Paul Moore,
Casey Schaufler, John Johansen, linux-security-module,
linux-kernel, linux-serial, linux-kselftest
Hello!
On Mon, Sep 14, 2026 at 09:40:13AM -0400, Christopher Lusk wrote:
> Your capability framing convinces me. Controlling who may attach to (or
> open a master for) the PTY is the right layer, and the master FD is best
> thought of as the capability, the same way socketpair() is.
>
> You are also right that the series is incomplete as a fix: the same three
> signals arrive through the n_tty control-character path (Ctrl-C / Ctrl-\ /
> Ctrl-Z) that my patch does not touch, and PTYs additionally raise SIGWINCH,
> SIGHUP and SIGCONT. That reinforces your point rather than mine. Chasing
> individual signal-delivery paths inside the TTY layer is the wrong layer,
> and a per-ioctl hook would only paper over one entry into a mechanism that
> is working as designed.
>
> One question, mostly so I have the line right in my own notes rather than
> to relitigate: how do you see this relative to the SIGIO/fowner path that
> 4b80320ca7ed brought under SCOPE_SIGNAL? My read of the distinction is
> that in the SIGIO case the sandboxed process unilaterally selects the target
> by arming the owner, whereas here the recipients have voluntarily attached
> to the terminal and the TTY driver delivers job-control signals over that
> attachment. If that is the intended boundary, it is a clean one, and I am
> happy to treat TTY-driven signals as outside the guarantee.
Yes, that is the difference why SIGIO had to be protected -- in the
SIGIO case, it was the already landlocked process which could itself
select the signal recipients through fcntl(fd, F_SETOWN, ...).
In the terminal case, it is the TTY-client-side processes that select
which processes are attached to the terminal. With the PTY master FD
alone, it is not possible to signal processes that aren't already
attached to the terminal.
> If it is useful, I would be glad to send a small documentation patch making
> that explicit: a note in the SCOPE_SIGNAL / IPC-scoping section of
> landlock.rst that TTY-driver signal delivery (TIOCSIG and the
> control-character path) is not mediated by SCOPE_SIGNAL, with the practical
> guidance to control PTY attachment instead. I will drop the task_kill
> approach.
Thank you, I would appreciate that!
Thanks,
–Günther
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-14 17:13 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-13 22:19 [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Christopher Lusk
2026-09-13 22:19 ` [RFC PATCH 1/2] tty: mediate TIOCSIG through task_kill LSM hooks Christopher Lusk
2026-09-13 23:49 ` Christopher Lusk
2026-09-13 22:19 ` [RFC PATCH 2/2] selftests/landlock: cover TIOCSIG signal scoping Christopher Lusk
2026-09-14 9:34 ` [RFC PATCH 0/2] Landlock signal scope and TIOCSIG Günther Noack
2026-09-14 13:40 ` Christopher Lusk
2026-09-14 17:13 ` Günther Noack
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®