lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite for Android: free password hash cracker in your pocket
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <20250622-toicsti-bug-v1-1-f374373b04b2@gmail.com>
Date: Sun, 22 Jun 2025 19:41:07 -0600
From: Abhinav Saxena via B4 Relay <devnull+xandfury.gmail.com@...nel.org>
To: Shuah Khan <shuah@...nel.org>, Nathan Chancellor <nathan@...nel.org>, 
 Nick Desaulniers <nick.desaulniers+lkml@...il.com>, 
 Bill Wendling <morbo@...gle.com>, Justin Stitt <justinstitt@...gle.com>, 
 Paul Moore <paul@...l-moore.com>, 
 Stephen Smalley <stephen.smalley.work@...il.com>, 
 Ondrej Mosnacek <omosnace@...hat.com>
Cc: linux-kernel@...r.kernel.org, linux-kselftest@...r.kernel.org, 
 llvm@...ts.linux.dev, selinux@...r.kernel.org, 
 Abhinav Saxena <xandfury@...il.com>, kees@...nel.org, 
 linux-hardening@...r.kernel.org
Subject: [PATCH 1/2] selftests/tty: add TIOCSTI test suite

From: Abhinav Saxena <xandfury@...il.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.

Signed-off-by: Abhinav Saxena <xandfury@...il.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@...il.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



Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ