1 /*- 2 * Copyright (c) 2025 Kyle Evans <kevans@FreeBSD.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7 #include <sys/param.h> 8 #include <sys/ioctl.h> 9 #include <sys/wait.h> 10 11 #include <assert.h> 12 #include <errno.h> 13 #include <fcntl.h> 14 #include <signal.h> 15 #include <stdbool.h> 16 #include <stdlib.h> 17 #include <termios.h> 18 19 #include <atf-c.h> 20 #include <libutil.h> 21 22 enum stierr { 23 STIERR_CONFIG_FETCH, 24 STIERR_CONFIG, 25 STIERR_INJECT, 26 STIERR_READFAIL, 27 STIERR_BADTEXT, 28 STIERR_DATAFOUND, 29 STIERR_ROTTY, 30 STIERR_WOTTY, 31 STIERR_WOOK, 32 STIERR_BADERR, 33 34 STIERR_MAXERR 35 }; 36 37 static const struct stierr_map { 38 enum stierr stierr; 39 const char *msg; 40 } stierr_map[] = { 41 { STIERR_CONFIG_FETCH, "Failed to fetch ctty configuration" }, 42 { STIERR_CONFIG, "Failed to configure ctty in the child" }, 43 { STIERR_INJECT, "Failed to inject characters via TIOCSTI" }, 44 { STIERR_READFAIL, "Failed to read(2) from stdin" }, 45 { STIERR_BADTEXT, "read(2) data did not match injected data" }, 46 { STIERR_DATAFOUND, "read(2) data when we did not expected to" }, 47 { STIERR_ROTTY, "Failed to open tty r/o" }, 48 { STIERR_WOTTY, "Failed to open tty w/o" }, 49 { STIERR_WOOK, "TIOCSTI on w/o tty succeeded" }, 50 { STIERR_BADERR, "Received wrong error from failed TIOCSTI" }, 51 }; 52 _Static_assert(nitems(stierr_map) == STIERR_MAXERR, 53 "Failed to describe all errors"); 54 55 /* 56 * Inject each character of the input string into the TTY. The caller can 57 * assume that errno is preserved on return. 58 */ 59 static ssize_t 60 inject(int fileno, const char *str) 61 { 62 size_t nb = 0; 63 64 for (const char *walker = str; *walker != '\0'; walker++) { 65 if (ioctl(fileno, TIOCSTI, walker) != 0) 66 return (-1); 67 nb++; 68 } 69 70 return (nb); 71 } 72 73 /* 74 * Forks off a new process, stashes the parent's handle for the pty in *termfd 75 * and returns the pid. 0 for the child, >0 for the parent, as usual. 76 * 77 * Most tests fork so that we can do them while unprivileged, which we can only 78 * do if we're operating on our ctty (and we don't want to touch the tty of 79 * whatever may be running the tests). 80 */ 81 static int 82 init_pty(int *termfd, bool canon) 83 { 84 int pid; 85 86 pid = forkpty(termfd, NULL, NULL, NULL); 87 ATF_REQUIRE(pid != -1); 88 89 if (pid == 0) { 90 struct termios term; 91 92 /* 93 * Child reconfigures tty to disable echo and put it into raw 94 * mode if requested. 95 */ 96 if (tcgetattr(STDIN_FILENO, &term) == -1) 97 _exit(STIERR_CONFIG_FETCH); 98 term.c_lflag &= ~ECHO; 99 if (!canon) 100 term.c_lflag &= ~ICANON; 101 if (tcsetattr(STDIN_FILENO, TCSANOW, &term) == -1) 102 _exit(STIERR_CONFIG); 103 } 104 105 return (pid); 106 } 107 108 static void 109 finalize_child(pid_t pid, int signo) 110 { 111 int status, wpid; 112 113 while ((wpid = waitpid(pid, &status, 0)) != pid) { 114 if (wpid != -1) 115 continue; 116 ATF_REQUIRE_EQ_MSG(EINTR, errno, 117 "waitpid: %s", strerror(errno)); 118 } 119 120 /* 121 * Some tests will signal the child for whatever reason, and we're 122 * expecting it to terminate it. For those cases, it's OK to just see 123 * that termination. For all other cases, we expect a graceful exit 124 * with an exit status that reflects a cause that we have an error 125 * mapped for. 126 */ 127 if (signo >= 0) { 128 ATF_REQUIRE(WIFSIGNALED(status)); 129 ATF_REQUIRE_EQ(signo, WTERMSIG(status)); 130 } else { 131 ATF_REQUIRE(WIFEXITED(status)); 132 if (WEXITSTATUS(status) != 0) { 133 int err = WEXITSTATUS(status); 134 135 for (size_t i = 0; i < nitems(stierr_map); i++) { 136 const struct stierr_map *map = &stierr_map[i]; 137 138 if ((int)map->stierr == err) { 139 atf_tc_fail("%s", map->msg); 140 __assert_unreachable(); 141 } 142 } 143 } 144 } 145 } 146 147 ATF_TC(basic); 148 ATF_TC_HEAD(basic, tc) 149 { 150 atf_tc_set_md_var(tc, "descr", 151 "Test for basic functionality of TIOCSTI"); 152 atf_tc_set_md_var(tc, "require.user", "unprivileged"); 153 } 154 ATF_TC_BODY(basic, tc) 155 { 156 int pid, term; 157 158 /* 159 * We don't canonicalize on this test because we can assume that the 160 * injected data will be available after TIOCSTI returns. This is all 161 * within a single thread for the basic test, so we simplify our lives 162 * slightly in raw mode. 163 */ 164 pid = init_pty(&term, false); 165 if (pid == 0) { 166 static const char sending[] = "Text"; 167 char readbuf[32]; 168 ssize_t injected, readsz; 169 170 injected = inject(STDIN_FILENO, sending); 171 if (injected != sizeof(sending) - 1) 172 _exit(STIERR_INJECT); 173 174 readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); 175 176 if (readsz < 0 || readsz != injected) 177 _exit(STIERR_READFAIL); 178 if (memcmp(readbuf, sending, readsz) != 0) 179 _exit(STIERR_BADTEXT); 180 181 _exit(0); 182 } 183 184 finalize_child(pid, -1); 185 } 186 187 ATF_TC(root); 188 ATF_TC_HEAD(root, tc) 189 { 190 atf_tc_set_md_var(tc, "descr", 191 "Test that root can inject into another TTY"); 192 atf_tc_set_md_var(tc, "require.user", "root"); 193 } 194 ATF_TC_BODY(root, tc) 195 { 196 static const char sending[] = "Text\r"; 197 ssize_t injected; 198 int pid, term; 199 200 /* 201 * We leave canonicalization enabled for this one so that the read(2) 202 * below hangs until we have all of the data available, rather than 203 * having to signal OOB that it's safe to read. 204 */ 205 pid = init_pty(&term, true); 206 if (pid == 0) { 207 char readbuf[32]; 208 ssize_t readsz; 209 210 readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); 211 if (readsz < 0 || readsz != sizeof(sending) - 1) 212 _exit(STIERR_READFAIL); 213 214 /* 215 * Here we ignore the trailing \r, because it won't have 216 * surfaced in our read(2). 217 */ 218 if (memcmp(readbuf, sending, readsz - 1) != 0) 219 _exit(STIERR_BADTEXT); 220 221 _exit(0); 222 } 223 224 injected = inject(term, sending); 225 ATF_REQUIRE_EQ_MSG(sizeof(sending) - 1, injected, 226 "Injected %zu characters, expected %zu", injected, 227 sizeof(sending) - 1); 228 229 finalize_child(pid, -1); 230 } 231 232 ATF_TC(unprivileged_fail_noctty); 233 ATF_TC_HEAD(unprivileged_fail_noctty, tc) 234 { 235 atf_tc_set_md_var(tc, "descr", 236 "Test that unprivileged cannot inject into non-controlling TTY"); 237 atf_tc_set_md_var(tc, "require.user", "unprivileged"); 238 } 239 ATF_TC_BODY(unprivileged_fail_noctty, tc) 240 { 241 const char sending[] = "Text"; 242 ssize_t injected; 243 int pid, serrno, term; 244 245 pid = init_pty(&term, false); 246 if (pid == 0) { 247 char readbuf[32]; 248 ssize_t readsz; 249 250 /* 251 * This should hang until we get terminated by the parent. 252 */ 253 readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); 254 if (readsz > 0) 255 _exit(STIERR_DATAFOUND); 256 257 _exit(0); 258 } 259 260 /* Should fail. */ 261 injected = inject(term, sending); 262 serrno = errno; 263 264 /* Done with the child, just kill it now to avoid problems later. */ 265 kill(pid, SIGINT); 266 finalize_child(pid, SIGINT); 267 268 ATF_REQUIRE_EQ_MSG(-1, (ssize_t)injected, 269 "TIOCSTI into non-ctty succeeded"); 270 ATF_REQUIRE_EQ(EACCES, serrno); 271 } 272 273 ATF_TC(unprivileged_fail_noread); 274 ATF_TC_HEAD(unprivileged_fail_noread, tc) 275 { 276 atf_tc_set_md_var(tc, "descr", 277 "Test that unprivileged cannot inject into TTY not opened for read"); 278 atf_tc_set_md_var(tc, "require.user", "unprivileged"); 279 } 280 ATF_TC_BODY(unprivileged_fail_noread, tc) 281 { 282 int pid, term; 283 284 /* 285 * Canonicalization actually doesn't matter for this one, we'll trust 286 * that the failure means we didn't inject anything. 287 */ 288 pid = init_pty(&term, true); 289 if (pid == 0) { 290 static const char sending[] = "Text"; 291 ssize_t injected; 292 int rotty, wotty; 293 294 /* 295 * We open the tty both r/o and w/o to ensure we got the device 296 * name right; one of these will pass, one of these will fail. 297 */ 298 wotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_WRONLY); 299 if (wotty == -1) 300 _exit(STIERR_WOTTY); 301 rotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_RDONLY); 302 if (rotty == -1) 303 _exit(STIERR_ROTTY); 304 305 /* 306 * This injection is expected to fail with EPERM, because it may 307 * be our controlling tty but it is not open for reading. 308 */ 309 injected = inject(wotty, sending); 310 if (injected != -1) 311 _exit(STIERR_WOOK); 312 if (errno != EPERM) 313 _exit(STIERR_BADERR); 314 315 /* 316 * Demonstrate that it does succeed on the other fd we opened, 317 * which is r/o. 318 */ 319 injected = inject(rotty, sending); 320 if (injected != sizeof(sending) - 1) 321 _exit(STIERR_INJECT); 322 323 _exit(0); 324 } 325 326 finalize_child(pid, -1); 327 } 328 329 ATF_TP_ADD_TCS(tp) 330 { 331 ATF_TP_ADD_TC(tp, basic); 332 ATF_TP_ADD_TC(tp, root); 333 ATF_TP_ADD_TC(tp, unprivileged_fail_noctty); 334 ATF_TP_ADD_TC(tp, unprivileged_fail_noread); 335 336 return (atf_no_error()); 337 } 338