xref: /freebsd/tests/sys/kern/tty/test_sti.c (revision 52a2b4bc5da21d7a54cb16b9450196244b59b8c0)
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