#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <termios.h>
#include <atf-c.h>
#include <libutil.h>
enum stierr {
STIERR_CONFIG_FETCH,
STIERR_CONFIG,
STIERR_INJECT,
STIERR_READFAIL,
STIERR_BADTEXT,
STIERR_DATAFOUND,
STIERR_ROTTY,
STIERR_WOTTY,
STIERR_WOOK,
STIERR_BADERR,
STIERR_MAXERR
};
static const struct stierr_map {
enum stierr stierr;
const char *msg;
} stierr_map[] = {
{ STIERR_CONFIG_FETCH, "Failed to fetch ctty configuration" },
{ STIERR_CONFIG, "Failed to configure ctty in the child" },
{ STIERR_INJECT, "Failed to inject characters via TIOCSTI" },
{ STIERR_READFAIL, "Failed to read(2) from stdin" },
{ STIERR_BADTEXT, "read(2) data did not match injected data" },
{ STIERR_DATAFOUND, "read(2) data when we did not expected to" },
{ STIERR_ROTTY, "Failed to open tty r/o" },
{ STIERR_WOTTY, "Failed to open tty w/o" },
{ STIERR_WOOK, "TIOCSTI on w/o tty succeeded" },
{ STIERR_BADERR, "Received wrong error from failed TIOCSTI" },
};
_Static_assert(nitems(stierr_map) == STIERR_MAXERR,
"Failed to describe all errors");
static ssize_t
inject(int fileno, const char *str)
{
size_t nb = 0;
for (const char *walker = str; *walker != '\0'; walker++) {
if (ioctl(fileno, TIOCSTI, walker) != 0)
return (-1);
nb++;
}
return (nb);
}
static int
init_pty(int *termfd, bool canon)
{
int pid;
pid = forkpty(termfd, NULL, NULL, NULL);
ATF_REQUIRE(pid != -1);
if (pid == 0) {
struct termios term;
if (tcgetattr(STDIN_FILENO, &term) == -1)
_exit(STIERR_CONFIG_FETCH);
term.c_lflag &= ~ECHO;
if (!canon)
term.c_lflag &= ~ICANON;
if (tcsetattr(STDIN_FILENO, TCSANOW, &term) == -1)
_exit(STIERR_CONFIG);
}
return (pid);
}
static void
finalize_child(pid_t pid, int signo)
{
int status, wpid;
while ((wpid = waitpid(pid, &status, 0)) != pid) {
if (wpid != -1)
continue;
ATF_REQUIRE_EQ_MSG(EINTR, errno,
"waitpid: %s", strerror(errno));
}
if (signo >= 0) {
ATF_REQUIRE(WIFSIGNALED(status));
ATF_REQUIRE_EQ(signo, WTERMSIG(status));
} else {
ATF_REQUIRE(WIFEXITED(status));
if (WEXITSTATUS(status) != 0) {
int err = WEXITSTATUS(status);
for (size_t i = 0; i < nitems(stierr_map); i++) {
const struct stierr_map *map = &stierr_map[i];
if ((int)map->stierr == err) {
atf_tc_fail("%s", map->msg);
__assert_unreachable();
}
}
}
}
}
ATF_TC(basic);
ATF_TC_HEAD(basic, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test for basic functionality of TIOCSTI");
atf_tc_set_md_var(tc, "require.user", "unprivileged");
}
ATF_TC_BODY(basic, tc)
{
int pid, term;
pid = init_pty(&term, false);
if (pid == 0) {
static const char sending[] = "Text";
char readbuf[32];
ssize_t injected, readsz;
injected = inject(STDIN_FILENO, sending);
if (injected != sizeof(sending) - 1)
_exit(STIERR_INJECT);
readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf));
if (readsz < 0 || readsz != injected)
_exit(STIERR_READFAIL);
if (memcmp(readbuf, sending, readsz) != 0)
_exit(STIERR_BADTEXT);
_exit(0);
}
finalize_child(pid, -1);
}
ATF_TC(root);
ATF_TC_HEAD(root, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test that root can inject into another TTY");
atf_tc_set_md_var(tc, "require.user", "root");
}
ATF_TC_BODY(root, tc)
{
static const char sending[] = "Text\r";
ssize_t injected;
int pid, term;
pid = init_pty(&term, true);
if (pid == 0) {
char readbuf[32];
ssize_t readsz;
readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf));
if (readsz < 0 || readsz != sizeof(sending) - 1)
_exit(STIERR_READFAIL);
if (memcmp(readbuf, sending, readsz - 1) != 0)
_exit(STIERR_BADTEXT);
_exit(0);
}
injected = inject(term, sending);
ATF_REQUIRE_EQ_MSG(sizeof(sending) - 1, injected,
"Injected %zu characters, expected %zu", injected,
sizeof(sending) - 1);
finalize_child(pid, -1);
}
ATF_TC(unprivileged_fail_noctty);
ATF_TC_HEAD(unprivileged_fail_noctty, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test that unprivileged cannot inject into non-controlling TTY");
atf_tc_set_md_var(tc, "require.user", "unprivileged");
}
ATF_TC_BODY(unprivileged_fail_noctty, tc)
{
const char sending[] = "Text";
ssize_t injected;
int pid, serrno, term;
pid = init_pty(&term, false);
if (pid == 0) {
char readbuf[32];
ssize_t readsz;
readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf));
if (readsz > 0)
_exit(STIERR_DATAFOUND);
_exit(0);
}
injected = inject(term, sending);
serrno = errno;
kill(pid, SIGINT);
finalize_child(pid, SIGINT);
ATF_REQUIRE_EQ_MSG(-1, (ssize_t)injected,
"TIOCSTI into non-ctty succeeded");
ATF_REQUIRE_EQ(EACCES, serrno);
}
ATF_TC(unprivileged_fail_noread);
ATF_TC_HEAD(unprivileged_fail_noread, tc)
{
atf_tc_set_md_var(tc, "descr",
"Test that unprivileged cannot inject into TTY not opened for read");
atf_tc_set_md_var(tc, "require.user", "unprivileged");
}
ATF_TC_BODY(unprivileged_fail_noread, tc)
{
int pid, term;
pid = init_pty(&term, true);
if (pid == 0) {
static const char sending[] = "Text";
ssize_t injected;
int rotty, wotty;
wotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_WRONLY);
if (wotty == -1)
_exit(STIERR_WOTTY);
rotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_RDONLY);
if (rotty == -1)
_exit(STIERR_ROTTY);
injected = inject(wotty, sending);
if (injected != -1)
_exit(STIERR_WOOK);
if (errno != EPERM)
_exit(STIERR_BADERR);
injected = inject(rotty, sending);
if (injected != sizeof(sending) - 1)
_exit(STIERR_INJECT);
_exit(0);
}
finalize_child(pid, -1);
}
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, basic);
ATF_TP_ADD_TC(tp, root);
ATF_TP_ADD_TC(tp, unprivileged_fail_noctty);
ATF_TP_ADD_TC(tp, unprivileged_fail_noread);
return (atf_no_error());
}