Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/tests/sys/kern/tty/test_sti.c
39488 views
1
/*-
2
* Copyright (c) 2025 Kyle Evans <[email protected]>
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
339