Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"` |
Expand Down Expand Up @@ -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. |
Expand Down
16 changes: 16 additions & 0 deletions examples/terminal/mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"
78 changes: 78 additions & 0 deletions examples/terminal/src/main.c
Original file line number Diff line number Diff line change
@@ -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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

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;
}
12 changes: 6 additions & 6 deletions mcpp.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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
Expand All @@ -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.
#
Expand All @@ -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.
#
Expand All @@ -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.
#
Expand Down
Loading
Loading