feat(termios): terminal attributes

This commit is contained in:
2026-09-05 23:01:54 -04:00
parent e9aa002453
commit 8c0f23fbd0
3 changed files with 1002 additions and 0 deletions
+366
View File
@@ -0,0 +1,366 @@
#ifndef VLIBC_TERMIOS_H
#define VLIBC_TERMIOS_H
/*
* vlibc — <termios.h>.
*
* 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 <sys/ioctl.h>).
*
* 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 <fcntl.h>).
*/
#include <vlibc/features.h>
#include <stddef.h>
#include <sys/types.h>
#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 */
+248
View File
@@ -0,0 +1,248 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <termios.h>
#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
* <sys/ioctl.h>'s names, not <termios.h>'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) */
+388
View File
@@ -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;
}