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
inject(int fileno,const char * str)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
init_pty(int * termfd,bool canon)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
finalize_child(pid_t pid,int signo)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);
ATF_TC_HEAD(basic,tc)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 }
ATF_TC_BODY(basic,tc)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);
ATF_TC_HEAD(root,tc)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 }
ATF_TC_BODY(root,tc)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);
ATF_TC_HEAD(unprivileged_fail_noctty,tc)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 }
ATF_TC_BODY(unprivileged_fail_noctty,tc)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);
ATF_TC_HEAD(unprivileged_fail_noread,tc)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 }
ATF_TC_BODY(unprivileged_fail_noread,tc)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
ATF_TP_ADD_TCS(tp)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