Building the Linux kernel with Clang and LLVM
 help / color / mirror / Atom feed
From: Abhinav Saxena <xandfury@gmail.com>
To: Justin Stitt <justinstitt@google.com>
Cc: Shuah Khan <shuah@kernel.org>,
	Nathan Chancellor <nathan@kernel.org>,
	Nick Desaulniers <nick.desaulniers+lkml@gmail.com>,
	Bill Wendling <morbo@google.com>,
	Paul Moore <paul@paul-moore.com>, Kees Cook <kees@kernel.org>,
	linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
	llvm@lists.linux.dev
Subject: Re: [PATCH v3] selftests/tty: add TIOCSTI test suite
Date: Tue, 02 Sep 2025 18:44:55 -0600	[thread overview]
Message-ID: <87ms7cjqz5.fsf@gmail.com> (raw)
In-Reply-To: <ytndgs2vwkhijeuruejvk55geqouuditkjpge6u7gb6qt6hxqa@uh2wnuapkb5f>

[-- Attachment #1: Type: text/plain, Size: 14149 bytes --]

Justin Stitt <justinstitt@google.com> writes:

Hi,

> Hi,
>
> On Wed, Jul 30, 2025 at 06:14:43PM -0600, Abhinav Saxena wrote:
>
> <snip>
>
>> —
>>  tools/testing/selftests/tty/Makefile           |   6 +-
>>  tools/testing/selftests/tty/config             |   1 +
>>  tools/testing/selftests/tty/tty_tiocsti_test.c | 650 +++++++++++++++++++++++++
>>  3 files changed, 656 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
>
> Is it necessary to append -lcap to LDLIBS twice? Once globally and once
> for that TU?
>

So I built the tests in two ways:

1. Building all targets with `make -C tools/testing/selftests/tty/`
2. Building a specific target which is tty_tiocsti_test in this case.
   I do this with `make -C tools/testing/selftests/tty/ tty_tiocsti_test`

I may be completely wrong here, but I think I need the global for (1)
and TU specific append for (2). There may better ways to do this,
however. Open to ideas :)

>> 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..1eafef6e36fa
>> — /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;
>> +}
>> +
>> +/*
>> + * Drop to nobody user (uid/gid 65534) to lose all capabilities
>> + */
>> +static inline bool drop_to_nobody(struct __test_metadata *_metadata)
>> +{
>
> Maybe we can retrieve the uid/gid from getpwnam(3) with:
>   const struct passwd *pw = getpwnam(“nobody”);
>
> … then use pw->pw_{uid,gid}. I suggest this because there might be
> portability issues with the hardcoded 65534 – not 100% sure though.
>

Thanks! Yep, I guess it is better to get rid of `nobody` logic
completely for better portability. I replaced it with `drop_all_privs`
in v4 [1].

>> +	ASSERT_EQ(setgroups(0, NULL), 0);
>> +	ASSERT_EQ(setgid(65534), 0);
>> +	ASSERT_EQ(setuid(65534), 0);
>> +
>> +	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) {
>> +		if (value < 0 || value > 1)
>> +			value = -1; /* Invalid value */
>> +	} else {
>> +		value = -1; /* Failed to parse */
>> +	}
>> +
>> +	fclose(fp);
>> +	return value;
>> +}
>> +
>
> <snip>
>
>> —
>> base-commit: 283564a43383d6f26a55546fe9ae345b5fa95e66
>> change-id: 20250618-toicsti-bug-7822b8e94a32
>> 
>> Best regards,
>> – 
>> Abhinav Saxena <xandfury@gmail.com>
>> 
>>
>
> Justin

Thanks for the feedback!

• Abhinav

[1] - Link to v4:
<https://lore.kernel.org/all/20250902-toicsti-bug-v4-1-e5c960e0b3d6@gmail.com/>

      reply	other threads:[~2025-09-03  0:54 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-07-31  0:14 [PATCH v3] selftests/tty: add TIOCSTI test suite Abhinav Saxena
2025-08-06 18:49 ` Kees Cook
2025-09-03  0:30   ` Abhinav Saxena
2025-08-11 23:39 ` Justin Stitt
2025-09-03  0:44   ` Abhinav Saxena [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=87ms7cjqz5.fsf@gmail.com \
    --to=xandfury@gmail.com \
    --cc=justinstitt@google.com \
    --cc=kees@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=llvm@lists.linux.dev \
    --cc=morbo@google.com \
    --cc=nathan@kernel.org \
    --cc=nick.desaulniers+lkml@gmail.com \
    --cc=paul@paul-moore.com \
    --cc=shuah@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox