Coverage Report

Created: 2026-11-12 06:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/sudo/lib/util/term.c
Line
Count
Source
1
/*
2
 * SPDX-License-Identifier: ISC
3
 *
4
 * Copyright (c) 2011-2015, 2017-2023 Todd C. Miller <Todd.Miller@sudo.ws>
5
 *
6
 * Permission to use, copy, modify, and distribute this software for any
7
 * purpose with or without fee is hereby granted, provided that the above
8
 * copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 */
18
19
#include <config.h>
20
21
#include <sys/ioctl.h>
22
#include <sys/stat.h>
23
#include <stdio.h>
24
#include <stdlib.h>
25
#include <string.h>
26
#include <errno.h>
27
#include <signal.h>
28
#include <termios.h>
29
#include <unistd.h>
30
31
#include <sudo_compat.h>
32
#include <sudo_debug.h>
33
#include <sudo_util.h>
34
35
/* TCSASOFT is a BSD extension that ignores control flags and speed. */
36
#ifndef TCSASOFT
37
0
# define TCSASOFT 0
38
#endif
39
40
/* Non-standard termios input flags */
41
#ifndef IUCLC
42
# define IUCLC    0
43
#endif
44
#ifndef IMAXBEL
45
# define IMAXBEL  0
46
#endif
47
#ifndef IUTF8
48
# define IUTF8  0
49
#endif
50
51
/* Non-standard termios output flags */
52
#ifndef OLCUC
53
# define OLCUC  0
54
#endif
55
#ifndef ONLCR
56
# define ONLCR  0
57
#endif
58
#ifndef OCRNL
59
# define OCRNL  0
60
#endif
61
#ifndef ONOCR
62
# define ONOCR  0
63
#endif
64
#ifndef ONLRET
65
# define ONLRET 0
66
#endif
67
68
/* Non-standard termios local flags */
69
#ifndef XCASE
70
# define XCASE    0
71
#endif
72
#ifndef IEXTEN
73
# define IEXTEN   0
74
#endif
75
#ifndef ECHOCTL
76
# define ECHOCTL  0
77
#endif
78
#ifndef ECHOKE
79
# define ECHOKE   0
80
#endif
81
82
/* Termios flags to copy between terminals. */
83
0
#define INPUT_FLAGS (IGNPAR|PARMRK|INPCK|ISTRIP|INLCR|IGNCR|ICRNL|IUCLC|IXON|IXANY|IXOFF|IMAXBEL|IUTF8)
84
0
#define OUTPUT_FLAGS (OPOST|OLCUC|ONLCR|OCRNL|ONOCR|ONLRET)
85
0
#define CONTROL_FLAGS (CS7|CS8|PARENB|PARODD)
86
0
#define LOCAL_FLAGS (ISIG|ICANON|XCASE|ECHO|ECHOE|ECHOK|ECHONL|NOFLSH|TOSTOP|IEXTEN|ECHOCTL|ECHOKE)
87
88
static struct termios orig_term;
89
static struct termios cur_term;
90
static bool changed;
91
92
/* tgetpass() needs to know the erase and kill chars for cbreak mode. */
93
sudo_dso_public int sudo_term_eof;
94
sudo_dso_public int sudo_term_erase;
95
sudo_dso_public int sudo_term_kill;
96
97
static volatile sig_atomic_t got_sigttou;
98
99
/*
100
 * SIGTTOU signal handler for tcsetattr_nobg() that just sets a flag.
101
 */
102
static void
103
sigttou(int signo)
104
0
{
105
0
    got_sigttou = 1;
106
0
}
107
108
/*
109
 * Like tcsetattr() but restarts on EINTR _except_ for SIGTTOU.
110
 * Returns 0 on success or -1 on failure, setting errno.
111
 * Sets got_sigttou on failure if interrupted by SIGTTOU.
112
 */
113
static int
114
tcsetattr_nobg(int fd, int flags, struct termios *tp)
115
0
{
116
0
    struct sigaction sa, osa;
117
0
    int rc;
118
0
    debug_decl(tcsetattr_nobg, SUDO_DEBUG_UTIL);
119
120
    /*
121
     * If we receive SIGTTOU from tcsetattr() it means we are
122
     * not in the foreground process group.
123
     * This should be less racy than using tcgetpgrp().
124
     */
125
0
    memset(&sa, 0, sizeof(sa));
126
0
    sigemptyset(&sa.sa_mask);
127
0
    sa.sa_handler = sigttou;
128
0
    got_sigttou = 0;
129
0
    sigaction(SIGTTOU, &sa, &osa);
130
0
    do {
131
0
  rc = tcsetattr(fd, flags, tp);
132
0
    } while (rc == -1 && errno == EINTR && !got_sigttou);
133
0
    sigaction(SIGTTOU, &osa, NULL);
134
135
0
    debug_return_int(rc);
136
0
}
137
138
/*
139
 * Restore saved terminal settings if we are in the foreground process group.
140
 * Returns true on success or false on failure.
141
 */
142
bool
143
sudo_term_restore_v2(int fd, int action)
144
0
{
145
0
    struct termios term = { 0 };
146
0
    bool ret = false;
147
0
    int flags;
148
0
    debug_decl(sudo_term_restore, SUDO_DEBUG_UTIL);
149
150
0
    switch (action) {
151
0
    case SUDO_TERM_NOW:
152
0
  flags = TCSASOFT|TCSANOW;
153
0
  break;
154
0
    case SUDO_TERM_DRAIN:
155
0
  flags = TCSASOFT|TCSADRAIN;
156
0
  break;
157
0
    case SUDO_TERM_FLUSH:
158
0
  flags = TCSASOFT|TCSAFLUSH;
159
0
  break;
160
0
    default:
161
0
  debug_return_bool(false);
162
0
    }
163
164
0
    if (!changed)
165
0
  debug_return_bool(true);
166
167
0
    sudo_lock_file(fd, SUDO_LOCK);
168
169
    /* Avoid changing term settings if changed out from under us. */
170
0
    if (tcgetattr(fd, &term) == -1) {
171
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
172
0
      "%s: tcgetattr(%d)", __func__, fd);
173
0
  goto unlock;
174
0
    }
175
0
    if ((term.c_iflag & INPUT_FLAGS) != (cur_term.c_iflag & INPUT_FLAGS)) {
176
0
  sudo_debug_printf(SUDO_DEBUG_INFO, "%s: not restoring terminal, "
177
0
      "c_iflag changed; 0x%x, expected 0x%x", __func__,
178
0
      (unsigned int)term.c_iflag, (unsigned int)cur_term.c_iflag);
179
  /* Not an error. */
180
0
  ret = true;
181
0
  goto unlock;
182
0
    }
183
0
    if ((term.c_oflag & OUTPUT_FLAGS) != (cur_term.c_oflag & OUTPUT_FLAGS)) {
184
0
  sudo_debug_printf(SUDO_DEBUG_INFO, "%s: not restoring terminal, "
185
0
      "c_oflag changed; 0x%x, expected 0x%x", __func__,
186
0
      (unsigned int)term.c_oflag, (unsigned int)cur_term.c_oflag);
187
  /* Not an error. */
188
0
  ret = true;
189
0
  goto unlock;
190
0
    }
191
0
#if !TCSASOFT
192
    /* Only systems without TCSASOFT make changes to c_cflag. */
193
0
    if ((term.c_cflag & CONTROL_FLAGS) != (cur_term.c_cflag & CONTROL_FLAGS)) {
194
0
  sudo_debug_printf(SUDO_DEBUG_INFO, "%s: not restoring terminal, "
195
0
      "c_cflag changed; 0x%x, expected 0x%x", __func__,
196
0
      (unsigned int)term.c_cflag, (unsigned int)cur_term.c_cflag);
197
  /* Not an error. */
198
0
  ret = true;
199
0
  goto unlock;
200
0
    }
201
0
#endif
202
0
    if ((term.c_lflag & LOCAL_FLAGS) != (cur_term.c_lflag & LOCAL_FLAGS)) {
203
0
  sudo_debug_printf(SUDO_DEBUG_INFO, "%s: not restoring terminal, "
204
0
      "c_lflag changed; 0x%x, expected 0x%x", __func__,
205
0
      (unsigned int)term.c_lflag, (unsigned int)cur_term.c_lflag);
206
  /* Not an error. */
207
0
  ret = true;
208
0
  goto unlock;
209
0
    }
210
0
    if (memcmp(term.c_cc, cur_term.c_cc, sizeof(term.c_cc)) != 0) {
211
0
  sudo_debug_printf(SUDO_DEBUG_INFO,
212
0
      "%s: not restoring terminal, c_cc[] changed", __func__);
213
  /* Not an error. */
214
0
  ret = true;
215
0
  goto unlock;
216
0
    }
217
218
0
    if (tcsetattr_nobg(fd, flags, &orig_term) == -1) {
219
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
220
0
      "%s: tcsetattr(%d)", __func__, fd);
221
0
  goto unlock;
222
0
    }
223
0
    cur_term = orig_term;
224
0
    changed = false;
225
0
    ret = true;
226
227
0
unlock:
228
0
    sudo_lock_file(fd, SUDO_UNLOCK);
229
230
0
    debug_return_bool(ret);
231
0
}
232
233
bool
234
sudo_term_restore_v1(int fd, bool flush)
235
0
{
236
0
    return sudo_term_restore_v2(fd, flush ? SUDO_TERM_FLUSH : SUDO_TERM_DRAIN);
237
0
}
238
239
/*
240
 * Disable terminal echo.
241
 * Returns true on success or false on failure.
242
 */
243
bool
244
sudo_term_noecho_v1(int fd)
245
0
{
246
0
    struct termios term = { 0 };
247
0
    bool ret = false;
248
0
    struct stat sb;
249
0
    debug_decl(sudo_term_noecho, SUDO_DEBUG_UTIL);
250
251
    /* Avoid calling ioctl on non-device to prevent CVE-2023-2002. */
252
0
    if (fstat(fd, &sb) != 0 || !S_ISCHR(sb.st_mode))
253
0
  debug_return_bool(false);
254
255
0
    sudo_lock_file(fd, SUDO_LOCK);
256
0
    if (!changed && tcgetattr(fd, &orig_term) == -1) {
257
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
258
0
      "%s: tcgetattr(%d)", __func__, fd);
259
0
  goto unlock;
260
0
    }
261
262
0
    term = orig_term;
263
0
    CLR(term.c_lflag, ECHO|ECHONL);
264
#ifdef VSTATUS
265
    term.c_cc[VSTATUS] = _POSIX_VDISABLE;
266
#endif
267
0
    if (tcsetattr_nobg(fd, TCSASOFT|TCSAFLUSH, &term) == -1) {
268
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
269
0
      "%s: tcsetattr(%d)", __func__, fd);
270
0
  goto unlock;
271
0
    }
272
0
    cur_term = term;
273
0
    changed = true;
274
0
    ret = true;
275
276
0
unlock:
277
0
    sudo_lock_file(fd, SUDO_UNLOCK);
278
0
    debug_return_bool(ret);
279
0
}
280
281
/*
282
 * Returns true if term is in raw mode, else false.
283
 */
284
static bool
285
sudo_term_is_raw_int(struct termios *term)
286
0
{
287
0
    debug_decl(sudo_term_is_raw_int, SUDO_DEBUG_UTIL);
288
289
0
    if (term->c_cc[VMIN] != 1 || term->c_cc[VTIME] != 0)
290
0
  debug_return_bool(false);
291
292
0
    if (ISSET(term->c_oflag, OPOST))
293
0
  debug_return_bool(false);
294
295
0
    if (ISSET(term->c_lflag, ECHO|ECHONL|ICANON))
296
0
  debug_return_bool(false);
297
298
0
    debug_return_bool(true);
299
0
}
300
301
/*
302
 * Returns true if fd refers to a tty in raw mode, else false.
303
 */
304
bool
305
sudo_term_is_raw_v1(int fd)
306
0
{
307
0
    struct termios term = { 0 };
308
0
    struct stat sb;
309
0
    debug_decl(sudo_term_is_raw, SUDO_DEBUG_UTIL);
310
311
    /* Avoid calling ioctl on non-device to prevent CVE-2023-2002. */
312
0
    if (fstat(fd, &sb) != 0 || !S_ISCHR(sb.st_mode))
313
0
  debug_return_bool(false);
314
315
0
    sudo_lock_file(fd, SUDO_LOCK);
316
0
    if (tcgetattr(fd, &term) == -1) {
317
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
318
0
      "%s: tcgetattr(%d)", __func__, fd);
319
0
  sudo_lock_file(fd, SUDO_UNLOCK);
320
0
  debug_return_bool(false);
321
0
    }
322
0
    sudo_lock_file(fd, SUDO_UNLOCK);
323
324
0
    debug_return_bool(sudo_term_is_raw_int(&term));
325
0
}
326
327
/*
328
 * Set terminal to raw mode as modified by flags.
329
 * Returns true on success or false on failure.
330
 */
331
bool
332
sudo_term_raw_v1(int fd, unsigned int flags)
333
0
{
334
0
    struct termios term = { 0 };
335
0
    bool ret = false;
336
0
    struct stat sb;
337
0
    tcflag_t oflag;
338
0
    debug_decl(sudo_term_raw, SUDO_DEBUG_UTIL);
339
340
    /* Avoid calling ioctl on non-device to prevent CVE-2023-2002. */
341
0
    if (fstat(fd, &sb) != 0 || !S_ISCHR(sb.st_mode))
342
0
  debug_return_bool(false);
343
344
0
    sudo_lock_file(fd, SUDO_LOCK);
345
0
    if (!changed && tcgetattr(fd, &orig_term) == -1) {
346
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
347
0
      "%s: tcgetattr(%d)", __func__, fd);
348
0
  goto unlock;
349
0
    }
350
351
0
    if (sudo_term_is_raw_int(&term)) {
352
0
  sudo_debug_printf(SUDO_DEBUG_INFO, "%s: fd %d already in raw mode",
353
0
      __func__, fd);
354
0
  ret = true;
355
0
  goto unlock;
356
0
    }
357
358
    /*
359
     * Set terminal to raw mode but optionally enable terminal signals
360
     * and/or preserve output flags.
361
     */
362
0
    term = orig_term;
363
0
    oflag = term.c_oflag;
364
0
    cfmakeraw(&term);
365
0
    if (ISSET(flags, SUDO_TERM_ISIG))
366
0
  SET(term.c_lflag, ISIG);
367
0
    if (ISSET(flags, SUDO_TERM_OFLAG))
368
0
  term.c_oflag = oflag;
369
0
    if (tcsetattr_nobg(fd, TCSASOFT|TCSADRAIN, &term) == -1) {
370
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
371
0
      "%s: tcsetattr(%d)", __func__, fd);
372
0
  goto unlock;
373
0
    }
374
0
    cur_term = term;
375
0
    changed = true;
376
0
    ret = true;
377
378
0
unlock:
379
0
    sudo_lock_file(fd, SUDO_UNLOCK);
380
0
    debug_return_bool(ret);
381
0
}
382
383
/*
384
 * Set terminal to cbreak mode.
385
 * Returns true on success or false on failure.
386
 */
387
bool
388
sudo_term_cbreak_v3(int fd, int action)
389
0
{
390
0
    struct termios term = { 0 };
391
0
    bool ret = false;
392
0
    struct stat sb;
393
0
    int flags;
394
0
    debug_decl(sudo_term_cbreak, SUDO_DEBUG_UTIL);
395
396
0
    switch (action) {
397
0
    case SUDO_TERM_NOW:
398
0
  flags = TCSASOFT|TCSANOW;
399
0
  break;
400
0
    case SUDO_TERM_DRAIN:
401
0
  flags = TCSASOFT|TCSADRAIN;
402
0
  break;
403
0
    case SUDO_TERM_FLUSH:
404
0
  flags = TCSASOFT|TCSAFLUSH;
405
0
  break;
406
0
    default:
407
0
  debug_return_bool(false);
408
0
    }
409
410
    /* Avoid calling ioctl on non-device to prevent CVE-2023-2002. */
411
0
    if (fstat(fd, &sb) != 0 || !S_ISCHR(sb.st_mode))
412
0
  debug_return_bool(false);
413
414
0
    sudo_lock_file(fd, SUDO_LOCK);
415
0
    if (!changed && tcgetattr(fd, &orig_term) == -1) {
416
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
417
0
      "%s: tcgetattr(%d)", __func__, fd);
418
0
  goto unlock;
419
0
    }
420
421
    /* Set terminal to half-cooked mode */
422
0
    term = orig_term;
423
0
    term.c_cc[VMIN] = 1;
424
0
    term.c_cc[VTIME] = 0;
425
    /* cppcheck-suppress redundantAssignment */
426
0
    CLR(term.c_lflag, ECHO | ECHONL | ICANON | IEXTEN);
427
    /* cppcheck-suppress redundantAssignment */
428
0
    SET(term.c_lflag, ISIG);
429
#ifdef VSTATUS
430
    term.c_cc[VSTATUS] = _POSIX_VDISABLE;
431
#endif
432
0
    if (tcsetattr_nobg(fd, flags, &term) == -1) {
433
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
434
0
      "%s: tcsetattr(%d)", __func__, fd);
435
0
  goto unlock;
436
0
    }
437
0
    sudo_term_eof = term.c_cc[VEOF];
438
0
    sudo_term_erase = term.c_cc[VERASE];
439
0
    sudo_term_kill = term.c_cc[VKILL];
440
0
    cur_term = term;
441
0
    changed = true;
442
0
    ret = true;
443
444
0
unlock:
445
0
    sudo_lock_file(fd, SUDO_UNLOCK);
446
0
    debug_return_bool(ret);
447
0
}
448
449
bool
450
sudo_term_cbreak_v2(int fd, bool flush)
451
0
{
452
0
    return sudo_term_cbreak_v3(fd, flush ? SUDO_TERM_FLUSH : SUDO_TERM_DRAIN);
453
0
}
454
455
bool
456
sudo_term_cbreak_v1(int fd)
457
0
{
458
0
    return sudo_term_cbreak_v2(fd, false);
459
0
}
460
461
/*
462
 * Copy terminal settings from one descriptor to another.
463
 * We cannot simply copy the struct termios as src and dst may be
464
 * different terminal types (pseudo-tty vs. console or glass tty).
465
 * Returns true on success or false on failure.
466
 */
467
bool
468
sudo_term_copy_v1(int src, int dst)
469
0
{
470
0
    struct termios tt_src, tt_dst;
471
0
    struct winsize wsize;
472
0
    speed_t speed;
473
0
    unsigned int i;
474
0
    bool ret = false;
475
0
    debug_decl(sudo_term_copy, SUDO_DEBUG_UTIL);
476
477
0
    sudo_lock_file(src, SUDO_LOCK);
478
0
    sudo_lock_file(dst, SUDO_LOCK);
479
0
    if (tcgetattr(src, &tt_src) == -1 || tcgetattr(dst, &tt_dst) == -1) {
480
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
481
0
      "%s: tcgetattr", __func__);
482
0
  goto unlock;
483
0
    }
484
485
    /* Clear select input, output, control and local flags. */
486
0
    CLR(tt_dst.c_iflag, INPUT_FLAGS);
487
0
    CLR(tt_dst.c_oflag, OUTPUT_FLAGS);
488
0
    CLR(tt_dst.c_cflag, CONTROL_FLAGS);
489
0
    CLR(tt_dst.c_lflag, LOCAL_FLAGS);
490
491
    /* Copy select input, output, control and local flags. */
492
0
    SET(tt_dst.c_iflag, (tt_src.c_iflag & INPUT_FLAGS));
493
0
    SET(tt_dst.c_oflag, (tt_src.c_oflag & OUTPUT_FLAGS));
494
0
    SET(tt_dst.c_cflag, (tt_src.c_cflag & CONTROL_FLAGS));
495
0
    SET(tt_dst.c_lflag, (tt_src.c_lflag & LOCAL_FLAGS));
496
497
    /* Copy special chars from src verbatim. */
498
0
    for (i = 0; i < NCCS; i++)
499
0
  tt_dst.c_cc[i] = tt_src.c_cc[i];
500
501
    /* Copy speed from src (zero output speed closes the connection). */
502
0
    if ((speed = cfgetospeed(&tt_src)) == B0)
503
0
  speed = B38400;
504
0
    cfsetospeed(&tt_dst, speed);
505
0
    speed = cfgetispeed(&tt_src);
506
0
    cfsetispeed(&tt_dst, speed);
507
508
0
    if (tcsetattr_nobg(dst, TCSASOFT|TCSAFLUSH, &tt_dst) == -1) {
509
0
  sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
510
0
      "%s: tcsetattr(%d)", __func__, dst);
511
0
  goto unlock;
512
0
    }
513
0
    ret = true;
514
515
0
    if (ioctl(src, TIOCGWINSZ, &wsize) == 0)
516
0
  (void)ioctl(dst, IOCTL_REQ_CAST TIOCSWINSZ, &wsize);
517
518
0
unlock:
519
0
    sudo_lock_file(dst, SUDO_UNLOCK);
520
0
    sudo_lock_file(src, SUDO_UNLOCK);
521
0
    debug_return_bool(ret);
522
0
}
523
524
/*
525
 * Like isatty(3) but stats the fd and stores the result in sb.
526
 * Only calls isatty(3) if fd is a character special device.
527
 * Returns true if a tty, else returns false and sets errno.
528
 * This is mitigation for CVE-2023-2002.
529
 */
530
bool
531
sudo_isatty_v1(int fd, struct stat *sbp)
532
8
{
533
8
    bool ret = false;
534
8
    struct stat sb;
535
8
    debug_decl(sudo_isatty, SUDO_DEBUG_EXEC);
536
537
8
    if (sbp == NULL)
538
0
  sbp = &sb;
539
540
8
    if (fstat(fd, sbp) == 0) {
541
8
        if (!S_ISCHR(sbp->st_mode)) {
542
8
            errno = ENOTTY;
543
8
        } else {
544
0
            ret = isatty(fd) == 1;
545
0
        }
546
8
    } else if (sbp != &sb) {
547
        /* Always initialize sbp. */
548
0
        memset(sbp, 0, sizeof(*sbp));
549
0
    }
550
    debug_return_bool(ret);
551
8
}