[<prev] [next>] [<thread-prev] [day] [month] [year] [list]
Message-ID: <87qzxxt25n.fsf@gmail.com>
Date: Wed, 30 Jul 2025 21:55:45 -0600
From: Abhinav Saxena <xandfury@...il.com>
To: Kees Cook <kees@...nel.org>
Cc: 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>, linux-kernel@...r.kernel.org,
linux-kselftest@...r.kernel.org, llvm@...ts.linux.dev
Subject: Re: [PATCH v2] selftests/tty: add TIOCSTI test suite
Hi!
Thank you so much for the detailed review! Based on the feedback, I
have updated v3 [1]. In v3, we also add additional tests using
the FIXTURE_ADD_VARIANT feature to test the matrix of possibilities.
The central idea is that FD passing tests would behave the same way as
the regular tests if the file credentials were also checked.
Kees Cook <kees@...nel.org> writes:
> On Sun, Jul 13, 2025 at 06:38:55PM -0600, Abhinav Saxena via B4 Relay wrote:
>> 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>
>> —
>> This patch adds comprehensive selftests for the TIOCSTI ioctl to validate
>> proper behaviour under different system configurations.
>
> Much of the text under the — line is useful details; I would merge it
> with the short commit log above to make a comprehensive commit message.
>
Thanks! I added more details to the commit message in v3.
>>
>> `============='
>>
>> The TIOCSTI ioctl allows inserting characters into the terminal input
>> queue, making it appear as if the user typed those characters. This
>> functionality has security implications and behaviour that varies based
>> on system configuration.
>>
>> Background
>> `========'
>>
>> CONFIG_LEGACY_TIOCSTI controls the default value for the dev.tty.legacy_tiocsti
>> sysctl, which remains runtime-configurable. The dev.tty.legacy_tiocsti sysctl
>> was introduced in commit 83efeeeb3d04 (“tty: Allow TIOCSTI to be disabled”)
>> to provide administrators control over TIOCSTI usage.
>>
>> When legacy_tiocsti is disabled, TIOCSTI requires CAP_SYS_ADMIN capability.
>> However, the current implementation only checks the current process’s
>> credentials via capable(CAP_SYS_ADMIN), which doesn’t validate against
>> the file opener’s credentials stored in file->f_cred.
>>
>> This creates a potential security scenario where an unprivileged process
>> can open a TTY fd and pass it to a privileged process via SCM_RIGHTS.
>>
>> Testing
>> `====='
>>
>> The test suite includes four comprehensive tests:
>> - 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
>>
>> All patches have been validated using:
>> - scripts/checkpatch.pl –strict (0 errors, 0 warnings)
>> - Functional testing on kernel v6.16-rc2
>> - File descriptor passing security test scenarios
>>
>> The fd_passing_security test demonstrates the security concern.
>> To verify, disable legacy TIOCSTI and run the test:
>>
>> $ echo “0” | sudo tee /proc/sys/dev/tty/legacy_tiocsti
>> $ sudo ./tools/testing/selftests/tty/tty_tiocsti_test -t fd_passing_security
>>
>> Patch Overview
>> `============'
>>
>> PATCH 1/1: selftests/tty: add TIOCSTI test suite
>> Comprehensive test suite demonstrating the issue and fix validation
>>
>> References
>> `========'
>>
>> - tty_ioctl(4) - documents TIOCSTI ioctl and capability requirements
>> - commit 83efeeeb3d04 (“tty: Allow TIOCSTI to be disabled”)
>> - Documentation/security/credentials.rst
>> - <https://github.com/KSPP/linux/issues/156>
>> - <https://lore.kernel.org/linux-hardening/Y0m9l52AKmw6Yxi1@hostpad>/
>> - drivers/tty/Kconfig
>>
>> Configuration References:
>> [1] - <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/tty/Kconfig#n149>
>> [2] - <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/tty/Kconfig#n162>
>> [3] - <https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/tty/Kconfig#n188>
>>
>> Signed-off-by: Abhinav Saxena <xandfury@...il.com>
>>
>> Changes in v2:
>> - Focused series on selftests only
>> - Removed SELinux capability checking patch for separate submission
>> - Link to v1: <https://lore.kernel.org/r/20250622-toicsti-bug-v1-0-f374373b04b2@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@...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;
>> +}
>
> All of the bare printfs above should use TH_LOG()
>
Done! I’ve updated all instances to use TH_LOG().
>> +
>> +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;
>> +}
>
> Same observation as above that reporting should go through the kselftest
> harness APIs.
>
Fixed.
>> +
>> +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;
>> +}
>
> Same. Note for each of these you’ll need to pass in the test metadata
> (struct __test_metadata *_metadata). See helper examples that do this
> in tools/testing/selftests/seccomp/seccomp_bpf.c like tracer_poke(),
> get_syscall(), etc.
>
Yep. Done! I added metadata as an argument to helper functions in v3.
>> +
>> +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));
>
> I would expect this to be:
>
> ASSERT_GE(self->tty_fd, 0) {
> TH_LOG(“Cannot open /dev/tty: %s”, strerror(errno));
> }
>
You are right. I fixed this everywhere.
>> +
>> + 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
>
> While there are instances in the tree of "//" please use /* */ for
> comments, even these format pragmas.
>
I fixed "//" comments to use /* */ everywhere.
>> + if (self->legacy_tiocsti_setting < 0)
>> + SKIP(return,
>> + “legacy_tiocsti sysctl not available (kernel < 6.2)”);
>
> I notice this check is in every test. I think it’d be cleaner to put it
> into FIXTURE_SETUP() instead.
>
Yes, I added a system-wide skip for cases such as when the
legacy_tiocsti option is not available, which I placed in FIXTURE_SETUP.
>> +
>> + if (self->legacy_tiocsti_setting == 0)
>> + SKIP(return,
>> + “Test requires permissive mode (legacy_tiocsti=1)”);
>
> What about changing this value during FIXTURE_SETUP() and restoring it
> in FIXTURE_TEARDOWN()?
Yes, that makes more sense. In v3 we change values during setup and
teardown based on the requirements of the test.
>
>> + // clang-format on
>> +
>> + ASSERT_TRUE(self->has_tty);
>
> Can opening a pty (i.e. openpty(3)) be used to always have a tty
> available?
>
Potentially, yes. Initially, I ran the tests on both pty and tty. But
running on pty makes more sense for the reason you highlighted below.
>> +
>> + 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)
>
> With openpty() I think it would also mean you can trivially throw
> away injected characters instead of having them actually end up on the
> real tty? But it’s been a while since I did tty work, so maybe I’m
> confusing myself about what openpty() gets you.
>
That’s true. I use openpty(3) in v3, and then in a child process and
then run the TIOCSCTTY ioctl call to set it as the controlling terminal
of the child. This allowed us to write even more comprehensive tests
without having injected characters on the real tty.
>> + {
>> + 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)”);
>
> I think it should be possible to _set_ the state you want (if running as
> root) and if not settable, then do the SKIP.
>
Yes, we do this in v3.
>> + // 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));
>> + * }
>> + */
>
> Why are these commented out?
>
I think this is from v1 where I also had added SELinux-related return
as a potential fix. Since that is no longer the case, v3 verifies the
error codes correctly.
>> +}
>> +
>> +/* 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);
>> + }
>
> Please double check the behavior but I think you can use ASSERTs in the
> child and you don’t need the “_exit(1)” stuff.
>
>> +
>> + 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 {
>
> This doesn’t need an else nor indenting: it is the parent no matter what
> due to the _exit above.
>
>> + /* 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);
>
> Doesn’t the child need to stay alive long enough to receive the
> character? i.e. is it a problem that the child immediately exits 0 after
> sending the fd to the parent?
>
Yep. In v3, we have the child wait for a character to be received from
the parent confirming that the fd has been received.
>> +
>> + 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);
>> + }
>
> Shouldn't this be arranged with an expect on ret =' 0 ?
>
>
>> + 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
>
> Looks like you’re on the right track!
>
> -Kees
Thanks!
I look forward to your comments and feedback on v3.
Best,
Abhinav
• Link to v3:
<https://lore.kernel.org/all/20250730-toicsti-bug-v3-1-dd2dac97f27a@gmail.com/>
Powered by blists - more mailing lists