#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,
};
FIXTURE(tiocsti)
{
int pty_master_fd;
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;
const int legacy_tiocsti;
const bool requires_cap;
const int expected_success;
};
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,
};
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,
};
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,
};
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;
}
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;
}
static inline bool drop_all_privs(struct __test_metadata *_metadata)
{
ASSERT_EQ(setgroups(0, NULL), 0);
ASSERT_EQ(setgid(1000), 0);
ASSERT_EQ(setuid(1000), 0);
cap_t empty = cap_init();
ASSERT_NE(empty, NULL);
ASSERT_EQ(cap_set_proc(empty), 0);
cap_free(empty);
ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 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) {
return -1;
}
if (fscanf(fp, "%d", &value) == 1 && fclose(fp) == 0) {
if (value < 0 || value > 1)
value = -1;
} else {
value = -1;
}
return value;
}
static inline bool set_legacy_tiocsti_setting(struct __test_metadata *_metadata,
int value)
{
FILE *fp;
bool success = false;
ASSERT_GE(value, 0);
ASSERT_LE(value, 1);
fp = fopen("/proc/sys/dev/tty/legacy_tiocsti", "w");
if (!fp)
return false;
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;
}
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;
}
static void run_basic_tiocsti_test(struct __test_metadata *_metadata,
FIXTURE_DATA(tiocsti) * self,
const FIXTURE_VARIANT(tiocsti) * variant)
{
if (self->initial_cap_sys_admin && !variant->requires_cap)
ASSERT_TRUE(drop_all_privs(_metadata));
if (variant->controlling_tty) {
pid_t sid = setsid();
ASSERT_GE(sid, 0);
ASSERT_EQ(ioctl(self->pty_slave_fd, TIOCSCTTY, 0), 0);
}
ASSERT_TRUE(self->has_pty);
ASSERT_EQ(has_cap_sys_admin(), variant->requires_cap);
int result = test_tiocsti_injection(_metadata, self->pty_slave_fd);
EXPECT_EQ(result, variant->expected_success);
_exit(0);
}
static void run_fdpass_tiocsti_test(struct __test_metadata *_metadata,
const FIXTURE_VARIANT(tiocsti) * variant,
int sockfd)
{
signal(SIGHUP, SIG_IGN);
if (!variant->requires_cap && has_cap_sys_admin())
ASSERT_TRUE(drop_all_privs(_metadata));
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);
}
int direct_result = test_tiocsti_injection(_metadata, child_slave_fd);
EXPECT_EQ(direct_result, variant->expected_success);
ASSERT_EQ(send_fd_via_socket(sockfd, child_slave_fd), 0);
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)
{
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)");
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");
self->can_modify_sysctl = set_legacy_tiocsti_setting(
_metadata, self->original_legacy_tiocsti_setting);
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)
{
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) {
child_pid = fork();
ASSERT_GE(child_pid, 0);
if (child_pid == 0)
run_basic_tiocsti_test(_metadata, self, variant);
} else {
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) {
close(sockpair[0]);
run_fdpass_tiocsti_test(_metadata, variant,
sockpair[1]);
}
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");
int result = test_tiocsti_injection(_metadata, received_fd);
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);
}
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]);
}
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