summaryrefslogtreecommitdiffstats
path: root/tools/testing/selftests/tty
diff options
context:
space:
mode:
authorNiklas Cassel <cassel@kernel.org>2026-09-16 15:00:32 +0200
committerNiklas Cassel <cassel@kernel.org>2026-09-16 16:18:00 +0200
commit8d836581f9b1f57ceaa2b47654754ef1260b410b (patch)
tree1dbd4d0dfb99c2e238356cbe947e5b0be3e3a571 /tools/testing/selftests/tty
downloadlinux-stable-8d836581f9b1f57ceaa2b47654754ef1260b410b.tar.gz
linux-stable-8d836581f9b1f57ceaa2b47654754ef1260b410b.zip
ata: libata-scsi: fix ata_dsm_trim_pages() kernel-docgrafted
make htmldocs fails with: Documentation/driver-api/libata:604: ./drivers/ata/libata-scsi.c:2301: ERROR: Unexpected indentation. [docutils] The Return: section of ata_dsm_trim_pages() ends its first sentence with a colon and continues with an indented bullet list. reStructuredText requires a blank line before an indented block, so docutils chokes on the list. Simply adding the missing blank line does not work either: Return: is a kernel-doc "special section", which is terminated by the first blank line, so the bullet list would end up in the Description section, detached from the sentence introducing it. Spell the two bounds out as prose instead, so that the Return: section stays self-contained. Documentation-only change. Fixes: e64e6b5dc867 ("ata: libata-scsi: scale DSM TRIM payload by MAX PAGES PER DSM COMMAND") Reported-by: Thomas Huth <thuth@redhat.com> Closes: https://lore.kernel.org/linux-ide/b0a0b8e8-cc4f-4c7b-8bc5-fee0712d405a@redhat.com/ Reviewed-by: Damien Le Moal <dlemoal@kernel.org> Reviewed-by: Thomas Huth <thuth@redhat.com> Link: https://lore.kernel.org/r/20260916130031.29990-2-cassel@kernel.org Signed-off-by: Niklas Cassel <cassel@kernel.org>
Diffstat (limited to 'tools/testing/selftests/tty')
-rw-r--r--tools/testing/selftests/tty/.gitignore3
-rw-r--r--tools/testing/selftests/tty/Makefile9
-rw-r--r--tools/testing/selftests/tty/config1
-rw-r--r--tools/testing/selftests/tty/tty_tiocsti_test.c650
-rw-r--r--tools/testing/selftests/tty/tty_tstamp_update.c106
5 files changed, 769 insertions, 0 deletions
diff --git a/tools/testing/selftests/tty/.gitignore b/tools/testing/selftests/tty/.gitignore
new file mode 100644
index 000000000..2453685d2
--- /dev/null
+++ b/tools/testing/selftests/tty/.gitignore
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+tty_tiocsti_test
+tty_tstamp_update
diff --git a/tools/testing/selftests/tty/Makefile b/tools/testing/selftests/tty/Makefile
new file mode 100644
index 000000000..7f6fbe5a0
--- /dev/null
+++ b/tools/testing/selftests/tty/Makefile
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS = -O2 -Wall
+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 000000000..c6373aba6
--- /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 000000000..5e767e6cb
--- /dev/null
+++ b/tools/testing/selftests/tty/tty_tiocsti_test.c
@@ -0,0 +1,650 @@
+// 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 <pty.h>
+#include <utmp.h>
+
+#include "../kselftest_harness.h"
+
+enum test_type {
+ TEST_PTY_TIOCSTI_BASIC,
+ TEST_PTY_TIOCSTI_FD_PASSING,
+ /* other tests cases such as serial may be added. */
+};
+
+/*
+ * Test Strategy:
+ * - Basic tests: Use PTY with/without TIOCSCTTY (controlling terminal for
+ * current process)
+ * - FD passing tests: Child creates PTY, parent receives FD (demonstrates
+ * security issue)
+ *
+ * SECURITY VULNERABILITY DEMONSTRATION:
+ * FD passing tests show that TIOCSTI uses CURRENT process credentials, not
+ * opener credentials. This means privileged processes can be given FDs from
+ * unprivileged processes and successfully perform TIOCSTI operations that the
+ * unprivileged process couldn't do directly.
+ *
+ * Attack scenario:
+ * 1. Unprivileged process opens TTY (direct TIOCSTI fails due to lack of
+ * privileges)
+ * 2. Unprivileged process passes FD to privileged process via SCM_RIGHTS
+ * 3. Privileged process can use TIOCSTI on the FD (succeeds due to its
+ * privileges)
+ * 4. Result: Effective privilege escalation via file descriptor passing
+ *
+ * This matches the kernel logic in tiocsti():
+ * 1. if (!tty_legacy_tiocsti && !capable(CAP_SYS_ADMIN)) return -EIO;
+ * 2. if ((current->signal->tty != tty) && !capable(CAP_SYS_ADMIN))
+ * return -EPERM;
+ * Note: Both checks use capable() on CURRENT process, not FD opener!
+ *
+ * If the file credentials were also checked along with the capable() checks
+ * then the results for FD pass tests would be consistent with the basic tests.
+ */
+
+FIXTURE(tiocsti)
+{
+ int pty_master_fd; /* PTY - for basic tests */
+ int pty_slave_fd;
+ bool has_pty;
+ bool initial_cap_sys_admin;
+ int original_legacy_tiocsti_setting;
+ bool can_modify_sysctl;
+};
+
+FIXTURE_VARIANT(tiocsti)
+{
+ const enum test_type test_type;
+ const bool controlling_tty; /* true=current->signal->tty == tty */
+ const int legacy_tiocsti; /* 0=restricted, 1=permissive */
+ const bool requires_cap; /* true=with CAP_SYS_ADMIN, false=without */
+ const int expected_success; /* 0=success, -EIO/-EPERM=specific error */
+};
+
+/*
+ * Tests Controlling Terminal Variants (current->signal->tty == tty)
+ *
+ * TIOCSTI Test Matrix:
+ *
+ * | legacy_tiocsti | CAP_SYS_ADMIN | Expected Result | Error |
+ * |----------------|---------------|-----------------|-------|
+ * | 1 (permissive) | true | SUCCESS | - |
+ * | 1 (permissive) | false | SUCCESS | - |
+ * | 0 (restricted) | true | SUCCESS | - |
+ * | 0 (restricted) | false | FAILURE | -EIO |
+ */
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tiocsti, basic_pty_permissive_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = true,
+ .legacy_tiocsti = 1,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, basic_pty_permissive_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = true,
+ .legacy_tiocsti = 1,
+ .requires_cap = false,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, basic_pty_restricted_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = true,
+ .legacy_tiocsti = 0,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, basic_pty_restricted_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = true,
+ .legacy_tiocsti = 0,
+ .requires_cap = false,
+ .expected_success = -EIO, /* FAILURE: legacy restriction */
+}; /* clang-format on */
+
+/*
+ * Note for FD Passing Test Variants
+ * Since we're testing the scenario where an unprivileged process pass an FD
+ * to a privileged one, .requires_cap here means the caps of the child process.
+ * Not the parent; parent would always be privileged.
+ */
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_permissive_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = true,
+ .legacy_tiocsti = 1,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_permissive_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = true,
+ .legacy_tiocsti = 1,
+ .requires_cap = false,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_restricted_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = true,
+ .legacy_tiocsti = 0,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_pty_restricted_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = true,
+ .legacy_tiocsti = 0,
+ .requires_cap = false,
+ .expected_success = -EIO,
+}; /* clang-format on */
+
+/*
+ * Non-Controlling Terminal Variants (current->signal->tty != tty)
+ *
+ * TIOCSTI Test Matrix:
+ *
+ * | legacy_tiocsti | CAP_SYS_ADMIN | Expected Result | Error |
+ * |----------------|---------------|-----------------|-------|
+ * | 1 (permissive) | true | SUCCESS | - |
+ * | 1 (permissive) | false | FAILURE | -EPERM|
+ * | 0 (restricted) | true | SUCCESS | - |
+ * | 0 (restricted) | false | FAILURE | -EIO |
+ */
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_permissive_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = false,
+ .legacy_tiocsti = 1,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_permissive_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = false,
+ .legacy_tiocsti = 1,
+ .requires_cap = false,
+ .expected_success = -EPERM,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_restricted_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = false,
+ .legacy_tiocsti = 0,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, basic_nopty_restricted_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_BASIC,
+ .controlling_tty = false,
+ .legacy_tiocsti = 0,
+ .requires_cap = false,
+ .expected_success = -EIO,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_permissive_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = false,
+ .legacy_tiocsti = 1,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_permissive_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = false,
+ .legacy_tiocsti = 1,
+ .requires_cap = false,
+ .expected_success = -EPERM,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_restricted_withcap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = false,
+ .legacy_tiocsti = 0,
+ .requires_cap = true,
+ .expected_success = 0,
+};
+
+FIXTURE_VARIANT_ADD(tiocsti, fdpass_nopty_restricted_nocap) {
+ .test_type = TEST_PTY_TIOCSTI_FD_PASSING,
+ .controlling_tty = false,
+ .legacy_tiocsti = 0,
+ .requires_cap = false,
+ .expected_success = -EIO,
+}; /* clang-format on */
+
+/* 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;
+}
+
+/*
+ * Switch to non-root user and clear all capabilities
+ */
+static inline bool drop_all_privs(struct __test_metadata *_metadata)
+{
+ /* Drop supplementary groups */
+ ASSERT_EQ(setgroups(0, NULL), 0);
+
+ /* Switch to non-root user */
+ ASSERT_EQ(setgid(1000), 0);
+ ASSERT_EQ(setuid(1000), 0);
+
+ /* Clear all capabilities */
+ cap_t empty = cap_init();
+
+ ASSERT_NE(empty, NULL);
+ ASSERT_EQ(cap_set_proc(empty), 0);
+ cap_free(empty);
+
+ /* Prevent privilege regain */
+ ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0);
+
+ /* Verify privilege drop */
+ ASSERT_FALSE(has_cap_sys_admin());
+ return true;
+}
+
+static inline int get_legacy_tiocsti_setting(struct __test_metadata *_metadata)
+{
+ FILE *fp;
+ int value = -1;
+
+ fp = fopen("/proc/sys/dev/tty/legacy_tiocsti", "r");
+ if (!fp) {
+ /* legacy_tiocsti sysctl not available (kernel < 6.2) */
+ return -1;
+ }
+
+ if (fscanf(fp, "%d", &value) == 1 && fclose(fp) == 0) {
+ if (value < 0 || value > 1)
+ value = -1; /* Invalid value */
+ } else {
+ value = -1; /* Failed to parse */
+ }
+
+ return value;
+}
+
+static inline bool set_legacy_tiocsti_setting(struct __test_metadata *_metadata,
+ int value)
+{
+ FILE *fp;
+ bool success = false;
+
+ /* Sanity-check the value */
+ ASSERT_GE(value, 0);
+ ASSERT_LE(value, 1);
+
+ /*
+ * Try to open for writing; if we lack permission, return false so
+ * the test harness will skip variants that need to change it
+ */
+ fp = fopen("/proc/sys/dev/tty/legacy_tiocsti", "w");
+ if (!fp)
+ return false;
+
+ /* Write the new setting */
+ if (fprintf(fp, "%d\n", value) > 0 && fclose(fp) == 0)
+ success = true;
+ else
+ TH_LOG("Failed to write legacy_tiocsti: %s", strerror(errno));
+
+ return success;
+}
+
+/*
+ * TIOCSTI injection test function
+ * @tty_fd: TTY slave file descriptor to test TIOCSTI on
+ * Returns: 0 on success, -errno on failure
+ */
+static inline int test_tiocsti_injection(struct __test_metadata *_metadata,
+ int tty_fd)
+{
+ int ret;
+ char inject_char = 'V';
+
+ errno = 0;
+ ret = ioctl(tty_fd, TIOCSTI, &inject_char);
+ return ret == 0 ? 0 : -errno;
+}
+
+/*
+ * Child process: test TIOCSTI directly with capability/controlling
+ * terminal setup
+ */
+static void run_basic_tiocsti_test(struct __test_metadata *_metadata,
+ FIXTURE_DATA(tiocsti) * self,
+ const FIXTURE_VARIANT(tiocsti) * variant)
+{
+ /* Handle capability requirements */
+ if (self->initial_cap_sys_admin && !variant->requires_cap)
+ ASSERT_TRUE(drop_all_privs(_metadata));
+
+ if (variant->controlling_tty) {
+ /*
+ * Create new session and set PTY as
+ * controlling terminal
+ */
+ pid_t sid = setsid();
+
+ ASSERT_GE(sid, 0);
+ ASSERT_EQ(ioctl(self->pty_slave_fd, TIOCSCTTY, 0), 0);
+ }
+
+ /*
+ * Validate test environment setup and verify final
+ * capability state matches expectation
+ * after potential drop.
+ */
+ ASSERT_TRUE(self->has_pty);
+ ASSERT_EQ(has_cap_sys_admin(), variant->requires_cap);
+
+ /* Test TIOCSTI and validate result */
+ int result = test_tiocsti_injection(_metadata, self->pty_slave_fd);
+
+ /* Check against expected result from variant */
+ EXPECT_EQ(result, variant->expected_success);
+ _exit(0);
+}
+
+/*
+ * Child process: create PTY and then pass FD to parent via SCM_RIGHTS
+ */
+static void run_fdpass_tiocsti_test(struct __test_metadata *_metadata,
+ const FIXTURE_VARIANT(tiocsti) * variant,
+ int sockfd)
+{
+ signal(SIGHUP, SIG_IGN);
+
+ /* Handle privilege dropping */
+ if (!variant->requires_cap && has_cap_sys_admin())
+ ASSERT_TRUE(drop_all_privs(_metadata));
+
+ /* Create child's PTY */
+ int child_master_fd, child_slave_fd;
+
+ ASSERT_EQ(openpty(&child_master_fd, &child_slave_fd, NULL, NULL, NULL),
+ 0);
+
+ if (variant->controlling_tty) {
+ pid_t sid = setsid();
+
+ ASSERT_GE(sid, 0);
+ ASSERT_EQ(ioctl(child_slave_fd, TIOCSCTTY, 0), 0);
+ }
+
+ /* Test child's direct TIOCSTI for reference */
+ int direct_result = test_tiocsti_injection(_metadata, child_slave_fd);
+
+ EXPECT_EQ(direct_result, variant->expected_success);
+
+ /* Send FD to parent */
+ ASSERT_EQ(send_fd_via_socket(sockfd, child_slave_fd), 0);
+
+ /* Wait for parent completion signal */
+ char sync_byte;
+ ssize_t bytes_read = read(sockfd, &sync_byte, 1);
+
+ ASSERT_EQ(bytes_read, 1);
+
+ close(child_master_fd);
+ close(child_slave_fd);
+ close(sockfd);
+ _exit(0);
+}
+
+FIXTURE_SETUP(tiocsti)
+{
+ /* Create PTY pair for basic tests */
+ self->has_pty = (openpty(&self->pty_master_fd, &self->pty_slave_fd,
+ NULL, NULL, NULL) == 0);
+ if (!self->has_pty) {
+ self->pty_master_fd = -1;
+ self->pty_slave_fd = -1;
+ }
+
+ self->initial_cap_sys_admin = has_cap_sys_admin();
+ self->original_legacy_tiocsti_setting =
+ get_legacy_tiocsti_setting(_metadata);
+
+ if (self->original_legacy_tiocsti_setting < 0)
+ SKIP(return,
+ "legacy_tiocsti sysctl not available (kernel < 6.2)");
+
+ /* Common skip conditions */
+ if (variant->test_type == TEST_PTY_TIOCSTI_BASIC && !self->has_pty)
+ SKIP(return, "PTY not available for controlling terminal test");
+
+ if (variant->test_type == TEST_PTY_TIOCSTI_FD_PASSING &&
+ !self->initial_cap_sys_admin)
+ SKIP(return, "FD Pass tests require CAP_SYS_ADMIN");
+
+ if (variant->requires_cap && !self->initial_cap_sys_admin)
+ SKIP(return, "Test requires initial CAP_SYS_ADMIN");
+
+ /* Test if we can modify the sysctl (requires appropriate privileges) */
+ self->can_modify_sysctl = set_legacy_tiocsti_setting(
+ _metadata, self->original_legacy_tiocsti_setting);
+
+ /* Sysctl setup based on variant */
+ if (self->can_modify_sysctl &&
+ self->original_legacy_tiocsti_setting != variant->legacy_tiocsti) {
+ if (!set_legacy_tiocsti_setting(_metadata,
+ variant->legacy_tiocsti))
+ SKIP(return, "Failed to set legacy_tiocsti sysctl");
+
+ } else if (!self->can_modify_sysctl &&
+ self->original_legacy_tiocsti_setting !=
+ variant->legacy_tiocsti)
+ SKIP(return, "legacy_tiocsti setting mismatch");
+}
+
+FIXTURE_TEARDOWN(tiocsti)
+{
+ /*
+ * Backup restoration -
+ * each test should restore its own sysctl changes
+ */
+ if (self->can_modify_sysctl) {
+ int current_value = get_legacy_tiocsti_setting(_metadata);
+
+ if (current_value != self->original_legacy_tiocsti_setting) {
+ TH_LOG("Backup: Restoring legacy_tiocsti from %d to %d",
+ current_value,
+ self->original_legacy_tiocsti_setting);
+ set_legacy_tiocsti_setting(
+ _metadata,
+ self->original_legacy_tiocsti_setting);
+ }
+ }
+
+ if (self->has_pty) {
+ if (self->pty_master_fd >= 0)
+ close(self->pty_master_fd);
+ if (self->pty_slave_fd >= 0)
+ close(self->pty_slave_fd);
+ }
+}
+
+TEST_F(tiocsti, test)
+{
+ int status;
+ pid_t child_pid;
+
+ if (variant->test_type == TEST_PTY_TIOCSTI_BASIC) {
+ /* ===== BASIC TIOCSTI TEST ===== */
+ child_pid = fork();
+ ASSERT_GE(child_pid, 0);
+
+ /* Perform the actual test in the child process */
+ if (child_pid == 0)
+ run_basic_tiocsti_test(_metadata, self, variant);
+
+ } else {
+ /* ===== FD PASSING SECURITY TEST ===== */
+ int sockpair[2];
+
+ ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair), 0);
+
+ child_pid = fork();
+ ASSERT_GE(child_pid, 0);
+
+ if (child_pid == 0) {
+ /* Child process - create PTY and send FD */
+ close(sockpair[0]);
+ run_fdpass_tiocsti_test(_metadata, variant,
+ sockpair[1]);
+ }
+
+ /* Parent process - receive FD and test TIOCSTI */
+ close(sockpair[1]);
+
+ int received_fd = recv_fd_via_socket(sockpair[0]);
+
+ ASSERT_GE(received_fd, 0);
+
+ bool parent_has_cap = self->initial_cap_sys_admin;
+
+ TH_LOG("=== TIOCSTI FD Passing Test Context ===");
+ TH_LOG("legacy_tiocsti: %d, Parent CAP_SYS_ADMIN: %s, Child: %s",
+ variant->legacy_tiocsti, parent_has_cap ? "yes" : "no",
+ variant->requires_cap ? "kept" : "dropped");
+
+ /* SECURITY TEST: Try TIOCSTI with FD opened by child */
+ int result = test_tiocsti_injection(_metadata, received_fd);
+
+ /* Log security concern if demonstrated */
+ if (result == 0 && !variant->requires_cap) {
+ TH_LOG("*** SECURITY CONCERN DEMONSTRATED ***");
+ TH_LOG("Privileged parent can use TIOCSTI on FD from unprivileged child");
+ TH_LOG("This shows current process credentials are used, not opener credentials");
+ }
+
+ EXPECT_EQ(result, variant->expected_success)
+ {
+ TH_LOG("FD passing: expected error %d, got %d",
+ variant->expected_success, result);
+ }
+
+ /* Signal child completion */
+ char sync_byte = 'D';
+ ssize_t bytes_written = write(sockpair[0], &sync_byte, 1);
+
+ ASSERT_EQ(bytes_written, 1);
+
+ close(received_fd);
+ close(sockpair[0]);
+ }
+
+ /* Common child process cleanup for both test types */
+ ASSERT_EQ(waitpid(child_pid, &status, 0), child_pid);
+
+ if (WIFSIGNALED(status)) {
+ TH_LOG("Child terminated by signal %d", WTERMSIG(status));
+ ASSERT_FALSE(WIFSIGNALED(status))
+ {
+ TH_LOG("Child process failed assertion");
+ }
+ } else {
+ EXPECT_EQ(WEXITSTATUS(status), 0);
+ }
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/tty/tty_tstamp_update.c b/tools/testing/selftests/tty/tty_tstamp_update.c
new file mode 100644
index 000000000..bc3291dcd
--- /dev/null
+++ b/tools/testing/selftests/tty/tty_tstamp_update.c
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <linux/limits.h>
+
+#include "kselftest.h"
+
+#define MIN_TTY_PATH_LEN 8
+
+static bool tty_valid(char *tty)
+{
+ if (strlen(tty) < MIN_TTY_PATH_LEN)
+ return false;
+
+ if (strncmp(tty, "/dev/tty", MIN_TTY_PATH_LEN) == 0 ||
+ strncmp(tty, "/dev/pts", MIN_TTY_PATH_LEN) == 0)
+ return true;
+
+ return false;
+}
+
+static int write_dev_tty(void)
+{
+ FILE *f;
+ int r = 0;
+
+ f = fopen("/dev/tty", "r+");
+ if (!f)
+ return -errno;
+
+ r = fprintf(f, "hello, world!\n");
+ if (r != strlen("hello, world!\n"))
+ r = -EIO;
+
+ fclose(f);
+ return r;
+}
+
+int main(int argc, char **argv)
+{
+ int r;
+ char tty[PATH_MAX] = {};
+ struct stat st1, st2;
+ int result = KSFT_FAIL;
+
+ ksft_print_header();
+ ksft_set_plan(1);
+
+ r = readlink("/proc/self/fd/0", tty, PATH_MAX);
+ if (r < 0) {
+ ksft_print_msg("readlink on /proc/self/fd/0 failed: %m\n");
+ goto out;
+ }
+
+ if (!tty_valid(tty)) {
+ ksft_print_msg("invalid tty path '%s'\n", tty);
+ result = KSFT_SKIP;
+ goto out;
+
+ }
+
+ r = stat(tty, &st1);
+ if (r < 0) {
+ ksft_print_msg("stat failed on tty path '%s': %m\n", tty);
+ goto out;
+ }
+
+ /* We need to wait at least 8 seconds in order to observe timestamp change */
+ /* https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fbf47635315ab308c9b58a1ea0906e711a9228de */
+ sleep(10);
+
+ r = write_dev_tty();
+ if (r < 0) {
+ ksft_print_msg("failed to write to /dev/tty: %s\n",
+ strerror(-r));
+ goto out;
+ }
+
+ r = stat(tty, &st2);
+ if (r < 0) {
+ ksft_print_msg("stat failed on tty path '%s': %m\n", tty);
+ goto out;
+ }
+
+ /* We wrote to the terminal so timestamps should have been updated */
+ if (st1.st_atim.tv_sec == st2.st_atim.tv_sec &&
+ st1.st_mtim.tv_sec == st2.st_mtim.tv_sec) {
+ ksft_print_msg("tty timestamps not updated\n");
+ goto out;
+ }
+
+ ksft_print_msg(
+ "timestamps of terminal '%s' updated after write to /dev/tty\n", tty);
+ result = KSFT_PASS;
+
+out:
+ ksft_test_result_report(result, "tty_tstamp_update\n");
+
+ ksft_finished();
+}