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