diff --git a/.agents/docs/2026-10-25-three-reports-optimisation-design.md b/.agents/docs/2026-10-25-three-reports-optimisation-design.md index 03811b7..84fd3bd 100644 --- a/.agents/docs/2026-10-25-three-reports-optimisation-design.md +++ b/.agents/docs/2026-10-25-three-reports-optimisation-design.md @@ -132,7 +132,7 @@ musl: | PASS_INPUT 的往返与"不重写" | Linux、macOS:自建 pty;Windows:自建控制台并写入键事件 | 成立 | | 规范 conformance | 三平台五行 + emscripten | Linux 200 held / 0 / 3 not observed;Windows 182 / 0 / 6 | -另见:macOS 上 musl 经 `fork` 复制的程序中,`pthread_create` 返回 `EAGAIN`;未修改的 main 同样如此(§7)。 +另见:macOS 上 musl 经 `fork` 复制的程序中,`pthread_create` 返回 `EAGAIN`;未修改的 main 同样如此,已在本轮修复(§7)。 --- @@ -174,6 +174,6 @@ musl: ## 7. 待定与已知限制 -1. **macOS(standalone)上 fork 后的线程启动**:复制出的程序中 `pthread_create` 返回 `EAGAIN`。在 CI 上以未修改的 main(openkal-musl 0.20.1、openkal-macos 0.13.0)复现:`parent rc=0`,`copy rc=11`。这是既有缺陷,不由本轮引入,另行报告。 +1. **macOS(standalone)上 fork 后的线程启动**:复制出的程序中 `pthread_create` 返回 `EAGAIN`。在 CI 上以未修改的 main(openkal-musl 0.20.1、openkal-macos 0.13.0)复现:`parent rc=0`,`copy rc=11`。这是既有缺陷,不由本轮引入(`mcpplibs/openkal-musl#55`),已在本轮修复:openkal-macos 0.14.1 在复制原语前后按系统库自身的顺序调用其各部分的 fork 准备函数,详见记录 §5。 2. **Windows 行输入模式**下回车以 `0d 0a` 结束一行,与 Unix 的 `0a` 不同。这是行编辑而非 raw 输入的差异,不在本轮范围内。 3. **`do_mmap` 对一切映射逐字节清零**(不止线程栈),根治需要 openkal.memory 声明零页属性。另行处理。 diff --git a/.agents/docs/2026-10-25-three-reports-record.md b/.agents/docs/2026-10-25-three-reports-record.md new file mode 100644 index 0000000..c0843d6 --- /dev/null +++ b/.agents/docs/2026-10-25-three-reports-record.md @@ -0,0 +1,182 @@ +# The record of the round: a least stack size, whether input is altered, and what closing a directory ends + +2026-10-25. The design is `2026-10-25-three-reports-optimisation-design.md` +(second edition); this document records what was released, what was measured, +and the review of the round at the scale of the ecosystem. The verification is +`2026-10-25-three-reports-verify.sh`. + +--- + +## 1. The releases + +| Repository | Version | Pull request | What it carries | +| --- | --- | --- | --- | +| `openkal` | 0.16.0 | #50 | `kal_task_start_sized`, `KAL_TERM_PASS_INPUT`, the enumeration paragraph of `fs.h`, clause 11 entries 22–24, two observations | +| `openkal-linux` | 0.17.0 | #37 (carries #35) | enumerations ended by the close; a guarded 8 MiB stack; the least size; the four input flags | +| `openkal-macos` | 0.14.0 | #28 | the same, with the thread attributes composed beneath a C library and compared with the system's | +| `openkal-windows` | 0.12.0 | #32 | the same, with the input position read from virtual terminal input | +| `openkal-emscripten` | 0.5.0 | #7 | the enumeration, the least size, the input position | +| `openkal-opensbi` | 0.8.3 | #21 | the pin | +| `openkal-musl` | 0.21.0 | #54 | the stack size passed on; no stack mapped for a thread; a supplied stack refused; raw input; the dispatcher's frame | +| `openkal-llvm-runtime` | 0.15.5 | #36 | the pin | +| `mcpp-index` | — | #565 | the rows; two consumer members; the candidate gate schedules a platform package on its own platform | +| `openkal-macos` | 0.14.1 | #29 | a copy of the space can start a thread (`mcpplibs/openkal-musl#55`) | +| `openkal-musl` | 0.21.1 | #56 | the pin that reaches it; a program duplicated by `fork` starts a thread, asserted on every row | +| `openkal-llvm-runtime` | 0.15.6 | #37 | the pin | +| `mcpp-index` | — | #566 | the rows for the three; the two members move | + +Each release is a tag on the merge commit. The GLOBAL address is the tag +archive and the CN address is the gitcode asset in `mcpp-res`, uploaded with +`gtc release publish`. Every pair was downloaded and compared against the +recorded sha256. + +Every branch was named `openkal-0.16.0` in every repository, so each +repository's continuous integration tested the others as written. One push +run of the specification's `main` failed: it was made in the seconds between +the specification's merge and the implementations' merges. The check exists to +catch this, it said the two were not in step, and a re-run after the +implementations merged passed. + +## 2. What the measurements changed + +| Assumed | Measured | Consequence | +| --- | --- | --- | +| A console with line input off delivers what is typed | The return key was 0x0d, and Ctrl+J, in three spellings of its key event, produced no byte; with `ENABLE_VIRTUAL_TERMINAL_INPUT` they were 0x0d and 0x0a; with line input on, `61 0d 0a` | On that environment the position is virtual terminal input with line input off | +| The C library performs `ICRNL` per descriptor where the environment does not | A program sets the mode through one standard stream and reads through another; under the measured mapping the per-descriptor condition never held | The state is the terminal's; the C library performs `ICRNL` and `ISTRIP` in raw mode without echo | +| The conformance suite can observe a descriptor leak | `RLIMIT_NOFILE` on a runner can be 1048576 | Each implementation observes the leak through its own table (clause 7.13's precedent) | +| A probe in a `fork`ed copy shows the guard on macOS | `pthread_create` in the copy returned `EAGAIN`, on `main` as on the branch | The guard is observed in a program started for the purpose; the `fork` defect is recorded in §5 | + +## 3. What was measured, and where + +| Claim | Instrument | Result | +| --- | --- | --- | +| PR #35's observation detects the defect | the Linux test with `main`'s `fs.cpp` | fails; passes with the change | +| An abandoned enumeration holds nothing | `/proc/self/fd`, `fcntl`, `GetProcessHandleCount` | equal before and after (Windows 104/104) | +| A close racing a read is safe | 200 races in each implementation's tests | every reader told, no fault | +| A least size is room | the suite (4 MiB, three quarters recursed) on Linux ×2, macOS, Windows ×2, and under node; each implementation's tests at 1, 16, 64 MiB | held everywhere; Windows told 8.25 MiB for 1 MiB | +| `setstacksize` is honoured above the C library | `examples/stack-size`, against `main` | `main` faults about 256 KiB into a 4 MiB request; the release recurses through 3 MiB | +| An unused stack is not committed | VmRSS around a 256 MiB request | no growth | +| A supplied stack is refused | the same probe | `EINVAL` | +| A read below a thread's stack faults | a program started by `posix_spawn`; macOS hosted tests in a `fork`ed child | faults on Linux and macOS | +| The dispatcher's frame | clang 22.1.8, `-O0` | `0x10d90` → `0x2e10` | +| Raw input above the C library matches the host's | `examples/terminal` through a pseudo terminal against the system's own C library, under `cfmakeraw` and with `ICRNL` kept | byte for byte: `0d 0a` and `0a 0a` | +| Raw input on the console | the Windows test, three toolchains | `0d 0a` with the position set | +| The suite | Linux, macOS, Windows, node | Linux 201 held, 0 not held; Windows 182 held, 0 not held | + +## 4. The verification + +`2026-10-25-three-reports-verify.sh` resolves every package from the published +index (artifact `ad0f019`, the index's merge of #566) with the CN mirror +selected, so each archive came from the gitcode asset. It builds the reporters' +own programs: +- a program duplicated by `fork` starting a thread (`mcpplibs/openkal-musl#55`); +- the directory scan of `mcpplibs/openkal-linux#34`; +- the recursion of `mcpplibs/openkal-musl#48`; +- the raw-mode reader of `mcpplibs/openkal#48`, typed at through a pseudo terminal. + +It builds them above the C library both directly and through +`openkal-llvm-runtime` 0.15.6 on `x86_64-linux-musl` with `llvm@23.1.3`, which +is the arrangement the reports were written in. + +It was run in the agent's own `xlings` environment, with the mirror chosen for +the run alone (`MCPP_VERIFY_NO_CONFIG=1`). Creating a SubOS is reserved to its +owner outside a sandbox, and this run was inside one. The script runs unchanged +in a SubOS of its own: + +``` +xlings subos new okl016 +cp .agents/docs/2026-10-25-three-reports-verify.sh ~/.xlings/subos/okl016/tmp/v.sh +xlings subos use okl016 --sandbox --cmd "MCPP_VERIFY_VERSION=2026.10.24.1 bash /tmp/v.sh" +``` + +The transcript is §6. + +## 5. Review of the round, at the scale of the ecosystem + +**What is consistent.** Two declarations and one paragraph, answered in the +same shape by every implementation that provides the interface: +- the enumeration fix uses one construction in five implementations: a table of listings, a search rather than a dereference, and a marker for a close that races a read; +- the least size has one meaning, and the three native implementations give the same 8 MiB default; +- the input position reads the same four termios flags on three implementations, and the one environment without them reads its own switch. + +Above the implementations, the C library gives a program the host's bytes in +raw mode on every system, which it can do because it knows what its caller +asked for and the implementations do not. + +**What a consumer must know.** A version written without an operator is an +exact pin, so a program moves by naming the new versions. A program that +carries the C++ runtime names `openkal-llvm-runtime` 0.15.6, which locks +`openkal-musl` 0.21.1 and through it the implementations of this round. + +What changes for such a program: +- An iterator used after its directory was closed is refused. `fs.h` had always said it was released. +- `pthread_attr_setstack` is refused at `pthread_create`. +- Threads that asked for nothing have 8 MiB instead of 256 KiB, 512 KiB or 1 MiB. This costs address space, not memory. +- On Windows in raw mode, arrow and function keys arrive as escape sequences. + +**What the index gained.** Its candidate gate could not have accepted a +platform package: mcpp locks what the host resolves, and the gate asked every +runner to prove every package. It now schedules a candidate on the runners +where its consumer builds it, and the openkal family has consumers. Three +packages are named only for targets no runner builds (a musl environment, +WebAssembly, a RISC-V machine), so the gate schedules none of them; their +repositories measure them on their targets. + +**What was found, and fixed in the same round.** +On macOS a program duplicated by `fork` could not start a thread: `pthread_create` returned `EAGAIN` in the copy (`mcpplibs/openkal-musl#55`). This was reproduced on unmodified `main`, so it predates the round. `kal_space_start` made the copy with the duplication primitive alone, and this system's `fork` does more: around the primitive each part of the system library takes its locks, and in the copy each makes its state true of the new process. + +Measured on macOS 14.8.9, in a program that duplicated itself with the primitive: + +| What the copy was given | What `pthread_create` returned in the copy | +| --- | --- | +| no preparation | `EAGAIN` | +| the two child preparations alone | the copy was killed | +| those, and the thread library's locks around the primitive | 0 | +| the whole set in the library's own order | 0, in five rounds of twenty threads each | + +The routine `fork` calls these from (`_libSystem_atfork_*`) is not exported, but each part is. `openkal-macos` 0.14.1 calls the parts in that order, which is how the copy of `examples/stack-size` on the macOS row starts a thread (`exited=1 status=3`). + +**What was found and not changed.** +1. On Windows, line assembly ends a line as `0d 0a`. That is line editing, not raw input, and is outside this round. +2. The C library's `mmap` zeroes every mapping it returns, not only thread stacks. Removing that needs `openkal.memory` to state that a fresh page-sized allocation reads as zero. + +## 6. Transcript + +``` + +== A. identity and mirror == +ok: mcpp 2026.10.24.1 from /home/speak/.xlings/data/xpkgs/xim-x-mcpp/2026.10.24.1/bin/mcpp +ok: the CN mirror is selected for this run only + +== B. openkal 0.16.0 and openkal-linux 0.17.0: a least stack size, an enumeration ended by its close, unaltered input == +ok: version 0.16.0 +ok: sized e=0 told>=16MiB=1 room=1 +ok: enumeration before=8 after=8 +ok: pty position=1 bytes=0d 0a +ok: the lock records openkal-linux 0.17.0 + +== C. openkal-musl 0.21.1: the reporters' three programs == +ok: #34: descriptors before=7 after=7 +ok: musl#48: 1024 KiB asked, 900 levels recursed +ok: musl#48: a supplied stack is refused with EINVAL +ok: musl#48: 4096 KiB asked, 3900 levels recursed +ok: musl#48: a supplied stack is refused with EINVAL +ok: musl#55: a program duplicated by fork starts a thread +ok: #48: ICRNL reads back cleared after cfmakeraw +ok: #48: Enter, Ctrl+J, q arrive as byte 0x0d byte 0x0a byte 0x71 +ok: the lock records openkal-musl 0.21.1 + +== D. openkal-llvm-runtime 0.15.6 on x86_64-linux-musl, the reporters' arrangement == +ok: the lock records openkal-llvm-runtime 0.15.6 over openkal-musl 0.21.1 +ok: #34 through the runtime: descriptors before=7 after=7 +ok: musl#48 through the runtime: 4 MiB, 3900 levels +ok: #48 through the runtime: byte 0x0d byte 0x0a byte 0x71 +NOT RUN: macOS, Windows and WebAssembly: their implementations answered on their own runners (openkal-macos#28 and #29, openkal-windows#32, openkal-emscripten#7, openkal#50; a program duplicated by fork starting a thread on macOS, openkal-musl#56) +NOT RUN: a machine with firmware and no operating system: openkal-opensbi 0.8.3 moves a pin and provides none of the three interfaces + +0 assertion(s) failed +not run: + - macOS, Windows and WebAssembly: their implementations answered on their own runners (openkal-macos#28 and #29, openkal-windows#32, openkal-emscripten#7, openkal#50; a program duplicated by fork starting a thread on macOS, openkal-musl#56) + - a machine with firmware and no operating system: openkal-opensbi 0.8.3 moves a pin and provides none of the three interfaces +exit=0 +``` diff --git a/.agents/docs/2026-10-25-three-reports-verify.sh b/.agents/docs/2026-10-25-three-reports-verify.sh new file mode 100644 index 0000000..92381e5 --- /dev/null +++ b/.agents/docs/2026-10-25-three-reports-verify.sh @@ -0,0 +1,344 @@ +#!/usr/bin/env bash +# Ecosystem verification for openkal 0.16.0 and the packages that move with it +# (openkal-linux 0.17.0, openkal-macos 0.14.1, openkal-windows 0.12.0, +# openkal-emscripten 0.5.0, openkal-opensbi 0.8.3, openkal-musl 0.21.1, +# openkal-llvm-runtime 0.15.6), resolved from the published index only. +# +# xlings subos new okl016 +# cp ~/.xlings/subos/okl016/tmp/v.sh +# xlings subos use okl016 --sandbox --cmd "MCPP_VERIFY_VERSION= bash /tmp/v.sh" +# +# The subject is what a consumer gets by naming the released versions, and the +# programs are the reporters' own: the directory scan of mcpplibs/openkal-linux#34, +# the recursion of mcpplibs/openkal-musl#48, and the raw-mode reader of +# mcpplibs/openkal#48. A step that cannot run here says so and is counted as not +# run, never as passed. +set -u + +VER="${MCPP_VERIFY_VERSION:?set MCPP_VERIFY_VERSION}" +STORE="${MCPP_VERIFY_BIN:-$HOME/.xlings/data/xpkgs/xim-x-mcpp/$VER/bin/mcpp}" +XL="${XLINGS_BIN:-$(command -v xlings)}" + +fails=0; skipped="" +fail() { printf 'ASSERT-FAIL: %s\n' "$1"; fails=$((fails + 1)); } +ok() { printf 'ok: %s\n' "$1"; } +section() { printf '\n== %s ==\n' "$1"; } +skip() { printf 'NOT RUN: %s\n' "$1"; skipped="$skipped + - $1"; } + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +# A pseudo terminal, typed at once the program has said `reading'. The same +# harness openkal-musl's continuous integration uses (tools/pty-keys.py). +cat > "$work/keys.py" <<'PY' +import os, pty, select, sys, time +keys, cmd = bytes.fromhex(sys.argv[1]), sys.argv[2:] +pid, fd = pty.fork() +if pid == 0: + os.execvp(cmd[0], cmd) +out, typed, deadline = bytearray(), False, time.monotonic() + 30 +while time.monotonic() < deadline: + r, _, _ = select.select([fd], [], [], 0.2) + if r: + try: chunk = os.read(fd, 4096) + except OSError: break + if not chunk: break + out += chunk + if not typed and b"reading" in out: + time.sleep(0.2); os.write(fd, keys); typed = True + elif typed and os.waitpid(pid, os.WNOHANG)[0] == pid: + break +sys.stdout.write(out.decode("latin-1")) +PY + +# -- A. identity and mirror --------------------------------------------------- +section "A. identity and mirror" +got=$("$STORE" --version 2>&1 | head -1) +[ "$got" = "mcpp $VER" ] && ok "$got from $STORE" || fail "version is '$got' at $STORE" +# In a SubOS of its own the mirror is configured; anywhere else it is chosen for +# these calls alone (MCPP_VERIFY_NO_CONFIG=1), so the environment is not changed. +if [ "${MCPP_VERIFY_NO_CONFIG:-0}" = 1 ]; then + export MCPP_INDEX_MIRROR=CN + ok "the CN mirror is selected for this run only" +else + "$STORE" self config --mirror CN >/dev/null 2>&1 || true + "$XL" config --mirror CN >/dev/null 2>&1 || true + xm=$(python3 -c "import json,os;print(json.load(open(os.path.expanduser('~/.xlings/.xlings.json'))).get('mirror',''))" 2>/dev/null) + [ "$xm" = "CN" ] && ok "xlings mirror is CN" || fail "xlings mirror is '$xm'" +fi +"$STORE" index update >/dev/null 2>&1 || true + +# -- B. the implementation beneath ------------------------------------------- +section "B. openkal 0.16.0 and openkal-linux 0.17.0: a least stack size, an enumeration ended by its close, unaltered input" +mkdir -p "$work/impl/src" +cat > "$work/impl/mcpp.toml" <<'TOML' +[package] +name = "implprobe" +version = "0.1.0" + +[dependencies] +openkal = "0.16.0" + +[target.'cfg(os = "linux")'.dependencies] +openkal-linux = "0.17.0" + +[targets.implprobe] +kind = "bin" +main = "src/main.c" +TOML +cat > "$work/impl/src/main.c" <<'C' +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static long recurse(long n); +static long (*volatile g_recurse)(long) = recurse; +static long recurse(long n) { volatile char pad[1000]; pad[0] = (char)n; return n ? g_recurse(n - 1) + pad[0] : 0; } + +struct probe { kal_uintptr told; int done; }; +static void room(void* p) { + struct probe* q = p; void* base = 0; kal_uintptr size = 0; + if (kal_task_stack(&base, &size) == kal_ok) q->told = size; + g_recurse(12 * 1024); + q->done = 1; +} + +static int fds(void) { + DIR* d = opendir("/proc/self/fd"); int n = 0; + if (!d) return -1; + while (readdir(d)) n++; + closedir(d); + return n; +} + +int main(void) { + printf("version %u.%u.%u\n", KAL_VERSION_MAJOR, KAL_VERSION_MINOR, KAL_VERSION_PATCH); + + struct probe q = {0, 0}; + struct kal_task t; + const int e = kal_task_start_sized(room, &q, 16u << 20, &t); + if (e == kal_ok) kal_task_join(t); + printf("sized e=%d told>=16MiB=%d room=%d\n", e, q.told >= (16u << 20), q.done); + + struct kal_dir root; + kal_fs_preopen(0, &root, 0, 0, 0); + kal_fs_mkdir(root, "v016.d", 6); + const int before = fds(); + for (int i = 0; i < 64; i++) { + struct kal_dir d; kal_uintptr it = 0; + if (kal_fs_open_dir(root, "v016.d", 6, &d) != kal_ok) break; + if (kal_fs_list_begin(d, &it) == kal_ok) { char n[64]; kal_uintptr l; int k; kal_fs_list_next(d, &it, n, sizeof n, &l, &k); } + kal_fs_close_dir(d); + } + printf("enumeration before=%d after=%d\n", before, fds()); + kal_fs_remove(root, "v016.d", 6); + + const int m = posix_openpt(O_RDWR | O_NOCTTY); + if (m < 0 || grantpt(m) || unlockpt(m)) { printf("pty none\n"); return 0; } + const int s = open(ptsname(m), O_RDWR | O_NOCTTY); + struct kal_stream st = { (kal_uintptr)s }; + kal_terminal_set_mode(st, KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT); + kal_uintptr mode = 0; kal_terminal_get_mode(st, &mode); + write(m, "\r\n", 2); + unsigned char b[2] = {0, 0}; int got = 0; + while (got < 2) { kal_intptr r = kal_stream_read(st, b + got, 2 - got); if (r <= 0) break; got += (int)r; } + printf("pty position=%d bytes=%02x %02x\n", (mode & KAL_TERM_PASS_INPUT) != 0, b[0], b[1]); + return 0; +} +C +out=$(cd "$work/impl" && "$STORE" run 2>&1); rc=$? +if [ $rc -ne 0 ] || ! printf '%s\n' "$out" | grep -q '^version '; then + fail "the implementation probe did not build or run"; printf '%s\n' "$out" | tail -8 +else + printf '%s\n' "$out" | grep -q '^version 0.16.0$' && ok "version 0.16.0" || fail "$(printf '%s\n' "$out" | grep '^version')" + l=$(printf '%s\n' "$out" | grep '^sized ') + [ "$l" = "sized e=0 told>=16MiB=1 room=1" ] && ok "$l" || fail "the sized start reported: $l" + l=$(printf '%s\n' "$out" | grep '^enumeration ') + b=$(printf '%s\n' "$l" | sed -n 's/.*before=\([0-9-]*\).*/\1/p'); a=$(printf '%s\n' "$l" | sed -n 's/.*after=\([0-9-]*\).*/\1/p') + [ -n "$b" ] && [ "$b" = "$a" ] && [ "$b" -gt 0 ] && ok "$l" || fail "the enumeration probe reported: $l" + l=$(printf '%s\n' "$out" | grep '^pty ') + if [ "$l" = "pty none" ]; then skip "this sandbox offers no pseudo terminal to the program" + else [ "$l" = "pty position=1 bytes=0d 0a" ] && ok "$l" || fail "the terminal probe reported: $l"; fi +fi +grep -q '0.17.0' "$work/impl/mcpp.lock" 2>/dev/null && ok "the lock records openkal-linux 0.17.0" || fail "the lock does not record openkal-linux 0.17.0" + +# -- C. the C library above it: the reporters' programs ----------------------- +section "C. openkal-musl 0.21.1: the reporters' three programs" +mkdir -p "$work/musl/src" +cat > "$work/musl/mcpp.toml" <<'TOML' +[package] +name = "muslprobe" +version = "0.1.0" + +[dependencies] +openkal-musl = "0.21.1" + +[targets.muslprobe] +kind = "bin" +main = "src/main.c" +TOML +cat > "$work/musl/src/main.c" <<'C' +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* mcpplibs/openkal-linux#34, as reported. */ +static int open_descriptors(void) { + DIR* d = opendir("/proc/self/fd"); if (!d) return -1; + int n = 0; while (readdir(d)) n++; closedir(d); return n; +} +static int dirleak(void) { + char name[64]; + mkdir("dirleak.d", 0755); + for (int i = 0; i < 300; i++) { snprintf(name, sizeof name, "dirleak.d/e%03d", i); close(open(name, O_WRONLY | O_CREAT, 0644)); } + const int before = open_descriptors(); + for (int i = 0; i < 64; i++) { DIR* d = opendir("dirleak.d"); readdir(d); closedir(d); } + const int after = open_descriptors(); + for (int i = 0; i < 300; i++) { snprintf(name, sizeof name, "dirleak.d/e%03d", i); unlink(name); } + rmdir("dirleak.d"); + printf("descriptors before=%d after=%d\n", before, after); + return 0; +} + +/* mcpplibs/openkal-musl#48, as reported. */ +static long recurse(long n) { volatile char pad[1000]; pad[0] = 1; return n ? recurse(n - 1) + pad[0] : 0; } +static void* body(void* arg) { printf("recursed %ld levels of about 1 KiB\n", recurse(*(long*)arg)); return 0; } +static int stack(long kib, long depth) { + pthread_attr_t attr; pthread_t t; + pthread_attr_init(&attr); + printf("pthread_attr_setstacksize(%ld KiB) -> %d\n", kib, pthread_attr_setstacksize(&attr, kib * 1024)); + printf("pthread_create -> %d\n", pthread_create(&t, &attr, body, &depth)); + pthread_join(t, 0); + static char region[1 << 20]; + pthread_attr_setstack(&attr, region, sizeof region); + printf("pthread_create with a supplied stack -> %d\n", pthread_create(&t, &attr, body, &depth)); + return 0; +} + +/* mcpplibs/openkal#48, as reported. */ +static int icrnl(void) { + setvbuf(stdout, NULL, _IONBF, 0); + struct termios old, raw, back; + if (tcgetattr(0, &old) != 0) { perror("tcgetattr"); return 1; } + raw = old; cfmakeraw(&raw); + int rc = tcsetattr(0, TCSANOW, &raw); + tcgetattr(0, &back); + printf("ICRNL before=%d requested=%d readback=%d (tcsetattr rc=%d)\r\n", + (old.c_iflag & ICRNL) != 0, (raw.c_iflag & ICRNL) != 0, (back.c_iflag & ICRNL) != 0, rc); + printf("reading: press Enter, Ctrl+J, then q\r\n"); + for (unsigned char c; read(0, &c, 1) == 1; ) { printf("byte 0x%02x\r\n", c); if (c == 'q') break; } + tcsetattr(0, TCSANOW, &old); + return 0; +} + +/* mcpplibs/openkal-musl#55: a program duplicated by fork starts a thread. */ +#include +static void* nothing(void* p) { return p; } +static int forkthread(void) { + const pid_t c = fork(); + if (c == 0) { pthread_t t; if (pthread_create(&t, 0, nothing, 0) != 0) _exit(4); pthread_join(t, 0); _exit(3); } + int st = 0; waitpid(c, &st, 0); + printf("fork then thread: status=%d\n", WIFEXITED(st) ? WEXITSTATUS(st) : -1); + return 0; +} + +int main(int argc, char** argv) { + if (argc > 1 && !strcmp(argv[1], "forkthread")) return forkthread(); + if (argc > 1 && !strcmp(argv[1], "dirleak")) return dirleak(); + if (argc > 3 && !strcmp(argv[1], "stack")) return stack(atol(argv[2]), atol(argv[3])); + if (argc > 1 && !strcmp(argv[1], "icrnl")) return icrnl(); + return 2; +} +C +out=$(cd "$work/musl" && "$STORE" build 2>&1); rc=$? +bin=$(find "$work/musl/target" -type f -name muslprobe 2>/dev/null | head -1) +if [ $rc -ne 0 ] || [ -z "$bin" ]; then + fail "the musl probe did not build"; printf '%s\n' "$out" | tail -8 +else + l=$(cd "$work" && timeout 60 "$bin" dirleak 2>&1 | grep '^descriptors') + b=$(printf '%s\n' "$l" | sed -n 's/.*before=\([0-9-]*\).*/\1/p'); a=$(printf '%s\n' "$l" | sed -n 's/.*after=\([0-9-]*\).*/\1/p') + [ -n "$b" ] && [ "$b" = "$a" ] && [ "$b" -gt 0 ] && ok "#34: $l" || fail "#34 reported: $l" + for spec in "1024 900" "4096 3900"; do + set -- $spec + r=$(timeout 60 "$bin" stack "$1" "$2" 2>&1); s=$? + printf '%s\n' "$r" | grep -q "^recursed $2 levels" && [ $s -eq 0 ] \ + && ok "musl#48: $1 KiB asked, $2 levels recursed" || { fail "musl#48: $1 KiB / $2 levels (status $s)"; printf '%s\n' "$r" | tail -4; } + printf '%s\n' "$r" | grep -q "^pthread_create with a supplied stack -> 22$" \ + && ok "musl#48: a supplied stack is refused with EINVAL" || fail "musl#48: a supplied stack was not refused" + done + l=$(timeout 60 "$bin" forkthread 2>&1 | grep '^fork then thread') + [ "$l" = "fork then thread: status=3" ] && ok "musl#55: a program duplicated by fork starts a thread" || fail "musl#55: $l" + if command -v python3 >/dev/null 2>&1; then + t=$(python3 "$work/keys.py" 0d0a71 "$bin" icrnl 2>&1 | tr -d '\r') + printf '%s\n' "$t" | grep -q 'readback=0' && ok "#48: ICRNL reads back cleared after cfmakeraw" || fail "#48: readback: $(printf '%s\n' "$t" | grep ICRNL)" + bytes=$(printf '%s\n' "$t" | grep '^byte ' | tr '\n' ' ') + [ "$bytes" = "byte 0x0d byte 0x0a byte 0x71 " ] && ok "#48: Enter, Ctrl+J, q arrive as $bytes" || fail "#48: the bytes were: $bytes" + else + skip "#48: no python3 in this sandbox to drive a pseudo terminal" + fi +fi +grep -q '0.21.1' "$work/musl/mcpp.lock" 2>/dev/null && ok "the lock records openkal-musl 0.21.1" || fail "the lock does not record openkal-musl 0.21.1" + +# -- D. the runtime the reporters build against ------------------------------- +section "D. openkal-llvm-runtime 0.15.6 on x86_64-linux-musl, the reporters' arrangement" +mkdir -p "$work/rt/src" +cat > "$work/rt/mcpp.toml" <<'TOML' +[package] +namespace = "probe" +name = "rtprobe" +version = "0.0.0" + +[toolchain] +default = "llvm@23.1.3" + +[targets.rtprobe] +kind = "bin" +main = "src/main.c" + +[target.'cfg(all(os = "linux", env = "musl"))'.dependencies] +openkal-llvm-runtime = "0.15.6" +TOML +cp "$work/musl/src/main.c" "$work/rt/src/main.c" +out=$(cd "$work/rt" && "$STORE" build --target x86_64-linux-musl 2>&1); rc=$? +rbin=$(find "$work/rt/target" -type f -name rtprobe 2>/dev/null | head -1) +if [ $rc -ne 0 ] || [ -z "$rbin" ]; then + fail "the runtime probe did not build"; printf '%s\n' "$out" | tail -8 +else + grep -q 'openkal-musl' "$work/rt/mcpp.lock" && grep -q '0.21.1' "$work/rt/mcpp.lock" \ + && ok "the lock records openkal-llvm-runtime 0.15.6 over openkal-musl 0.21.1" || fail "the runtime lock does not reach openkal-musl 0.21.1" + l=$(cd "$work" && timeout 60 "$rbin" dirleak 2>&1 | grep '^descriptors') + b=$(printf '%s\n' "$l" | sed -n 's/.*before=\([0-9-]*\).*/\1/p'); a=$(printf '%s\n' "$l" | sed -n 's/.*after=\([0-9-]*\).*/\1/p') + [ -n "$b" ] && [ "$b" = "$a" ] && ok "#34 through the runtime: $l" || fail "#34 through the runtime: $l" + r=$(timeout 60 "$rbin" stack 4096 3900 2>&1) + printf '%s\n' "$r" | grep -q '^recursed 3900 levels' && ok "musl#48 through the runtime: 4 MiB, 3900 levels" || fail "musl#48 through the runtime" + if command -v python3 >/dev/null 2>&1; then + bytes=$(python3 "$work/keys.py" 0d0a71 "$rbin" icrnl 2>&1 | tr -d '\r' | grep '^byte ' | tr '\n' ' ') + [ "$bytes" = "byte 0x0d byte 0x0a byte 0x71 " ] && ok "#48 through the runtime: $bytes" || fail "#48 through the runtime: $bytes" + fi +fi + +# -- E. what this sandbox cannot observe -------------------------------------- +skip "macOS, Windows and WebAssembly: their implementations answered on their own runners (openkal-macos#28 and #29, openkal-windows#32, openkal-emscripten#7, openkal#50; a program duplicated by fork starting a thread on macOS, openkal-musl#56)" +skip "a machine with firmware and no operating system: openkal-opensbi 0.8.3 moves a pin and provides none of the three interfaces" + +printf '\n%s assertion(s) failed\n' "$fails" +[ -n "$skipped" ] && printf 'not run:%s\n' "$skipped" +exit "$fails"