diff --git a/include/termios.h b/include/termios.h new file mode 100644 index 0000000..7b5aa62 --- /dev/null +++ b/include/termios.h @@ -0,0 +1,366 @@ +#ifndef VLIBC_TERMIOS_H +#define VLIBC_TERMIOS_H + +/* + * vlibc — . + * + * General terminal interface (POSIX.1-2008 base): the termios structure and + * the tcgetattr/tcsetattr/tcsendbreak/tcdrain/tcflush/tcflow query-and-set + * functions plus the cfgetispeed/cfgetospeed/cfsetispeed/cfsetospeed baud + * helpers, over the kernel's TCGETS/TCSETS ioctls (SYS_ioctl). + * + * Level 1 (onlyposix): tcgetattr, tcsetattr, tcsendbreak, tcdrain, + * tcflush, tcflow, cfgetispeed, cfgetospeed, + * cfsetispeed, cfsetospeed (POSIX base). + * Level 2 (muslmimic): cfmakeraw, cfsetspeed (BSD/glibc extensions), + * tcgetsid (XSI [CX] — glibc declares it only under + * __USE_XOPEN2K8, not base __USE_POSIX). + * + * struct termios is a byte-for-byte copy of the x86_64 kernel termios + * (asm-generic/termbits.h), pinned by the static assertions below. The + * kernel TCGETS/TCSETS ioctls copy exactly sizeof(struct termios) bytes of + * this layout (verified empirically on this host: TCGETS writes 36 bytes); + * any divergence would silently corrupt c_cc. Unlike glibc's 60-byte + * userspace struct (NCCS 32 + trailing ispeed/ospeed members), the speed + * is carried only in the c_cflag CBAUD/CIBAUD bit fields, exactly as the + * kernel encodes it — so the ioctl copies the whole structure both ways + * with nothing left stale. + * + * The baud-rate constants B0..B4000000 are the KERNEL CBAUD encodings + * (B9600 is 0x0d, B38400 is 0x0f, B57600 is 0x1001, ...), NOT glibc's + * real-numbers-in-speed_t scheme. cfgetospeed()/cfgetispeed() return these + * constants and cfsetospeed()/cfsetispeed()/cfsetspeed() accept them, so + * a speed round-trips through the c_cflag bits untouched. This is the same + * choice musl makes. + * + * All flag, control-character, baud, and action constants below are Linux + * x86_64 kernel-UAPI facts transcribed from asm-generic/termbits.h and + * asm-generic/termbits-common.h. The ioctl request numbers (TCGETS, + * TCSETS, TCSBRK, TCXONC, TCFLSH, TIOCGSID) are Linux-only and stay + * private to src/termios/termios.c, matching the sys/ioctl.h-less layout + * of the tree today (they belong to a future ). + * + * No declaration carries an intent attribute: tcgetattr/tcsetattr &c + * perform I/O, and the cf* speed helpers read or write the caller's + * termios through a pointer that may alias arbitrary memory — const/pure + * would be unsound (same rationale as ). + */ + +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ---- Basic types ---- */ + +typedef unsigned char cc_t; /* control-character value */ +typedef unsigned int speed_t; /* baud rate (a B* constant) */ +typedef unsigned int tcflag_t; /* mode-flag bit mask */ + +/* ---- struct termios (x86_64 kernel layout) ---- */ + +#define NCCS 19 /* control-character slots (kernel UAPI) */ + +struct termios +{ + tcflag_t c_iflag; /* 0: input mode flags */ + tcflag_t c_oflag; /* 4: output mode flags */ + tcflag_t c_cflag; /* 8: control mode flags */ + tcflag_t c_lflag; /* 12: local mode flags */ + cc_t c_line; /* 16: line discipline */ + cc_t c_cc[NCCS]; /* 17: control characters */ +}; + +/* Pin the kernel layout (TCGETS/TCSETS copy this exact size and shape). */ +_Static_assert(sizeof(struct termios) == 36, "struct termios must match the x86_64 kernel layout"); +_Static_assert(offsetof(struct termios, c_lflag) == 12, "c_lflag must sit at offset 12"); +_Static_assert(offsetof(struct termios, c_line) == 16, "c_line must sit at offset 16"); +_Static_assert(offsetof(struct termios, c_cc) == 17, "c_cc must sit at offset 17"); + +/* ---- Control characters (indices into c_cc) ---- */ + +#define VINTR 0 /* interrupt character */ +#define VQUIT 1 /* quit character */ +#define VERASE 2 /* erase character */ +#define VKILL 3 /* kill character */ +#define VEOF 4 /* end-of-file character */ +#define VTIME 5 /* timeout for non-canonical read */ +#define VMIN 6 /* min characters for non-canonical read */ +#define VSWTC 7 /* switch character (System V) */ +#define VSTART 8 /* start character */ +#define VSTOP 9 /* stop character */ +#define VSUSP 10 /* suspend character */ +#define VEOL 11 /* alternate end-of-line character */ +#define VREPRINT 12 /* reprint character */ +#define VDISCARD 13 /* discard character */ +#define VWERASE 14 /* word-erase character */ +#define VLNEXT 15 /* literal-next character */ +#define VEOL2 16 /* alternate end-of-line character */ + +/* ---- c_iflag: input mode flags ---- */ + +#define IGNBRK 0x0001 /* ignore break condition */ +#define BRKINT 0x0002 /* signal interrupt on break */ +#define IGNPAR 0x0004 /* ignore characters with parity errors */ +#define PARMRK 0x0008 /* mark parity and framing errors */ +#define INPCK 0x0010 /* enable input parity check */ +#define ISTRIP 0x0020 /* strip 8th bit off characters */ +#define INLCR 0x0040 /* map NL to CR on input */ +#define IGNCR 0x0080 /* ignore CR */ +#define ICRNL 0x0100 /* map CR to NL on input */ +#define IUCLC 0x0200 /* map uppercase to lowercase on input */ +#define IXON 0x0400 /* enable start/stop output control */ +#define IXANY 0x0800 /* any character will restart after stop */ +#define IXOFF 0x1000 /* enable start/stop input control */ +#define IMAXBEL 0x2000 /* ring bell on input queue full */ +#define IUTF8 0x4000 /* input is UTF-8 */ + +/* ---- c_oflag: output mode flags ---- */ + +#define OPOST 0x0001 /* perform output processing */ +#define OLCUC 0x0002 /* map lowercase to uppercase on output */ +#define ONLCR 0x0004 /* map NL to CR-NL on output */ +#define OCRNL 0x0008 /* map CR to NL on output */ +#define ONOCR 0x0010 /* no CR output at column 0 */ +#define ONLRET 0x0020 /* NL performs CR function on output */ +#define OFILL 0x0040 /* use fill characters for delay */ +#define OFDEL 0x0080 /* fill is DEL, else NUL */ +#define NLDLY 0x0100 /* newline delay mask */ +#define NL0 0x0000 +#define NL1 0x0100 +#define CRDLY 0x0600 /* carriage-return delay mask */ +#define CR0 0x0000 +#define CR1 0x0200 +#define CR2 0x0400 +#define CR3 0x0600 +#define TABDLY 0x1800 /* horizontal-tab delay mask */ +#define TAB0 0x0000 +#define TAB1 0x0800 +#define TAB2 0x1000 +#define TAB3 0x1800 +#define XTABS 0x1800 /* TAB3: expand tabs to spaces */ +#define BSDLY 0x2000 /* backspace delay mask */ +#define BS0 0x0000 +#define BS1 0x2000 +#define VTDLY 0x4000 /* vertical-tab delay mask */ +#define VT0 0x0000 +#define VT1 0x4000 +#define FFDLY 0x8000 /* form-feed delay mask */ +#define FF0 0x0000 +#define FF1 0x8000 + +/* ---- c_cflag: control mode flags ---- */ + +#define CBAUD 0x100f /* baud-rate mask (includes CBAUDEX) */ +#define CSIZE 0x0030 /* character-size mask */ +#define CS5 0x0000 /* 5 bits */ +#define CS6 0x0010 /* 6 bits */ +#define CS7 0x0020 /* 7 bits */ +#define CS8 0x0030 /* 8 bits */ +#define CSTOPB 0x0040 /* two stop bits, else one */ +#define CREAD 0x0080 /* enable receiver */ +#define PARENB 0x0100 /* enable parity */ +#define PARODD 0x0200 /* odd parity, else even */ +#define HUPCL 0x0400 /* hang up on last close */ +#define CLOCAL 0x0800 /* ignore modem status lines */ +#define CBAUDEX 0x1000 /* extended baud-rate mask (above B38400) */ +#define CRTSCTS 0x80000000 /* enable RTS/CTS flow control */ + +/* ---- c_lflag: local mode flags ---- */ + +#define ISIG 0x0001 /* enable signals */ +#define ICANON 0x0002 /* canonical mode (line-buffered) */ +#define XCASE 0x0004 /* canonical uppercase/lowercase presentation */ +#define ECHO 0x0008 /* enable echo */ +#define ECHOE 0x0010 /* echo erase as backspace */ +#define ECHOK 0x0020 /* echo KILL */ +#define ECHONL 0x0040 /* echo NL */ +#define NOFLSH 0x0080 /* disable flush after interrupt/quit/suspend */ +#define TOSTOP 0x0100 /* send SIGTTOU on background output */ +#define ECHOCTL 0x0200 /* echo control characters as ^X */ +#define ECHOPRT 0x0400 /* echo erase as backspace-print-backspace */ +#define ECHOKE 0x0800 /* BSD: erase entire line on KILL */ +#define FLUSHO 0x1000 /* output is being flushed */ +#define PENDIN 0x4000 /* reprint pending input at next read */ +#define IEXTEN 0x8000 /* enable implementation-defined input processing */ +#define EXTPROC 0x10000 /* external processing */ + +/* ---- Baud rates (kernel CBAUD encodings — see the header comment) ---- */ + +#define B0 0x00000000 /* hang up */ +#define B50 0x00000001 +#define B75 0x00000002 +#define B110 0x00000003 +#define B134 0x00000004 +#define B150 0x00000005 +#define B200 0x00000006 +#define B300 0x00000007 +#define B600 0x00000008 +#define B1200 0x00000009 +#define B1800 0x0000000a +#define B2400 0x0000000b +#define B4800 0x0000000c +#define B9600 0x0000000d +#define B19200 0x0000000e +#define B38400 0x0000000f +#define B57600 0x00001001 /* CBAUDEX | 1 */ +#define B115200 0x00001002 +#define B230400 0x00001003 +#define B460800 0x00001004 +#define B500000 0x00001005 +#define B576000 0x00001006 +#define B921600 0x00001007 +#define B1000000 0x00001008 +#define B1152000 0x00001009 +#define B1500000 0x0000100a +#define B2000000 0x0000100b +#define B2500000 0x0000100c +#define B3000000 0x0000100d +#define B3500000 0x0000100e +#define B4000000 0x0000100f + +/* ---- tcsetattr optional_actions ---- */ + +#define TCSANOW 0 /* change immediately */ +#define TCSADRAIN 1 /* change after output drains */ +#define TCSAFLUSH 2 /* drain output and discard input */ + +/* ---- tcflush queue_selector ---- */ + +#define TCIFLUSH 0 /* discard data received but not yet read */ +#define TCOFLUSH 1 /* discard data written but not yet transmitted */ +#define TCIOFLUSH 2 /* discard both */ + +/* ---- tcflow action ---- */ + +#define TCOOFF 0 /* suspend output */ +#define TCOON 1 /* restart suspended output */ +#define TCIOFF 2 /* transmit a STOP character */ +#define TCION 3 /* transmit a START character */ + +/* ---- Level 1 (POSIX base) ---- */ + +/* + * Store the terminal attributes of the terminal open on fildes into + * *termios_p. Return 0 on success, or -1 with errno set (ENOTTY when + * fildes is not a terminal, EBADF on a bad descriptor). + */ +int +tcgetattr(int fildes, struct termios *termios_p); + +/* + * Set the terminal attributes of the terminal open on fildes from + * *termios_p, applying optional_actions (TCSANOW apply immediately, + * TCSADRAIN wait for output to drain, TCSAFLUSH drain and then discard + * unread input). Return 0 on success, or -1 with errno set (EINVAL for an + * invalid optional_actions, ENOTTY/EBADF as for tcgetattr). + */ +int +tcsetattr(int fildes, int optional_actions, const struct termios *termios_p); + +/* + * Transmit a break condition on the terminal open on fildes. The duration + * argument is ignored (POSIX leaves a nonzero duration + * implementation-defined). Return 0 on success or -1 with errno set. + */ +int +tcsendbreak(int fildes, int duration); + +/* + * Wait until all output written to the terminal open on fildes has been + * transmitted. Return 0 on success or -1 with errno set. + */ +int +tcdrain(int fildes); + +/* + * Discard data written but not yet transmitted (TCOFLUSH), data received + * but not yet read (TCIFLUSH), or both (TCIOFLUSH) on the terminal open on + * fildes. Return 0 on success or -1 with errno set (EINVAL for an invalid + * queue_selector). + */ +int +tcflush(int fildes, int queue_selector); + +/* + * Suspend or restart transmission (TCOOFF/TCOON) or transmit a STOP/START + * character (TCIOFF/TCION) on the terminal open on fildes. Return 0 on + * success or -1 with errno set (EINVAL for an invalid action). + */ +int +tcflow(int fildes, int action); + +/* + * Return the input baud rate stored in *termios_p (a B* constant), as + * encoded in the c_cflag CIBAUD field, or B0 when the input rate is not + * separately stored (the kernel then treats it as equal to the output + * rate, and cfsetispeed with B0 selects exactly that). + */ +speed_t +cfgetispeed(const struct termios *termios_p); + +/* + * Return the output baud rate stored in *termios_p (a B* constant), as + * encoded in the c_cflag CBAUD field. + */ +speed_t +cfgetospeed(const struct termios *termios_p); + +/* + * Store speed as the input baud rate of *termios_p. Accept any value that + * fits the CBAUD encoding (B0 means "input rate follows the output rate"); + * an out-of-range speed yields -1 with errno EINVAL. + */ +int +cfsetispeed(struct termios *termios_p, speed_t speed); + +/* + * Store speed as the output baud rate of *termios_p. Accept any value that + * fits the CBAUD encoding (see cfsetispeed); an out-of-range speed yields + * -1 with errno EINVAL. + */ +int +cfsetospeed(struct termios *termios_p, speed_t speed); + +#if VLIBC_LEVEL_GE(2) + +/* Level 2 (muslmimic): BSD/glibc extensions and XSI [CX] additions. */ + +/* + * Set *termios_p to the "raw" mode of the old Version-7 terminal driver: + * input available character by character, echoing and all special + * processing of input and output characters disabled (clear IGNBRK BRKINT + * PARMRK ISTRIP INLCR IGNCR ICRNL IXON, clear OPOST, clear ECHO ECHONL + * ICANON ISIG IEXTEN, select CS8 and no parity). Does not report errors. + */ +void +cfmakeraw(struct termios *termios_p); + +/* + * Store speed as both the input and the output baud rate of *termios_p + * (cfsetospeed then cfsetispeed with the same value). Return 0 on success + * or -1 with errno EINVAL for an out-of-range speed. + */ +int +cfsetspeed(struct termios *termios_p, speed_t speed); + +/* + * Return the session id of the session that has the terminal open on + * fildes as its controlling terminal, or -1 with errno set when fildes is + * not a controlling terminal (ENOTTY). + */ +pid_t +tcgetsid(int fildes); + +#endif /* VLIBC_LEVEL_GE(2) */ + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_TERMIOS_H */ diff --git a/src/termios/termios.c b/src/termios/termios.c new file mode 100644 index 0000000..912f4c5 --- /dev/null +++ b/src/termios/termios.c @@ -0,0 +1,248 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/syscall.h" + +/* + * vlibc — terminal attributes (todo 29). + * + * The ten POSIX base functions plus the three level-2 extras, all thin + * pass-throughs to the kernel tty ioctls over SYS_ioctl. struct termios + * (include/termios.h) is byte-identical to the x86_64 kernel termios, so + * the ioctl argument pointer is passed straight through: the kernel copies + * exactly sizeof(struct termios) bytes in each direction. + * + * The ioctl request numbers are Linux-only kernel-UAPI facts + * (asm-generic/ioctls.h), kept private here — they are the future + * 's names, not 's (POSIX exposes only the + * actions/queues/actions constants, which the header defines). + * + * The cf* speed helpers never touch the kernel: they only re-encode the + * caller's c_cflag CBAUD/CIBAUD bit fields, which the kernel's termios + * layer itself uses (a later tcsetattr carries the bits verbatim). The + * CIBAUD field (input speed) is shifted IBSHIFT = 16 bits above CBAUD and + * is not part of any public constant set, so it is transcribed locally. + * + * Return-value convention follows syscall_ret for every I/O function + * (0 or positive on success, -1 with errno on failure). + */ + +/* Kernel tty ioctls (asm-generic/ioctls.h on x86_64). */ +#define VLIBC_TERMIOS_TCGETS 0x5401 +#define VLIBC_TERMIOS_TCSETS 0x5402 +#define VLIBC_TERMIOS_TCSETSW 0x5403 +#define VLIBC_TERMIOS_TCSETSF 0x5404 +#define VLIBC_TERMIOS_TCSBRK 0x5409 +#define VLIBC_TERMIOS_TCXONC 0x540a +#define VLIBC_TERMIOS_TCFLSH 0x540b +#define VLIBC_TERMIOS_TIOCGSID 0x5429 + +/* Kernel input-baud encoding (asm-generic/termbits.h): CIBAUD = CBAUD << 16. */ +#define VLIBC_TERMIOS_CIBAUD 0x100f0000 +#define VLIBC_TERMIOS_IBSHIFT 16 + +/* + * tcgetattr: read the terminal attributes of the terminal open on fildes. + * The kernel returns -ENOTTY when fildes is not a terminal; syscall_ret + * converts that to -1 with errno ENOTTY. + */ +int +tcgetattr(int fildes, struct termios *termios_p) +{ + return syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_TERMIOS_TCGETS, (long)termios_p)); +} + +/* + * tcsetattr: write the terminal attributes. optional_actions selects the + * ioctl: TCSANOW applies immediately (TCSETS), TCSADRAIN waits for output + * to drain (TCSETSW), TCSAFLUSH additionally discards unread input + * (TCSETSF). Anything else is EINVAL before the kernel is consulted. + */ +int +tcsetattr(int fildes, int optional_actions, // NOLINT(bugprone-easily-swappable-parameters) + const struct termios *termios_p) +{ + long request; + + if (optional_actions == TCSANOW) + { + request = VLIBC_TERMIOS_TCSETS; + } + else if (optional_actions == TCSADRAIN) + { + request = VLIBC_TERMIOS_TCSETSW; + } + else if (optional_actions == TCSAFLUSH) + { + request = VLIBC_TERMIOS_TCSETSF; + } + else + { + errno = EINVAL; + return -1; + } + return syscall_ret(__syscall3(SYS_ioctl, fildes, request, (long)termios_p)); +} + +/* + * tcsendbreak: transmit a break condition. The kernel sends a break of + * implementation-defined duration; POSIX leaves the meaning of a nonzero + * duration to the implementation, so the argument is ignored. + */ +int +tcsendbreak(int fildes, int duration) // NOLINT(bugprone-easily-swappable-parameters) +{ + (void)duration; + return syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_TERMIOS_TCSBRK, 0)); +} + +/* + * tcdrain: wait until all output written to the terminal has been + * transmitted. The kernel implements this as the TCSBRK ioctl with + * argument 1 (as opposed to tcsendbreak's argument 0). + */ +int +tcdrain(int fildes) +{ + return syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_TERMIOS_TCSBRK, 1)); +} + +/* + * tcflush: discard pending input, output, or both per queue_selector. + * The kernel validates the selector (EINVAL on anything but TCIFLUSH/ + * TCOFLUSH/TCIOFLUSH). + */ +int +tcflush(int fildes, int queue_selector) // NOLINT(bugprone-easily-swappable-parameters) +{ + return syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_TERMIOS_TCFLSH, queue_selector)); +} + +/* + * tcflow: suspend/restart transmission or send a STOP/START character per + * action. The kernel validates the action (EINVAL on anything but + * TCOOFF/TCOON/TCIOFF/TCION). + */ +int +tcflow(int fildes, int action) // NOLINT(bugprone-easily-swappable-parameters) +{ + return syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_TERMIOS_TCXONC, action)); +} + +/* + * cfgetispeed: the input baud rate as stored in *termios_p. The kernel + * encodes it in the CIBAUD field (c_cflag bits IBSHIFT..); when that field + * is clear the input rate is defined to follow the output rate and the + * stored value is B0. + */ +speed_t +cfgetispeed(const struct termios *termios_p) +{ + return (speed_t)((termios_p->c_cflag & VLIBC_TERMIOS_CIBAUD) >> VLIBC_TERMIOS_IBSHIFT); +} + +/* + * cfgetospeed: the output baud rate as stored in *termios_p (the CBAUD + * field, which already spans the CBAUDEX extension bits). + */ +speed_t +cfgetospeed(const struct termios *termios_p) +{ + return termios_p->c_cflag & CBAUD; +} + +/* + * cfsetispeed: store speed in the CIBAUD field of *termios_p. Only values + * representable in the CBAUD encoding are valid (B0 clears the field, + * meaning "input rate follows the output rate"). + */ +int +cfsetispeed(struct termios *termios_p, speed_t speed) +{ + if ((speed & ~CBAUD) != 0) + { + errno = EINVAL; + return -1; + } + termios_p->c_cflag &= ~VLIBC_TERMIOS_CIBAUD; + termios_p->c_cflag |= (speed & CBAUD) << VLIBC_TERMIOS_IBSHIFT; + return 0; +} + +/* + * cfsetospeed: store speed in the CBAUD field of *termios_p. Only values + * representable in the CBAUD encoding (B0..B38400, B57600..B4000000) are + * valid. + */ +int +cfsetospeed(struct termios *termios_p, speed_t speed) +{ + if ((speed & ~CBAUD) != 0) + { + errno = EINVAL; + return -1; + } + termios_p->c_cflag &= ~CBAUD; + termios_p->c_cflag |= speed & CBAUD; + return 0; +} + +#if VLIBC_LEVEL_GE(2) + +/* Level 2 (muslmimic): BSD/glibc extensions and XSI [CX] additions. */ + +/* + * cfmakeraw: switch *termios_p to raw mode. Only the termios flags are + * touched — speeds, character size beyond CS8, and the c_cc array are the + * caller's business (POSIX does not define the function, so its exact + * effect is a glibc-compatible choice). + */ +void +cfmakeraw(struct termios *termios_p) +{ + termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + termios_p->c_oflag &= ~OPOST; + termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + termios_p->c_cflag &= ~(CSIZE | PARENB); + termios_p->c_cflag |= CS8; +} + +/* + * cfsetspeed: set both the input and the output baud rate to speed. Both + * fields are written explicitly (cfsetospeed then cfsetispeed), so the + * structure round-trips cfgetospeed/cfgetispeed and the kernel decodes the + * same rate on both sides. + */ +int +cfsetspeed(struct termios *termios_p, speed_t speed) +{ + if (cfsetospeed(termios_p, speed) != 0) + { + return -1; + } + return cfsetispeed(termios_p, speed); +} + +/* + * tcgetsid: the session id of the session whose controlling terminal is + * the terminal open on fildes. The kernel fills an int via the TIOCGSID ioctl + * and fails with -ENOTTY when the terminal has no session. + */ +pid_t +tcgetsid(int fildes) +{ + int sid = 0; /* written by TIOCGSID; zeroed for the static analyzers */ + + if (syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_TERMIOS_TIOCGSID, (long)&sid)) < 0) + { + return (pid_t)-1; + } + return (pid_t)sid; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/tests/test_termios.c b/tests/test_termios.c new file mode 100644 index 0000000..6dac648 --- /dev/null +++ b/tests/test_termios.c @@ -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 + +#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; +}