diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0698ce..fe6c0c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -405,6 +405,58 @@ jobs: || { echo "::error::isatty over this port disagrees with the system's own C library"; exit 1; } echo " ok isatty answers the same as the system's own C library" + # AND A TERMINAL PUT INTO RAW MODE BEHAVES AS ONE. + # + # `isatty` above establishes that this port can tell a terminal from a + # pipe. This establishes what a program does with the answer: enter raw + # mode, read the interrupt keystroke as the byte 0x03 rather than being + # ended by it, and leave the terminal as it was found. Until openkal 0.14 + # the mode word had no position for the reserved keystrokes, and this port + # answered TCGETS and TIOCGWINSZ with a bare success while refusing TCSETS + # (mcpplibs/openkal-musl#36). + # + # THE CRITERION IS AGAIN A RELATION, AND FOR THE SAME REASON. The + # transcript above this port is compared with the transcript of the same + # source above the system's own C library, upon a pseudo-terminal of the + # same kind. An assertion written here --- "0x03 arrives" --- would pass on + # a system whose terminal does not deliver it at all, and would fail on a + # runner whose pty starts in a mode neither library chose. + - name: A terminal put into raw mode delivers the interrupt keystroke + if: runner.os == 'Linux' && matrix.target == '' + run: | + set -euo pipefail + ( cd examples/terminal && mcpp build --toolchain '${{ matrix.toolchain }}' ) + bin="$(find examples/terminal/target -name terminal -type f | head -1)" + test -n "$bin" || { echo "::error::the terminal probe did not build"; exit 1; } + + cc examples/terminal/src/main.c -o "$RUNNER_TEMP/terminal-control" + # THE CARRIAGE RETURNS ARE REMOVED BEFORE COMPARING, AND WHAT THAT + # EXCLUDES IS NAMED. openkal's mode word carries what is TYPED at a + # terminal and names nothing about what is written to one, so the + # output post-processing this environment performs (a newline written + # as a carriage return and a newline) is left in place by a + # `tcsetattr' over this port and cleared by one over the system's own + # C library. The probe's own lines therefore differ in that byte and + # in nothing else --- README's limits table records it --- while the + # keystrokes it read, the modes it reported and the order of both are + # what this step is for. + python3 tools/pty-keys.py reading 61620371 "$RUNNER_TEMP/terminal-control" \ + | tr -d '\r' > "$RUNNER_TEMP/control.log" + python3 tools/pty-keys.py reading 61620371 "$bin" | tr -d '\r' > "$RUNNER_TEMP/port.log" + + echo "--- the system's own C library"; cat "$RUNNER_TEMP/control.log" + echo "--- this port"; cat "$RUNNER_TEMP/port.log" + + # The control must show the thing being measured, or the comparison + # proves nothing: a pty that swallowed the keystroke would make two + # identical and equally wrong transcripts. + grep -q 'byte 0x03' "$RUNNER_TEMP/control.log" \ + || { echo "::error::the harness cannot deliver the interrupt keystroke as data — this check would prove nothing"; exit 1; } + + diff -u "$RUNNER_TEMP/control.log" "$RUNNER_TEMP/port.log" \ + || { echo "::error::a terminal over this port does not behave as the system's own C library"; exit 1; } + echo " ok the transcripts agree, keystroke for keystroke" + # THE INTERNAL OVERLAY STOPS AT THIS PACKAGE'S BOUNDARY. # # musl reaches its own declarations through `src/include`, whose headers diff --git a/README.md b/README.md index 2f76ba2..835e8ab 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ the claim can be checked rather than repeated. ```toml [dependencies] -openkal-musl = "0.15.0" +openkal-musl = "0.16.0" ``` > **Engine floor (mcpp 2026.9.18.3):** this version of this package declares @@ -41,7 +41,7 @@ error: dependency 'mcpplibs.openkal-musl' has irreconcilable versions: ```toml [dependencies] -openkal-llvm-runtime = "0.11.0" # carries openkal-musl 0.15.0 +openkal-llvm-runtime = "0.12.0" # carries openkal-musl 0.16.0 ``` Recorded here because it was got wrong by the people who maintain this @@ -51,6 +51,7 @@ consumer needs to answer it without asking. | this package | is carried by | | --- | --- | +| 0.16.0 | `openkal-llvm-runtime = "0.12.0"` | | 0.15.0 | `openkal-llvm-runtime = "0.11.0"` | | 0.14.0 | `openkal-llvm-runtime = "0.10.0"` | | 0.13.5 | `openkal-llvm-runtime = "0.9.6"`, `"0.9.7"` | @@ -253,7 +254,8 @@ answer that leaves a program wrong without telling it. | Absent | What a program observes | Why | | --- | --- | --- | -| signal handlers | `sigaction` reports `ENOSYS` for any handler other than the default or ignore | openkal has no asynchronous delivery. A handler that was accepted and could never run would be silently wrong; masking, which has nothing to mask, succeeds. | +| signal handlers | `sigaction` reports `ENOSYS` for any handler other than the default or ignore. **Since 0.16.0 a disposition is accepted only where it is the one already in effect**: `SIG_DFL` succeeds for every signal but `SIGPIPE`, `SIG_IGN` succeeds for `SIGPIPE` alone, and the enquiry reports `SIG_IGN` for `SIGPIPE` rather than a zeroed record | openkal has no asynchronous delivery. A handler that was accepted and could never run would be silently wrong; masking, which has nothing to mask, succeeds. Until 0.16.0 `SIG_IGN` was accepted for every signal and installed for none, so a program that asked not to be ended by the interrupt keystroke was told it had succeeded and was ended by it. `SIGPIPE` is the one disposition that is not the default, and not by accident: openkal requires a write to a stream whose far end is gone to report the condition rather than end the program, so an implementation beneath has already arranged that the signal does nothing. | +| a terminal's whole state | `tcgetattr` and `tcsetattr` carry line assembly, the echo, and whether the environment reserves keystrokes — the three positions openkal names. **Since 0.16.0 they reach the terminal**: `TCGETS`, `TCSETS`/`TCSETSW`/`TCSETSF` and `TIOCGWINSZ` are performed through `openkal.terminal`, so `cfmakeraw` followed by `tcsetattr` puts the terminal into raw mode and the interrupt keystroke arrives as the byte `0x03`. What a program cannot change is everything the structure carries that openkal does not name: output post-processing (`OPOST`), the line speed, the control characters, `VMIN`/`VTIME`, and the draining the `W` and `F` forms ask for. A `tcsetattr` that alters one of them is accepted and that part has no effect --- measurably: a program in raw mode that writes a newline still gets a carriage return before it, where the same program above the system's own C library does not; `tcgetattr` reports the composition port/src/okm_syscall.c states | openkal's mode word has three positions and `struct termios` has four flag words and twenty characters. The three are the ones a program needs in order to read keystrokes; the rest are either the terminal's own (the speed, the characters) or output-side, and openkal names none of them. Until 0.16.0 `TCGETS` and `TIOCGWINSZ` reported success and wrote nothing into the caller's structure while `TCSETS` was refused, which is mcpplibs/openkal-musl#36. A program that wants a read to give up asks `kal_timeout_read`, which is where openkal states a bound upon waiting. | | memory protection | `mprotect` reports `ENOSYS` | openkal has no operation upon a mapping's protection. musl asks for a guard page below a thread's stack and proceeds without one when told this, so the honest answer is also the one it is prepared for. | | out-of-band data | `MSG_OOB`, `MSG_PEEK`, and `POLLPRI` are never reported and `recv` refuses the flags | openkal's transfer operations move bytes and have no second channel and no non-destructive read. | | readiness *sets* | `epoll` is not built at all, so the link names it | a set held by the environment is a facility of one kernel rather than a capability. `poll` and `select` ask each descriptor in turn, which is what an interface without a set permits. | diff --git a/examples/terminal/mcpp.toml b/examples/terminal/mcpp.toml new file mode 100644 index 0000000..dbc6875 --- /dev/null +++ b/examples/terminal/mcpp.toml @@ -0,0 +1,16 @@ +[package] +name = "terminal" +version = "0.1.0" + +[dependencies] +openkal-musl = { path = "../.." } + +[targets.terminal] +kind = "bin" +main = "src/main.c" + +# The same one line every probe here writes, and for the reason examples/posix +# states: a program above this package carries no other runtime, so the build +# tool must embed none. +[build] +cxx_runtime = "host-coupled" diff --git a/examples/terminal/src/main.c b/examples/terminal/src/main.c new file mode 100644 index 0000000..03b1cbc --- /dev/null +++ b/examples/terminal/src/main.c @@ -0,0 +1,78 @@ +/* Does a terminal put into raw mode behave as one? + * + * The program this port was reported against (mcpplibs/openkal-musl#36), + * reduced to what can be asserted by a harness rather than read by a person. + * It is built twice --- above this package and above the system's own C + * library --- and the two transcripts are compared, because the question is + * not "does this port do something" but "does it do what a C library does". + * + * THE STRUCTURE IS POISONED BEFORE EVERY ENQUIRY. `tcgetattr' that reports + * success and writes nothing is indistinguishable from one that worked, unless + * the caller can tell what was there before; the sentinel is what makes the + * difference visible. That was the quieter half of the report: the loud half + * was `tcsetattr' refused with ENOTTY, and this program would have passed a + * test that only checked the loud one. + * + * WHAT THE HARNESS TYPES, AND WHY THE MARKER EXISTS. The keystrokes are sent + * after `reading' appears, because a keystroke that arrives before the mode is + * established is assembled into a line by the terminal and the interrupt one + * ends the program --- which is the defect, not the test. */ +/* `cfmakeraw' is not ISO C, and this package presents the POSIX view of musl + * rather than the BSD one (README, "The C environment this package presents"). + * The probe asks for the wider view explicitly, because the call under + * examination is exactly the one a program reaches for. */ +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include + +static void show(const char* tag, const struct termios* t) { + printf("%s lflag_icanon=%d lflag_echo=%d lflag_isig=%d iflag_ixon=%d vmin=%d vtime=%d\r\n", + tag, + (t->c_lflag & ICANON) != 0, + (t->c_lflag & ECHO) != 0, + (t->c_lflag & ISIG) != 0, + (t->c_iflag & IXON) != 0, + (int)t->c_cc[VMIN], (int)t->c_cc[VTIME]); +} + +int main(void) { + setvbuf(stdout, NULL, _IONBF, 0); + printf("isatty %d\r\n", isatty(0)); + + struct termios original; + memset(&original, 0x5a, sizeof original); + errno = 0; + const int got = tcgetattr(0, &original); + printf("tcgetattr rc=%d errno=%d\r\n", got, got == 0 ? 0 : errno); + if (got != 0) { printf("no terminal\r\n"); return 1; } + show("before", &original); + + struct termios raw = original; + cfmakeraw(&raw); + errno = 0; + const int set = tcsetattr(0, TCSANOW, &raw); + printf("tcsetattr rc=%d errno=%d\r\n", set, set == 0 ? 0 : errno); + + struct termios back; + memset(&back, 0x5a, sizeof back); + errno = 0; + const int again = tcgetattr(0, &back); + printf("tcgetattr(readback) rc=%d errno=%d\r\n", again, again == 0 ? 0 : errno); + show("readback", &back); + + printf("reading\r\n"); + for (char c; read(0, &c, 1) == 1; ) { + printf("byte 0x%02x\r\n", (unsigned char)c); + if (c == 'q') break; + } + + tcsetattr(0, TCSANOW, &original); + struct termios restored; + memset(&restored, 0x5a, sizeof restored); + if (tcgetattr(0, &restored) == 0) show("restored", &restored); + printf("done\r\n"); + return 0; +} diff --git a/mcpp.toml b/mcpp.toml index a2a9ab7..65ca208 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-musl" -version = "0.15.0" +version = "0.16.0" description = "musl 1.2.5 redirected onto openkal: one C library, ported once, above every implementation of the specification rather than above one kernel." license = "Apache-2.0" @@ -39,7 +39,7 @@ wchar = 32 builtins = "iso" [dependencies] -openkal = "0.13.0" +openkal = "0.14.0" # An ordinary consumer of openkal declares the specification and leaves the # choice of implementation to whoever builds the program, which is what the @@ -54,10 +54,10 @@ openkal = "0.13.0" # # The consequence for a program is that it names this package and nothing else. [target.'cfg(os = "linux")'.dependencies] -openkal-linux = { version = "0.13.0", features = ["standalone"] } +openkal-linux = { version = "0.14.0", features = ["standalone"] } [target.'cfg(os = "macos")'.dependencies] -openkal-macos = { version = "0.10.0", features = ["standalone"] } +openkal-macos = { version = "0.11.0", features = ["standalone"] } # FIRST STEP TOWARD A BARE MACHINE, AND NOT THE WHOLE OF IT. # @@ -68,7 +68,7 @@ openkal-macos = { version = "0.10.0", features = ["standalone"] } # runtime that receives control, and a C library configured for an environment # with no process to exit from. So this declares the implementation and stops. [target.'cfg(os = "none")'.dependencies] -openkal-opensbi = { version = "0.6.0", features = ["standalone"] } +openkal-opensbi = { version = "0.8.0", features = ["standalone"] } # WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH IS EXPECTED TO PROVIDE. # @@ -93,7 +93,7 @@ openkal-opensbi = { version = "0.6.0", features = ["standalone"] } defines = ["OKM_HAS_FS=0", "OKM_HAS_PROCESS=0", "OKM_HAS_TASK=0"] [target.'cfg(windows)'.dependencies] -openkal-windows = { version = "0.8.0", features = ["standalone"] } +openkal-windows = { version = "0.9.0", features = ["standalone"] } # The feature macros musl's own build establishes. # diff --git a/port/src/okm_syscall.c b/port/src/okm_syscall.c index 0e02514..e1a2b97 100644 --- a/port/src/okm_syscall.c +++ b/port/src/okm_syscall.c @@ -28,6 +28,7 @@ /* For pipe and pipe2, which are kal_process_channel. Included here rather than * through okm.h because this is the only source that reaches for it. */ #include +#include /* WEAK, OR AN INTERFACE A BACKEND MAY DECLINE BECOMES ONE IT MUST PROVIDE. * * Clause 6.1 expresses an interface an implementation does not provide as the @@ -49,6 +50,15 @@ extern __typeof(kal_process_channel_close) kal_process_channel_close __attribute * optional, and an implementation that does not provide it is absent as a * definition rather than present and refusing. */ extern __typeof(kal_random_fill) kal_random_fill __attribute__((__weak__)); +/* openkal.terminal, weak by the same rule. An environment with no terminal --- + * a bare machine, firmware --- provides none of these, and clause 6.1 states + * that absence as the absence of a definition. A strong reference here would + * make every program above this library fail to link upon such a backend, + * whether or not it ever asked a terminal anything. The ioctl branch tests + * each pointer before it calls. */ +extern __typeof(kal_terminal_get_mode) kal_terminal_get_mode __attribute__((__weak__)); +extern __typeof(kal_terminal_set_mode) kal_terminal_set_mode __attribute__((__weak__)); +extern __typeof(kal_terminal_size) kal_terminal_size __attribute__((__weak__)); /* WHAT `WNOHANG' IS EXPRESSED AS, AND IT WAS ALREADY IN THE SPECIFICATION. * * `waitpid' discarded its options, so a caller polling for a child that had not @@ -534,6 +544,111 @@ struct linux_dirent64 { char d_name[]; }; +/* --- the terminal ------------------------------------------------------- * + * + * A C library asks a terminal four things through `ioctl': what its mode is, + * that its mode be this, how large its display is, and --- by way of the third + * --- whether it is a terminal at all. openkal answers all four through + * `openkal.terminal', and until 0.14 this port answered none of them: TCGETS + * and TIOCGWINSZ reported success and wrote nothing into the caller's + * structure, and TCSETS was refused with ENOTTY. A program that entered raw + * mode therefore edited its own uninitialised stack and offered it to a + * terminal that never received it, which is reported as mcpplibs/openkal-musl#36. + * + * WHAT THE STRUCTURE CARRIES THAT openkal DOES NOT NAME. `struct termios' has + * four flag words, a line speed and twenty control characters; openkal names + * three positions of one mode word. The composition below states the rest + * rather than inventing it: the flags whose effect this environment really has + * (the newline translation the terminal performs, the output post-processing), + * the control characters at their agreed values, and a line speed --- because + * zero in that field is not "unknown" but "hang up", and a caller that asked + * `cfgetospeed' would be told the line had dropped. What a program cannot do + * over this port is change any of them: a `tcsetattr' that clears OPOST is + * accepted and the post-processing remains, which port/README.md lists among + * the things this environment does not carry. */ +static int okm_termios_get(struct kal_stream s, struct termios* t) +{ + if (!t) return -EFAULT; + if (!kal_terminal_get_mode) return -ENOTTY; + kal_uintptr mode = 0; + const int e = kal_terminal_get_mode(s, &mode); + if (e != kal_ok) return -okm_errno(e); + + struct termios out = { 0 }; + out.c_iflag = ICRNL; + out.c_oflag = OPOST | ONLCR; + out.c_cflag = CREAD | CS8 | B38400; + if (mode & KAL_TERM_LINE_EDIT) out.c_lflag |= ICANON | ECHOE | ECHOK; + if (mode & KAL_TERM_ECHO) out.c_lflag |= ECHO; + /* The three mechanisms this environment reserves keystrokes with are the + * three openkal states as one position, so they are reported together. */ + if (!(mode & KAL_TERM_PASS_CONTROL)) { + out.c_lflag |= ISIG | IEXTEN; + out.c_iflag |= IXON; + } + out.c_cc[VINTR] = 3; out.c_cc[VQUIT] = 28; + out.c_cc[VERASE] = 127; out.c_cc[VKILL] = 21; + out.c_cc[VEOF] = 4; out.c_cc[VSTART] = 17; + out.c_cc[VSTOP] = 19; out.c_cc[VSUSP] = 26; + out.c_cc[VREPRINT] = 18; out.c_cc[VWERASE] = 23; + out.c_cc[VLNEXT] = 22; out.c_cc[VDISCARD] = 15; + /* A read of a terminal with line assembly off waits for one byte, which is + * what the implementation beneath establishes and what openkal requires of + * it: a read reporting zero would mean the input had ended. */ + out.c_cc[VMIN] = 1; + out.c_cc[VTIME] = 0; + *t = out; + return 0; +} + +/* THE MODE IS TAKEN FROM ISIG AND NOT FROM ALL THREE FLAGS, WHICH IS NOT THE + * READING DIRECTION'S RULE AND IS DELIBERATE. + * + * `KAL_TERM_PASS_CONTROL' set means that NO keystroke is reserved, so the + * reading direction sets it only where all three flags are clear --- that is + * what the terminal is. Asking is a different question: a program clears ISIG + * because it wants the interrupt keystroke as data, and that is the whole of + * what it can say here. Requiring all three to be clear before asking would + * make `cfmakeraw' work and a program that cleared ISIG alone silently do + * nothing, which is the shape of the defect this branch exists to remove. + * + * A program that clears IXON alone keeps every keystroke reserved: openkal has + * one position and this environment has three mechanisms, and the position is + * about the one that decides whether the program survives the keystroke. */ +static int okm_termios_set(struct kal_stream s, const struct termios* t) +{ + if (!t) return -EFAULT; + if (!kal_terminal_set_mode) return -ENOTTY; + kal_uintptr mode = 0; + if (t->c_lflag & ICANON) mode |= KAL_TERM_LINE_EDIT; + if (t->c_lflag & ECHO) mode |= KAL_TERM_ECHO; + if (!(t->c_lflag & ISIG)) mode |= KAL_TERM_PASS_CONTROL; + const int e = kal_terminal_set_mode(s, mode); + return e == kal_ok ? 0 : -okm_errno(e); +} + +/* THE SIZE, AND THE QUESTION THAT TRAVELS WITH IT. `isatty' asks this request + * and reads only whether it succeeded (musl/src/unistd/isatty.c), so the answer + * here decides whether every program above this library believes it is talking + * to a terminal. An interactive stream therefore answers, and an environment + * that does not know the size answers with zero --- which is what a serial line + * reports natively, and is a written answer rather than the untouched structure + * this branch used to leave behind. */ +static int okm_winsize_get(struct kal_stream s, struct winsize* w) +{ + if (!w) return -EFAULT; + kal_uintptr cols = 0, rows = 0; + if (kal_terminal_size && kal_terminal_size(s, &cols, &rows) != kal_ok) { + cols = 0; + rows = 0; + } + w->ws_row = (unsigned short)rows; + w->ws_col = (unsigned short)cols; + w->ws_xpixel = 0; + w->ws_ypixel = 0; + return 0; +} + static syscall_arg_t do_getdents(int fd, void* buf, size_t cap) { struct okm_desc* d = okm_desc_of(fd); @@ -1842,16 +1957,38 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * that decides on colour or on line buffering by asking decided * wrongly and in silence. * - * THE SIZE IS REPORTED AS UNKNOWN RATHER THAN GUESSED. openkal - * has no operation that answers it, and `winsize' is already - * zeroed by the caller; a fabricated 80x24 would be this file's one - * forbidden shape --- reporting success having done nothing. - * A caller that wants the size reads zero, which is what a serial - * line reports too. */ + * AND THE REST OF THE TERMINAL IS NOW openkal's, WHICH IT WAS + * NOT UNTIL 0.16. This branch recognised the two requests above and + * answered both with a bare `return 0' --- success, with the + * caller's own structure left exactly as it was found. What + * `cfmakeraw' then edited was the caller's uninitialised stack, and + * `tcsetattr' offered it to a terminal that never received it, + * because TCSETS fell through to the refusal below. openkal 0.14 + * names the third position a raw mode needs, so every one of these + * requests corresponds to an operation and is performed. */ if (!interactive) return -ENOTTY; - if ((unsigned long)a2 == TCGETS) return 0; - if ((unsigned long)a2 == TIOCGWINSZ) return 0; - return -ENOTTY; + switch ((unsigned long)a2) { + case TCGETS: + return okm_termios_get(s, (struct termios*)a3); + /* TCSETS, TCSETSW and TCSETSF, which musl composes as TCSETS plus + * the action (musl/src/termios/tcsetattr.c). The three differ in + * what happens to the bytes already in flight, and openkal has no + * operation that drains or discards them: the mode is established + * for all three, and port/README.md records that the draining forms + * do not drain. */ + case TCSETS: + case TCSETSW: + case TCSETSF: + return okm_termios_set(s, (const struct termios*)a3); + case TIOCGWINSZ: + return okm_winsize_get(s, (struct winsize*)a3); + default: + /* Everything else a terminal can be asked --- the size being + * SET, the line being flushed, the pseudo-terminal pair being + * unlocked --- names no openkal operation, and a refusal is + * what a caller can act upon. */ + return -ENOTTY; + } } return -ENOTTY; } @@ -2657,6 +2794,26 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * takes it, and the layout from the architecture the port is built * for. */ const struct { void* handler; unsigned long flags; void* restorer; }* act = (const void*)a2; + /* WHICH DISPOSITION IS ALREADY IN EFFECT, WHICH IS THE WHOLE OF WHAT + * THIS PORT CAN ANSWER WITH. + * + * openkal has no operation upon a signal, so nothing here installs + * anything. What a program asks for is therefore either the disposition + * that is already in effect --- in which case saying yes is true --- or + * one that is not, in which case saying yes is the one shape this file + * does not contain. It contained it: `SIG_IGN' was accepted for every + * signal and installed for none, so a program that asked not to be + * ended by the interrupt keystroke was told it had succeeded and was + * ended by it anyway (mcpplibs/openkal-musl#36). + * + * SIGPIPE IS THE ONE SIGNAL WHOSE DISPOSITION IS NOT THE DEFAULT, and + * it is not an accident of a backend. openkal requires a write to a + * stream whose far end is gone to REPORT the condition rather than end + * the program, so an implementation beneath this library has already + * arranged that the signal does nothing. A program asking to ignore it + * is asking for what it already has. */ + const int signo = (int)a1; + const uintptr_t ignored_here = (signo == SIGPIPE); if (a3) { /* handler, flags, restorer, and the mask whose width the caller * declared in a4. */ @@ -2664,10 +2821,19 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, if (mask > sizeof(sigset_t)) return -EINVAL; char* old = (char*)a3; for (kal_uintptr i = 0; i < sizeof *act + mask; i++) old[i] = 0; + /* AND THE ENQUIRY ANSWERS WITH THE DISPOSITION, NOT WITH ZERO. + * A program that asks what SIGPIPE is set to is told SIG_IGN, + * which is what it is; the zeroed structure said SIG_DFL, and a + * program that reads its way to that conclusion acts upon it. */ + if (ignored_here) { + struct { void* handler; unsigned long flags; void* restorer; }* o = (void*)a3; + o->handler = (void*)(uintptr_t)1; /* SIG_IGN */ + } } if (!act) return 0; const uintptr_t h = (uintptr_t)act->handler; - if (h == 0 || h == 1) return 0; /* SIG_DFL and SIG_IGN */ + if (h == 0) return ignored_here ? -ENOSYS : 0; /* SIG_DFL */ + if (h == 1) return ignored_here ? 0 : -ENOSYS; /* SIG_IGN */ return -ENOSYS; } #ifdef SYS_sigaltstack diff --git a/tools/pty-keys.py b/tools/pty-keys.py new file mode 100755 index 0000000..f5ea0c9 --- /dev/null +++ b/tools/pty-keys.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Run a program upon a pseudo-terminal and type at it. + + pty-keys.py [arguments...] + +The program is started with a pseudo-terminal for its three standard streams; +once appears in its output the bytes named by are written +into the terminal, and everything the program printed is written to this +script's standard output. The exit status is the program's, except that a +program which has not ended within the timeout is reported as 124 --- the +status `timeout' uses --- because a program that never returns is as much a +failure as one that returns wrongly. + +WHY A PSEUDO-TERMINAL AND NOT `script -qec'. The keystrokes have to arrive +AFTER the program has established its mode. A keystroke that arrives before is +assembled into a line by the terminal, and the interrupt one ends the program: +the transcript then shows the defect this probe exists to detect on a port that +does not have it. `script' offers nowhere to wait, so the wait is here. + +WHY THE OUTPUT IS COMPARED AND NOT ASSERTED HERE. The claim this probe carries +is a relation --- that a program above this port behaves as the same program +above the system's own C library --- so the script reports and the caller +compares. A script that asserted "0x03 arrives" would pass on a system whose +terminal does not deliver it at all. +""" +import os +import pty +import select +import sys +import time + +TIMEOUT = 30.0 + + +def main(argv): + if len(argv) < 4: + sys.stderr.write(__doc__) + return 2 + marker, keys, command = argv[1].encode(), bytes.fromhex(argv[2]), argv[3:] + + pid, fd = pty.fork() + if pid == 0: + os.execvp(command[0], command) + os._exit(127) + + out = bytearray() + typed = False + deadline = time.monotonic() + TIMEOUT + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + os.kill(pid, 9) + os.waitpid(pid, 0) + sys.stdout.buffer.write(bytes(out)) + sys.stdout.flush() + sys.stderr.write("pty-keys: the program did not end within %gs\n" % TIMEOUT) + return 124 + ready, _, _ = select.select([fd], [], [], min(remaining, 0.5)) + if ready: + try: + chunk = os.read(fd, 4096) + except OSError: # the far end closed: the program has gone + chunk = b"" + if not chunk: + break + out += chunk + if not typed and marker in out: + # A pause before typing, because the marker is printed before the + # read that receives the keystrokes begins. Bytes written into the + # terminal are buffered by it either way; the pause keeps the + # transcript in the order a reader expects. + time.sleep(0.2) + os.write(fd, keys) + typed = True + + _, status = os.waitpid(pid, 0) + sys.stdout.buffer.write(bytes(out)) + sys.stdout.flush() + if os.WIFSIGNALED(status): + sys.stderr.write("pty-keys: the program was ended by signal %d\n" + % os.WTERMSIG(status)) + return 128 + os.WTERMSIG(status) + return os.WEXITSTATUS(status) + + +if __name__ == "__main__": + sys.exit(main(sys.argv))