* [PATCH v3 0/2] landlock: clarify TTY signal scoping
@ 2026-09-23 16:33 Christopher Lusk
2026-09-23 16:33 ` [PATCH v3 1/2] docs: " Christopher Lusk
2026-09-23 16:33 ` [PATCH v3 2/2] selftests/landlock: cover " Christopher Lusk
0 siblings, 2 replies; 3+ messages in thread
From: Christopher Lusk @ 2026-09-23 16:33 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-security-module,
linux-doc, linux-kselftest, linux-kernel
Document that a PTY master retains control over processes attached to the
terminal, and add a selftest covering this boundary alongside the direct
signal restriction.
Since v2, the documentation follows Mickaël's capability framing, the UAPI
description refers to arbitrary signal targets, and the series includes the
requested selftest.
Link: https://lore.kernel.org/r/20260916152336.1589383-1-clusk@northecho.dev
Christopher Lusk (2):
docs: landlock: clarify TTY signal scoping
selftests/landlock: cover TTY signal scoping
Documentation/userspace-api/landlock.rst | 6 +
include/uapi/linux/landlock.h | 2 +-
.../selftests/landlock/scoped_signal_test.c | 175 ++++++++++++++++++
3 files changed, 182 insertions(+), 1 deletion(-)
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v3 1/2] docs: landlock: clarify TTY signal scoping
2026-09-23 16:33 [PATCH v3 0/2] landlock: clarify TTY signal scoping Christopher Lusk
@ 2026-09-23 16:33 ` Christopher Lusk
2026-09-23 16:33 ` [PATCH v3 2/2] selftests/landlock: cover " Christopher Lusk
1 sibling, 0 replies; 3+ messages in thread
From: Christopher Lusk @ 2026-09-23 16:33 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-security-module,
linux-doc, linux-kselftest, linux-kernel
LANDLOCK_SCOPE_SIGNAL limits signals sent to arbitrary processes outside
a domain. A PTY master differs because its holder controls attached
processes through input and signal injection.
Clarify this boundary in the userspace guide and narrow the UAPI
description to arbitrary target processes.
Built the userspace-api docs; no new warnings.
Link: https://lore.kernel.org/r/20260916152336.1589383-1-clusk@northecho.dev
Suggested-by: Günther Noack <gnoack@google.com>
Suggested-by: Mickaël Salaün <mic@digikod.net>
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Christopher Lusk <clusk@northecho.dev>
---
Documentation/userspace-api/landlock.rst | 6 ++++++
include/uapi/linux/landlock.h | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 84cb7bf6b3ed..4d9fb1f1588c 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -430,6 +430,12 @@ The operations which can be scoped are:
This limits the sending of signals to target processes which run within the
same or a nested Landlock domain.
+ Holding a PTY master FD grants control over the processes running under that
+ terminal, including the ability to inject input and signals. A process able
+ to write to the master should therefore be considered as privileged as those
+ processes, and this capability is not restricted by
+ ``LANDLOCK_SCOPE_SIGNAL``.
+
``LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET``
This limits the set of abstract :manpage:`unix(7)` sockets to which we can
:manpage:`connect(2)` to socket addresses which were created by a process in
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index cceda3b3b961..ad7b1ebd62ed 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -500,7 +500,7 @@ struct landlock_net_port_attr {
* connecting to an abstract UNIX socket created by a process outside the
* related Landlock domain (e.g., a parent domain or a non-sandboxed process).
* - %LANDLOCK_SCOPE_SIGNAL: Restrict a sandboxed process from sending a signal
- * to another process outside the domain.
+ * to an arbitrary process outside the domain.
*/
/* clang-format off */
#define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0)
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v3 2/2] selftests/landlock: cover TTY signal scoping
2026-09-23 16:33 [PATCH v3 0/2] landlock: clarify TTY signal scoping Christopher Lusk
2026-09-23 16:33 ` [PATCH v3 1/2] docs: " Christopher Lusk
@ 2026-09-23 16:33 ` Christopher Lusk
1 sibling, 0 replies; 3+ messages in thread
From: Christopher Lusk @ 2026-09-23 16:33 UTC (permalink / raw)
To: Mickaël Salaün, Günther Noack
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-security-module,
linux-doc, linux-kselftest, linux-kernel
LANDLOCK_SCOPE_SIGNAL blocks signaling an arbitrary out-of-domain process,
but a PTY master retains signal delivery to its terminal foreground process
group.
Add a test that enters a signal-scoped domain, checks that kill(SIGUSR1)
is denied with EPERM, and checks that TIOCSIG(SIGTSTP) succeeds and reaches
the target's signal handler.
Tested the focused case in three QEMU boots and ran the complete Landlock
selftest suite (12 programs, no failures). Built all Landlock selftests
with W=1.
Link: https://lore.kernel.org/r/20260916152336.1589383-1-clusk@northecho.dev
Suggested-by: Günther Noack <gnoack@google.com>
Suggested-by: Mickaël Salaün <mic@digikod.net>
Assisted-by: Codex:gpt-5.6-sol
Signed-off-by: Christopher Lusk <clusk@northecho.dev>
---
.../selftests/landlock/scoped_signal_test.c | 175 ++++++++++++++++++
1 file changed, 175 insertions(+)
diff --git a/tools/testing/selftests/landlock/scoped_signal_test.c b/tools/testing/selftests/landlock/scoped_signal_test.c
index 259cdcc8aa5c..9a50817f33eb 100644
--- a/tools/testing/selftests/landlock/scoped_signal_test.c
+++ b/tools/testing/selftests/landlock/scoped_signal_test.c
@@ -9,9 +9,12 @@
#include <errno.h>
#include <fcntl.h>
#include <linux/landlock.h>
+#include <poll.h>
#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>
@@ -767,6 +770,178 @@ TEST(sigio_to_pgid_self)
EXPECT_EQ(0, close(trigger[1]));
}
+struct tiocsig_result {
+ int kill_ret;
+ int kill_errno;
+ int ioctl_ret;
+ int ioctl_errno;
+};
+
+static int tty_effect_fd = -1;
+
+static void handle_tty_signal(int sig)
+{
+ const char effect = sig;
+
+ if (tty_effect_fd >= 0)
+ (void)write(tty_effect_fd, &effect, sizeof(effect));
+}
+
+static int setup_tty_signal_handler(int sig)
+{
+ struct sigaction action = {
+ .sa_handler = handle_tty_signal,
+ .sa_flags = SA_RESTART,
+ };
+
+ if (sigemptyset(&action.sa_mask))
+ return -1;
+ return sigaction(sig, &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;
+}
+
+/*
+ * A PTY master grants control over its attached terminal, including signal
+ * delivery to the foreground process group. LANDLOCK_SCOPE_SIGNAL blocks
+ * arbitrary signal targets, but it does not restrict this terminal capability.
+ */
+TEST(tiocsig_to_foreground_pgrp)
+{
+ struct tiocsig_result result = {};
+ struct pollfd poll_fd = {
+ .events = POLLIN,
+ };
+ char slave_path[64], byte, effect_signal = 0;
+ int ready[2], release[2], effect[2], report[2];
+ int master_fd, poll_ret, status;
+ ssize_t report_size;
+ 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));
+ tty_effect_fd = effect[1];
+ ASSERT_EQ(0, setup_tty_signal_handler(SIGUSR1));
+ ASSERT_EQ(0, setup_tty_signal_handler(SIGTSTP));
+ ASSERT_EQ(0, tcsetpgrp(slave_fd, getpgrp()));
+ ASSERT_EQ(1, write(ready[1], ".", 1));
+ ASSERT_EQ(1, read(release[0], &byte, 1));
+ EXPECT_EQ(0, close(slave_fd));
+ EXPECT_EQ(0, close(effect[1]));
+ _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.kill_ret = kill(target, SIGUSR1);
+ result.kill_errno = errno;
+ errno = 0;
+ result.ioctl_ret = ioctl(master_fd, TIOCSIG, SIGTSTP);
+ result.ioctl_errno = errno;
+ ASSERT_EQ((ssize_t)sizeof(result),
+ write(report[1], &result, sizeof(result)));
+ EXPECT_EQ(0, close(report[1]));
+ EXPECT_EQ(0, close(master_fd));
+ _exit(_metadata->exit_code);
+ return;
+ }
+ EXPECT_EQ(0, close(report[1]));
+ report_size = read(report[0], &result, sizeof(result));
+ EXPECT_EQ((ssize_t)sizeof(result), report_size);
+ EXPECT_EQ(0, close(report[0]));
+ EXPECT_EQ(attacker, waitpid(attacker, &status, 0));
+ EXPECT_TRUE(WIFEXITED(status));
+ if (WIFEXITED(status))
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ EXPECT_EQ(-1, result.kill_ret);
+ EXPECT_EQ(EPERM, result.kill_errno);
+ EXPECT_EQ(0, result.ioctl_ret);
+ EXPECT_EQ(0, result.ioctl_errno);
+
+ poll_fd.fd = effect[0];
+ poll_ret = poll(&poll_fd, 1, 1000);
+ EXPECT_EQ(1, poll_ret);
+ if (poll_ret == 1 && (poll_fd.revents & POLLIN)) {
+ EXPECT_EQ((ssize_t)sizeof(effect_signal),
+ read(effect[0], &effect_signal,
+ sizeof(effect_signal)));
+ EXPECT_EQ(SIGTSTP, effect_signal);
+ } else {
+ EXPECT_TRUE(poll_ret == 1 && (poll_fd.revents & POLLIN));
+ }
+
+ ASSERT_EQ(1, write(release[1], ".", 1));
+ EXPECT_EQ(target, waitpid(target, &status, 0));
+ EXPECT_TRUE(WIFEXITED(status));
+ if (WIFEXITED(status))
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ EXPECT_EQ(0, close(ready[0]));
+ EXPECT_EQ(0, close(release[1]));
+ EXPECT_EQ(0, close(effect[0]));
+ EXPECT_EQ(0, close(master_fd));
+}
+
/* Trace tests */
/* clang-format off */
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-23 16:36 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-23 16:33 [PATCH v3 0/2] landlock: clarify TTY signal scoping Christopher Lusk
2026-09-23 16:33 ` [PATCH v3 1/2] docs: " Christopher Lusk
2026-09-23 16:33 ` [PATCH v3 2/2] selftests/landlock: cover " Christopher Lusk
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®