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