Files
vlibc/tests/test_termios.c

389 lines
14 KiB
C

/*
* vlibc — terminal attributes test (todo 29).
*
* Exercises include/termios.h + src/termios/termios.c end to end over a
* pty master opened with the raw SYS_openat syscall (deliberately not the
* open() wrapper — the termios functions are what is under test). When
* /dev/ptmx cannot be opened the default run prints SKIP and exits 0
* (guarded scenario, house pattern).
*
* Default-mode scenarios (in run order, all on the pty master):
*
* 1. tcgetattr returns 0 with sane defaults: c_cflag carries CREAD and a
* nonzero CSIZE, c_lflag carries ICANON|ECHO.
* 2. tcsetattr(TCSANOW) with ICANON cleared and c_cc[VMIN]/c_cc[VTIME]
* set, then tcgetattr reflects the cleared ICANON and the VMIN/VTIME
* values (proves the c_cc[NCCS] layout is not corrupted across the
* kernel copy, and that indices 5/6 are the right slots).
* 3. The blocking/flush/flow/break pass-throughs succeed: tcdrain,
* tcflush for all three selectors, tcflow TCOOFF/TCOON, tcsendbreak,
* and tcsetattr with the TCSADRAIN/TCSAFLUSH actions.
* 4. (L1 speed helpers, in-memory) cfsetospeed(B9600)/cfgetospeed and
* cfsetispeed(B9600)/cfgetispeed round-trip through the c_cflag bit
* fields, and an extended rate (B115200, CBAUDEX region) round-trips
* cfsetospeed/cfgetospeed as well.
*
* Level-2 scenarios (#if VLIBC_LEVEL_GE(2)):
*
* 5. cfmakeraw clears ICANON and ECHO, drops parity, selects CS8; after
* tcsetattr + tcgetattr the cleared ICANON/ECHO are reflected.
* 6. cfsetspeed(B9600) stores B9600 for both the input and the output
* rate.
*
* The -f mode runs the failure scenarios and exits via raw SYS_exit_group
* (house pattern): every negative makes the LIBRARY write errno
* (syscall_ret -> %fs:0+8, glibc's private dtv slot under a host-linked
* binary), so no host cleanup may run afterwards. The test itself NEVER
* reads errno; every negative is asserted on the return value only:
* tcsetattr(-1, ...) -> -1 (EBADF), tcgetattr on /dev/null -> -1 (ENOTTY,
* the kernel pass-through), tcflush/tcflow with a bad selector/action on a
* real pty -> -1, tcsetattr with an invalid action -> -1 (EINVAL, checked
* before the kernel), an out-of-range speed to the cfset* helpers -> -1,
* and tcgetsid(-1) -> -1 (level 2).
*
* All diagnostics go through raw SYS_write (no stdio): under -Iinclude the
* vlibc public headers shadow GCC's internal ones, so a host header would
* not compile. Only vlibc headers are included.
*/
#include <stddef.h>
#include "../include/fcntl.h"
#include "../include/termios.h"
#include "../src/internal/syscall.h"
static int failures;
/* Write a NUL-terminated string to fd via the raw syscall layer. The
* optimize attribute keeps GCC from lowering the length loop into a
* strlen call, which would leave a vlibc-owned symbol undefined in this
* host-linked standalone binary (house idiom, see src/string). */
static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void
say(int fd, const char *s)
{
long n = 0;
while (s[n] != '\0')
{
n++;
}
__syscall3(SYS_write, fd, (long)s, n);
}
/* Write v in decimal to fd. */
static void
say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters)
{
char buf[24];
int i = (int)sizeof(buf);
buf[--i] = '\0';
do
{
buf[--i] = (char)('0' + (v % 10));
v /= 10;
} while (v != 0);
__syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i));
}
static void
check(int cond, const char *what)
{
if (cond)
{
say(1, "PASS: ");
say(1, what);
say(1, "\n");
}
else
{
say(2, "FAIL: ");
say(2, what);
say(2, "\n");
failures++;
}
}
/* Open a pty master via the raw syscall (no open() wrapper, no errno
* write): returns the fd, or -1 when /dev/ptmx is unavailable. */
static int
open_ptmx(void)
{
long r = __syscall3(SYS_openat, AT_FDCWD, (long)"/dev/ptmx", O_RDWR | O_NOCTTY);
if (r < 0)
{
return -1;
}
return (int)r;
}
/* 1. tcgetattr on a fresh pty master: 0 and sane defaults. */
static void
defaults_scenario(int fd)
{
struct termios t;
if (tcgetattr(fd, &t) != 0)
{
check(0, "tcgetattr on a pty master returns 0");
check(0, "the defaults carry CREAD/CSIZE and ICANON|ECHO");
return;
}
check(1, "tcgetattr on a pty master returns 0");
check((t.c_cflag & CREAD) != 0 && (t.c_cflag & CSIZE) != 0,
"the default c_cflag carries CREAD and a CSIZE");
check((t.c_lflag & ICANON) != 0 && (t.c_lflag & ECHO) != 0,
"the default c_lflag carries ICANON and ECHO");
}
/* 2. tcsetattr with ICANON cleared + VMIN/VTIME set, reflected by
* tcgetattr (c_cc[NCCS] layout survives the kernel copy intact). */
static void
attr_roundtrip_scenario(int fd)
{
struct termios t;
struct termios u;
if (tcgetattr(fd, &t) != 0)
{
check(0, "tcsetattr(ICANON off) round trip");
check(0, "tcgetattr reflects the cleared ICANON");
check(0, "c_cc[VMIN]/c_cc[VTIME] survive the round trip");
return;
}
t.c_lflag &= ~ICANON;
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
if (tcsetattr(fd, TCSANOW, &t) != 0)
{
check(0, "tcsetattr(ICANON off) round trip");
check(0, "tcgetattr reflects the cleared ICANON");
check(0, "c_cc[VMIN]/c_cc[VTIME] survive the round trip");
return;
}
check(1, "tcsetattr(TCSANOW) with ICANON cleared returns 0");
if (tcgetattr(fd, &u) != 0)
{
check(0, "tcgetattr reflects the cleared ICANON");
check(0, "c_cc[VMIN]/c_cc[VTIME] survive the round trip");
return;
}
check((u.c_lflag & ICANON) == 0, "tcgetattr reflects the cleared ICANON");
check(u.c_cc[VMIN] == 1 && u.c_cc[VTIME] == 0, "c_cc[VMIN]/c_cc[VTIME] survive the round trip");
/* Restore canonical mode for any later scenario. */
u.c_lflag |= ICANON;
(void)tcsetattr(fd, TCSANOW, &u);
}
/* 3. The blocking/flush/flow/break pass-throughs and the other two
* tcsetattr actions all succeed on a pty. */
static void
flow_scenario(int fd)
{
struct termios t;
if (tcgetattr(fd, &t) != 0)
{
check(0, "tcdrain on a pty");
check(0, "tcflush for all three selectors on a pty");
check(0, "tcflow TCOOFF/TCOON on a pty");
check(0, "tcsendbreak on a pty");
check(0, "tcsetattr TCSADRAIN/TCSAFLUSH on a pty");
return;
}
check(tcdrain(fd) == 0, "tcdrain on a pty returns 0");
check(tcflush(fd, TCIFLUSH) == 0 && tcflush(fd, TCOFLUSH) == 0 && tcflush(fd, TCIOFLUSH) == 0,
"tcflush for all three selectors returns 0");
check(tcflow(fd, TCOOFF) == 0 && tcflow(fd, TCOON) == 0, "tcflow TCOOFF/TCOON returns 0");
check(tcsendbreak(fd, 0) == 0, "tcsendbreak on a pty returns 0");
check(tcsetattr(fd, TCSADRAIN, &t) == 0 && tcsetattr(fd, TCSAFLUSH, &t) == 0,
"tcsetattr TCSADRAIN/TCSAFLUSH returns 0");
}
/* 4. (L1) In-memory speed round trips through the c_cflag bit fields. */
static void
speed_scenario(int fd)
{
struct termios t;
if (tcgetattr(fd, &t) != 0)
{
check(0, "cfsetospeed/cfgetospeed B9600 round trip");
check(0, "cfsetispeed/cfgetispeed B9600 round trip");
check(0, "the extended B115200 rate round trip");
return;
}
check(cfsetospeed(&t, B9600) == 0 && cfgetospeed(&t) == B9600,
"cfsetospeed(B9600)/cfgetospeed round trip");
check(cfsetispeed(&t, B9600) == 0 && cfgetispeed(&t) == B9600,
"cfsetispeed(B9600)/cfgetispeed round trip");
check(cfsetospeed(&t, B115200) == 0 && cfgetospeed(&t) == B115200,
"the extended B115200 rate round trips through CBAUDEX");
}
#if VLIBC_LEVEL_GE(2)
/* 5+6. cfmakeraw clears the canonical/echo bits (reflected by tcgetattr
* after a tcsetattr) and cfsetspeed stores both rates. */
static void
raw_scenario(int fd)
{
struct termios t;
struct termios u;
if (tcgetattr(fd, &t) != 0)
{
check(0, "cfmakeraw clears ICANON and ECHO");
check(0, "cfmakeraw selects CS8 with no parity");
check(0, "tcgetattr reflects the cfmakeraw state");
check(0, "cfsetspeed(B9600) stores both rates");
return;
}
cfmakeraw(&t);
check((t.c_lflag & (ICANON | ECHO)) == 0, "cfmakeraw clears ICANON and ECHO");
check((t.c_cflag & PARENB) == 0 && (t.c_cflag & CSIZE) == CS8,
"cfmakeraw selects CS8 with no parity");
if (tcsetattr(fd, TCSANOW, &t) == 0 && tcgetattr(fd, &u) == 0)
{
check((u.c_lflag & (ICANON | ECHO)) == 0,
"tcgetattr reflects the cfmakeraw-cleared ICANON/ECHO");
/* Restore canonical mode for any later scenario. */
u.c_lflag |= ICANON | ECHO;
(void)tcsetattr(fd, TCSANOW, &u);
}
else
{
check(0, "tcgetattr reflects the cfmakeraw-cleared ICANON/ECHO");
}
if (tcgetattr(fd, &t) != 0)
{
check(0, "cfsetspeed(B9600) stores both rates");
return;
}
check(cfsetspeed(&t, B9600) == 0 && cfgetospeed(&t) == B9600 && cfgetispeed(&t) == B9600,
"cfsetspeed(B9600) stores both the input and the output rate");
}
#endif /* VLIBC_LEVEL_GE(2) */
/*
* Failure scenarios (-f): every assertion is on the return value only, and
* the process exits through raw SYS_exit_group because the library writes
* errno on these paths (host-TCB hazard). A fresh pty master supplies the
* real-terminal fd for the kernel-side EINVAL checks; /dev/null supplies
* the ENOTTY case.
*/
static int
failure_scenarios(void)
{
struct termios t;
int fd_pty;
int fd_null;
int rc = 0;
fd_pty = open_ptmx();
if (fd_pty < 0)
{
say(2, "FAIL: failure scenarios need a pty master\n");
rc = 1;
}
fd_null = (int)__syscall3(SYS_openat, AT_FDCWD, (long)"/dev/null", O_RDONLY);
#define FAIL_ONE(cond, msg) \
do \
{ \
if (cond) \
{ \
say(1, "PASS: "); \
say(1, msg); \
say(1, "\n"); \
} \
else \
{ \
say(2, "FAIL: "); \
say(2, msg); \
say(2, "\n"); \
rc = 1; \
} \
} while (0)
FAIL_ONE(tcgetattr(-1, &t) == -1, "tcgetattr(-1) -> -1");
FAIL_ONE(tcsetattr(-1, TCSANOW, &t) == -1, "tcsetattr(-1, TCSANOW) -> -1");
FAIL_ONE(tcsetattr(fd_pty >= 0 ? fd_pty : -1, 99, &t) == -1,
"tcsetattr with an invalid action -> -1");
FAIL_ONE(tcdrain(-1) == -1, "tcdrain(-1) -> -1");
FAIL_ONE(tcflush(-1, TCIFLUSH) == -1, "tcflush(-1) -> -1");
FAIL_ONE(tcflow(-1, TCOOFF) == -1, "tcflow(-1) -> -1");
FAIL_ONE(tcsendbreak(-1, 0) == -1, "tcsendbreak(-1) -> -1");
if (fd_null >= 0)
{
FAIL_ONE(tcgetattr(fd_null, &t) == -1, "tcgetattr on /dev/null -> -1 (ENOTTY)");
(void)__syscall1(SYS_close, fd_null);
}
if (fd_pty >= 0)
{
FAIL_ONE(tcflush(fd_pty, 9) == -1, "tcflush with a bad selector -> -1 (EINVAL)");
FAIL_ONE(tcflow(fd_pty, 9) == -1, "tcflow with a bad action -> -1 (EINVAL)");
(void)__syscall1(SYS_close, fd_pty);
}
FAIL_ONE(cfsetospeed(&t, (speed_t)0x1010) == -1,
"cfsetospeed with an out-of-range speed -> -1");
FAIL_ONE(cfsetispeed(&t, (speed_t)0x1010) == -1,
"cfsetispeed with an out-of-range speed -> -1");
#if VLIBC_LEVEL_GE(2)
FAIL_ONE(cfsetspeed(&t, (speed_t)0x1010) == -1, "cfsetspeed with an out-of-range speed -> -1");
FAIL_ONE(tcgetsid(-1) == -1, "tcgetsid(-1) -> -1");
#endif
#undef FAIL_ONE
return rc;
}
int
main(int argc, char **argv)
{
if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f')
{
/*
* The failure scenarios write errno inside the library; under the
* host libc that slot is glibc's private TLS state, so leave via
* the raw syscall without running host cleanup.
*/
int rc = failure_scenarios();
__syscall1(SYS_exit_group, rc);
return rc; /* not reached */
}
int fd = open_ptmx();
if (fd < 0)
{
say(1, "SKIP: /dev/ptmx is unavailable; the termios scenarios are skipped\n");
return 0;
}
defaults_scenario(fd);
attr_roundtrip_scenario(fd);
flow_scenario(fd);
speed_scenario(fd);
#if VLIBC_LEVEL_GE(2)
raw_scenario(fd);
#endif
(void)__syscall1(SYS_close, fd);
if (failures > 0)
{
say(2, "FAILED (");
say_dec(2, (unsigned long)failures);
say(2, " check(s))\n");
return 1;
}
say(1, "all termios tests passed\n");
return 0;
}