feat(termios): terminal attributes
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user