On Sun, Jun 22, 2025 at 9:41 PM Abhinav Saxena via B4 Relay devnull+xandfury.gmail.com@kernel.org wrote:
From: Abhinav Saxena xandfury@gmail.com
TIOCSTI is a TTY ioctl command that allows inserting characters into the terminal input queue, making it appear as if the user typed those characters.
Add a test suite with four tests to verify TIOCSTI behaviour in different scenarios when dev.tty.legacy_tiocsti is both enabled and disabled:
- Test TIOCSTI functionality when legacy support is enabled
- Test TIOCSTI rejection when legacy support is disabled
- Test capability requirements for TIOCSTI usage
- Test TIOCSTI security with file descriptor passing
The tests validate proper enforcement of the legacy_tiocsti sysctl introduced in commit 83efeeeb3d04 ("tty: Allow TIOCSTI to be disabled"). See tty_ioctl(4) for details on TIOCSTI behavior and security requirements.
SELinux has its own testsuite at [1] since not everyone enables SELinux, which is where any tests specific to SELinux functionality should be added.
[1] https://github.com/selinuxproject/selinux-testsuite
Signed-off-by: Abhinav Saxena xandfury@gmail.com
tools/testing/selftests/tty/Makefile | 6 +- tools/testing/selftests/tty/config | 1 + tools/testing/selftests/tty/tty_tiocsti_test.c | 421 +++++++++++++++++++++++++ 3 files changed, 427 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/tty/Makefile b/tools/testing/selftests/tty/Makefile index 50d7027b2ae3..7f6fbe5a0cd5 100644 --- a/tools/testing/selftests/tty/Makefile +++ b/tools/testing/selftests/tty/Makefile @@ -1,5 +1,9 @@ # SPDX-License-Identifier: GPL-2.0 CFLAGS = -O2 -Wall -TEST_GEN_PROGS := tty_tstamp_update +TEST_GEN_PROGS := tty_tstamp_update tty_tiocsti_test +LDLIBS += -lcap
include ../lib.mk
+# Add libcap for TIOCSTI test +$(OUTPUT)/tty_tiocsti_test: LDLIBS += -lcap diff --git a/tools/testing/selftests/tty/config b/tools/testing/selftests/tty/config new file mode 100644 index 000000000000..c6373aba6636 --- /dev/null +++ b/tools/testing/selftests/tty/config @@ -0,0 +1 @@ +CONFIG_LEGACY_TIOCSTI=y diff --git a/tools/testing/selftests/tty/tty_tiocsti_test.c b/tools/testing/selftests/tty/tty_tiocsti_test.c new file mode 100644 index 000000000000..6a4b497078b0 --- /dev/null +++ b/tools/testing/selftests/tty/tty_tiocsti_test.c @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: GPL-2.0 +/*
- TTY Tests - TIOCSTI
- Copyright © 2025 Abhinav Saxena xandfury@gmail.com
- */
+#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/ioctl.h> +#include <errno.h> +#include <stdbool.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <pwd.h> +#include <termios.h> +#include <grp.h> +#include <sys/capability.h> +#include <sys/prctl.h>
+#include "../kselftest_harness.h"
+/* Helper function to send FD via SCM_RIGHTS */ +static int send_fd_via_socket(int socket_fd, int fd_to_send) +{
struct msghdr msg = { 0 };
struct cmsghdr *cmsg;
char cmsg_buf[CMSG_SPACE(sizeof(int))];
char dummy_data = 'F';
struct iovec iov = { .iov_base = &dummy_data, .iov_len = 1 };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd_to_send, sizeof(int));
return sendmsg(socket_fd, &msg, 0) < 0 ? -1 : 0;
+}
+/* Helper function to receive FD via SCM_RIGHTS */ +static int recv_fd_via_socket(int socket_fd) +{
struct msghdr msg = { 0 };
struct cmsghdr *cmsg;
char cmsg_buf[CMSG_SPACE(sizeof(int))];
char dummy_data;
struct iovec iov = { .iov_base = &dummy_data, .iov_len = 1 };
int received_fd = -1;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);
if (recvmsg(socket_fd, &msg, 0) < 0)
return -1;
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET &&
cmsg->cmsg_type == SCM_RIGHTS) {
memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));
break;
}
}
return received_fd;
+}
+static inline bool has_cap_sys_admin(void) +{
cap_t caps = cap_get_proc();
if (!caps)
return false;
cap_flag_value_t cap_val;
bool has_cap = (cap_get_flag(caps, CAP_SYS_ADMIN, CAP_EFFECTIVE,
&cap_val) == 0) &&
(cap_val == CAP_SET);
cap_free(caps);
return has_cap;
+}
+/*
- Simple privilege drop that just changes uid/gid in current process
- and also capabilities like CAP_SYS_ADMIN
- */
+static inline bool drop_to_nobody(void) +{
/* Drop supplementary groups */
if (setgroups(0, NULL) != 0) {
printf("setgroups failed: %s", strerror(errno));
return false;
}
/* Change group to nobody */
if (setgid(65534) != 0) {
printf("setgid failed: %s", strerror(errno));
return false;
}
/* Change user to nobody (this drops capabilities) */
if (setuid(65534) != 0) {
printf("setuid failed: %s", strerror(errno));
return false;
}
/* Verify we no longer have CAP_SYS_ADMIN */
if (has_cap_sys_admin()) {
printf("ERROR: Still have CAP_SYS_ADMIN after changing to nobody");
return false;
}
printf("Successfully changed to nobody (uid:%d gid:%d)\n", getuid(),
getgid());
return true;
+}
+static inline int get_legacy_tiocsti_setting(void) +{
FILE *fp;
int value = -1;
fp = fopen("/proc/sys/dev/tty/legacy_tiocsti", "r");
if (!fp) {
if (errno == ENOENT) {
printf("legacy_tiocsti sysctl not available (kernel < 6.2)\n");
} else {
printf("Cannot read legacy_tiocsti: %s\n",
strerror(errno));
}
return -1;
}
if (fscanf(fp, "%d", &value) == 1) {
printf("legacy_tiocsti setting=%d\n", value);
if (value < 0 || value > 1) {
printf("legacy_tiocsti unexpected value %d\n", value);
value = -1;
} else {
printf("legacy_tiocsti=%d (%s mode)\n", value,
value == 0 ? "restricted" : "permissive");
}
} else {
printf("Failed to parse legacy_tiocsti value");
value = -1;
}
fclose(fp);
return value;
+}
+static inline int test_tiocsti_injection(int fd) +{
int ret;
char test_char = 'X';
ret = ioctl(fd, TIOCSTI, &test_char);
if (ret == 0) {
/* Clear the injected character */
printf("TIOCSTI injection succeeded\n");
} else {
printf("TIOCSTI injection failed: %s (errno=%d)\n",
strerror(errno), errno);
}
return ret == 0 ? 0 : -1;
+}
+FIXTURE(tty_tiocsti) +{
int tty_fd;
char *tty_name;
bool has_tty;
bool initial_cap_sys_admin;
int legacy_tiocsti_setting;
+};
+FIXTURE_SETUP(tty_tiocsti) +{
TH_LOG("Running as UID: %d with effective UID: %d", getuid(),
geteuid());
self->tty_fd = open("/dev/tty", O_RDWR);
self->has_tty = (self->tty_fd >= 0);
if (self->tty_fd < 0)
TH_LOG("Cannot open /dev/tty: %s", strerror(errno));
self->tty_name = ttyname(STDIN_FILENO);
TH_LOG("Current TTY: %s", self->tty_name ? self->tty_name : "none");
self->initial_cap_sys_admin = has_cap_sys_admin();
TH_LOG("Initial CAP_SYS_ADMIN: %s",
self->initial_cap_sys_admin ? "yes" : "no");
self->legacy_tiocsti_setting = get_legacy_tiocsti_setting();
+}
+FIXTURE_TEARDOWN(tty_tiocsti) +{
if (self->has_tty && self->tty_fd >= 0)
close(self->tty_fd);
+}
+/* Test case 1: legacy_tiocsti != 0 (permissive mode) */ +TEST_F(tty_tiocsti, permissive_mode) +{
// clang-format off
if (self->legacy_tiocsti_setting < 0)
SKIP(return,
"legacy_tiocsti sysctl not available (kernel < 6.2)");
if (self->legacy_tiocsti_setting == 0)
SKIP(return,
"Test requires permissive mode (legacy_tiocsti=1)");
// clang-format on
ASSERT_TRUE(self->has_tty);
if (self->initial_cap_sys_admin) {
ASSERT_TRUE(drop_to_nobody());
ASSERT_FALSE(has_cap_sys_admin());
}
/* In permissive mode, TIOCSTI should work without CAP_SYS_ADMIN */
EXPECT_EQ(test_tiocsti_injection(self->tty_fd), 0)
{
TH_LOG("TIOCSTI should succeed in permissive mode without CAP_SYS_ADMIN");
}
+}
+/* Test case 2: legacy_tiocsti == 0, without CAP_SYS_ADMIN (should fail) */ +TEST_F(tty_tiocsti, restricted_mode_nopriv) +{
// clang-format off
if (self->legacy_tiocsti_setting < 0)
SKIP(return,
"legacy_tiocsti sysctl not available (kernel < 6.2)");
if (self->legacy_tiocsti_setting != 0)
SKIP(return,
"Test requires restricted mode (legacy_tiocsti=0)");
// clang-format on
ASSERT_TRUE(self->has_tty);
if (self->initial_cap_sys_admin) {
ASSERT_TRUE(drop_to_nobody());
ASSERT_FALSE(has_cap_sys_admin());
}
/* In restricted mode, TIOCSTI should fail without CAP_SYS_ADMIN */
EXPECT_EQ(test_tiocsti_injection(self->tty_fd), -1);
/*
* it might fail with either EPERM or EIO
* EXPECT_TRUE(errno == EPERM || errno == EIO)
* {
* TH_LOG("Expected EPERM, got: %s", strerror(errno));
* }
*/
+}
+/* Test case 3: legacy_tiocsti == 0, with CAP_SYS_ADMIN (should succeed) */ +TEST_F(tty_tiocsti, restricted_mode_priv) +{
// clang-format off
if (self->legacy_tiocsti_setting < 0)
SKIP(return,
"legacy_tiocsti sysctl not available (kernel < 6.2)");
if (self->legacy_tiocsti_setting != 0)
SKIP(return,
"Test requires restricted mode (legacy_tiocsti=0)");
// clang-format on
/* Must have CAP_SYS_ADMIN for this test */
if (!self->initial_cap_sys_admin)
SKIP(return, "Test requires CAP_SYS_ADMIN");
ASSERT_TRUE(self->has_tty);
ASSERT_TRUE(has_cap_sys_admin());
/* In restricted mode, TIOCSTI should succeed with CAP_SYS_ADMIN */
EXPECT_EQ(test_tiocsti_injection(self->tty_fd), 0)
{
TH_LOG("TIOCSTI should succeed in restricted mode with CAP_SYS_ADMIN");
}
+}
+/* Test TIOCSTI security with file descriptor passing */ +TEST_F(tty_tiocsti, fd_passing_security) +{
// clang-format off
if (self->legacy_tiocsti_setting < 0)
SKIP(return,
"legacy_tiocsti sysctl not available (kernel < 6.2)");
if (self->legacy_tiocsti_setting != 0)
SKIP(return,
"Test requires restricted mode (legacy_tiocsti=0)");
// clang-format on
/* Must start with CAP_SYS_ADMIN */
if (!self->initial_cap_sys_admin)
SKIP(return, "Test requires initial CAP_SYS_ADMIN");
int sockpair[2];
pid_t child_pid;
ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair), 0);
child_pid = fork();
ASSERT_GE(child_pid, 0)
TH_LOG("Fork failed: %s", strerror(errno));
if (child_pid == 0) {
/* Child process - become unprivileged, open TTY, send FD to parent */
close(sockpair[0]);
TH_LOG("Child: Dropping privileges...");
/* Drop to nobody user (loses all capabilities) */
drop_to_nobody();
/* Verify we no longer have CAP_SYS_ADMIN */
if (has_cap_sys_admin()) {
TH_LOG("Child: Failed to drop CAP_SYS_ADMIN");
_exit(1);
}
TH_LOG("Child: Opening TTY as unprivileged user...");
int unprivileged_tty_fd = open("/dev/tty", O_RDWR);
if (unprivileged_tty_fd < 0) {
TH_LOG("Child: Cannot open TTY: %s", strerror(errno));
_exit(1);
}
/* Test that we can't use TIOCSTI directly (should fail) */
char test_char = 'X';
if (ioctl(unprivileged_tty_fd, TIOCSTI, &test_char) == 0) {
TH_LOG("Child: ERROR - Direct TIOCSTI succeeded unexpectedly!");
close(unprivileged_tty_fd);
_exit(1);
}
TH_LOG("Child: Good - Direct TIOCSTI failed as expected: %s",
strerror(errno));
/* Send the TTY FD to privileged parent via SCM_RIGHTS */
TH_LOG("Child: Sending TTY FD to privileged parent...");
if (send_fd_via_socket(sockpair[1], unprivileged_tty_fd) != 0) {
TH_LOG("Child: Failed to send FD");
close(unprivileged_tty_fd);
_exit(1);
}
close(unprivileged_tty_fd);
close(sockpair[1]);
_exit(0); /* Child success */
} else {
/* Parent process - keep CAP_SYS_ADMIN, receive FD, test TIOCSTI */
close(sockpair[1]);
TH_LOG("Parent: Waiting for TTY FD from unprivileged child...");
/* Verify we still have CAP_SYS_ADMIN */
ASSERT_TRUE(has_cap_sys_admin());
/* Receive the TTY FD from unprivileged child */
int received_fd = recv_fd_via_socket(sockpair[0]);
ASSERT_GE(received_fd, 0)
TH_LOG("Parent: Received FD %d (opened by unprivileged process)",
received_fd);
/*
* VULNERABILITY TEST: Try TIOCSTI with FD opened by unprivileged process
* This should FAIL even though parent has CAP_SYS_ADMIN
* because the FD was opened by unprivileged process
*/
char attack_char = 'V'; /* V for Vulnerability */
int ret = ioctl(received_fd, TIOCSTI, &attack_char);
TH_LOG("Parent: Testing TIOCSTI on FD from unprivileged process...");
if (ret == 0) {
TH_LOG("*** VULNERABILITY DETECTED ***");
TH_LOG("Privileged process can use TIOCSTI on unprivileged FD");
} else {
TH_LOG("TIOCSTI failed on unprivileged FD: %s",
strerror(errno));
EXPECT_EQ(errno, EPERM);
}
close(received_fd);
close(sockpair[0]);
/* Wait for child */
int status;
ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
EXPECT_EQ(WEXITSTATUS(status), 0);
ASSERT_NE(ret, 0);
}
+}
+TEST_HARNESS_MAIN
-- 2.43.0