651 lines
17 KiB
C
651 lines
17 KiB
C
// 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
|