From cfd8e8ac50c04cca3b2b22451fa3a44808f4c004 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 17 Sep 2026 16:56:06 +0800 Subject: [PATCH 1/3] 0.14.0 --- a start that is not a program reports ENOEXEC, and chmod can grant the one bit openkal now answers openkal 0.13 closed two gaps a consumer met from opposite sides (issue #28). `okm_errno` had no case for `kal_err_not_program`, so `posix_spawn` of a file that exists, is executable, and is not a program fell to the default arm and reported EIO --- a device failure that never occurred --- instead of ENOEXEC, the condition every host this port targets reports natively. It now does, and no `/bin/sh` fallback is added for it: musl's own `execvp` has none either (musl/PATCHES.md already declines the analogous case for `:`). `chmod`, `fchmodat` and whatever `std::filesystem::permissions` reaches now route to `kal_fs_set_executable_at` for the one request this port can honestly grant: the execute bits, set to match the read bits this port already reports, or cleared. A request that changes anything else --- the read or write bits, which are always uniform across the three classes here --- is refused with ENOSYS rather than rounded to a mode `stat` will not then show. A no-op equal to the mode already reported succeeds even where the volume declines KAL_FS_PROP_EXECUTABLE. `fchmod` stays ENOSYS unconditionally: this port keeps no name for an open file to hand the call. `stat`/`fstat`/`fstatat`/`statx` and `access(X_OK)` read the same field back. examples/subprocess asserts both, on every row, and failed against the unmodified port before this change. README's limits table states what changed and versions it per implementation, and adds the ecosystem design record's R1 and R2 as limits of their own: one C runtime and one C++ runtime per image, and a context a platform library creates has no C library state. Tracked docs and comments are stripped of emoji. Carries openkal 0.13.0, openkal-linux 0.13.0, openkal-macos 0.10.0 and openkal-windows 0.8.0. --- ...28-issue13-spawn-streams-abort-and-wait.md | 38 +- ...-30-issue13-exec-search-and-what-it-hid.md | 30 +- .../2026-08-30-issue13-full-sweep-design.md | 78 +-- ...nd-four-timeout-truncate-and-provenance.md | 2 +- .../2026-08-30-openkal-0.10-ecosystem-plan.md | 130 ++--- .../2026-08-30-openkal-0.11-start-design.md | 18 +- ...four-remaining-and-what-they-are-really.md | 50 +- ...tem3-start-failure-and-error-vocabulary.md | 473 ++++++++++++++++++ .github/workflows/ci.yml | 90 ++-- README.md | 70 +-- build.mcpp | 18 +- examples/cross-hello/src/main.c | 4 +- examples/identifiers/src/main.c | 4 +- examples/malloc-large/src/main.c | 2 +- examples/net/src/main.c | 8 +- examples/posix/src/main.c | 26 +- examples/subprocess/src/main.c | 260 ++++++++-- examples/surface/src/main.c | 22 +- examples/threads-cxx/src/main.cpp | 2 +- examples/threads-detached/src/main.c | 2 +- mcpp.toml | 74 +-- musl-generated/README.md | 2 +- musl/PATCHES.md | 36 +- port/include/features.h | 16 +- port/include/sys/random.h | 4 +- port/include/sys/stat.h | 6 +- port/src/mach/early_init.c | 6 +- port/src/okm.h | 16 +- port/src/okm_fcntl.c | 8 +- port/src/okm_fd.c | 23 +- port/src/okm_fork.c | 26 +- port/src/okm_net.c | 24 +- port/src/okm_opt.h | 25 +- port/src/okm_phdr.c | 2 +- port/src/okm_poll.c | 12 +- port/src/okm_spawn.c | 66 +-- port/src/okm_start.c | 14 +- port/src/okm_syscall.c | 307 ++++++++---- port/src/okm_thread.c | 10 +- port/src/win/main_stub.c | 6 +- tools/cross-build-macos.sh | 4 +- tools/one-artifact.sh | 8 +- tools/probe-cross-macos.sh | 10 +- tools/run-probe.sh | 10 +- tools/working-trees.sh | 6 +- 45 files changed, 1419 insertions(+), 629 deletions(-) create mode 100644 .agents/docs/2026-09-04-issue28-item3-start-failure-and-error-vocabulary.md diff --git a/.agents/docs/2026-08-28-issue13-spawn-streams-abort-and-wait.md b/.agents/docs/2026-08-28-issue13-spawn-streams-abort-and-wait.md index 0788d6b..90c3316 100644 --- a/.agents/docs/2026-08-28-issue13-spawn-streams-abort-and-wait.md +++ b/.agents/docs/2026-08-28-issue13-spawn-streams-abort-and-wait.md @@ -34,7 +34,7 @@ $ bash tools/run-probe.sh examples/subprocess subprocess --fork --shell --abort- **七条与三个缺陷的对应**:重定向四条(posix_spawn / execve / addopen / system)、 `addclose` 拒绝一条、abort 一条、WNOHANG 一条。 -### 0.1 ⚠️⚠️ 「无流」不能用句柄的值判定 —— 零是一个合法的流 +### 0.1 「无流」不能用句柄的值判定 —— 零是一个合法的流 方案 §1.3 D1 写的是「`d->stream` 为零 ⇒ 拒绝」。**第一次运行时每一个 spawn 都 返回 EBADF**,包括不做任何重定向的控制项。 @@ -51,7 +51,7 @@ $ bash tools/run-probe.sh examples/subprocess subprocess --fork --shell --abort- `dup2(0, 1)` 之后 spawn,位置 1 要放的句柄是 0,与「继承」无法区分。按端口自己的 规矩**拒绝**(ENOSYS),记入 `musl/PATCHES.md`,并上报规范。 -### 0.2 ⚠️ B 的目标标识**不能**比较,而方案说要比较 +### 0.2 B 的目标标识**不能**比较,而方案说要比较 方案 §2.2 写「目标必须是调用方自身(`tid == OKM_CONTEXT_ID()`)」。读 `port/src/okm_thread.c:130` 发现:线程的 `tid` 是端口自己的计数器 `++g_tid`, @@ -61,7 +61,7 @@ $ bash tools/run-probe.sh examples/subprocess subprocess --fork --shell --abort- 正确的规则更简单也更对:**终止性信号的默认动作结束的是进程而不是被点名的上下文** (Linux 上也是如此),所以目标根本不必检查。 -### 0.3 ⚠️ abort 判据要三层,两层会在缺陷上变绿 +### 0.3 abort 判据要三层,两层会在缺陷上变绿 方案 §2.3 的「层 1 Linux 专有 / 层 2 三系统通用(能与 exit(0) 区分)」不够: **缺陷产生的是一个 fault,而 fault 与 exit(0) 也是能区分的**,所以层 2 在修复前 @@ -96,12 +96,12 @@ $ bash tools/run-probe.sh examples/subprocess subprocess --fork --shell --abort- 行动的读数。阻塞形态没有这个选择要做。判据:**先启动那个慢的**,让它占住靠前的 槽位;只问前一个的实现会一直回答零,直到慢的结束然后报出错的那个。 -5. ⚠️⚠️ **`tools/run-probe.sh` 用 `find target … | head -1` 挑二进制,而版本号进 +5. **`tools/run-probe.sh` 用 `find target … | head -1` 挑二进制,而版本号进 指纹。** 0.5.0→0.6.0 之后 `examples/*/target/` 长出第二个指纹目录,`head -1` 挑到了**改动之前**那个程序。我新加的两条观察**没有出现在输出里**,而那次运行 报的是 `-- failures: 0 --`。判据没有失败,它们没有跑,而且没有任何东西说这件事。 - ⚠️ **CI 永远看不见它**:干净 checkout 只构建一种配置。它只咬在写改动的那台 + **CI 永远看不见它**:干净 checkout 只构建一种配置。它只咬在写改动的那台 机器上 —— 判据最被信任的地方。抽成 `tools/one-artifact.sh`,读任何产物之前先 断言「恰好一个指纹目录」,三个调用点共用。 @@ -177,7 +177,7 @@ static kal_uintptr g_std_stream[3]; /* 程序启动时的 in/out/err */ kal_uintptr okm_std_stream(int fd); /* fd ∈ [0,3) 时返回,否则 0 */ ``` -> ⭐ **为什么记初值而不是当场再调 `kal_stdout()`**:两者当前等价,但"描述符是否仍 +> **为什么记初值而不是当场再调 `kal_stdout()`**:两者当前等价,但"描述符是否仍 > 指着程序启动时那个流"是一条关于**历史**的判断,把它表达成一次记录而不是一次 > 重新提问,是这条规则唯一说得清的形式。 @@ -190,7 +190,7 @@ kal_uintptr okm_std_stream(int fd); /* fd ∈ [0,3) 时返回,否则 0 */ 若确需放置流而该位缺席 ⇒ `posix_spawn` 返回 `ENOSYS`。理由与 `okm_spawn.c:104-108` 现有的注释同一条:把程序启动到调用方没有要求的状态,比不启动它更坏。 -> ⚠️⚠️ **`kal_process_props` 是数据不是函数,弱引用为空时读它就是解引用零。** +> **`kal_process_props` 是数据不是函数,弱引用为空时读它就是解引用零。** > 端口现有 25 处弱引用全是函数(`okm_fork.c:60`、`okm_net.c:68-86` 等),测的是 > `!= 0` 后再调用;数据符号不能照抄这个写法。判据必须是 `&kal_process_props != 0`, > 而不是 `kal_process_props != 0`。这正是 #13 里报告的那类空跳,只是发生在数据上。 @@ -208,7 +208,7 @@ kal_uintptr okm_std_stream(int fd); /* fd ∈ [0,3) 时返回,否则 0 */ | `FDOP_CLOSE`,`fd ∈ {0,1,2}` | 空操作 | **`ENOSYS`** | 播种之后子进程确实继承 0/1/2,"忽略"就变成了被接受而未执行的动作 | | `FDOP_OPEN` | `ENOSYS` | **实现** | 可表达:在此打开,取 `kal_fs_stream` 放进对应位置 | -> ⭐ `FDOP_CLOSE` 这一行是本方案里唯一一处**语义翻转**:现在注释写着"没有要求的 +> `FDOP_CLOSE` 这一行是本方案里唯一一处**语义翻转**:现在注释写着"没有要求的 > 东西不会被继承",而 `{0,0,0}` 恰恰让 0/1/2 被继承了 —— 注释描述的是一个当时不 > 成立的前提。修完之后前提在 `fd > 2` 上成立,在 `fd ≤ 2` 上仍不成立,所以按 fd > 分成两行,而不是把注释改一改留着。 @@ -216,7 +216,7 @@ kal_uintptr okm_std_stream(int fd); /* fd ∈ [0,3) 时返回,否则 0 */ `FDOP_OPEN` 的落地形状:`okm_resolve` → `okm_fs_open`(标志翻译与 `do_openat:169-179` 同一张表)→ `kal_fs_stream` → 放位 → **spawn 返回后释放该文件**。 -> ⚠️ **待决 Q1(需规范澄清,不需新操作)**:调用方在 spawn 之后释放它放进去的流, +> **待决 Q1(需规范澄清,不需新操作)**:调用方在 spawn 之后释放它放进去的流, > 被启动的程序是否仍持有?`process.h:83-87` 对 channel **已经这样要求**了 > ("父方不释放 `theirs` 就永远看不到输入结束"),但没有对一般的流说同一句话。 > 建议在 `kal_process_spawn` 的注释里补一句陈述,而不是加操作 —— 不触 clause 8。 @@ -243,7 +243,7 @@ kal_uintptr okm_std_stream(int fd); /* fd ∈ [0,3) 时返回,否则 0 */ ### 1.4 判据 -> ⚠️ 现有 `examples/subprocess/src/main.c` 七项观察全绿,而**它从不在 spawn 之前 +> 现有 `examples/subprocess/src/main.c` 七项观察全绿,而**它从不在 spawn 之前 > 重定向父方描述符**。这次的判据必须落到"子进程写进了哪里",不是"spawn 成功了"。 新增观察(建议放进 `examples/subprocess`,与既有七项同一个二进制,三系统同跑): @@ -257,7 +257,7 @@ kal_uintptr okm_std_stream(int fd); /* fd ∈ [0,3) 时返回,否则 0 */ | 5 | `addclose(&fa, 1)` 返回 `ENOSYS` | **拒绝也是判据** | | 6 | 不做任何重定向的 spawn 仍然继承 | 反向对照:确认零路径没被改坏 | -> ⭐ 第 1 条的"两侧"是刻意的:只断言文件里有内容,`printf` 恰好两处都写也会绿。 +> 第 1 条的"两侧"是刻意的:只断言文件里有内容,`printf` 恰好两处都写也会绿。 > 两个反向对照抓的是不同的东西。 --- @@ -304,12 +304,12 @@ exit=139 | 默认动作为忽略(`SIGCHLD`/`SIGURG`/`SIGWINCH`) | 返回 0 | 完成于无事可做 | | 默认动作为停止(`SIGSTOP`/`SIGTSTP`/`SIGCONT`) | `ENOSYS` | 无法表达,拒绝 | -> ⭐ 第二行是这条设计的全部价值所在,而它是**读 openkal-linux 的实现读出来的**, +> 第二行是这条设计的全部价值所在,而它是**读 openkal-linux 的实现读出来的**, > 不是设计出来的:`kal_abort` 已经在发真信号。若改成自造 `kal_exit(134)`,父方看到 > 的是 `WIFEXITED && 134` 而不是 `WIFSIGNALED && SIGABRT` —— 与 Linux 的读数不同, > 而现在不必不同。 -> ⚠️ **待决 Q4**:第三行在 macOS / Windows 后端上退化成退出码而非信号死亡。 +> **待决 Q4**:第三行在 macOS / Windows 后端上退化成退出码而非信号死亡。 > 判据因此要分两层写(见 §2.3),否则 Linux 之外两格会以 SKIP 或假绿收场。 与 `SYS_rt_sigaction` 拒绝真 handler(`okm_syscall.c:1474-1483`)一致:这里做的 @@ -325,7 +325,7 @@ exit=139 | 4 | 子程序 `exit(3)` ⇒ `WIFEXITED && 3` | **反向对照**:确认正常退出没被改坏 | | 5 | `pthread_kill(其他线程, SIGTERM)` ⇒ `ENOSYS` | 三系统通用,拒绝也是判据 | -> ⚠️ 判据 1/2 不能写成"退出码不是 139"。139 在修好之后仍然是一个合法读数 +> 判据 1/2 不能写成"退出码不是 139"。139 在修好之后仍然是一个合法读数 > (真的段错误)。**判据是 `WTERMSIG == SIGABRT`,不是"不等于某个值"。** --- @@ -340,7 +340,7 @@ exit=139 `OKM_NOW_NS == 1`(`okm.h:271`)是端口已有的约定,`do_read`/`do_write` 的 `O_NONBLOCK` 路径用的就是它。 -> ⚠️ `timeout.h:12-14` 明说**零表示不设界**,所以 `WNOHANG` 不能传 0;传 1 会被 +> `timeout.h:12-14` 明说**零表示不设界**,所以 `WNOHANG` 不能传 0;传 1 会被 > 实现向上舍到 `kal_timeout_granularity_ns`。分歧("WNOHANG 至多阻塞一个时钟粒度") > 记进 `musl/PATCHES.md`。 > `openkal.timeout` 是可选接口 ⇒ 弱引用 + 空判,缺席时 `WNOHANG` 报 `ENOSYS`。 @@ -402,11 +402,11 @@ exit=139 两条元规则,是这批缺陷本身教出来的: -> ⭐ **一处改动答复三个入口,不等于三个入口都有判据。** A 的改点只有一个 +> **一处改动答复三个入口,不等于三个入口都有判据。** A 的改点只有一个 > (`__posix_spawn` 内部),但 `posix_spawn` / `execve` / `system` 是三条独立的 > 调用链,必须各测一条。 > -> ⚠️ **探针的绿必须能回答消费者问的那个问题。** `examples/subprocess` 七项全绿, +> **探针的绿必须能回答消费者问的那个问题。** `examples/subprocess` 七项全绿, > 而 #13 报的那件事它一次都没问过。新增观察之前先检查:这条观察若被删掉,哪一条 > 缺陷会重新变成绿的?答不上来的观察不要加。 @@ -435,11 +435,11 @@ exit=139 ⇒ 若坐实,是第四条缺陷,且与 §3 的 `WNOHANG` 同一片区域。 2. **启动段错误 rip = 0。** 报告者的两条排除已复核成立 (`rt_sigaction` `okm_syscall.c:1474-1483`;25 处弱引用逐条有空判)。 - ⚠️ §2 的结论**否掉了**报告者"139 与空跳是同一件事"的相关性猜测:`hlt` 会把 + §2 的结论**否掉了**报告者"139 与空跳是同一件事"的相关性猜测:`hlt` 会把 指令指针留在那条指令上,不是 0,所以是两件事。 已请求的决定性观察:故障时的 `x/4gx $rsp` —— 栈顶那个字就是通过零调用的那个 调用方的返回地址。 -> ⚠️ **两条都不该在 §1–§4 落地之后被当成"顺带就好了"。** 修好硬失败会暴露下一条, +> **两条都不该在 §1–§4 落地之后被当成"顺带就好了"。** 修好硬失败会暴露下一条, > 而 §2 一修,"SIGSEGV"这个读数的含义就变了 —— 这两条要在修完之后**重测一遍** > 才谈得上归因。 diff --git a/.agents/docs/2026-08-30-issue13-exec-search-and-what-it-hid.md b/.agents/docs/2026-08-30-issue13-exec-search-and-what-it-hid.md index af3afaf..318b748 100644 --- a/.agents/docs/2026-08-30-issue13-exec-search-and-what-it-hid.md +++ b/.agents/docs/2026-08-30-issue13-exec-search-and-what-it-hid.md @@ -6,7 +6,7 @@ (`aab97bc`)、openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0, 目标 `x86_64-linux-musl`。 -> ⚠️ 写这份文档时本地检出停在 0.7.0(`250f002`),比 `origin/main` 落后两个提交。 +> 写这份文档时本地检出停在 0.7.0(`250f002`),比 `origin/main` 落后两个提交。 > 已 `git fetch` 并核对 0.7.0→0.9.0 的差异:`port/src/okm_syscall.c` 只增加了版本 > 横幅、`SYS_truncate` 和 `uname` 的 release 字段,`port/src/okm_spawn.c` 与 > `musl/PATCHES.md` **一字未改**。本文所有行号均取自 **0.9.0**,所有读数均取自 @@ -60,7 +60,7 @@ execvp "sh" PATH=/nope:/usr/bin child said (nothing) exit=127 execvp "sh" PATH=/usr/bin:/nope child said hi exit=7 ``` -⭐ **`OPENKAL_MUSL_TRACE=enosys` 一行都没打。** 这条不是缺失的系统调用,所以 +**`OPENKAL_MUSL_TRACE=enosys` 一行都没打。** 这条不是缺失的系统调用,所以 上一轮加的那个诊断通道看不见它——这一点本身值得记下来(§8)。 ### 1.2 读码 —— 三环,每一环可复核 @@ -126,7 +126,7 @@ default: return -1; 这个循环**完全建立在「`execve` 失败会返回」之上**。这里它不返回,所以第一次未命中 就是终点。名字里带 `/` 直接走 `execve`,这就是第一行为什么过。 -### 1.4 ⚠️ 两份文档现在说的话是错的,必须改 +### 1.4 两份文档现在说的话是错的,必须改 `README.md:315` 和 `musl/PATCHES.md:114` 都写着 `execve` 这个表达 **「A caller cannot distinguish that through this library」**。 @@ -209,7 +209,7 @@ return posix_spawn(res, file, fa, &spawnp_attr, argv, envp); musl 自己的 `posix_spawn.c:152` 在子进程里读这个字段: `attr->__fn ? (int (*)())attr->__fn : execve`。 -⚠️ **本端口替换掉了 `posix_spawn.c`,而 `okm_spawn.c:180-181` 只看 `__flags`:** +**本端口替换掉了 `posix_spawn.c`,而 `okm_spawn.c:180-181` 只看 `__flags`:** ```c if (attr && (attr->__flags & ~(POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK))) @@ -257,7 +257,7 @@ waitpid = 1194981 raw status = 0x000f WIFSIGNALED=1 WTERMSIG=15 marker.txt absent <-- SIGTERM 打到了程序 ``` -⚠️ **状态字一模一样(`0x000f`),两边都告诉调用者「它死于 SIGTERM」。** +**状态字一模一样(`0x000f`),两边都告诉调用者「它死于 SIGTERM」。** 一边是真的,一边是假的,而调用者手上没有任何东西能把两者分开。 范围是精确的——只有 `fork`+`execve` 这条路: @@ -282,7 +282,7 @@ posix_spawn status=0x000f program was killed 而父亲收到的是「1001 死于 SIGTERM」,因为等待者确实死于 SIGTERM。 -### 3.3 ⚠️ 今天在本仓修不完,这一点要说清楚 +### 3.3 今天在本仓修不完,这一点要说清楚 `kill` 发生在父亲这一侧,父亲**无法知道**自己的哪个孩子是等待者;等待者阻塞在 `kal_process_wait` 里,**收不到任何东西也跑不了代码**。openkal 今天没有任何原子 @@ -293,7 +293,7 @@ posix_spawn status=0x000f program was killed - **C1(必做,先做)**:`README.md:315` 与 `musl/PATCHES.md:114` 那句 「a caller cannot distinguish」**是错的,要改掉**,并在分歧表里如实写明: 经 `fork`+`execve` 起的程序,`kill` 只到达中间映像,状态字仍报信号死亡。 - ⚠️ 这一句现在读起来像是「已经想清楚且无代价」,而它正是这条缺陷藏身的地方。 + 这一句现在读起来像是「已经想清楚且无代价」,而它正是这条缺陷藏身的地方。 - **C2(必做)**:补一条判据把当前行为钉住(§8),否则改好了也没人知道。 - **C3(上报)**:向 openkal 要一个「寿命受调用者约束」的起法(Linux 侧是 `PR_SET_PDEATHSIG`,Windows 侧是 job object),`execve` 用它起替身。 @@ -352,7 +352,7 @@ int e = okm_fs_open(at.base, at.rel, slen(at.rel), `openkal-linux/src/fs.cpp` 把 `READ|WRITE` 映射成 `O_RDWR`,对目录 → `EISDIR` → `kal_err_is_directory`(`sys.h:278`)→ `okm_fd.c:58` → `EISDIR`。 -### 4.4 ⭐ 实测:这件事身下**做得到**,只是我们要错了权限 +### 4.4 实测:这件事身下**做得到**,只是我们要错了权限 直接调 openkal 层: @@ -365,7 +365,7 @@ kal_fs_open(dir, READ) -> 0 (ok) **目录的时间被真的改掉了。** 所以这不是「身下做不到」。 -⚠️ 但它**在规范说的话之外**:`fs.h:273` 明写 +但它**在规范说的话之外**:`fs.h:273` 明写 「The file shall have been opened with KAL_OPEN_WRITE」,而 `kal_fs_open` 说的是 「Opening a file」,目录归 `kal_fs_open_dir`(产出 `kal_dir`),而 `kal_fs_set_modified` **没有收 `kal_dir` 的形式**。 @@ -386,7 +386,7 @@ kal_fs_open(dir, READ) -> 0 (ok) 自己的代码;`ENOSYS` 是在说「这个环境没有这个操作」。 - 代价:一个每个身下环境都做得到的普通 POSIX 操作就此长期不可用。 -⚠️ 无论选哪条,`musl/PATCHES.md:125` 那段都要补:它今天只记了「要写权限而不是 +无论选哪条,`musl/PATCHES.md:125` 那段都要补:它今天只记了「要写权限而不是 要所有权」,**没有记「目录的时间根本设不了」**。 --- @@ -407,7 +407,7 @@ getpid=1 getpgid(0)=1 getsid(0)=1 `getpgid`/`getsid` 诚实地回答「就一个程序,它自成一组」。然后接着说 「`setpgid` 与 `setsid` 仍然拒绝:造一个组和身处一个组不是一回事」。 -⚠️ **但这两个调用问的恰恰不是「造一个组」:** +**但这两个调用问的恰恰不是「造一个组」:** - `setpgid(0, 0)` 请求的状态是「调用者自成一组」——按上面那三行读数, **这个状态已经成立**。它不是要求一个不存在的效果,它要求的是已经为真的事。 @@ -423,7 +423,7 @@ getpid=1 getpgid(0)=1 getsid(0)=1 顺带:这会消掉报告者 trace 里 16 行(`setpgid` 12 + `setsid` 4)。 -> ⚠️ 这条是**判断**而不是读码结论,和 §1-§4 不同级别,单独列出来等 review 否决。 +> 这条是**判断**而不是读码结论,和 §1-§4 不同级别,单独列出来等 review 否决。 --- @@ -474,16 +474,16 @@ getpid=1 getpgid(0)=1 getsid(0)=1 | 2 | `execve("/不存在")` **返回 -1 且 `errno==ENOENT`**,调用者活着 | A | | 3 | `execve("<一个目录>")` 返回 -1 且 `errno==EACCES` | A1 | | 4 | `posix_spawnp("sh", …)` 起得来;`posix_spawnp("/不存在")` 返回 ENOENT | B | -| 5 | ⭐ `fork`+`execve` 起的程序被 `kill` 后**确实停了**(用它写不出的 marker 判) | C | +| 5 | `fork`+`execve` 起的程序被 `kill` 后**确实停了**(用它写不出的 marker 判) | C | | 6 | 目录的 `last_write_time` 设得上(或按 D2 报 ENOSYS,二选一钉死) | D | | 7 | `setpgid(0,0)==0`、`setsid()==-1 && errno==EPERM` | E | | 8 | **控制项**:上述每一条都在宿主目标上跑同一份源码并给出同样读数 | 全部 | -⚠️ **A/B 对照是必须的**:把 `port/src` 退回 `aab97bc` 只留新探针,1-7 必须**红**。 +**A/B 对照是必须的**:把 `port/src` 退回 `aab97bc` 只留新探针,1-7 必须**红**。 一条在缺陷上就是绿的判据,证明不了任何事——上一轮的自我 review 已经在这上面栽过 一次。 -⭐ 另外记一笔:**这一族缺陷 `OPENKAL_MUSL_TRACE=enosys` 一条都看不见**,因为它们 +另外记一笔:**这一族缺陷 `OPENKAL_MUSL_TRACE=enosys` 一条都看不见**,因为它们 不是缺失的操作,而是**在场却答错的操作**。上一轮把诊断通道当成「下一轮更便宜」的 答案,这一轮证明它只覆盖了一半。是否要一个「起程序失败」的 trace 位,留待 review。 diff --git a/.agents/docs/2026-08-30-issue13-full-sweep-design.md b/.agents/docs/2026-08-30-issue13-full-sweep-design.md index 91be298..32e4272 100644 --- a/.agents/docs/2026-08-30-issue13-full-sweep-design.md +++ b/.agents/docs/2026-08-30-issue13-full-sweep-design.md @@ -26,7 +26,7 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` 其中**七条是「接受了、没执行、报成功」**——这个端口反复声明要拒绝的那个形状。 -⚠️ **最重的一条不在报告者的清单里:文件锁形同虚设。** 两个进程可以同时持有同一把 +**最重的一条不在报告者的清单里:文件锁形同虚设。** 两个进程可以同时持有同一把 排他锁,而且 `F_GETLK` 报告的锁状态**与事实相反**。 --- @@ -100,7 +100,7 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` 收尾路径(`:314-318`),不要另开 return。 - 前置检查写成 `startable()` 小函数,`_WIN32` 的 `.exe` 重试(`:342-357`)复用它, 否则那条路再也走不到。 -- ⚠️ **残留**:名字在但不可执行,前置检查放行,仍以 127 结束调用者。openkal 没有 +- **残留**:名字在但不可执行,前置检查放行,仍以 127 结束调用者。openkal 没有 可执行位——这和 **F5** 是同一个缺口的两面,两处要一起记进分歧表。 - 完整修法在 openkal-linux(CLOEXEC 回报管道),另开 issue,**不阻塞本次发布**。 @@ -115,14 +115,14 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` ### 3.3 C —— `kill` 打不到 `fork`+`execve` 起的程序 -⚠️ **本仓今天修不完,而且现在有证据说明为什么。** +**本仓今天修不完,而且现在有证据说明为什么。** `openkal-linux/src/process.cpp` 的 `kal_process_terminate` 是 `kill(pid, SIGTERM)` ——**单个 pid,不是进程组**;而中间那个等待者阻塞在 `kal_process_wait` 里,这个端口没有信号投递,它跑不了任何转发代码。 #### 3.3.0 这是规范的缺口,不是后端的缺陷 -⭐ **`kal_process_terminate` 没有做错任何事。** 它被要求终止某个被起的程序,它就 +**`kal_process_terminate` 没有做错任何事。** 它被要求终止某个被起的程序,它就 终止了那一个。规范说的就是这个,后端做的就是这个。缺的是**一种表达不出来的意图**: 「这个程序是我为了表达『替换我自己』而起的,它的寿命应当以我为界」。openkal 今天 没有任何原子说得出这句话。 @@ -133,7 +133,7 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` 接受了这个判断,就没人再去找可观察的差别。目前已知两处: **A(exec 失败时)** 与 **C(信号能不能打到)**。 -⚠️ **和 A3 要分清,那一条恰恰相反,是后端缺陷。** +**和 A3 要分清,那一条恰恰相反,是后端缺陷。** `kal_process_spawn` 的子进程**已经知道** `execveat` 失败了(它紧接着 `exit_group(127)`),却没有把这件事回报给父亲;而 `kal_err_not_found` 这个值早就 存在,本端口的 `okm_spawn.c:342` 还专门为它写了一条分支。 @@ -148,7 +148,7 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` 3. **上报**:向 openkal 要一个「寿命受调用者约束」的起法。 映射:Linux `PR_SET_PDEATHSIG`、Windows job object、macOS `kqueue`/`NOTE_EXIT` 看门狗。 -> ⚠️ 排除一个看似可行的替代:让 `kal_process_terminate` 杀进程组。要让它成立, +> 排除一个看似可行的替代:让 `kal_process_terminate` 杀进程组。要让它成立, > `kal_space_start` 得给复制出的 space 开新组——而**新组会脱离终端的前台组**, > 一个带终端界面的程序里,任何读终端的子上下文会拿到 SIGTTIN 而停住。 > **换了一个更难查的错,不是修好。** @@ -167,7 +167,7 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` `KAL_OPEN_READ`;失败如实上报。 - 这不是模拟:调用者要的效果实实在在发生,做不到的实现会返回错误而我们照实翻译。 与 `chmod` 的情形**不同**——`chmod` 被拒是因为会「报成功而做了别的事」。 - - ⚠️ 踩在 `fs.h:273` 的前置条件之外(「shall have been opened with + - 踩在 `fs.h:273` 的前置条件之外(「shall have been opened with KAL_OPEN_WRITE」),**必须记进分歧表并上报规范**(要一个目录形式,或把 `kal_fs_set_modified` 改述在名字上)。openkal-windows 很可能做不到,那里如实失败。 - **备选 D2(纯粹派)**:报 `ENOSYS` 而不是 `EISDIR`。`EISDIR` 在说「你传错了类型」, @@ -183,7 +183,7 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` `getpid=1 getpgid(0)=1 getsid(0)=1`)。然后接着拒绝 `setpgid`/`setsid`, 理由是「造一个组和身处一个组不是一回事」。 -⚠️ **但这两个调用问的不是「造一个组」:** +**但这两个调用问的不是「造一个组」:** - `setpgid(0, 0)` 请求的状态是「调用者自成一组」——**按上面三行读数这已经成立**。 ⇒ 返回 **0**。这不是报告一个不存在的效果,是报告一个**已经存在**的效果。 @@ -195,7 +195,7 @@ openkal-linux **0.7.1**、openkal-llvm-runtime 0.5.0,目标 `x86_64-linux-musl` 就是为它存在的),**没有一份处理 `ENOSYS`**。成文的失败调用者接得住,陌生的接不住。 顺带消掉报告者 trace 里 16 行(`setpgid` 12 + `setsid` 4)。 -### 3.6 ⚠️⚠️ F1 / F2 —— 文件锁形同虚设(本轮最重的一条) +### 3.6 F1 / F2 —— 文件锁形同虚设(本轮最重的一条) #### 读码 @@ -230,7 +230,7 @@ child F_SETLK(F_WRLCK) -> -1 errno=11 Resource temporarily unavailable `F_WRLCK`,于是**读回 `F_WRLCK`,结论是「有人持锁」**——永远。 一个「等到锁释放为止」的循环**永不退出**。 -#### ⭐ 「按理应该支持得了吧?」—— 对,而且这一条**不该学 `chmod` 长期拒绝** +#### 「按理应该支持得了吧?」—— 对,而且这一条**不该学 `chmod` 长期拒绝** 先把结论摆清楚,因为它和 `chmod` 是**两种完全不同的情形**: @@ -256,7 +256,7 @@ int kal_fs_lock(struct kal_file, kal_u64 start, kal_u64 len, kal_uintptr mode); int kal_fs_unlock(struct kal_file, kal_u64 start, kal_u64 len); ``` -⭐ **关键的一点:实现放在身下,「持有者死了就释放」就是白拿的**——三个环境的内核 +**关键的一点:实现放在身下,「持有者死了就释放」就是白拿的**——三个环境的内核 都自带这条。而这恰恰是端口自己造不出来的那一条(见下)。 #### 为什么**不能**在端口里模拟 @@ -270,7 +270,7 @@ int kal_fs_unlock(struct kal_file, kal_u64 start, kal_u64 len); 2. **它会在用户的名字空间里凭空造文件**。sidecar 会出现在 `readdir` 里、 出现在 `remove_all` 里、出现在校验和里、出现在报告者自己的目录清单里。 一个 C 库不该往调用者的目录树里放东西。 -3. ⚠️⚠️ **没有崩溃恢复,而这是致命的。** 内核锁由内核在进程死亡时释放;sidecar +3. **没有崩溃恢复,而这是致命的。** 内核锁由内核在进程死亡时释放;sidecar 没有人释放。**一个持锁时段错误的程序会把自己永久锁死**,而且下一次运行看到的 只是「打不开」。这个端口自己的历史里就有段错误的程序。 ——加「陈旧超时」能绕过,但**一个 C 库没有资格替调用者选那个秒数** @@ -290,7 +290,7 @@ int kal_fs_unlock(struct kal_file, kal_u64 start, kal_u64 len); - **F2 `F_GETLK` 无论如何都要修**:要么一起 ENOSYS,要么至少把 `l_type` 写成 `F_UNLCK`。「假装没有锁」是自洽的;「假装永远有锁」不是——后者让等锁的循环 **永不退出**。 -- ⚠️ **代价,以及消费者可以怎么办**:sqlite 拿到 `ENOSYS` 会 `SQLITE_IOERR_LOCK` +- **代价,以及消费者可以怎么办**:sqlite 拿到 `ENOSYS` 会 `SQLITE_IOERR_LOCK` 而拒绝打开。**它有出口**:URI 参数 `nolock=1`,或 `unix-none` VFS。 单实例守卫、状态文件互斥这类用法则要改用报告者已经在用的那种 `mkdir` 协议。 ⇒ 这是从**静默的数据损坏**换成**响亮的打不开加一个成文的出口**,方向对。 @@ -322,7 +322,7 @@ pid_t getppid(void) { return __syscall(SYS_getppid); } 意思是「这个环境没有可命名的父亲」,不会触发那条路径。 - 关键的是**不能再返回 -38**。 -⭐ **并且要做一次同族普查**:musl 里所有**不经 `__syscall_ret`** 的调用点, +**并且要做一次同族普查**:musl 里所有**不经 `__syscall_ret`** 的调用点, 在 default 支下都会把 `-38` 当结果交出去。这是一类而不是一个,§4 的判据里 单列一条。 @@ -345,11 +345,11 @@ pid_t getppid(void) { return __syscall(SYS_getppid); } ### 3.9 F7 / F8 —— 两条小的 - **F7 `sigaltstack` 报成功而什么都没装**:实测装完再查,`ss_sp=0 ss_size=0` - (宿主 `ss_sp=0x4040a0 ss_size=65536`)。⚠️ **查询这一半也是虚构的**:它返回 0 并 + (宿主 `ss_sp=0x4040a0 ss_size=65536`)。**查询这一半也是虚构的**:它返回 0 并 交出一个全零的 `stack_t`,而不是「没有装过」。这个环境没有信号,备用栈没有意义, 按本端口自己的规矩改成 **ENOSYS**。风险低(libc++/libunwind 不把失败当致命)。 - **F8 `getrlimit(RLIMIT_NOFILE)` / `sysconf(_SC_OPEN_MAX)`**: - ⚠️ 现在 `sysconf(_SC_OPEN_MAX)` 返回 **0**,而程序会拿它去循环关 fd、定尺寸。 + 现在 `sysconf(_SC_OPEN_MAX)` 返回 **0**,而程序会拿它去循环关 fd、定尺寸。 **端口自己就知道答案**:`OKM_MAX_FD = 1024`(`okm.h:83`)。 ⇒ `case SYS_getrlimit/prlimit64`:`RLIMIT_NOFILE` 答 1024;其余仍拒绝。 一并把 `OKM_MAX_CHILD = 256` 写进 README 的界限表(已有 fd/open-description 两行)。 @@ -360,7 +360,7 @@ pid_t getppid(void) { return __syscall(SYS_getppid); } | 操作 | 记录要点 | 上报 | | --- | --- | --- | -| **F4** `sched_getaffinity` | ⚠️ 后果是**静默的**:`hardware_concurrency()` 答 1(宿主 32),线程池按 1 开 | `openkal.task` 加一个处理器数量的询问,紧挨 `KAL_TASK_PROP_PARALLEL` | +| **F4** `sched_getaffinity` | 后果是**静默的**:`hardware_concurrency()` 答 1(宿主 32),线程池按 1 开 | `openkal.task` 加一个处理器数量的询问,紧挨 `KAL_TASK_PROP_PARALLEL` | | F9 `statvfs` | `fs::space()` 不可用 | `openkal.fs` 加卷容量询问 | | F10 `link` | 只有符号链接,没有硬链接 | 与 `symlink` 那条合并上报 | | F11 `mkfifo` | 无 | 低优先 | @@ -375,7 +375,7 @@ pid_t getppid(void) { return __syscall(SYS_getppid); } ## 4. 判据(与修复同批,缺一条都不算修完) -⚠️ **现有 `examples/subprocess` 正是漏掉这一整族的那个**:它从不按裸名字起程序、 +**现有 `examples/subprocess` 正是漏掉这一整族的那个**:它从不按裸名字起程序、 从不 `kill` 一个 `fork`+`execve` 起的程序、从不加锁、从不问自己的身份。 | # | 判据 | 对应 | @@ -384,13 +384,13 @@ pid_t getppid(void) { return __syscall(SYS_getppid); } | 2 | `execve("/不存在")` 返回 -1 且 `errno==ENOENT`,**调用者活着** | A | | 3 | `execve("<目录>")` 返回 -1 且 `errno==EACCES` | A | | 4 | `posix_spawnp("sh")` 起得来;`posix_spawnp("/不存在")` 返回 ENOENT | B | -| 5 | ⭐ `fork`+`execve` 起的程序被 `kill` 后**确实停了**(用它写不出的 marker 判) | C | +| 5 | `fork`+`execve` 起的程序被 `kill` 后**确实停了**(用它写不出的 marker 判) | C | | 6 | 目录 `last_write_time` 设得上(D1)/ 报 ENOSYS(D2),二选一钉死 | D | | 7 | `setpgid(0,0)==0`;`setsid()==-1 && errno==EPERM` | E | -| 8 | ⭐ 两个进程,第二个 `F_SETLK` **拿不到**锁(或两个都拿到 ENOSYS) | F1 | -| 9 | ⭐ 无人持锁时 `F_GETLK` 把 `l_type` 写成 `F_UNLCK`(或返回 ENOSYS) | F2 | -| 10 | ⭐ `getppid() >= 0` | F3 | -| 11 | ⭐ **同族普查**:musl 里每个不走 `__syscall_ret` 的调用点都不返回负的 errno | F3 类 | +| 8 | 两个进程,第二个 `F_SETLK` **拿不到**锁(或两个都拿到 ENOSYS) | F1 | +| 9 | 无人持锁时 `F_GETLK` 把 `l_type` 写成 `F_UNLCK`(或返回 ENOSYS) | F2 | +| 10 | `getppid() >= 0` | F3 | +| 11 | **同族普查**:musl 里每个不走 `__syscall_ret` 的调用点都不返回负的 errno | F3 类 | | 12 | `sysconf(_SC_OPEN_MAX) > 0` 且等于 README 记的界限 | F8 | | 13 | `sigaltstack()` 返回 ENOSYS | F7 | | 14 | `fork` 的复制自称的标识 == 父亲拿到的那个,且嵌套复制也各自成立 | §6 | @@ -399,17 +399,17 @@ pid_t getppid(void) { return __syscall(SYS_getppid); } | # | **回归护栏**(两边都必须绿,红了说明修复弄坏了别的) | 对应 | | --- | --- | --- | -| G1 | ⭐ **复制里的 `abort()` 仍是 SIGABRT**,`kill(getpid(),0)` 仍返回 0 | §6 的 ③ —— 改标识会牵动 `signal_self` 那条路 | +| G1 | **复制里的 `abort()` 仍是 SIGABRT**,`kill(getpid(),0)` 仍返回 0 | §6 的 ③ —— 改标识会牵动 `signal_self` 那条路 | | G2 | `fork` 失败时表槽被退还(连续失败不会耗尽表) | §6 的 ② —— 表槽提前占用引入的新失效模式 | | G3 | `examples/subprocess --fork --shell --abort-signal` 与 `examples/posix` 全绿 | 全部 | | G4 | **控制项**:每条判据都在宿主目标上跑同一份源码 | 全部 | -⚠️ **A/B 对照是必须的**:把 `port/src` 退回 `aab97bc` 只留新探针,**1-14 必须红, +**A/B 对照是必须的**:把 `port/src` 退回 `aab97bc` 只留新探针,**1-14 必须红, G1-G4 必须绿**。一条在缺陷上就是绿的判据证明不了任何事——上一轮的自我 review 已经在 -这上面栽过一次。⭐ 而**把护栏和判据分开列**正是为了不再栽第二次: +这上面栽过一次。而**把护栏和判据分开列**正是为了不再栽第二次: §6 实测时 7 条里有 5 条两边都绿,它们是护栏而不是成绩。 -⭐ **并且要记一笔**:这一族**没有一条**能被 `OPENKAL_MUSL_TRACE=enosys` 看见, +**并且要记一笔**:这一族**没有一条**能被 `OPENKAL_MUSL_TRACE=enosys` 看见, 因为它们不是缺失的操作,而是**在场却答错的操作**。上一轮把诊断通道当成「下一轮更 便宜」的答案,这一轮证明它只覆盖了一半。是否加一个「答案可疑」的 trace 位,请 review。 @@ -421,9 +421,9 @@ G1-G4 必须绿**。一条在缺陷上就是绿的判据证明不了任何事— | 候选 | 解释力 | 本轮是否消失 | | --- | --- | --- | -| **F2** `F_GETLK` 永远报「有锁」 | ⭐ 最契合那处 hang:**无子进程、无缺失系统调用、无输出** 三条全中 | ✅ 修掉 | -| **C** 孤儿仍持有输出端,EOF 永不到来 | 契合「后台任务不终结」:超时 kill 之后程序还活着 | ❌ 需后端 | -| **pid 恒为 1** | 实测:父、fork 子、被起的程序 `getpid()` **全是 1**。若监督方从 pidfile 读 pid 再 `kill(pid,0)` 轮询,`kill(1,0)` 永远答「活着」 | ❌ 需设计,§6 | +| **F2** `F_GETLK` 永远报「有锁」 | 最契合那处 hang:**无子进程、无缺失系统调用、无输出** 三条全中 | 修掉 | +| **C** 孤儿仍持有输出端,EOF 永不到来 | 契合「后台任务不终结」:超时 kill 之后程序还活着 | 需后端 | +| **pid 恒为 1** | 实测:父、fork 子、被起的程序 `getpid()` **全是 1**。若监督方从 pidfile 读 pid 再 `kill(pid,0)` 轮询,`kill(1,0)` 永远答「活着」 | 需设计,§6 | ⇒ **建议:先发修复,再问。** F2 修掉之后那处 hang 若消失,就不必再往下查。 若仍在,要的观察只有三个: @@ -471,7 +471,7 @@ parent: getpid()=1 父亲看到 fork 子是 1001 `__okm_child_reserve(&pid)` / `__okm_child_commit(slot, h)` / `__okm_child_release(slot)`, `__okm_child_record` 用它们重写(对 `okm_spawn.c` 的调用者签名不变)。 -⚠️ `reserve` **不取锁**,因为 `__okm_fork` 调用它时已经持有——那把锁不可重入, +`reserve` **不取锁**,因为 `__okm_fork` 调用它时已经持有——那把锁不可重入, 而复制必须在「没有别的上下文正改到一半」的时刻取,所以本来就得在锁内。 **② `okm_fork.c` —— 标识在复制之前就存在** @@ -491,10 +491,10 @@ if (e != kal_ok) { __okm_child_release(slot); okm_unlock(); return -okm_errno(e) __okm_child_commit(slot, child); ``` -⚠️ **表槽是在上下文之前拿的,所以启动失败必须退还**,否则一个每次 `fork` 都失败的 +**表槽是在上下文之前拿的,所以启动失败必须退还**,否则一个每次 `fork` 都失败的 程序会把表耗尽,然后为一个与「它有几个孩子」毫无关系的理由开始报 EAGAIN。 -**③ ⚠️ 被牵出来的:所有拿 `1` 当「自己」的比较都得跟着改** +**③ 被牵出来的:所有拿 `1` 当「自己」的比较都得跟着改** 这是我一开始没预见、写的时候才撞上的。`SYS_kill` 用 `if (pid == 1 || ...) return signal_self(sig);` 判断「打给自己」—— @@ -521,7 +521,7 @@ a nested copy also names itself FAIL grandchild says 1 -- failures: 2 -- ``` -⭐ **A/B 干净**:退回 `aab97bc` 只留探针,红的**正好是**这次要修的两条, +**A/B 干净**:退回 `aab97bc` 只留探针,红的**正好是**这次要修的两条, 其余五条(含 `abort`、`kill(getpid(),0)`)两边都绿——说明判据卡住了改动本身, 而不是卡住了一堆无关的东西。 @@ -541,11 +541,11 @@ a nested copy also names itself FAIL grandchild says 1 ⇒ **建议纳入本次发布(P1)**。风险已经从「未评估」变成「已跑过」:改动集中在 `__okm_fork` 的时序和三处 `1` 的比较,判据 7 条 + 回归 2 套全绿。 -⚠️ 唯一要 review 盯的是 ③ ——**它说明这个改动会牵动 `abort` 那条路**, +唯一要 review 盯的是 ③ ——**它说明这个改动会牵动 `abort` 那条路**, 合入时判据里必须保留「复制里的 abort 仍是 SIGABRT」这一条。 (下面是这条最初被列为未决时写的理由,保留不改。) -⚠️ 这条**只有读码和实测,没有做过改动**,风险未评估(改的是 fork 的时序), +这条**只有读码和实测,没有做过改动**,风险未评估(改的是 fork 的时序), **请 review 决定是否纳入本次发布**,还是单独一轮。 --- @@ -557,7 +557,7 @@ a nested copy also names itself FAIL grandchild says 1 | **P0** | A、B、F1、F2、F3 | 全是「报了成功而没做」或「把错误当结果返回」。F1/F2 是数据完整性级别 | | **P1** | D、E、F7、F8、**§6 `getpid`** | 答得出却没答,或答得不诚实。改动都很小;`getpid` 已实现并跑过判据与回归 | | **P2** | 文档:C、F5、F6、F4、F9-F13 全部进分歧表;改掉 `execve` 那句错话 | 不改行为,但**没有它这次发布是在重复上一轮的错误**——上一轮的分歧表漏了正是这些 | -| **P3** | 上报:openkal(**`kal_fs_lock` + `KAL_FS_PROP_LOCKS`**、寿命受限的起法、目录时间、处理器数量、卷容量、fd 位置一般化)、openkal-linux(CLOEXEC 回报管道) | 不阻塞发布。⭐ 锁这一条**优先级最高**:它是唯一一条「三个环境都做得到、只差一个词」的 | +| **P3** | 上报:openkal(**`kal_fs_lock` + `KAL_FS_PROP_LOCKS`**、寿命受限的起法、目录时间、处理器数量、卷容量、fd 位置一般化)、openkal-linux(CLOEXEC 回报管道) | 不阻塞发布。锁这一条**优先级最高**:它是唯一一条「三个环境都做得到、只差一个词」的 | | 待定 | §3.6:F1 接受 ENOSYS(推荐),还是暂时只修 F2 | 请 review 拍板 | --- @@ -593,9 +593,9 @@ a nested copy also names itself FAIL grandchild says 1 | **`xprobe`** | **F5、F7**,两条都要「装了再查」才看得出,单看返回值全是 0 | | **`pidfix`** | §6 的实现验证:打了补丁的端口副本 + A/B 对照 + 回归 | -⭐ §6 的补丁在 `scratchpad/musl-patched/`(`port/src/okm_syscall.c`、 +§6 的补丁在 `scratchpad/musl-patched/`(`port/src/okm_syscall.c`、 `port/src/okm_fork.c` 两个文件),**工作树未动**。合入时直接取这两个文件的差异即可。 -⭐ 落地时 `surface` 应当**整个搬进 `examples/`**,而不只是搬那 13 条: +落地时 `surface` 应当**整个搬进 `examples/`**,而不只是搬那 13 条: 它的价值在于**下一族缺陷会先撞上它**,而不在于它这次命中了什么。 这正是上一轮 `examples/subprocess` 七条全绿却漏掉整族的反面。 diff --git a/.agents/docs/2026-08-30-issue13-round-four-timeout-truncate-and-provenance.md b/.agents/docs/2026-08-30-issue13-round-four-timeout-truncate-and-provenance.md index 4e7990c..75ada74 100644 --- a/.agents/docs/2026-08-30-issue13-round-four-timeout-truncate-and-provenance.md +++ b/.agents/docs/2026-08-30-issue13-round-four-timeout-truncate-and-provenance.md @@ -436,7 +436,7 @@ banner: FAIL got=[] uname: FAIL got=[0.5.0] ``` -⚠️ The release field now MOVES AT EVERY RELEASE. Nothing here or in musl reads +The release field now MOVES AT EVERY RELEASE. Nothing here or in musl reads it — `gethostname` and `getdomainname` are musl's only consumers of `uname` and both read `nodename` — but a program above that compares it against a fixed string will see it change. Recorded in `README.md`'s divergence table. The diff --git a/.agents/docs/2026-08-30-openkal-0.10-ecosystem-plan.md b/.agents/docs/2026-08-30-openkal-0.10-ecosystem-plan.md index 72ce84f..2c0e46c 100644 --- a/.agents/docs/2026-08-30-openkal-0.10-ecosystem-plan.md +++ b/.agents/docs/2026-08-30-openkal-0.10-ecosystem-plan.md @@ -12,7 +12,7 @@ 一点没变。这一轮把四条**做掉**:改规范、改所有实现、改消费者、发布、在干净沙箱里 用真实索引验证。 -⚠️⚠️ **并且规范一动,整个图必须整体移动。** 这不是洁癖,是 mcpp 的解析规则: +**并且规范一动,整个图必须整体移动。** 这不是洁癖,是 mcpp 的解析规则: 版本要求是精确的、不向上浮动,而 `openkal` 是图里所有包的**共享依赖**。所以 openkal-musl → openkal 0.10.0 @@ -35,12 +35,12 @@ | openkal-windows | 0.9.0 | 0.4.0 | ✓ ✓ ✓ | | openkal-opensbi | 0.9.0 | 0.3.0 | ✓ ✗ ✗ | | openkal-uefi | 0.9.0 | 0.3.0 | ✗ ✗ ✗ | -| ~~openkal-macos-abi~~ | — | — | ⚠️ **不是一个包** —— 见 §1.1 | +| ~~openkal-macos-abi~~ | — | — | **不是一个包** —— 见 §1.1 | | **openkal-musl** | 0.9.0 | 0.10.0 | 消费者 | | **openkal-llvm-runtime** | 经 musl | 0.6.0 | 消费者 | | std-freestanding-alloc-kal | 0.8.0 | 0.1.1 | 消费者 | -### 1.1 ⚠️⚠️ 我把同一个包数成了两个,而这正是"全局 review"要抓的东西 +### 1.1 我把同一个包数成了两个,而这正是"全局 review"要抓的东西 第一次清点时,`~/workspace/github/mcpplibs/` 下有 `openkal-macos` 和 `openkal-macos-abi` 两个目录,两者 `mcpp.toml` 里的版本还不一样(0.5.0 / 0.6.0), @@ -54,10 +54,10 @@ `declarations-0.10` 只有一份),第二个 worktree 已还回它原来的 `abi/one-idea-one-spelling` 分支。 -⚠️ 教训写在这里而不是抹掉:**目录名不是包名**。清点依赖图要读 +教训写在这里而不是抹掉:**目录名不是包名**。清点依赖图要读 `mcpp.toml` 的 `name`,不是读目录。我下次清点先按 `name` 去重。 -### 1.2 ⭐ 把索引整个扫一遍,而不是从正在改的包往外推 +### 1.2 把索引整个扫一遍,而不是从正在改的包往外推 上面那句「本轮不扩大范围去动 `std-freestanding-alloc-kal`」是**写错了**,而且错的 不是结论是方法:「生态闭环」不允许有一个包留在旧图里。所以改成扫 @@ -70,10 +70,10 @@ | `openkal-uefi` | 同上,一模一样 | 补版本号 | | `std-freestanding-alloc-kal` | 钉在 `openkal = "0.9.0"`,**根本不在原清单里** | 0.1.3 + 重钉 0.10.0,PR #3 | -⚠️ 前两条的形态值得记:**一次「只改依赖」的重钉,最容易漏掉的就是自己的版本号。** +前两条的形态值得记:**一次「只改依赖」的重钉,最容易漏掉的就是自己的版本号。** tag 是 0.4.0、清单自称 0.3.0,发出去就是一个不认自己名字的包,而那条精确要求指着它。 -⭐ 后一条说明**方法比结论重要**:它是被 `std-freestanding` 以 `^0.1.0` 引的,顺着 +后一条说明**方法比结论重要**:它是被 `std-freestanding` 以 `^0.1.0` 引的,顺着 「谁依赖 openkal-musl」往外推永远推不到它;只有反过来问「谁提到了 openkal」才会 出现。核对完全链之后,**只有那两个自称版本对不上**,其余六个都吻合。 @@ -82,7 +82,7 @@ tag 是 0.4.0、清单自称 0.3.0,发出去就是一个不认自己名字的包 `openkal-musl`,两边 0.10.0 的发布时间戳精确到同一秒。这正是 1.1 那个「一个东西 两个名字」的老问题,只是这次落在索引里。它早于本轮改动。 -⚠️ **clause 9 的后果**:「An implementation exports the names of the interfaces it +**clause 9 的后果**:「An implementation exports the names of the interfaces it provides and no other」。新操作属于 `openkal.fs` / `openkal.process` / `openkal.task` 这三组,所以**提供该接口的实现必须导出这些名字**,不能只是不实现。 能力上做不到的用 `kal_fs_props` 位说明 —— 这正是链接操作的既有模型。 @@ -104,7 +104,7 @@ provides and no other」。新操作属于 `openkal.fs` / `openkal.process` / --- -## 3. ⭐ 全局设计 review —— 我自己的四处问题 +## 3. 全局设计 review —— 我自己的四处问题 写完之后按规范自己的规则逐条过了一遍,**发现四处问题并已修**。记在这里,因为 "我 review 过了"和"review 发现了什么"是两回事。 @@ -124,7 +124,7 @@ it, **which reports kal_err_not_supported instead**」—— 前半句说留空, 整个操作做不到 → `kal_err_not_supported`;某一个位置分辨不出 → 写 0, 而 0 的意思是"说不出"而不是"没有"。**调用者必须能把"没有空间"和"没有回答"分开。** -### 3.3 ⚠️⚠️ 锁的持有者写成了"程序",而这会把一个著名的坑写进规范(已修) +### 3.3 锁的持有者写成了"程序",而这会把一个著名的坑写进规范(已修) 原文写「An implementation shall release what a program held when the program ends」。这听起来对,但它默许了 POSIX `fcntl` 最老那套语义:**一个程序只要关闭该节点 @@ -135,7 +135,7 @@ ends」。这听起来对,但它默许了 POSIX `fcntl` 最老那套语义:**一 实现**不得**暴露那套旧语义。Linux/macOS 都有更新的形式(持有者恰好就是打开的文件), 那才是该建在上面的东西。 -⭐ 同时补上了"为什么这件事非得在线下面做":调用者**能**用 `KAL_OPEN_EXCLUSIVE` 加 +同时补上了"为什么这件事非得在线下面做":调用者**能**用 `KAL_OPEN_EXCLUSIVE` 加 一个名字自己造互斥,造不出来的是**持有者死掉时的释放**。这是只有环境能给的那一半。 ### 3.4 两个 spawn 变体不能组合,而这必须写明(已修) @@ -144,7 +144,7 @@ ends」。这听起来对,但它默许了 POSIX `fcntl` 最老那套语义:**一 按 clause 8 只能新增声明,那么"授予目录 + 绑定寿命"就得是第四个声明。 ⇒ 不声明,并写明理由:一个接口就是这样先有四个 spawn 再有八个的。等真有东西需要时 -再加(clause 8 允许)。⚠️ 但必须同时警告:**调用者不能退而用 +再加(clause 8 允许)。但必须同时警告:**调用者不能退而用 `kal_process_spawn_with` 并假设它绑定** —— 它不绑定,而"程序活得比调用者久"正是这条 要消掉的失败。 @@ -192,29 +192,29 @@ ends」。这听起来对,但它默许了 POSIX `fcntl` 最老那套语义:**一 | # | 观察 | 修之前 | 沙箱实测 | | --- | --- | --- | --- | -| 1 | 两个进程,第二个 `F_SETLK` **拿不到**锁 | 两个都拿到 | ✅ 第二个进程被拒 | -| 2 | `fork`+`execve` 起的程序被 `kill` 后**真的停了** | 跑完全程 | ✅ 真停了 | -| 3 | `hardware_concurrency()` == 宿主的核数 | 1 | ✅ **32** | -| 4 | `std::filesystem::space()` 有答案 | ENOSYS | ✅ **33573859328** | -| 5 | 锁目录的 `last_write_time(dir, t)` 走规范说的路 | 走的是规范外的路 | ✅ 设了能读回 | -| 6 | 起一个"存在但不是程序"的名字 → EACCES,调用者活着 | 已在 openkal-linux 0.8.0 修掉 | ✅ 调用者活着 | -| 7 | **每个依赖 openkal 的包仍然构建得起来** | — | ✅ 九个包全绿 | -| 8 | 轮询过的流,一次 `read` 拿回**写方写的那一段**,不是一个字节 | 一次一字节 | ✅ **4 字节** | -| 9 | 一个条件变量上两个等待者,广播把**第二个**也放出来 | 永远醒不来 | ✅ 两个都放出来 | - -⚠️ 第 7 条是这一轮新加的,而且是最容易漏的:前六条都是"新功能对不对",第 7 条是 +| 1 | 两个进程,第二个 `F_SETLK` **拿不到**锁 | 两个都拿到 | 第二个进程被拒 | +| 2 | `fork`+`execve` 起的程序被 `kill` 后**真的停了** | 跑完全程 | 真停了 | +| 3 | `hardware_concurrency()` == 宿主的核数 | 1 | **32** | +| 4 | `std::filesystem::space()` 有答案 | ENOSYS | **33573859328** | +| 5 | 锁目录的 `last_write_time(dir, t)` 走规范说的路 | 走的是规范外的路 | 设了能读回 | +| 6 | 起一个"存在但不是程序"的名字 → EACCES,调用者活着 | 已在 openkal-linux 0.8.0 修掉 | 调用者活着 | +| 7 | **每个依赖 openkal 的包仍然构建得起来** | — | 九个包全绿 | +| 8 | 轮询过的流,一次 `read` 拿回**写方写的那一段**,不是一个字节 | 一次一字节 | **4 字节** | +| 9 | 一个条件变量上两个等待者,广播把**第二个**也放出来 | 永远醒不来 | 两个都放出来 | + +第 7 条是这一轮新加的,而且是最容易漏的:前六条都是"新功能对不对",第 7 条是 "我有没有把别人弄坏"。规范一动就是整图移动,所以它必须逐包验证而不是抽查。 -⚠️⚠️ **第 8、9 条是消费者的测试逼出来的,不是我想出来的**,而这正是前七条的方法 +**第 8、9 条是消费者的测试逼出来的,不是我想出来的**,而这正是前七条的方法 问题:1–6 是照着「规范新加了什么」写的,7 是照着「我可能弄坏什么」写的,**没有 一条是照着「消费者拿它做什么」写的**。8 和 9 两条缺陷都活过了本仓全部既有判据, 其中第 9 条本仓早就有「四个上下文抢一把锁八万次」而一路全绿——**抢锁不是等条件 变量,一个等待者不是两个**。 -⭐ 两条都已经写成本仓自己的判据(第 8 条在 `examples/subprocess`,第 9 条在 +两条都已经写成本仓自己的判据(第 8 条在 `examples/subprocess`,第 9 条在 `examples/posix`),并且**双向验证过**:摘掉修复它们变红。不双向验证的判据不算判据。 -### 5.1 ✅ 闭环实测(干净沙箱 + CN 镜像 + 只写版本号) +### 5.1 闭环实测(干净沙箱 + CN 镜像 + 只写版本号) 九条全绿,零失败。工程清单**只写版本号不写路径**——用 path 依赖等于验证我的硬盘, 不是验证这次发布: @@ -227,18 +227,18 @@ openkal-llvm-runtime = "0.7.0" 沙箱确实从索引下载了包(`Downloading mcpplibs.openkal v0.10.0` 等四行),这不是 grep 出来的,是**解析出来的**。 -⚠️ 中途我写过一个「在沙箱里 `find` 出各包的 `.lua` 再 grep 版本号」的检查,九条 +中途我写过一个「在沙箱里 `find` 出各包的 `.lua` 再 grep 版本号」的检查,九条 全报 MISS——**那是我的检查错了**:`find` 命中了另一个 subos 里的**陈旧快照**。 换成直接下载消费者真正会同步的那份产物(`mcpp-index-900771c.tar.gz`)去查,九个 -包每个都在三张平台表里。⭐ **一个错的检查给出的红,和一个真的红长得一模一样**, +包每个都在三张平台表里。**一个错的检查给出的红,和一个真的红长得一模一样**, 这条记在这里因为它差点让我以为发布失败了。 -⚠️ 另一个坑:沙箱有**自己的 `/tmp` 和自己的 home**,工程要放到 +另一个坑:沙箱有**自己的 `/tmp` 和自己的 home**,工程要放到 `~/.xlings/subos//home/speak/` 才看得见。两次 `No such file or directory` 之后才反应过来——而这恰恰说明隔离是真的:**如果沙箱能看见我的硬盘,这次验证就 没有意义**。 -### 5.2 ✅ 报告者的工程,对着已发布的包再跑一遍 +### 5.2 报告者的工程,对着已发布的包再跑一遍 把他们七个清单改成上面那个推荐写法(published 版本号,不是路径),两个架构都构建 通过,`kaos` 的流式断言两个架构都绿: @@ -258,7 +258,7 @@ issue 给他们,由他们自己决定。 | 阶段 | 状态 | | --- | --- | -| openkal-linux 0.8.0 —— exec 失败回报(#20) | ✅ **已合并**。实测:0.7.1 对"存在但不是程序"报成功、调用者死在 127;修后 EACCES 且 `execve` 返回 | +| openkal-linux 0.8.0 —— exec 失败回报(#20) | **已合并**。实测:0.7.1 对"存在但不是程序"报成功、调用者死在 127;修后 EACCES 且 `execve` 返回 | | openkal 0.10.0 规范 | PR #25,CI 跑中,**1 项红**(见下) | | openkal-linux 0.9.0 | PR #22。**十条判据全绿**:锁真排他(同一进程第二个打开的文件被拒)、容量 1500829MB、目录时间盖上、并行度 32 | | openkal-macos 0.7.0 | PR #16。四条实现,`spawn_bound` 拒绝(无从被起映像内部武装的原语) | @@ -268,7 +268,7 @@ issue 给他们,由他们自己决定。 | openkal-llvm-runtime 0.7.0 | 待 musl 发布后重钉 | | mcpp-index / 沙箱闭环 | 待整链绿 | -### 6.1 ⭐ openkal-musl 侧的两条决定性读数 +### 6.1 openkal-musl 侧的两条决定性读数 ``` 锁: parent ACQUIRED / child EAGAIN => only one holds it (原来两个都拿到) @@ -277,12 +277,12 @@ kill: fork + execve -> program was killed (原来跑完全 两条都是 0.10.0 的分歧表里写着"本仓修不了"的。规范加了词之后它们就不再是分歧。 -### 6.2 ⚠️ 一项 CI 未过,待日志 +### 6.2 一项 CI 未过,待日志 `conformance (openkal-windows, msvc@system)` 红。运行未结束前 GitHub 不给日志, 待取。其余 linux/macos/windows-llvm 各腿在跑。 -⚠️ 在整链绿之前**没有任何东西被发布**,所以消费者手里的生态没有被动过 —— 这是 +在整链绿之前**没有任何东西被发布**,所以消费者手里的生态没有被动过 —— 这是 这一轮可以随时停在这里而不留下半成品的原因。 --- @@ -292,7 +292,7 @@ kill: fork + execve -> program was killed (原来跑完全 不再靠探针猜,直接构建他们的工程。**三条新缺陷是他们自己的测试套件发现的,我写的 探针一条都没碰到。** -### 7.1 ✅ 交叉编译两个架构都通,而且不需要他们那个绕过脚本 +### 7.1 交叉编译两个架构都通,而且不需要他们那个绕过脚本 ``` x86_64-linux-musl 静态, INTERP=0, 未定义符号=0, 132M @@ -302,7 +302,7 @@ aarch64-linux-musl 静态, INTERP=0, 未定义符号=0, 135M `scripts/build-static.sh` 的开头写着「bypassing mcpp's gcc-pinned musl target」—— **现在原生路径就能走通**,那个脚本可以退役。 -⚠️ aarch64 之前不通的原因不是编译,是**清单里少三行**: +aarch64 之前不通的原因不是编译,是**清单里少三行**: `openkal-llvm-runtime` 只声明在 `[target.x86_64-linux-musl.dependencies]` 下。 错误信息本身就说清楚了:目标的 C 库由工具链约定提供,而 `[toolchain]` 指名 llvm 把约定顶掉了,于是这个目标在图里没有任何东西供给 C 库。加同样的块即可。 @@ -310,7 +310,7 @@ aarch64-linux-musl 静态, INTERP=0, 未定义符号=0, 135M 产物跑起来:进入备用屏、画帧、退出 0。`OPENKAL_MUSL_TRACE=enosys` 只报一个 268(`fchmodat`)——就是双方已达成一致归他们的那条。 -### 7.2 ⚠️⚠️ `chdir` 到不了被起的程序(新,且是他们 cwd 测试挂掉的真因) +### 7.2 `chdir` 到不了被起的程序(新,且是他们 cwd 测试挂掉的真因) 实测,带宿主对照: @@ -324,24 +324,24 @@ started program pwd : /tmp ← 宿主 任何操作能改一个运行中程序的工作目录**;而 openkal-linux 用 `execveat(dirfd, …)`, 那个 dirfd 只用来**解析名字**,不改 cwd。 -⚠️ 并且 openkal-linux 的源码注释写着「The started program's working directory is +并且 openkal-linux 的源码注释写着「The started program's working directory is the directory supplied here」——**它自称的这条性质它并没有实现**。规范的 `process.h` 则对被起程序的工作目录只字未提。 ⇒ 这是规范缺口 + 后端注释与实现不符,两处都要报。 -### 7.3 ⚠️ 没有进程组,所以超时杀不掉后代(新) +### 7.3 没有进程组,所以超时杀不掉后代(新) 他们的超时逻辑是标准做法:子进程 `setpgid(0,0)` 自立组,父侧同步 `setpgid(pid,pid)`, 超时 `killpg` 整组。这个端口上没有组:父侧那次 `setpgid(pid,pid)` 报 EPERM, `kill(-pgid)` 报 ESRCH。他们有兜底所以**直接子进程杀得掉**(「期限杀 → 124」是绿的), 但 bash 后台起的 `sleep` 没人杀 ⇒ 两条「无残留进程」判据红。 -⚠️ 这正是我在 §3.4 评估 `kal_process_spawn_bound` 时**排除掉的那个替代方案** +这正是我在 §3.4 评估 `kal_process_spawn_bound` 时**排除掉的那个替代方案** (让 terminate 杀进程组)——排除的理由仍然成立(新组会脱离终端前台组),但这说明 「杀掉一棵进程树」是消费者真实需要的东西,而 `spawn_bound` 只解决了直接子进程那一层。 -### 7.4 ✅ 增量分块 —— 一次 `poll` 只留一个字节,于是流式变成了逐字节(已修) +### 7.4 增量分块 —— 一次 `poll` 只留一个字节,于是流式变成了逐字节(已修) `echo one; sleep 0.4; echo two` 要求两段**分次**到达。内容顺序是对的 (「stdout 全量按序」绿),分块不对。 @@ -350,7 +350,7 @@ the directory supplied here」——**它自称的这条性质它并没有实现 传输**并把读到的字节留下(`okm_take_ahead`)。但留下的只有**一个**字节,随后的 `read` 就把那一个字节直接返回了——一个 4 字节的行要四次 `read` 才读完。 -⭐ 实测,同一个探针,宿主作对照: +实测,同一个探针,宿主作对照: ``` openkal-musl "o" "n" "e" "." "t" "w" "o" "." ← 八次,每次一字节 @@ -361,20 +361,20 @@ openkal-musl "o" "n" "e" "." "t" "w" "o" "." ← 八次,每次一字节 ⇒ 修法:留下的那个字节交出去之后,若调用者的缓冲还有空间且**还能再做一次有界读**, 就接着把余下的读完再一起返回。`held + more`,一次系统调用一段。 -⚠️⚠️ **这就是 openkal-linux#13 最初那句「只输出 1 字节」的真正含义**,而我此前把它 +**这就是 openkal-linux#13 最初那句「只输出 1 字节」的真正含义**,而我此前把它 读成了「输出被截断」。它没有被截断——它是**一次只到一个字节**,内容一个不少。 半年前的报告用的词是准确的,读错的是我。 -⭐ 并且这一条是**四条里唯一一条不需要动规范**的:`kal_stream` 的读已经够用,错的是 +并且这一条是**四条里唯一一条不需要动规范**的:`kal_stream` 的读已经够用,错的是 这个端口怎么用它。7.2 / 7.3 都得等规范。 -### 7.5 ✅ aarch64 的测试第一次真的跑起来了(qemu-user + binfmt_misc) +### 7.5 aarch64 的测试第一次真的跑起来了(qemu-user + binfmt_misc) 此前 aarch64 **一次都没跑过**。原因还是 §7.1 那三行清单:六个 package 里 **只有 `apps/cloudcode` 补了 aarch64 的块**,六个库 package 一个都没补,于是 `mcpp test --target aarch64-linux-musl` 在解析阶段就停了,根本没到编译。 -⭐ 更好的写法是不要按架构重复,`cfg` 谓词一条覆盖两个架构: +更好的写法是不要按架构重复,`cfg` 谓词一条覆盖两个架构: ```toml [target.'cfg(all(os = "linux", env = "musl"))'.dependencies] @@ -387,10 +387,10 @@ openkal-llvm-runtime = "0.7.0" 所以 `mcpp test --target aarch64-linux-musl` 直接就地执行,不用起 VM,也不用改 测试脚本。 -⚠️ **两个架构的失败集合完全一致**——这本身是一条读数:挂掉的没有一条是与架构相关的, +**两个架构的失败集合完全一致**——这本身是一条读数:挂掉的没有一条是与架构相关的, 全部是 7.2 / 7.3 那两个语义缺口,以及双方已达成一致归项目侧的权限位。 -### 7.6 ⚠️⚠️ 两个等待者的条件变量永远醒不来(新,而且这是最严重的一条) +### 7.6 两个等待者的条件变量永远醒不来(新,而且这是最严重的一条) `test_chat_approval` 在 musl 上 300s 超时,零输出。**宿主对照 0.06s 通过**——这一对 数字就是全部诊断,也正是为什么必须跑宿主对照。 @@ -404,7 +404,7 @@ openkal-llvm-runtime = "0.7.0" 3. strace 的终态是两个上下文各自阻塞在 `FUTEX_WAIT_PRIVATE, 2`,而 `2` 正是 `lock()` 里 `a_cas(l,1,2)` 之后等待的那个值。 -⭐⭐ **要两个等待者才会犯,这就是它活到现在的原因。** 那次 requeue 只在 +**要两个等待者才会犯,这就是它活到现在的原因。** 那次 requeue 只在 `node.prev` 非空时发出——也就是**有第二个上下文排在被释放者后面**时。一个等待者 永远碰不到它,而一个等待者是绝大多数程序的样子。 @@ -414,10 +414,10 @@ musl 的等待者永远在 `while (a_cas(l, 0, 2))` 里,所以在原地址上唤 实测:同一条测试,300s 超时 → **0.10s 通过**。 -⚠️ **这条是本仓自己的缺陷,不需要动规范**,和 7.4 一样。四条里两条是规范缺口 +**这条是本仓自己的缺陷,不需要动规范**,和 7.4 一样。四条里两条是规范缺口 (7.2 / 7.3),两条是这个端口怎么用现有原语的问题(7.4 / 7.6)。 -⭐ 并且它暴露了判据方法本身的毛病:`examples/posix` 里早就有「四个上下文抢一把锁 +并且它暴露了判据方法本身的毛病:`examples/posix` 里早就有「四个上下文抢一把锁 八万次」,一路全绿——**抢锁不是等条件变量,一个等待者不是两个**。已补一条只看 「第二个等待者有没有被放出来」的判据,并且**双向验证过**:带修复三条 ok,把修复 摘掉探针就停在这一块,报「it did not return; where it was」。 @@ -442,29 +442,29 @@ musl 的等待者永远在 `while (a_cas(l, 0, 2))` 里,所以在原地址上唤 | pi-tui | 7/0 | 7/0 | | **合计** | **96 通过 / 12 失败** | 同上 | -⚠️ **每一条归属都有判别依据,不是看名字猜的**: +**每一条归属都有判别依据,不是看名字猜的**: **项目侧 8 条** - 权限位 7 条(agent-core 5 + oauth 2)。双方早已达成一致,`fchmodat` 是唯一还在 报 ENOSYS 的那个号(268)。 -- `test_chat_live` 1 条。⭐ **宿主目标上也红**(0 passed / 1 failed,0.02s),报 +- `test_chat_live` 1 条。**宿主目标上也红**(0 passed / 1 failed,0.02s),报 `config.toml is missing defaultProvider`——缺配置,与 openkal 无关。 **openkal 侧 4 条**——**只剩 §7.2 / §7.3 那两类语义缺口** - cwd:`test_chat_tools`、`test_chat_mcp`,加 kaos 的 `cwd: 子进程 chdir 生效` - 进程组:`test_chat_bashbg`(`任务未在 10s 内终态`),加 kaos 两条「无残留进程」 -⭐ **判别法只有一个,就是同一套跑宿主目标。** `test_chat_approval` 宿主 0.06s 绿、 +**判别法只有一个,就是同一套跑宿主目标。** `test_chat_approval` 宿主 0.06s 绿、 musl 300s 不返回 ⇒ 本仓的;`test_chat_live` 宿主也红 ⇒ 不是本仓的。凭断言名字归类 会把这两条都归错。 ### 8.1 项目侧要改的两处(与失败无关,是构建用法) -⚠️ **一、六个库 package 都缺 aarch64 的依赖块。** 只有 `apps/cloudcode` 补了,所以 +**一、六个库 package 都缺 aarch64 的依赖块。** 只有 `apps/cloudcode` 补了,所以 `mcpp test --target aarch64-linux-musl` 在解析阶段就停,**根本没到编译器**—— 「aarch64 跑不了」不是编译问题。 -⭐ 更好的写法是不按架构重复,一条 `cfg` 谓词覆盖两个架构(实测两个架构都解析得到): +更好的写法是不按架构重复,一条 `cfg` 谓词覆盖两个架构(实测两个架构都解析得到): ```toml # 七个 mcpp.toml 里都把 @@ -474,7 +474,7 @@ musl 300s 不返回 ⇒ 本仓的;`test_chat_live` 宿主也红 ⇒ 不是本仓 openkal-llvm-runtime = "0.7.0" ``` -⚠️ **二、`scripts/build-static.sh` 可以退役。** 它开头写着「bypassing mcpp's +**二、`scripts/build-static.sh` 可以退役。** 它开头写着「bypassing mcpp's gcc-pinned musl target」,而原生路径现在两个架构都通,产物静态、`INTERP=0`、 未定义符号 0。跑测试也不需要虚拟机:宿主注册了 `qemu-aarch64` 的 binfmt_misc, 静态产物直接就地执行。 @@ -494,15 +494,15 @@ gcc-pinned musl target」,而原生路径现在两个架构都通,产物静态 spawn 从三个声明收敛成一个(`kal_spawn` 记录),`_with` / `_bound` **删掉**—— openkal 还没有外部用户,是唯一能免费改形状的时候。 -⭐⭐ 单位那一位**第一版是 flag,被 clause 7.1 判掉了**。两个实现零状态就能满足 +单位那一位**第一版是 flag,被 clause 7.1 判掉了**。两个实现零状态就能满足 (`getpgid(pid) == pid` 从内核恢复关联),第三个不行:能组建、无法从进程句柄恢复。 clause 7.1 的机械判据说得很直白——**需要登记表说明规范借了某个环境的形状**。 -⚠️ 而显而易见的修法(开一个空单位)只会把缺陷搬到另一边:进程组由第一个成员创建, +而显而易见的修法(开一个空单位)只会把缺陷搬到另一边:进程组由第一个成员创建, 没有成员就没有组可开。⇒ 最终是**身份在第一个成员启动时确立并回报**,两边都不需要 记住任何东西。 -⭐ 两个不是目标的收益:`kal_process_terminate` 恢复成「永远只是一个程序」;多个 +两个不是目标的收益:`kal_process_terminate` 恢复成「永远只是一个程序」;多个 程序可以共享一个单位。 ### 9.2 又挖出三条,都是「报成功但什么都没做」 @@ -511,14 +511,14 @@ clause 7.1 的机械判据说得很直白——**需要登记表说明规范借 |---|---| | `SIG_IGN` 被接受、被忽略,程序被真内核杀 | C 库**诚实地**报成功(它确实没东西要设),杀它的机制**任何一层都叫不出名字** | | `setpgid(0,0)` 形成不了组 | 在一个没有组的世界里返回 0 不算说谎——也不算做了事 | -| `execve` 的等待镜像攥着管道写端 | ⭐ **0.10 那个缺陷的第三张面孔**:那次是信号打到中间镜像,这次是中间镜像攥着资源 | +| `execve` 的等待镜像攥着管道写端 | **0.10 那个缺陷的第三张面孔**:那次是信号打到中间镜像,这次是中间镜像攥着资源 | -### 9.3 ⚠️⚠️ CI 抓到三条我本地抓不到的 +### 9.3 CI 抓到三条我本地抓不到的 - **实现仓库自己的测试**也在调 `kal_process_spawn`。我只改了 `src/` 也只 grep 了 `src/`。 - Windows 的 `.def` 导出——和 0.10 的 `NtLockFile` 是同一个守卫。 -- ⭐⭐ **一次跨文件正则替换删掉了 `kal_process_wait`**。本地编译毫无反应: +- **一次跨文件正则替换删掉了 `kal_process_wait`**。本地编译毫无反应: 「定义缺失」不是编译错误,是**链接**错误,且只在有人调用它的程序里才报。抓到它的是 三个仓库之外的交叉链接任务。**这个仓库只构建库、不链接程序,它自己永远抓不到。** @@ -529,10 +529,10 @@ clause 7.1 的机械判据说得很直白——**需要登记表说明规范借 ### 9.5 项目侧最终变动 -⭐⭐ 最尖锐的一条是**产品代码**不是测试:`ensure_cloud_code_home` 把「收紧不了 +最尖锐的一条是**产品代码**不是测试:`ensure_cloud_code_home` 把「收紧不了 权限」当成了「建不出目录」,于是在没有权限模型的平台上**程序连自己的配置目录都 建不出来**,而目录其实已经建好了。区分的是**错误的种类**而不是平台的名字。 其余四类:`fork/exec` → `posix_spawn`(三处);权限断言改成**运行时探测** (断言一字未弱化,改的是「什么时候适用」);信号断言改成断言「死于信号」而不是 -「死于哪个信号」;`test_chat_live` 配置缺失时 SKIP(⚠️ 宿主上同样失败)。 +「死于哪个信号」;`test_chat_live` 配置缺失时 SKIP(宿主上同样失败)。 diff --git a/.agents/docs/2026-08-30-openkal-0.11-start-design.md b/.agents/docs/2026-08-30-openkal-0.11-start-design.md index 4b085d7..1fbc4c8 100644 --- a/.agents/docs/2026-08-30-openkal-0.11-start-design.md +++ b/.agents/docs/2026-08-30-openkal-0.11-start-design.md @@ -10,7 +10,7 @@ > Declaring every combination is how an interface acquires four spawns and then > eight, so the combination is declared when something needs it and not before. -⭐ 现在「something needs it」到了,而且是**两件事同时到**。所以要谈的不是「再加一个变体」, +现在「something needs it」到了,而且是**两件事同时到**。所以要谈的不是「再加一个变体」, 是**这个族该长成什么样**。 ## 1. 两个缺口(都由报告者的测试套件发现,不是我猜的) @@ -28,11 +28,11 @@ chdir(/tmp) 之后 : /tmp 被起程序的 pwd : /tmp ← 宿主 ``` -⚠️ **后端修不了。** `fchdir(b)` 会让 openkal-linux 那句注释成真而行为不会变好:`b` 是名字 +**后端修不了。** `fchdir(b)` 会让 openkal-linux 那句注释成真而行为不会变好:`b` 是名字 落在哪个 preopen 就是哪个,`/usr/bin/sh` 的 `b` 就是根。**给程序起名**和**说它在哪儿跑** 是两个目录,接口只带了一个。 -⚠️ 并且这**不是**「openkal 拒绝可变 cwd」那条设计的问题。那条拒绝的理由是 +并且这**不是**「openkal 拒绝可变 cwd」那条设计的问题。那条拒绝的理由是 「a working directory that can be changed is shared mutable state between execution contexts」——**这个理由完全成立,而且不适用于这里**:在**起程序的那一刻**说明它在哪儿跑, 是每次 spawn 各自说一次的、不可变的、不被任何两个上下文共享的东西。⇒ 该拒绝的继续拒绝, @@ -46,10 +46,10 @@ contexts」——**这个理由完全成立,而且不适用于这里**:在**起 `setpgid` 报 EPERM、`kill(-pgid)` 报 ESRCH。他们有兜底所以**直接子进程杀得掉**, 后代杀不掉。 -⚠️ 0.10 的 `KAL_PROCESS_PROP_BOUND_LIFETIME` 解决的是**另一层**:它把被起程序绑在调用者 +0.10 的 `KAL_PROCESS_PROP_BOUND_LIFETIME` 解决的是**另一层**:它把被起程序绑在调用者 的命上(`PR_SET_PDEATHSIG`),够不到孙子。两条不重叠。 -## 2. ⭐ 提案:一个 `kal_process_start`,而不是第四、第五个 spawn +## 2. 提案:一个 `kal_process_start`,而不是第四、第五个 spawn ```c /* 声明时定死,clause 5.3 */ @@ -76,16 +76,16 @@ int kal_process_start(const struct kal_start*, 因为**两个缺口都要求「每次 spawn 各自选」**,而不是「实现要么总这样要么总不这样」: -⚠️ 新进程组会**脱离终端前台组**,于是带界面的程序里,子上下文读终端会拿到 SIGTTIN 停住。 +新进程组会**脱离终端前台组**,于是带界面的程序里,子上下文读终端会拿到 SIGTTIN 停住。 我在 0.10 评估 `spawn_bound` 时就是因为这个排除了「让 terminate 杀进程组」——**那个理由 现在仍然成立**。三个流全是管道的调用者(跑 shell 的那种)要这个行为;交互式的调用者绝不要。 ⇒ 这个区别**只有按次表达才对**。做成默认行为是错的,做成实现属性也是错的。flags 正好。 -⭐ 而 flags 与 openkal 的既有风格一致:`kal_*_props` 本来就是位集,实现对做不到的那一位 +而 flags 与 openkal 的既有风格一致:`kal_*_props` 本来就是位集,实现对做不到的那一位 答 `kal_err_not_supported`,和现在 `spawn_bound` 的约定一模一样。 -### 2.2 ⚠️ 这个提案的代价,先说清楚 +### 2.2 这个提案的代价,先说清楚 **它让 `spawn` / `spawn_with` / `spawn_bound` 三个都变成冗余的,而 clause 8 不许删。** 末态是四个声明,其中三个是历史。 @@ -115,5 +115,5 @@ int kal_process_start(const struct kal_start*, 4. openkal-llvm-runtime 跟版本 5. 用报告者的工程验证:kaos 的三条红判据、agent-core 的三条,应当全绿 -⚠️ **必须在 0.10 全部合流、发布、进 index 之后再开始**,否则两条链在同一批仓库里交叉, +**必须在 0.10 全部合流、发布、进 index 之后再开始**,否则两条链在同一批仓库里交叉, 版本要求又是精确的,任何一个包对不上整张图都解析不了。 diff --git a/.agents/docs/2026-08-31-four-remaining-and-what-they-are-really.md b/.agents/docs/2026-08-31-four-remaining-and-what-they-are-really.md index 8e6ff32..8da5f6d 100644 --- a/.agents/docs/2026-08-31-four-remaining-and-what-they-are-really.md +++ b/.agents/docs/2026-08-31-four-remaining-and-what-they-are-really.md @@ -2,7 +2,7 @@ > 状态:**待 review,一行代码都还没写。** > -> ⚠️⚠️ 这份文档的第一个结论是:**四条里只有一条是「待做的工作」,另外三条分别是 +> 这份文档的第一个结论是:**四条里只有一条是「待做的工作」,另外三条分别是 > 「已经能做但没做」「已经做对但没被指出来」「已经处理正确只差一句话」。** > 而我在写它的过程中提过一个错的方案,那个错法本身比方案更值得记——见 §3.1。 @@ -12,10 +12,10 @@ | # | 事项 | 真实性质 | 动规范吗 | 落点 | |---|---|---|---|---| -| 1 | `fork(); setpgid(0,0); exec` 够不着 | ⭐ **现有操作就能做,只是没接上** | ❌ | openkal-musl | -| 2 | macOS 不 claim `stop_requested` | 能做,缺的是**能失败的测试** | ❌ | openkal-macos | -| 3 | 权限模型 | ⭐⭐ **规范已经答对了**,缺的是指路 | ❌ | fs.h 注释 + musl README | -| 4 | `openkal-libc` 双条目 | 索引自洽,只有一个松头 | ❌ | 一句注释 | +| 1 | `fork(); setpgid(0,0); exec` 够不着 | **现有操作就能做,只是没接上** | no | openkal-musl | +| 2 | macOS 不 claim `stop_requested` | 能做,缺的是**能失败的测试** | no | openkal-macos | +| 3 | 权限模型 | **规范已经答对了**,缺的是指路 | no | fs.h 注释 + musl README | +| 4 | `openkal-libc` 双条目 | 索引自洽,只有一个松头 | no | 一句注释 | ⇒ **零 ABI 变动。** 这不是巧合——三条的「缺口感」都来自我从**需求**倒推,而不是从 **接口已有什么**出发。 @@ -29,10 +29,10 @@ 副本 `setpgid(0,0)` 形成的单位,身份是**副本的真实标识**;父进程手里是这个 C 库 分配的号。两者不是同一个东西,所以父侧 `kill(-n)` 指向一个不存在的组。 -⚠️ 我之前把它记成「够不着,需要新声明」。**那是错的**:缺的不是「命名单位的能力」, +我之前把它记成「够不着,需要新声明」。**那是错的**:缺的不是「命名单位的能力」, 是**副本把身份告诉父进程的通道**。 -### 1.2 ⭐ 而那条通道 openkal 已经有,并且正是为这件事存在的 +### 1.2 而那条通道 openkal 已经有,并且正是为这件事存在的 `kal_process_channel` 的声明处写着: @@ -50,20 +50,20 @@ kill(-n) 父按 n 找到 slot,对 mine 做一次有界读 读到 EOF → 副本没形成单位 ``` -⭐ **EOF 天然表示「没有单位」**,因为本轮刚加的 `__okm_close_all_for_exec` 会在 +**EOF 天然表示「没有单位」**,因为本轮刚加的 `__okm_close_all_for_exec` 会在 exec 前关掉副本持有的一切。两个改动正好咬合——这不是设计出来的,是发现的。 ### 1.4 代价与边界 -- 每次 `fork` 多两个流句柄。⚠️ openkal-windows 本就不提供 `openkal.space`,不受影响。 +- 每次 `fork` 多两个流句柄。openkal-windows 本就不提供 `openkal.space`,不受影响。 - 读**必须有界**(`openkal.timeout`),否则副本还没 `setpgid` 时父进程会阻塞。 -- ⚠️ 副本形成单位与父进程询问之间有窗口。**这个窗口在真 Unix 上也存在**——消费者 +- 副本形成单位与父进程询问之间有窗口。**这个窗口在真 Unix 上也存在**——消费者 代码里那句「父侧同步 setpgid 关竞态窗」就是在处理它。不是新问题,而且这个方案 下父进程可以**重试读**,比 Unix 那边的重发 `setpgid` 更直接。 ### 1.5 怎么验证 -现成的:`/tmp/jobp` 那个探针改回 `fork` 形态。⚠️ **必须双向**——摘掉这个改动之后 +现成的:`/tmp/jobp` 那个探针改回 `fork` 形态。**必须双向**——摘掉这个改动之后 `kill(-n)` 回到 ESRCH、后台的 `sleep` 存活。 --- @@ -75,7 +75,7 @@ exec 前关掉副本持有的一切。两个改动正好咬合——这不是设 这个内核的 `sigaction` 收的 `struct __sigaction` 带 `sa_tramp`,内核用它从 handler 返回;C 库平时提供 `_sigtramp`,而 openkal-macos 底下**没有 C 库**。 -⚠️ 装错的表现是「程序以没人能追查的方式死掉」——这正是本轮 SIGPIPE 那条缺陷的 +装错的表现是「程序以没人能追查的方式死掉」——这正是本轮 SIGPIPE 那条缺陷的 形态,从另一侧遇到。所以「没测过就不 claim」不是保守,是这个仓库的既有标准。 ### 2.2 方案是两半,**顺序不能反** @@ -86,7 +86,7 @@ exec 前关掉副本持有的一切。两个改动正好咬合——这不是设 安装 → 对自己发 SIGTERM → 断言 ①字被置位 ②程序还活着 ③跑到结尾 ``` -⭐ 就是 Linux 上那个探针(`ok: told, and still running`)。openkal-macos 的 CI 跑 +就是 Linux 上那个探针(`ok: told, and still running`)。openkal-macos 的 CI 跑 `macos-14`,所以**它能被测**,只是我这台机器不能。 **(a) 再写 trampoline。** 内核以 `(handler, infostyle, sig, siginfo, ucontext)` @@ -95,16 +95,16 @@ infostyle)`(syscall 184)。arm64 上参数在 x0–x4:把 x2/x3/x4 挪到 x0/x1/ handler,用保存下来的 x4/x1 调 sigreturn。约十条指令,和 openkal-linux 上那三条 x86_64 指令是同一类工作。 -⭐ 而且**只需要 arm64 一份**:那个矩阵只有 `macos-14`,注释写明了原因——构建工具 +而且**只需要 arm64 一份**:那个矩阵只有 `macos-14`,注释写明了原因——构建工具 在这个系统上没有 x86_64 发布。所以这不是「先做一半」,是全部。 -⚠️ **先 (b) 后 (a)**,否则又是一次「编过了所以以为对了」。 +**先 (b) 后 (a)**,否则又是一次「编过了所以以为对了」。 --- -## 3. 权限模型 ——⭐⭐ 规范已经答对了 +## 3. 权限模型 ——规范已经答对了 -### 3.1 ⚠️⚠️ 我先提了一个错的方案,记在这里 +### 3.1 我先提了一个错的方案,记在这里 我提过 `KAL_OPEN_PRIVATE` + `kal_fs_mkdir_private`,论据是「『只有我能读』两边都能, 是真正通用的原子能力」。 @@ -124,12 +124,12 @@ clause 7.1 判定的借形状**。 |---|---|---| | POSIX | 文件带 owner/group + 9 位;进程带 **ambient uid/gid** | 拿环境级身份比对 | | Windows NT | 对象带 security descriptor;线程带 **access token** | 拿令牌比对 ACL | -| **WASI** | ⭐ **没有权限模型**:`filestat` 无 mode/uid/gid,`path_open` 不收 mode | 不比对——只能到达被授予的 | +| **WASI** | **没有权限模型**:`filestat` 无 mode/uid/gid,`path_open` 不收 mode | 不比对——只能到达被授予的 | | Zircon / seL4 / Capsicum | handle + rights | 同上 | -⭐⭐ **关键不是「哪些位」,是有没有 subject。** openkal 和 WASI 站在同一个立场上。 +**关键不是「哪些位」,是有没有 subject。** openkal 和 WASI 站在同一个立场上。 -### 3.3 ⭐ 而 WASI 恰好证明这条线画在哪 +### 3.3 而 WASI 恰好证明这条线画在哪 WASI **有** `fs_rights_base` / `fs_rights_inheriting`——但那是**挂在句柄上的能力** (这个句柄允许做什么),不是**挂在文件上的权限**(别人允许做什么)。 @@ -148,20 +148,20 @@ clause 11 第 6 条: 并且按**威胁来源**给了三条替代路径:同程序的另一部分靠能力本身;机器上的另一个 用户靠**启动方给的 preopen**;不信任的位置靠加密。 -⭐ 第二条是要点:在能力模型里,「让这个文件私有」**不是对文件的操作**,是**你被授予 +第二条是要点:在能力模型里,「让这个文件私有」**不是对文件的操作**,是**你被授予 的那个目录的属性**。授予者知道机器上有谁,程序不知道。 ### 3.5 那么真正要做的两件小事 -⚠️ 核实过:`fs.h` 里**确实有**指向 clause 11 item 6 的交叉引用——在 `KAL_INFO_IDENTITY` +核实过:`fs.h` 里**确实有**指向 clause 11 item 6 的交叉引用——在 `KAL_INFO_IDENTITY` (102 行)和 `kal_fs_lock`(328 行)处。**但不在 `kal_fs_open` 和 `kal_fs_mkdir` 处**, 而那两个才是「我要建一个私有文件」的人会看的地方。 **① fs.h:在两个 CREATE 操作处加交叉引用。** 一句话,指向 clause 11 item 6 的三条 -替代路径。⚠️ 我是读了消费者代码才发现这个缺口,不是读了 fs.h——那就是证据。 +替代路径。我是读了消费者代码才发现这个缺口,不是读了 fs.h——那就是证据。 **② openkal-musl README:限制表里 `fchmodat`/`access(X_OK)` 那两行补上替代路径。** -⚠️ 消费者会读 README,不会读 SPEC。现在那两行只说了「不能」。 +消费者会读 README,不会读 SPEC。现在那两行只说了「不能」。 ### 3.6 可执行位:同理,而且更彻底 @@ -213,4 +213,4 @@ it」不一致。 3. **§4.3 (a)**(mcpp-index):一句注释 4. **§2**(openkal-macos):先测试后实现,由 CI 的 macos runner 判定 -⚠️ 1–3 可以并行且互不依赖;4 单独走,因为它是这台机器**验证不了**的唯一一条。 +1–3 可以并行且互不依赖;4 单独走,因为它是这台机器**验证不了**的唯一一条。 diff --git a/.agents/docs/2026-09-04-issue28-item3-start-failure-and-error-vocabulary.md b/.agents/docs/2026-09-04-issue28-item3-start-failure-and-error-vocabulary.md new file mode 100644 index 0000000..cf191bc --- /dev/null +++ b/.agents/docs/2026-09-04-issue28-item3-start-failure-and-error-vocabulary.md @@ -0,0 +1,473 @@ +# issue #28 第 3 条:反馈核实、生态适配范围与方案 + +2026-09-04 · 待 review,**一行代码都还没写** +来源:(yspbwx2010) +前置:`2026-08-31-four-remaining-and-what-they-are-really.md`、 +`2026-08-30-openkal-0.10-ecosystem-plan.md`(§0 的「整张图必须整体移动」是本轮的成本依据) + +--- + +## 0. 一句话总览 + +**第一个结论:反馈成立,但它测到的不是最严重的那一条。** + +顺着它去核实四个实现时发现:**`openkal-macos` 的 `kal_process_spawn` 对任何起不来的 +名字都返回 `kal_ok` 加一个句柄 —— 包括一个根本不存在的名字。** 这正是 clause 3.1 +用来区分「供给」与「模拟」的那条线,而且它**可以被 conformance 可移植地测到**, +不需要文件系统、不需要可执行位、不需要新的错误值。反馈者看不到它,是因为 +`openkal-musl` 的 `startable()` 先问了 `kal_fs_info`,替 macOS 挡住了。 + +| # | 事项 | 判据 | 动规范吗 | 落点 | +|---|---|---|---|---| +| 1 | openkal-macos:起不来的名字返回 `kal_ok` | **M1** clause 3.1 | no | openkal-macos | +| 2 | `kal_error` 缺「不是这个环境能启动的形式」 | **M1** clause 3.1 + 5.2 先例 | **要** | openkal + 3 实现 + musl | +| 3 | 上面两条今天没有任何检查能发现 | **M3** clause 9.1 | no | openkal/conformance + musl probe | + +**只有第 2 条动规范。** 第 1、3 条是实现与套件内部。 + +⇒ 另外核出一条**真实存在、但本轮明确不做**的缺陷(网络错误值三个实现不一致), +理由在 §6.1 —— 它的每一种可选修法都是「把一个错答案换成另一个错答案」。 + +--- + +## 1. 反馈核实 + +### 1.1 逐条对着源码核,全部成立 + +| 反馈的主张 | 核实位置 | 结论 | +|---|---|---| +| `translate` 无 ENOEXEC 分支,落 `default` → `kal_err_io` | `openkal-linux/src/sys.h:304-322` | yes | +| ENOEXEC 在该文件里根本没定义 | 同上 `:210-216` 的 errno 常量表 | yes | +| 报告通道本身是通的 | `openkal-linux/src/process.cpp:319` `report.say(why)` → `:324` `report.heard()` | yes | +| `okm_errno` 的 `default: EIO` 轮不到 | `openkal-musl/port/src/okm_fd.c:43-62` | yes | +| `kal_error` 今天十三个值 | `openkal/include/openkal/types.h` | yes | +| 644 的那个 case 完好到达 | `e_acces→6`,`kal_err_permission→EACCES` | yes | +| 端口的前置检查救不了这一条 | `okm_spawn.c` 的 `startable()` 只问 `KAL_INFO_KIND` | yes | +| 这条路径上没有 127 | `process.cpp:324-327` 先 `reap` 再 `return translate(why)` | yes | + +### 1.2 我另做的独立控制读数 + +在本机(glibc 2.44,x86_64)对一个 755、非 ELF、无 `#!` 的文件: + +``` +glibc posix_spawn -> 8 (Exec format error) +glibc posix_spawnp -> 8 (Exec format error) +raw execveat -> ret -1 errno 8 (Exec format error) +``` + +⇒ **内核交给 `report.say(why)` 的确实是 ENOEXEC(8)**,而 `translate` 把它折成 3。 +反馈给的链条 8 → 3 → 5 端到端成立,两端我都独立看过。 + +### 1.3 三处要修正反馈的说法 + +**(a) 「ENOEXEC 是 `execvp` 据以回退到 `/bin/sh` 的答案」—— 对 POSIX/glibc 成立, +对本仓库里的 musl 不成立。** + +`musl/src/process/execvp.c` 的 `__execvpe` 只认 `EACCES/ENOENT/ENOTDIR`, +ENOEXEC 走 `default: return -1`,**它没有 `/bin/sh` 回退**。端口自己替换的 +`__posix_spawnp`(`port/src/okm_spawn.c:648`)沿用同一套规则,也没有。 + +⇒ 「回退不可达」在这里**不是被 EIO 破坏的,而是本来就不存在**。修好之后端口内部 +没有任何控制流会变。**这一条的必要性因此不建立在「回退坏了」上,而建立在 +「`kal_err_io` 是一句假话」上** —— 见 §3.2。这个区别必须写清楚,否则方案会被 +写成去补一个 musl 上游有意不做的回退(§6.2)。 + +**(b) 「还是以 127 结束调用者」在 Linux 上确实过时,但 README 那一行不算错。** + +`openkal-linux` 0.8.0(`3f16c78`「a replacement that failed is reported to the caller, +not to nobody」)就装上了报告通道。但 **openkal-macos 至今没有** +(`src/process.cpp:178` `for(;;) exit(127)`),所以那句话在 macOS 后端上今天仍然字面 +成立。反馈者只测了 Linux。README 那一行的问题是**没有分后端**,不是写错。 + +**(c) 这不是孤例。** `translate` 的 `default → kal_err_io` 同样吞掉了 `EXDEV`、 +`EMFILE/ENFILE`、`ENOTTY` 以及**全部网络 errno**。其中只有网络那一组构成实现间分歧 +(§6.1),其余在三个实现之间是一致的。 + +--- + +## 2. 判据:什么算「必须」 + +本轮只做能指到下面三条之一的事。**指不到的一律不做,并在 §6 逐条写明为什么不做。** + +| 代号 | 判据 | 出处 | +|---|---|---| +| **M1** | 某个实现今天让调用者**无声地错**(报成功却没做,或报一句关于环境的假话) | clause 3.1:*"An implementation that would leave its callers silently wrong is a simulation."* | +| **M2** | **两个实现**对同一个条件给出**不同**的 `kal_error` | clause 8:*"changing implementation is a change to one line of the manifest and to no line of the source"* | +| **M3** | 上述任一条**没有任何检查能发现它** | clause 9.1:*"Every operation the implementation declares shall behave as this specification requires."* | + +M2 单独出现时**不足以决定修法**:只有在「哪个答案是对的」有明确答案时才动手。 +两个都错的情况下强行统一,是把分歧换成一致的错误 —— 这正是 §6.1 不做的理由。 + +--- + +## 3. 必须做的三条 + +### 3.1 【M1】openkal-macos:起不来的名字返回 `kal_ok` + +#### 问题的准确形状 + +`openkal-macos/src/process.cpp` 的 spawn:复制映像 → 在副本里 `dup2`/`fchdir`/ +`setpgid` → `execve` → `for(;;) exit(127)`。父映像**没有任何通道**,直接落到: + +```cpp +if (unit && join == 0) how->job->h = static_cast(child); +*out = kal_process{ static_cast(child) }; +return kal_ok; +``` + +⇒ **名字不存在、是目录、没有可执行位、不是程序 —— 四种情况一律 `kal_ok` 加句柄。** + +对照: + +| 实现 | 失败报告 | 一个不存在的名字 | +|---|---|---| +| openkal-linux | 0.8.0 的 `exec_report` 管道 | `kal_err_not_found` | +| openkal-windows | **不需要管道** —— `CreateProcessW` 在父映像里就失败(`process.cpp:247`) | `kal_err_not_found` | +| **openkal-macos** | 没有 | **`kal_ok`** | + +`openkal-macos-abi/` **不是第二个实现**:两个目录的 `mcpp.toml` 里 +`name` 都是 `openkal-macos`,是同一个仓库的两个 worktree(`main` / +`abi/one-idea-one-spelling`,后者停在 openkal 0.9.0)。这条教训记在 +`2026-08-30-openkal-0.10-ecosystem-plan.md` §1.1,本轮**按 `name` 去重后确认要改的 +实现是一个,不是两个**。那个 worktree 本轮不动。 + +#### 方案:把 openkal-linux 的 `exec_report` 移过去 + +结构原样照搬(`openkal-linux/src/process.cpp:79-130`),只换两处这个内核不同的地方: + +1. **建管道**。macOS 没有 `pipe2`,但 `src/sys.h:133` 已经有 `pipe_pair(writing)` + —— 这个内核用两个返回寄存器交出两端。所以: + +```cpp +okm_long w = -1; +const okm_long r = okm::pipe_pair(w); +if (okm::failed(r)) return false; +fd[0] = static_cast(r); +fd[1] = static_cast(w); +// 没有 O_CLOEXEC 可以在创建时给,所以两端各设一次。 +okm::sys(okm::nr_fcntl, fd[0], okm::f_setfd, okm::fd_cloexec); +okm::sys(okm::nr_fcntl, fd[1], okm::f_setfd, okm::fd_cloexec); +``` + + `f_setfd = 2, fd_cloexec = 1` 在 `src/sys.h:217` 已有,不用新增。 + +2. **把两端抬到 placement 之上**。Linux 用 `F_DUPFD_CLOEXEC` 找「不低于下界的最低 + 空闲描述符」。macOS 的 `F_DUPFD_CLOEXEC` 是 **67**,`src/sys.h` 里还没有,需要加: + +```cpp +f_dupfd_cloexec = 67, // 这个内核的编号,和另一个内核的 1030 不同 +``` + + **不能用 `f_dupfd` 再补 `f_setfd` 两步代替**:两步之间另一个线程的 exec 会 + 带走这个描述符,而这个管道的整个存在意义就是「被 exec 关掉才算成功」。 + Linux 侧选原子形式的理由在 `process.cpp:83-91`,这里同理。 + +3. 副本里 `execve` 之后 `report.say(why)`;父映像 `report.heard()`,非零则 + `reap(child)` 并 `return okm::translate(why)`。 + +#### 代价与边界 + +- 管道占两个描述符,必须抬到 `3 + grant_count` 之上,否则会被副本自己的 + placement 关掉,父侧读到 EOF 并把它当成功。Linux 侧的注释把这个坑写在 + `process.cpp:83-91`,照抄。 +- 复制原语是 `okm::duplicate`(BSD 的双返回值约定),副本里 `say` 之后仍 + `exit(127)`,不变。 +- **不需要新的错误值。** 补上通道之后 macOS 的映射与 Linux 今天完全一致: + ENOENT→`not_found`、EACCES→`permission`、EISDIR→`is_directory`、 + ENOEXEC→`io`(第 2 条修完之后是新词)。 + +--- + +### 3.2 【M1】`kal_error` 加一个值 + +#### 为什么这是 M1 而不是「更精确一点更好」 + +`kal_err_io` 的定义是 *"the device or medium reported a failure"*。这条路径上 +**没有任何设备报过故障** —— 文件读得好好的,内核只是不认识它的格式。所以这不是 +一个粗糙的答案,是一句**关于环境的假话**,调用者据此做的任何判断都是错的。 + +而 SPEC 5.2 自己已经把这个论证写下来过了,一字不差地用在 `kal_err_exists` 上: + +> `kal_fs_open` with `exclusive`, and `kal_fs_mkdir`, fail because the name is +> already there. It is an expected outcome, and **mapping it to `kal_err_io` +> would report a medium failure for one.** + +「名字在,也允许运行,但不是这个环境能启动的形式」是同一个形状的预期结果。 + +#### 为什么必须是加词,而不是别的办法 + +- SPEC 5.2:*"Detail beyond these values is not available. A per-thread channel + carrying the environment's own error value was **considered and rejected**"* + ⇒ 「把原始 errno 带上来」这条路规范已经封死,不必再讨论。 +- 现有十三个词里没有一个能用:`kal_err_not_supported` 会告诉调用者**别再问了** + (这个理由 `openkal-windows/src/endpoint.h:51-54` 已经为 `WSANOTINITIALISED` + 写过);`kal_err_invalid` 说的是参数不对,而这个名字是对的; + `kal_err_permission` 已经被 644 那个 case 占着,而两者必须分得开。 + +#### 命名 + +推荐 **`kal_err_not_program = 14`**,与 `kal_err_not_directory`(*"a directory +operation applied to a file"*)同形:一个**启动**操作被用在了不是程序的东西上。 + +**不用 `kal_err_not_executable`**:`executable` 在每个读者脑子里都是一个权限位, +而这个接口刚刚在 issue #28 里以「一个 FAT 卷、一个 UEFI 系统分区和一个 Windows ACL +不共享一个模型」为由拒绝了权限模型。用它会把刚推开的东西从名字上请回来。 + +#### 落点(五处,每处一行) + +| 文件 | 改动 | +|---|---| +| `openkal/include/openkal/types.h` | `kal_err_not_program = 14, /* the name is there and may be run, but is not in a form this environment can start */` | +| `openkal/SPEC.md` §5.2 | 照 0.5 那张表的格式加一行论证(见下) | +| `openkal-linux/src/sys.h:210` | errno 表加 `e_noexec = 8`;`translate` 加 `case e_noexec: return 14;` | +| `openkal-macos/src/sys.h` | 同上(这个内核的 ENOEXEC 也是 8) | +| `openkal-windows/src/win.cpp:73` | `case ERROR_BAD_EXE_FORMAT: return kal_err_not_program;` | +| `openkal-musl/port/src/okm_fd.c:59` | `case kal_err_not_program: return ENOEXEC;` | + +SPEC 5.2 那一行: + +> | `kal_err_not_program` | 一个存在、可运行、而这个环境不认识其格式的名字。启动它 +> 失败是一个预期结果,映射到 `kal_err_io` 会为它报一次介质故障 —— 与 +> `kal_err_exists` 同一个理由。每个环境都有对应的原生条件(`ENOEXEC`、 +> `ERROR_BAD_EXE_FORMAT`),所以这不是某一个内核的特产。测量来自消费者: +> openkal#28,一个 C 库因此无法把 `posix_spawn` 的失败译回 `ENOEXEC`。 | + +#### 代价与边界 + +- **`SURFACE.txt` 不动。** 它一行一个导出名,枚举常量不在其中(已核: + `grep kal_err_not_directory SURFACE.txt` 无命中)⇒ clause 9.2 / 9.3 的两项静态 + 检查不受影响。 +- **两个方向都安全。** 新端口配旧后端:后端从不产出 14,行为不变。 + 旧端口配新后端:`okm_errno` 的 `default: return EIO` 接住,退回今天的行为。 +- **但它触发整张图的联动**,见 §4。这是本轮唯一真正花钱的地方。 + +--- + +### 3.3 【M3】上面两条今天没有任何检查能发现 + +`openkal/conformance` 全部 15 个 section 加起来只有 23 处 `kal_err_*`,其中 +`kal_err_io` 出现 **1 次**;`sections/process.cpp:195` 和 `:217` 检的全是「不支持的 +东西有没有被拒绝」。**没有一条用例起过一个起不来的名字。** + +#### (a) conformance:一条可移植的用例,直接抓 §3.1 + +放进 `openkal/conformance/src/sections/process.cpp`: + +```cpp +// 一个起不完成的启动不得报成 kal_ok。clause 3.1:报了成功却没有程序, +// 调用者只能靠等待和读退出码来发现,而那与一个真的跑过并返回 127 的程序 +// 无法区分。 +{ + kal_process p{}; + const char* argv[1] = { "no-such-program-openkal-conformance" }; + const kal_uintptr lens[1] = { 35 }; + const kal_spawn how{ kal::fs::working(), kal::fs::working(), nullptr, + nullptr, 0, 0 }; + const int e = kal_process_spawn(&how, argv[0], 35, argv, lens, 1, + nullptr, nullptr, 0, nullptr, &p); + observe(kind::behaviour, e != kal_ok, + "a start that did not happen is not reported as success"); + observe(kind::behaviour, e == kal_err_not_found, + "and a name that is not there is reported as absent"); +} +``` + +**这条用例可移植,而 ENOEXEC 那条不可移植** —— 区别值得记下来: +构造「存在但不是程序」需要在 POSIX 上设可执行位,而 `openkal.fs` **没有**这个操作 +(那正是 issue #28 第 3 条的前半)。所以套件能表达的是「起不来 ⇒ 不是 `kal_ok`」, +用一个不存在的名字就够,而具体到 ENOEXEC 的那一条只能放到消费者侧(下)。 + +#### (b) openkal-musl probe:ENOEXEC 那一条 + +放进 `examples/subprocess`,CI 已经在四行上跑它 +(`.github/workflows/ci.yml:629`,`tools/run-probe.sh`)。 + +- POSIX 行:probe 自己写一个 `#!` 都没有的一行文本文件,然后 + `posix_spawn("/bin/chmod", {"chmod","755",path})` 并等它结束 —— 端口的 `chmod` + 是 `ENOSYS`,但**起一个程序不是**,而 `--shell` 行本来就断言有 shell。 +- Windows 行:没有可执行位,文件存在即可(`.txt` 后缀,避开 `startable_name` 的 + `.exe` 重试)。 +- 三行断言同一件事: + +```c +int e = posix_spawn(&pid, path, NULL, NULL, av, environ); +CHECK(e == ENOEXEC); /* 不是 EIO,也不是 0 */ +``` + +这一条**必须先在打补丁前跑一次并看它红**。`2026-08-31` 那份文档 §2 记过同样的 +教训:*「能做,缺的是**能失败的测试**」*。只绿不红的用例不算数。 + +--- + +## 4. 代价:规范一动,整张图必须整体移动 + +依据是 `2026-08-30-openkal-0.10-ecosystem-plan.md` §0 **实测**过的规则:mcpp 的版本 +要求是精确的、不向上浮动,而 `openkal` 是图里所有包的共享依赖 —— 混版得到的是 +`irreconcilable versions`,不是「各用各的」。 + +| 包 | 现在 | 之后 | 本轮为什么动 | +|---|---|---|---| +| `openkal` | 0.12.0 | **0.13.0** | §3.2 加值 + §3.3(a) 用例 | +| `openkal-linux` | 0.12.0 | 0.13.0 | 认新值(两行) | +| `openkal-macos` | 0.9.0 | 0.10.0 | **§3.1 报告通道** + 认新值 | +| `openkal-windows` | 0.7.0 | 0.8.0 | `translate_win32` 一个 arm | +| `openkal-opensbi` | 0.6.0 | 0.7.0 | 只重钉(无 `openkal.process`) | +| `openkal-uefi` | 0.6.0 | 0.7.0 | 只重钉(连 fs 都不提供) | +| `openkal-musl` | 0.13.1 | 0.14.0 | `okm_errno` 一行 + §3.3(b) + README | +| `std-freestanding-alloc-kal` | 0.1.5 | 0.1.6 | 只重钉 | +| `openkal-llvm-runtime` | 0.9.1 | 0.9.2 | 钉 `openkal-musl = "0.13.1"`,跟着走 | +| `openkal-kit` | — | 跟 openkal 的归档 | 索引条目写明它从 openkal 的 tarball 的 `*/kit/mcpp.toml` 发布 | +| `mcpp-index` | — | 每包一条 xpm 版本 + sha256 | `pkgs/o/*.lua`、`pkgs/s/std-freestanding-alloc-kal.lua` | + +清点方法按 §1.2 的教训**反向扫索引**,而不是从改动点外推: +`grep -rl openkal mcpp-index/pkgs/` 命中 17 个文件,其中 4 个 `freedesktop.*` 与 +`sbase`、`std-freestanding-alloc-libc` 只是提到、不钉 `openkal`,已排除。 + +⇒ **十个包一起发,一个都不能落。** 这是本轮唯一的大成本,而它整个是由 §3.2 一条 +带来的 —— §3.1 和 §3.3 本身不需要任何版本联动。 + +### 4.1 所以为什么不拆成两轮 + +先做 §3.1(不动规范)、后做 §3.2(动规范),听起来更稳。**但版本要求是精确的, +`openkal-macos` 单独发 0.9.1 也要求它的每一个消费者重钉一次** —— 拆成两轮等于把 +上面这张表走两遍。⇒ **一轮做完,openkal 0.13.0。** 「先看红再看绿」的纪律靠本地 +构建保证,不靠发布顺序。 + +--- + +## 5. 落地顺序 + +- [ ] **1. 先写会红的检查**(不发布) + §3.3(a) 的 conformance 用例;本地对 **今天的** openkal-macos 跑, + 必须报「a start that did not happen is not reported as success」失败。 + ⇒ 这一步同时**证实** §3.1 的读数,把它从「读源码得出」变成「测到」。 +- [ ] **2. openkal-macos 报告通道**(§3.1)。用例转绿。此时还没有新错误值, + ENOEXEC 在 macOS 上和 Linux 一样折成 `kal_err_io` —— 这是预期的中间态。 +- [ ] **3. openkal 0.13.0**:`types.h` 加值 + `SPEC.md` §5.2 加论证行 + 用例入库。 + 核对 `SURFACE.txt` 无需改动。 +- [ ] **4. 三个实现认新值**:linux / macos 各两行,windows 一行。 +- [ ] **5. openkal-musl**:`okm_errno` 一行 + §3.3(b) 的 probe 用例 + + README 限制表那一行改写(见 §5.1)。 +- [ ] **6. 重钉与发布**:按 §4 的表,十个包 + 索引。 +- [ ] **7. 闭环**:干净沙箱 + CN 镜像,只写版本号解析一遍(0.10 那轮的 §5.1 流程)。 + +### 5.1 README 限制表那一行怎么改 + +现在这一行(`README.md:163`)对 Linux 已经是假的,对 macOS 曾经是真的: + +> | whether a file may be executed | `access(path, X_OK)` answers **yes for anything +> that exists**, and starting a name that exists and cannot be run still ends the +> caller with 127 | … | + +改成分成两半、并标出各自的归属: + +> | whether a file may be executed | `access(path, X_OK)` 仍然对任何存在的名字答 +> **yes** —— 这一半没有答案,`kal_node_info` 只带 `writable`。**另一半自 0.14.0 +> 起答了**:起一个存在而起不来的名字**报错并且不返回 pid**,理由完整传上来 +> ——`ENOENT`、`EACCES`、`EISDIR`、以及 `ENOEXEC`(openkal 0.13 的 +> `kal_err_not_program`)。0.13.1 及以前把最后一种报成 `EIO`;而在 +> openkal-macos 0.10.0 以前,**四种一律报成功**,调用者只能等到 127 | + +--- + +## 6. 明确不做,以及为什么 + +### 6.1 网络错误值:三个实现今天不一致,而本轮仍然不动 + +**这是核实过程中挖出来的、比 ENOEXEC 更普遍的一条**,也是本轮最难的一个判断。 + +`openkal-windows/src/endpoint.h:35` 有一整张 `translate_wsa`,注释自陈其存在理由: +*"Passing one of them to `translate_win32` produces `kal_err_io` for all of them, +which is a mapping that compiles, runs, and tells a caller nothing."* +而 `openkal-linux/src/sys.h` 与 `openkal-macos/src/sys.h` **一个网络值都没有** +—— 连常量都没定义,全部走 `default → kal_err_io`,`net.cpp` 用的正是这个 `translate`。 + +| 条件 | Windows | Linux / macOS | +|---|---|---| +| 连接被拒 | `not_found` | `io` | +| 主机 / 网络不可达 | `not_found` | `io` | +| 地址已占用 | `exists` | `io` | +| 连接进行中 | `again` | `io` | +| 不是套接字 / 地址族不支持 | `invalid` | `io` | +| 描述符耗尽(套接字) | `no_space` | `io` | +| 连接被重置 | `closed` | `closed` | + +这满足 **M2**。但 §2 写明了 M2 单独出现不足以决定修法,而这里恰好就是那种情况: + +**把 Linux 改成跟 Windows 一致,是把一个错答案换成另一个错答案。** +`kal_err_not_found` 经 `okm_errno` 是 **`ENOENT`**。一个 C 库程序连一个没人监听的 +端口,今天拿到 `EIO`,改完拿到 `ENOENT` —— 而它要的是 `ECONNREFUSED`。 +所有检查 `errno == ECONNREFUSED` 的重试循环,改前改后一样不触发;而 +`ENOENT` 对一个套接字来说比 `EIO` 更容易把人引到错误的方向(去找一个不存在的路径)。 + +⇒ 这一组的**正确**修法是给 `kal_error` 加网络词(refused / unreachable / in-use), +那是一次远大于本轮的接口变动,而**今天没有任何消费者测量支撑它**。 + +**本轮的处理:记录,不动手。** +- 在 openkal-musl `README.md` 限制表加一行,写明「网络失败的原因在这个接口里没有 + 词,三个实现的答案还不一致」,并把两张表并列指出来; +- 在 issue #28 的回复里把它作为「同一形状的第四条」提出,等一次真实测量。 + +这和 §3.2 的区别正是本轮的分界线:`ENOEXEC → kal_err_not_program → ENOEXEC` +是**一一对应、无判断余地**的;网络那一组的每一个候选词都要做判断,而判断需要 +测量。**能一一对应的加词,需要判断的等测量。** + +### 6.2 musl 的 `execvp` 不补 `/bin/sh` 回退 + +POSIX 要求 `execvp` 在 ENOEXEC 时改用 shell 运行;musl 上游有意不做 +(`musl/src/process/execvp.c` 的 `default: return -1`)。补它会让这个端口和它所移植 +的 C 库在同一个函数上分家。`musl/PATCHES.md:93` 已经为**同一类问题**做过一次决定 +(PATH 分隔符即使在一个环境上是错的也保持冒号,理由是「两种搜法互相矛盾比 +一起错更糟」)。⇒ 同一条理由,不补。 + +### 6.3 `access(X_OK)` 的询问半边 + +issue #28 第 3 条的前半,需要接口获得一个权限词,而那在 issue 里已经被否决且理由 +成立。反馈者自己也明确说「这一半我没有测量可提供」。⇒ 不做,README 那一行继续 +如实记着它没有答案(§5.1)。 + +### 6.4 `EXDEV` / `ENOTTY` / `ERANGE` 加词 + +`EXDEV` 在 Windows 上是 `ERROR_NOT_SAME_DEVICE`,`translate_win32` 也没有 arm +—— **三个实现在 `kal_err_io` 上是一致的**,不构成 M2;也没有任何测量。 +⇒ 不满足任何判据,不做。 + +### 6.5 `WSAEINTR → kal_err_again` 与 clause 7.5 的张力(只记录) + +clause 7.5 要求被中断的调用**重试而不是上报**;`endpoint.h:42` 把 `WSAEINTR` +报成 `kal_err_again`。现代 Winsock 不再产生它(`WSACancelBlockingCall` 在 +Winsock 2 已移除),所以既无测量也无实际后果。⇒ 记在这里,本轮不动。 + +### 6.6 `openkal-macos-abi` worktree + +同一个包的另一个分支,停在 openkal 0.9.0。本轮不动;它跟上来的时候会一起带上 +§3.1 的改动,因为那是同一个仓库的 `main`。 + +--- + +## 7. 需要 review 决定的三点 + +1. **新值叫什么。** 我推荐 `kal_err_not_program`(与 `kal_err_not_directory` 同形)。 + 反对 `kal_err_not_executable` 的理由见 §3.2,但如果你认为「executable」在这里 + 足够清楚,这是一个可以推翻我的地方。 + +2. **§6.1 记录而不动,是否接受。** 三个实现今天在网络错误上不一致是**事实**, + 我判断动它会让消费者从一个错答案换到另一个错答案。如果你认为「一致」本身 + 就值得先拿到,那 §6.1 要从「不做」移到「做」,`openkal-linux/src/sys.h` 与 + `openkal-macos/src/sys.h` 各加约 12 个 case。 + +3. **是否一轮做完。** §4.1 论证了拆两轮反而更贵(精确版本要求 ⇒ 走两遍全图)。 + 如果你希望 §3.1 先落地救 macOS,那就是接受走两遍。 + +--- + +## 8. 这份方案自己的边界 + +- §3.1 的结论**目前是读源码得出的,还没跑过**。落地顺序第 1 步就是把它变成一次 + 真实的失败读数;如果那一步没红,§3.1 整条要撤回重写。 +- §1.2 的控制读数是在 **host glibc 2.44 / x86_64** 上取的,不是在这个端口里取的。 + 端口侧的 `EIO` 是反馈者测的,我只核了源码链条。§3.3(b) 的 probe 用例会把这一半 + 也变成本仓库自己的读数。 +- 反馈里「一个 755、非 ELF、无 `#!` 的文件」在 **aarch64 + binfmt_misc** 上可能匹配 + 到别的处理器而不是 ENOEXEC。probe 用例要用一行普通文本(不以 `#!` 开头、不是 + 任何已注册格式),并且**只在 CI 的四行上断言**,不对其他环境作承诺。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e36a446..677eb21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,14 +44,14 @@ jobs: fail-fast: false matrix: include: - # ⭐ `net`, `fork` and `shell` STATE WHAT THE BACKEND BENEATH THIS ROW + # `net`, `fork` and `shell` STATE WHAT THE BACKEND BENEATH THIS ROW # PROVIDES, and they are part of the criterion rather than a # convenience. openkal permits an implementation to decline an # interface in whole (clause 3), so "the probe was not run" and "the # probe was run and the refusal was the expected answer" are # different outcomes, and only the second is evidence. # - # ⭐⭐ `abort` NAMES WHAT EACH BACKEND'S `kal_abort` PRODUCES, AND IT IS + # `abort` NAMES WHAT EACH BACKEND'S `kal_abort` PRODUCES, AND IT IS # THREE DIFFERENT THINGS ON PURPOSE. # # Until 0.6.0 `abort` did not end the program at all: musl's `raise` is @@ -71,7 +71,7 @@ jobs: - { name: 'linux, gcc', os: ubuntu-24.04, toolchain: 'gcc@16.1.0', target: '', net: 'yes', fork: '--fork', shell: '--shell', abort: '--abort-signal', dirtime: '--dir-time' } - { name: 'linux, llvm', os: ubuntu-24.04, toolchain: 'llvm@22.1.8', target: '', net: 'yes', fork: '--fork', shell: '--shell', abort: '--abort-signal', dirtime: '--dir-time' } - { name: 'macos, llvm', os: macos-14, toolchain: 'llvm@20.1.7', target: '', net: 'yes', fork: '--fork', shell: '--shell', abort: '--abort-status', dirtime: '--dir-time' } - # ⚠️ openkal-windows declines `openkal.space`: this system has no + # openkal-windows declines `openkal.space`: this system has no # primitive that copies an address space and starts a context in the # copy, and inventing one would be the simulation clause 3.1 # forbids. `--no-fork` asserts the refusal. @@ -91,7 +91,7 @@ jobs: # the index is handed a manifest pointing at a directory that exists # nowhere. # - # ⚠️ THAT HAS HAPPENED. This step runs BEFORE the rewrite, so what it + # THAT HAS HAPPENED. This step runs BEFORE the rewrite, so what it # examines is what the commit contains rather than what the job has done # to it. - name: The committed manifest names no local directory @@ -140,7 +140,7 @@ jobs: - name: Install mcpp run: | xlings update - # ⚠️ THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does + # THE PIN MAY NAME THE RELEASE THIS RUN IS VALIDATING, which does # not exist yet — that is the whole point of MCPP_SOURCE_REF. Bootstrap # from whatever the index has; the step below replaces it with the # build under review, and the pin is what an ordinary run tests. @@ -151,13 +151,13 @@ jobs: fi mcpp --version mcpp self config --mirror GLOBAL - # ⭐⭐ CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. + # CROSS-VALIDATION: BUILD THE mcpp UNDER REVIEW AND USE THAT ONE. # # Empty in the ordinary run, so this job keeps testing the RELEASED # mcpp the pin above names. Set it — `workflow_dispatch` input, or the # repository variable — and the same job runs against that source. # - # ⚠️ THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp + # THIS EXISTS BECAUSE THE ORDER USED TO BE WRONG. Several mcpp # releases went out green and only then turned this ecosystem red: the # engine's own CI cannot see a defect that appears only in a real # dependency graph, and this repository could not see the engine until @@ -170,7 +170,7 @@ jobs: [ -d "$src" ] || git clone --quiet --depth 1 \ --branch "$MCPP_SOURCE_REF" \ https://github.com/mcpp-community/mcpp.git "$src" - # ⚠️ THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp + # THE CLONE'S OWN WORKSPACE PIN MUST NOT DECIDE WHICH mcpp # BUILDS IT. `.xlings.json` at mcpp's root pins the mcpp that # compiles mcpp, and that pin does not move when mcpp is released — # so a build inside the checkout obeys it and tries to install a @@ -183,7 +183,7 @@ jobs: # above, which is exactly what removing the file leaves. rm -f "$src/.xlings.json" ( cd "$src" && mcpp build --release ) - # ⚠️ BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and + # BOTH SPELLINGS, AND NO `-perm`. The matrix reaches Windows and # macOS runners too: on Windows the artefact is `mcpp.exe`, and # `-perm -u+x` is not a question that filesystem answers the way this # expects. Measured: `Finished release [optimized] in 173.44s` @@ -197,7 +197,7 @@ jobs: \( -name mcpp -o -name mcpp.exe \) | head -1) [ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; } echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" - # ⚠️ Reported, because a PATH entry that does not win looks exactly + # Reported, because a PATH entry that does not win looks exactly # like one that does until something built with the wrong engine. echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" fi @@ -215,7 +215,7 @@ jobs: [ -n '${{ matrix.target }}' ] && extra='--target ${{ matrix.target }}' mcpp build $extra - # ⭐ AN OPTIONAL INTERFACE IS REFERENCED WEAKLY, WHICH IS A PROPERTY OF + # AN OPTIONAL INTERFACE IS REFERENCED WEAKLY, WHICH IS A PROPERTY OF # THE OBJECT AND NOT OF ANY ONE LINK. # # `openkal.random` is optional, and clause 6.1 expresses an @@ -225,7 +225,7 @@ jobs: # every program over a backend without it fails to link, whether or not # it wanted a random byte. # - # ⚠️ THAT DEFECT SHIPPED, AND NOTHING HERE SAW IT. It was found one + # THAT DEFECT SHIPPED, AND NOTHING HERE SAW IT. It was found one # repository downstream, by a bare-metal program in openkal-llvm-runtime # linking over openkal-opensbi: # @@ -239,7 +239,7 @@ jobs: if: runner.os == 'Linux' run: | set -euo pipefail - # ⚠️ ONE FINGERPRINT DIRECTORY, ASSERTED BEFORE ANYTHING IS READ. + # ONE FINGERPRINT DIRECTORY, ASSERTED BEFORE ANYTHING IS READ. # `target/` accumulates one per configuration, and a search across all # of them answers for a build that is not this one. The step that # checks the withheld set already learned this; the assertion is made @@ -249,7 +249,7 @@ jobs: ls -d target/*/*/ 2>/dev/null | sed 's/^/ /' exit 1; } - # ⚠️⚠️ AND THIS PACKAGE'S OWN OBJECTS, WHICH IS NOT WHAT A SEARCH UNDER + # AND THIS PACKAGE'S OWN OBJECTS, WHICH IS NOT WHAT A SEARCH UNDER # `target/` FINDS. Measured while this step was extended: a search of # everything under `target/` reported `kal_net_accept` and # `kal_datagram_recv_from` as undefined STRONG references and the other @@ -275,7 +275,7 @@ jobs: || { echo "::error::kal_random_fill is not a weak reference; an optional interface has been made mandatory" grep kal_random syms.txt; exit 1; } - # ⭐⭐ AND THE SAME FOR EVERY INTERFACE THE 0.8 ROUTES REACH, WHICH IS + # AND THE SAME FOR EVERY INTERFACE THE 0.8 ROUTES REACH, WHICH IS # WHY THIS STEP IS NOT A SINGLE ASSERTION ANY MORE. # # `openkal.net`, `openkal.datagram`, `openkal.timeout` and @@ -285,7 +285,7 @@ jobs: # never opens a socket fail to link — which is the defect this step # already exists because of, arriving through a different name. # - # ⚠️ THE SYMBOL IS SOUGHT ACROSS THE PORT'S OBJECTS RATHER THAN IN ONE, + # THE SYMBOL IS SOUGHT ACROSS THE PORT'S OBJECTS RATHER THAN IN ONE, # because the routes are spread over three sources: okm_net.c, # okm_poll.c and okm_fork.c. Naming one file would assert a property of # that file and report nothing about the others. @@ -309,7 +309,7 @@ jobs: [ "$weakfail" = 0 ] || exit 1 echo " ok every optional interface is referenced weakly, and kal_time_sleep is strong" - # ⚠️⚠️ AND ONE OF THEM IS AN OBJECT, WHICH IS NOT TESTED THE WAY THE + # AND ONE OF THEM IS AN OBJECT, WHICH IS NOT TESTED THE WAY THE # ELEVEN ABOVE ARE. # # Every name above is a function, so `if (kal_net_connect)` tests the @@ -319,7 +319,7 @@ jobs: # null dereference would be one. okm_opt.h takes the address, once, and # every caller reaches the word through that. # - # ⚠️ WHAT THIS STEP CAN AND CANNOT SEE. That the reference is weak, it + # WHAT THIS STEP CAN AND CANNOT SEE. That the reference is weak, it # can. That the source takes the address rather than reading the value, # it cannot --- both forms leave a relocation. The rule is stated where # it is used and asserted here only against the failure a link can @@ -332,7 +332,7 @@ jobs: *) echo " weak object: kal_process_props" ;; esac - # ⭐⭐ ASKING WHETHER A STREAM IS A TERMINAL GETS THE RIGHT ANSWER. + # ASKING WHETHER A STREAM IS A TERMINAL GETS THE RIGHT ANSWER. # # musl's `isatty' asks with TIOCGWINSZ; this port answered only TCGETS, # so every `isatty' returned 0 — for a real terminal as readily as for a @@ -340,7 +340,7 @@ jobs: # and a program deciding on colour or on line buffering decided wrongly # and in silence. # - # ⭐ THE CRITERION IS A RELATION, NOT A VALUE. `isatty` under a pipe and + # THE CRITERION IS A RELATION, NOT A VALUE. `isatty` under a pipe and # under a pseudo-terminal must DIFFER, and must differ the same way the # system's own C library does. A test asserting "0 in a pipe" alone would # have passed throughout the defect. @@ -387,7 +387,7 @@ 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" - # ⭐⭐ THE INTERNAL OVERLAY STOPS AT THIS PACKAGE'S BOUNDARY. + # THE INTERNAL OVERLAY STOPS AT THIS PACKAGE'S BOUNDARY. # # musl reaches its own declarations through `src/include`, whose headers # define `hidden`, `weak` and `weak_alias` — names that mean something @@ -403,7 +403,7 @@ jobs: # WHAT IS WITHHELD IS NOT DEFINED BY THIS PACKAGE, AND WHAT IS NOT # WITHHELD IS. # - # ⚠️⚠️ THE CRITERION IS THE PACKAGE'S OWN OBJECTS, NOT A PROGRAM'S LINK. + # THE CRITERION IS THE PACKAGE'S OWN OBJECTS, NOT A PROGRAM'S LINK. # # It was a program's link, and that measured the wrong thing. Under one # toolchain the probe built successfully for symbols this package does not @@ -416,7 +416,7 @@ jobs: # A definition either is in these objects or is not. That holds under every # toolchain and cannot be satisfied by a host C library. # - # ⚠️ AND THE OBJECTS ARE SCOPED TO ONE FINGERPRINT DIRECTORY. `target/` + # AND THE OBJECTS ARE SCOPED TO ONE FINGERPRINT DIRECTORY. `target/` # accumulates one per configuration, so a search across all of them reads # definitions from a build made before the change --- which is how this # check first reported every withheld symbol as still present. @@ -467,13 +467,13 @@ jobs: test -s compile_commands.json \ || { echo "::error::no compile_commands.json — nothing to check"; exit 1; } - # ⚠️⚠️ PER ROW, NOT OVER THE FILE. This example depends on the + # PER ROW, NOT OVER THE FILE. This example depends on the # package by path, so THE PROVIDER'S OWN ROWS ARE IN THIS SAME FILE # and they carry the overlay legitimately — that is what "private, # not unused" means. A `grep` over the whole file cannot tell the two # apart and would call the package's own build a leak. # - # ⚠️ Separators are normalised because the Windows runner writes + # Separators are normalised because the Windows runner writes # `…\musl\src\include`, and a check that silently stops matching on # one platform is a check that platform does not have. norm='(.arguments // (.command | split(" "))) | join(" ") | gsub("\\\\"; "/")' @@ -482,7 +482,7 @@ jobs: jq -r ".[] | select((.file | gsub(\"\\\\\\\\\"; \"/\")) | test(\"examples/cross-hello\") | not) | $norm" \ compile_commands.json > provider.txt - # ⚠️ DENOMINATORS ON BOTH SIDES. With no consumer row every absence + # DENOMINATORS ON BOTH SIDES. With no consumer row every absence # below is vacuously true; with no provider row the control is. cons="$(wc -l < consumer.txt)"; prov="$(wc -l < provider.txt)" echo " rows: consumer=$cons provider=$prov" @@ -513,7 +513,7 @@ jobs: # openkal, it does not name an implementation, and it says nothing about # the platform. # - # ⚠️ FOUR PROBES, ONE RUNNER. tools/run-probe.sh holds the watchdog, the + # FOUR PROBES, ONE RUNNER. tools/run-probe.sh holds the watchdog, the # report of where a program that stopped was, and the two readings of the # output. Four copies of that would be four places for one of them to fall # behind. @@ -585,7 +585,7 @@ jobs: echo "::error::uname reports '$got' and the manifest says '$want'"; exit 1; } echo " ok uname reports $want" - # ⭐ THE NAMES THIS LIBRARY MUST NOT TAKE FROM A PROGRAM ABOVE IT. + # THE NAMES THIS LIBRARY MUST NOT TAKE FROM A PROGRAM ABOVE IT. # # Reported as openkal-musl#13. This one is a COMPILE-TIME criterion: the # source declares `hidden`, `weak` and `weak_alias` as ordinary @@ -593,7 +593,7 @@ jobs: # compile. It is run as well as built so that something links afterwards, # which is what distinguishes a source that compiles from a package that # works. - # ⭐ A THREAD CREATED FROM C++ IS THE THREAD THAT IS JOINED. + # A THREAD CREATED FROM C++ IS THE THREAD THAT IS JOINED. # # musl's C++ `pthread_t` was `unsigned long`, which is thirty-two bits on # Windows, so every std::thread there faulted when joined. The probe's @@ -606,7 +606,7 @@ jobs: bash tools/run-probe.sh examples/threads-cxx threads-cxx grep -q 'value 42, result is the argument' examples/threads-cxx/run.log - # ⭐ A DETACHED THREAD ENDS, AND THE PROGRAM THAT STARTED IT GOES ON. + # A DETACHED THREAD ENDS, AND THE PROGRAM THAT STARTED IT GOES ON. # # musl's `__unmapself` left for a 256-byte shared stack before the calls # that end a detached thread; this port's path for those calls needs far @@ -619,7 +619,7 @@ jobs: bash tools/run-probe.sh examples/threads-detached threads-detached grep -q 'detached: 8 started, 8 ended' examples/threads-detached/run.log - # ⭐ A LARGE ALLOCATION IS A MAPPING, AND A MAPPING IS WHOLE PAGES. + # A LARGE ALLOCATION IS A MAPPING, AND A MAPPING IS WHOLE PAGES. # # musl's allocator uses a mapping up to the end of its last page; the port # obtained only the length asked for, and on Windows the rest of that page @@ -639,7 +639,7 @@ jobs: bash tools/run-probe.sh examples/identifiers identifiers grep -q 'hidden+weak+weak_alias = 41' examples/identifiers/run.log - # ⭐⭐ THE ROUTES openkal 0.8 MADE POSSIBLE, EXERCISED THROUGH POSIX. + # THE ROUTES openkal 0.8 MADE POSSIBLE, EXERCISED THROUGH POSIX. # # `socket`, `bind`, `listen`, `accept`, `connect`, `sendto`, `recvfrom`, # `poll` and `select` reached this port's default arm and returned ENOSYS @@ -648,7 +648,7 @@ jobs: # a probe that called `kal_net_connect` to check that `connect` works # would be checking the wrong thing. # - # ⚠️ `matrix.net` DECIDES WHETHER THE ROW RUNS IT, and the value is a + # `matrix.net` DECIDES WHETHER THE ROW RUNS IT, and the value is a # property of the BACKEND rather than of the system. A backend that # declines `openkal.net` is behaving correctly; a row that expected it and # silently did not get it is what this must not read as a pass. @@ -658,7 +658,7 @@ jobs: MCPP_TARGET: ${{ matrix.target }} run: bash tools/run-probe.sh examples/net net - # Another program, started three ways. ⭐ WHAT IS EXPECTED IS PASSED IN + # Another program, started three ways. WHAT IS EXPECTED IS PASSED IN # RATHER THAN INFERRED: `--no-fork` requires that duplicating the calling # image be REFUSED, so a system whose backend declines `openkal.space` is # asserted to decline it rather than merely not asked. @@ -667,7 +667,7 @@ jobs: MCPP_TARGET: ${{ matrix.target }} run: bash tools/run-probe.sh examples/subprocess subprocess ${{ matrix.fork }} ${{ matrix.shell }} ${{ matrix.abort }} - # ⭐⭐ WHETHER AN ANSWER IS TRUE, WHICH IS A DIFFERENT QUESTION FROM + # WHETHER AN ANSWER IS TRUE, WHICH IS A DIFFERENT QUESTION FROM # WHETHER AN OPERATION WORKS --- AND THE ONE NO PROBE HERE WAS ASKING. # # Every probe above starts something, opens something or transfers @@ -679,7 +679,7 @@ jobs: # installation it had not performed; `sysconf(_SC_OPEN_MAX)' answered 0 # for a bound this library sets itself. # - # ⚠️ NOT ONE OF THEM IS A MISSING OPERATION, so not one appears in + # NOT ONE OF THEM IS A MISSING OPERATION, so not one appears in # `OPENKAL_MUSL_TRACE=enosys'. They were found by comparing every answer # against the host's, and this probe is that comparison kept. - name: The surface probe @@ -687,7 +687,7 @@ jobs: MCPP_TARGET: ${{ matrix.target }} run: bash tools/run-probe.sh examples/surface surface ${{ matrix.dirtime }} - # ⭐⭐ WHERE A STARTED PROGRAM'S OUTPUT WENT, WHICH THE PROBE CANNOT ASK + # WHERE A STARTED PROGRAM'S OUTPUT WENT, WHICH THE PROBE CANNOT ASK # ABOUT ITSELF. # # A caller that redirects descriptor 1 and then starts a program used to @@ -696,7 +696,7 @@ jobs: # asserts the file; it cannot assert the other half, because a program # cannot read its own output. This log is that output. # - # ⚠️ BOTH DIRECTIONS, AND THE SECOND IS NOT DECORATION. Requiring only that + # BOTH DIRECTIONS, AND THE SECOND IS NOT DECORATION. Requiring only that # the redirected markers are absent would pass for a library that had # stopped letting a started program write anywhere at all; requiring only # that the inherited one is present would pass for the defect. The probe's @@ -716,14 +716,14 @@ jobs: exit 1; } echo " ok redirected output reached the file; inherited output reached the caller" - # ⭐ AN OPERATION THIS LIBRARY DOES NOT HAVE CAN SAY SO WHEN ASKED. + # AN OPERATION THIS LIBRARY DOES NOT HAVE CAN SAY SO WHEN ASKED. # # The default arm of the dispatcher answers ENOSYS in silence, and the only # way a consumer could learn WHICH operation was missing was to read # port/src/okm_syscall.c. Two rounds of openkal-linux#13 went on that # question. # - # ⚠️ THREE OBSERVATIONS, BECAUSE THE INTERESTING FAILURES ARE THE QUIET + # THREE OBSERVATIONS, BECAUSE THE INTERESTING FAILURES ARE THE QUIET # ONES: a channel that reports nothing when asked, a channel that reports # when it was not asked, and a channel that reports the same thing once per # attempt so that a reader counting lines concludes it happened once. @@ -748,7 +748,7 @@ jobs: cxx_runtime = "host-coupled" TOML sed -i 's/^ //' "$d/mcpp.toml" - # ⚠️⚠️ THE OPERATION THIS PROBE NAMES MUST STILL BE ABSENT, AND THE + # THE OPERATION THIS PROBE NAMES MUST STILL BE ABSENT, AND THE # ONE IT NAMED STOPPED BEING SO. # # It was `symlinkat`, chosen because openkal had no atom that creates a @@ -758,7 +758,7 @@ jobs: # diagnostics for a hundred attempts, which is the same reading a # broken diagnostic channel gives. # - # ⭐ SO THE ABSENCE IS NOW ASSERTED RATHER THAN ASSUMED. `mknodat` + # SO THE ABSENCE IS NOW ASSERTED RATHER THAN ASSUMED. `mknodat` # creates a device node, which openkal does not express and is not # going to; if a case for it ever appears, the grep below fails and # says to pick another operation --- instead of this step passing @@ -810,7 +810,7 @@ jobs: - name: The same program, built the ordinary way, as a control run: | - # ⚠️ THIS STEP EXISTS BECAUSE IT ALREADY EARNED ITS PLACE. + # THIS STEP EXISTS BECAUSE IT ALREADY EARNED ITS PLACE. # # The cross-built copy failed on a machine of the other system, and the # obvious reading was a defect in this port or in the way it was @@ -821,7 +821,7 @@ jobs: # # Two failures that look alike are told apart by building the same # source two ways, and that is cheap enough to do every time. - # ⚠️ The target has to be named here as it is everywhere else in this + # The target has to be named here as it is everywhere else in this # job: one system needs it, and a step that omitted it built for that # system's other toolchain and failed at the link with names kernel32 # supplies. Measured, on the first run of this step. @@ -899,7 +899,7 @@ jobs: bash tools/probe-cross-macos.sh aarch64 bash tools/probe-cross-macos.sh x86_64 - # ⚠️ LINKING IS NOT RUNNING, AND THIS IS WHERE THAT IS ADMITTED. + # LINKING IS NOT RUNNING, AND THIS IS WHERE THAT IS ADMITTED. # # Everything above happens on a machine that is not the system in # question, and everything above is about the objects and the link. Whether diff --git a/README.md b/README.md index 38e8959..1e6a777 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ openkal-musl = "0.11.0" It names no implementation and no platform: a C library is the one consumer that must know which implementation it needs, so it declares that itself. -⚠️⚠️ **That line is for a C program. A C++ program names the runtime instead, and +**That line is for a C program. A C++ program names the runtime instead, and naming both is an error rather than a redundancy.** `openkal-llvm-runtime` supplies libc++, libc++abi and libunwind configured for @@ -34,7 +34,7 @@ error: dependency 'mcpplibs.openkal-musl' has irreconcilable versions: openkal-llvm-runtime = "0.6.0" # carries openkal-musl 0.10.0 ``` -⭐ Recorded here because it was got wrong by the people who maintain this +Recorded here because it was got wrong by the people who maintain this package: the consumer who reported openkal-linux#13 builds C++, and was told to change the line above rather than this one. The version table below is what a consumer needs to answer it without asking. @@ -137,7 +137,7 @@ four is now composed above them rather than refused. `examples/net` and `examples/subprocess` assert them, written against POSIX and naming no openkal symbol. -**⚠️ Every one of those four depends on what is beneath.** Clause 3 permits an +**Every one of those four depends on what is beneath.** Clause 3 permits an implementation to provide an interface in whole or not at all, and the four interfaces are optional. Where a backend declines one, the routes that use it report `ENOSYS` — the port takes a weak reference and tests it before calling, @@ -155,8 +155,8 @@ answer that leaves a program wrong without telling it. | 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. | | ~~symbolic links~~ | **answered since 0.7.0** — `symlink`, `readlink`, and `stat`/`lstat` telling the two questions apart | openkal 0.9 carries `kal_fs_link_create` and `kal_fs_link_read` as operations of `openkal.fs`, and `kal_fs_props` takes the directory, so this port asks whether the volume has such nodes before it uses them. Where it does not, the refusal is what the enquiry already said. | -| permission bits | `chmod` reports `ENOSYS`; `stat` reports a mode assembled from what openkal knows | `kal_node_info` carries `writable` — one boolean, not a mode word — and `kal_fs_open` takes flags rather than a mode. Mapping the owner-write bit onto it would make `chmod(0600)` succeed and `stat` report something else, which is the shape this port exists to avoid. | -| the identity of a node | `st_dev` and `st_ino` are the implementation's answer where it has one, and **zero for both where it has none** | ⚠️ They were the constants 0 and 1, so every file compared equal to every other: `std::filesystem::equivalent` on two separately created files answered `true` **with no error**. openkal 0.9 carries an identity and reports whether it knows one; a caller must not read two zeroes as sameness, which is why nothing is invented for an implementation that cannot distinguish nodes. | +| permission bits | `chmod`/`fchmodat` report `ENOSYS` for a request whose read or write bits differ from what `stat` already reports for the node; **since 0.14.0** a request that changes only the execute bits, to the one shape a class with a read bit can be given — all three set, or all three clear — succeeds and round-trips through `stat`. A request equal to the mode already reported always succeeds, even where the volume does not claim `KAL_FS_PROP_EXECUTABLE`, because nothing is then being asked of it. `fchmod` still reports `ENOSYS` unconditionally: this port keeps no name for an open file to hand `kal_fs_set_executable_at` | `kal_node_info` carries `writable` — one boolean, not a mode word — so the read and write bits this port can report are always the same across the three classes, and a request for anything else would report success for a mode `stat` does not then show, which is the shape this port exists to avoid. openkal 0.13 added `kal_fs_set_executable_at` for exactly the one bit this port *can* honestly change; SPEC.md clause 11 entry 6 states why it is a property of the node and not a permission. | +| the identity of a node | `st_dev` and `st_ino` are the implementation's answer where it has one, and **zero for both where it has none** | They were the constants 0 and 1, so every file compared equal to every other: `std::filesystem::equivalent` on two separately created files answered `true` **with no error**. openkal 0.9 carries an identity and reports whether it knows one; a caller must not read two zeroes as sameness, which is why nothing is invented for an implementation that cannot distinguish nodes. | | ownership | `chown` reports `ENOSYS`; `stat` reports 1000 for both | as the row above: a capability-oriented environment has no principal for an owner to name. | | a mode given at creation | `open(…, O_CREAT, 0600)` and `mkdir(path, 0700)` **succeed** and `stat` afterwards reports 0666 and 0777 | the row above, in the one place where it does not read as a refusal. openkal opens a file for a purpose and not for an audience, so the argument has nowhere to go. Refusing every mode but the one `stat` will report would refuse nearly every program; what a caller can rely on instead is stated below. | | entropy | `getrandom` reports `ENOSYS` where the backend declines `openkal.random` | openkal has no source of one to require, and this port does not invent one. The allocator's cookie and the stack canary are derived from the clock and from an address; neither is a security property here. | @@ -164,19 +164,21 @@ answer that leaves a program wrong without telling it. | an immediate answer about a started program | `waitpid(…, WNOHANG)` returns without the program having finished, but may wait up to one polling interval of the implementation beneath (one millisecond on Linux) | `kal_timeout_wait_process` takes a bound and openkal spells "no bound" as zero, so a caller that does not want to wait asks for the smallest bound there is. An environment rounds a bound up to what its clock can distinguish; a bound shorter than the clock is a promise no environment can keep. | | closing a standard stream in a program being started | `posix_spawn_file_actions_addclose(&fa, 0…2)` makes the spawn report `ENOSYS`; above position two it is performed, because nothing there is inherited | openkal has no value meaning "no stream", and the value that looks like one — zero — means the opposite: the stream the caller has. Accepting the action and not performing it would hand a program the standard input its caller had just taken away. | | starting a program upon a stream whose handle is zero | a caller that redirects its **output** onto its own standard input and then starts a program gets `ENOSYS` | `kal_spawn_streams` reserves zero for inheritance and `kal_stream` reserves nothing, so an implementation whose streams are the environment's own descriptors hands out zero for standard input. The two agree at position zero and cannot be told apart anywhere else. Reported upstream; refused here rather than answered wrongly. | -| ~~a version a program can read~~ | **answered since 0.9.0.** `uname`'s `release` field is this package's version, and `OPENKAL_MUSL_TRACE=enosys` names it on the error stream once per process before the program runs | It was the string literal `0.5.0` through every release after 0.5.0, so a program that asked was not left without an answer -- it was given a false one. ⚠️ It therefore MOVES AT EVERY RELEASE: nothing here or in musl reads it (`gethostname` and `getdomainname` are musl's only consumers of `uname` and both read `nodename`), but a program above it that compares the field against a fixed string will see it change. `sysname` is `openkal` and not `Linux`, so nothing can have been reading it as a kernel version. | -| ~~**setting** the modification time of a directory~~ | **answered everywhere since 0.11.0.** ⚠️ 0.10.0 answered it only where the implementation could open a directory --- Linux and macOS could and Windows could not, because its `kal_fs_open` names `FILE_NON_DIRECTORY_FILE`. **Reading** it was never affected | 0.10.0 reached a directory's time by opening the directory for READING and stamping that, which worked and was **outside anything `fs.h` stated** --- there was no route to a directory's time at all. openkal 0.10 added `kal_fs_set_modified_at`, which takes a NAME, and every implementation answers it: the Windows one opens for the attribute alone, which reaches a directory where opening a FILE cannot. ⭐ The divergence this row recorded was caused by a missing declaration, and it went away when the declaration arrived. | -| ~~a lock on a file~~ | **`fcntl(F_SETLK)` and `F_SETLKW` take a real lock since 0.11.0.** ⚠️ They used to answer 0 and do nothing, so **two programs took one exclusive lock and both were told they had it**. ⭐ The holder is the **open file**, not the program: a SECOND open file of one name is refused here, where the older process-held form would have granted it and a library that opened one file twice would have destroyed its own lock. `F_GETLK` is still refused, and `flock` has no case | 0.10.0 refused all three and said the refusal was **temporary** in a way the permission row is not — every environment beneath openkal can lock a byte range and what was missing was a word. openkal 0.10 added `kal_fs_lock` with `KAL_FS_PROP_LOCKS`, and this is what that record said would happen. `F_GETLK` remains refused because it asks whether a lock **would** block without taking one, and openkal has no operation that answers a question without performing it — the absence clause 6.3 records for readiness. | -| whether a file may be executed | `access(path, X_OK)` answers **yes for anything that exists**, and starting a name that exists and cannot be run still ends the caller with 127 | `kal_node_info` carries `writable` and no other permission, so "it is there" is the whole of what this port can answer. The two halves are the same gap: the enquiry cannot tell, and neither can the check `posix_spawn` makes before starting. openkal-linux knows — its own duplicate is the thing that fails — and has been asked to report it. | +| ~~a version a program can read~~ | **answered since 0.9.0.** `uname`'s `release` field is this package's version, and `OPENKAL_MUSL_TRACE=enosys` names it on the error stream once per process before the program runs | It was the string literal `0.5.0` through every release after 0.5.0, so a program that asked was not left without an answer -- it was given a false one. It therefore MOVES AT EVERY RELEASE: nothing here or in musl reads it (`gethostname` and `getdomainname` are musl's only consumers of `uname` and both read `nodename`), but a program above it that compares the field against a fixed string will see it change. `sysname` is `openkal` and not `Linux`, so nothing can have been reading it as a kernel version. | +| ~~**setting** the modification time of a directory~~ | **answered everywhere since 0.11.0.** 0.10.0 answered it only where the implementation could open a directory --- Linux and macOS could and Windows could not, because its `kal_fs_open` names `FILE_NON_DIRECTORY_FILE`. **Reading** it was never affected | 0.10.0 reached a directory's time by opening the directory for READING and stamping that, which worked and was **outside anything `fs.h` stated** --- there was no route to a directory's time at all. openkal 0.10 added `kal_fs_set_modified_at`, which takes a NAME, and every implementation answers it: the Windows one opens for the attribute alone, which reaches a directory where opening a FILE cannot. The divergence this row recorded was caused by a missing declaration, and it went away when the declaration arrived. | +| ~~a lock on a file~~ | **`fcntl(F_SETLK)` and `F_SETLKW` take a real lock since 0.11.0.** They used to answer 0 and do nothing, so **two programs took one exclusive lock and both were told they had it**. The holder is the **open file**, not the program: a SECOND open file of one name is refused here, where the older process-held form would have granted it and a library that opened one file twice would have destroyed its own lock. `F_GETLK` is still refused, and `flock` has no case | 0.10.0 refused all three and said the refusal was **temporary** in a way the permission row is not — every environment beneath openkal can lock a byte range and what was missing was a word. openkal 0.10 added `kal_fs_lock` with `KAL_FS_PROP_LOCKS`, and this is what that record said would happen. `F_GETLK` remains refused because it asks whether a lock **would** block without taking one, and openkal has no operation that answers a question without performing it — the absence clause 6.3 records for readiness. | +| whether a file may be executed | **Answered on both halves since openkal 0.13.** `access(path, X_OK)` and `stat`'s execute bits follow `KAL_INFO_EXECUTABLE` when the enquiry reports the position filled (a file, on a volume that claims `KAL_FS_PROP_EXECUTABLE`, since 0.14.0 also settable — see the row above) and answer **yes** where it does not — a directory, or a volume that does not claim the property. Starting a name that exists and cannot be run no longer ends the caller at 127 either: `posix_spawn` reports a reason and returns no pid — `ENOENT`, `EACCES`, `EISDIR`, and, since openkal 0.13's `kal_err_not_program`, **`ENOEXEC`** for a name that is there, may be started, and is not in a form this environment recognises | `kal_node_info` carried `writable` and no other permission before 0.13, so "it is there" was the whole of what this port could answer, and a start that could not happen was folded into `kal_err_io` — reporting a device failure when none occurred, and giving a C library above no way to map the failure back to `ENOEXEC`, which every environment this port targets has natively. **Versioned, because the three implementations did not move together.** openkal-linux has reported the reason for a start that did not happen since 0.8.0, and reports `kal_err_not_program` for it since 0.13.0. openkal-macos reported `kal_ok` and a handle for **every** name that could not be started — including one that does not exist at all — until 0.10.0; a program above an older openkal-macos still meets 127, indistinguishable from a program that ran and returned that status. openkal-windows fails synchronously in the caller (`CreateProcessW` needs no pipeline for it) and maps the condition to `kal_err_not_program` since 0.8.0. | | descriptors above 2 crossing into a started program | a started program receives standard input, output and error and **nothing else**; a non-close-on-exec descriptor 4 is not there, and `fcntl(F_SETFD, 0)` upon one therefore changes nothing | `kal_spawn_streams` has exactly three positions and openkal has no general form for placing a stream at position *n*. `posix_spawn_file_actions_adddup2` above position two is already refused rather than accepted, so the two agree; only implicit inheritance is lost. A general form has been asked for. | -| ~~**the working directory a started program runs in**~~ | **answered since 0.12.0**, together with `posix_spawn_file_actions_addchdir_np` and `addfchdir_np`. openkal 0.11 gave `kal_spawn` a second directory — `base` resolves the program's NAME, `work` is where it RUNS — and a copy that called `chdir` carries its own into the `execve` it then performs. The rest of this row is what it said before | ⚠️ this one was silent, and it is the one a consumer is most likely to meet: the call reported success, the caller's own paths followed it, and only the started program disagreed. `kal_process_spawn`'s `base` is what the program's NAME resolves against, and resolving a name is not entering a directory — so naming a program and naming where it runs are two directories and openkal 0.10 carried one. ⭐ It could not be composed here either: `chdir` in a copy before starting would need openkal to have an operation that moves a running program's working directory, and it deliberately has none. The route was a declaration rather than a composition, which is the same shape as the directory-time row above. | ⚠️ this one is silent, and it is the one a consumer is most likely to meet: the call reports success, the caller's own paths follow it, and only the started program disagrees. `kal_process_spawn`'s `base` is what the program's NAME resolves against, and resolving a name is not entering a directory — so naming a program and naming where it runs are two directories and openkal 0.10 carries one. ⭐ It cannot be composed here either: `chdir` in a copy before starting would need openkal to have an operation that moves a running program's working directory, and it deliberately has none. A per-spawn form has been asked for; see `.agents/docs/2026-08-30-openkal-0.11-start-design.md`. | -| ~~**terminating what a started program itself started**~~ | **answered since 0.12.0 for the form that works**, which is `posix_spawn` with `POSIX_SPAWN_SETPGROUP` and a zero group: the started program forms a unit, what it starts inherits the unit, and `kill(-n)` upon the identifier `posix_spawn` returned reaches all of it — including a shell's backgrounded work, which is the case a timeout has to reach. `setpgid(0, 0)` in **this** program forms a unit too. Naming another program's group is still `EPERM`, and `setsid` is still `EPERM` | openkal 0.11 gave `kal_spawn` a `job`, which is where a unit is established: the caller says which unit, and the implementation performs the placement. ⭐ What this costs is stated in the row below, because it is one composition and not a general gap. | -| **`fork(); setpgid(0, 0); exec …`** | the copy forms a unit and the original **cannot name it**, so `kill(-n)` from the original reports `ESRCH`. Use `posix_spawn` with `POSIX_SPAWN_SETPGROUP`, which is the row above | ⭐⭐ this is a property of the interface and not a shortfall of this port. A unit is named by a handle, openkal handles are built from an index into the holder's own table (clause 6.7), and nothing conveys one across a spawn (clause 11 entry 9) — so the unit the copy forms is the copy's, and the original never learns of it. ⚠️ **0.12.0 reached for the nearest unit it could name, which was its own**, and every negative identifier that matched no child therefore named the caller's group: an enquiry about a unit that did not exist was answered *yes*, and a signal aimed at one **ended the caller together with everything it led**. 0.13.0 answers `ESRCH` and `examples/subprocess` checks it. openkal 0.12 records the whole of it in clause 11 entry 9, so the next implementation meets it in the specification. | -| ~~how many processors there are~~ | **answered since 0.11.0.** `sched_getaffinity`, and therefore `std::thread::hardware_concurrency()` and `sysconf(_SC_NPROCESSORS_ONLN)`, report the real count | ⚠️ it used to be **silent**: a program sizing a pool of workers got one worker and no error. openkal 0.10 added `kal_task_parallelism`. ⭐ Zero from that enquiry means *cannot say* and is reported here as a refusal rather than as a bitmap of one processor, because musl would read the latter as a fact this port had invented. | +| ~~**the working directory a started program runs in**~~ | **answered since 0.12.0**, together with `posix_spawn_file_actions_addchdir_np` and `addfchdir_np`. openkal 0.11 gave `kal_spawn` a second directory — `base` resolves the program's NAME, `work` is where it RUNS — and a copy that called `chdir` carries its own into the `execve` it then performs. The rest of this row is what it said before | this one was silent, and it is the one a consumer is most likely to meet: the call reported success, the caller's own paths followed it, and only the started program disagreed. `kal_process_spawn`'s `base` is what the program's NAME resolves against, and resolving a name is not entering a directory — so naming a program and naming where it runs are two directories and openkal 0.10 carried one. It could not be composed here either: `chdir` in a copy before starting would need openkal to have an operation that moves a running program's working directory, and it deliberately has none. The route was a declaration rather than a composition, which is the same shape as the directory-time row above. | this one is silent, and it is the one a consumer is most likely to meet: the call reports success, the caller's own paths follow it, and only the started program disagrees. `kal_process_spawn`'s `base` is what the program's NAME resolves against, and resolving a name is not entering a directory — so naming a program and naming where it runs are two directories and openkal 0.10 carries one. It cannot be composed here either: `chdir` in a copy before starting would need openkal to have an operation that moves a running program's working directory, and it deliberately has none. A per-spawn form has been asked for; see `.agents/docs/2026-08-30-openkal-0.11-start-design.md`. | +| ~~**terminating what a started program itself started**~~ | **answered since 0.12.0 for the form that works**, which is `posix_spawn` with `POSIX_SPAWN_SETPGROUP` and a zero group: the started program forms a unit, what it starts inherits the unit, and `kill(-n)` upon the identifier `posix_spawn` returned reaches all of it — including a shell's backgrounded work, which is the case a timeout has to reach. `setpgid(0, 0)` in **this** program forms a unit too. Naming another program's group is still `EPERM`, and `setsid` is still `EPERM` | openkal 0.11 gave `kal_spawn` a `job`, which is where a unit is established: the caller says which unit, and the implementation performs the placement. What this costs is stated in the row below, because it is one composition and not a general gap. | +| **`fork(); setpgid(0, 0); exec …`** | the copy forms a unit and the original **cannot name it**, so `kill(-n)` from the original reports `ESRCH`. Use `posix_spawn` with `POSIX_SPAWN_SETPGROUP`, which is the row above | this is a property of the interface and not a shortfall of this port. A unit is named by a handle, openkal handles are built from an index into the holder's own table (clause 6.7), and nothing conveys one across a spawn (clause 11 entry 9) — so the unit the copy forms is the copy's, and the original never learns of it. **0.12.0 reached for the nearest unit it could name, which was its own**, and every negative identifier that matched no child therefore named the caller's group: an enquiry about a unit that did not exist was answered *yes*, and a signal aimed at one **ended the caller together with everything it led**. 0.13.0 answers `ESRCH` and `examples/subprocess` checks it. openkal 0.12 records the whole of it in clause 11 entry 9, so the next implementation meets it in the specification. | +| ~~how many processors there are~~ | **answered since 0.11.0.** `sched_getaffinity`, and therefore `std::thread::hardware_concurrency()` and `sysconf(_SC_NPROCESSORS_ONLN)`, report the real count | it used to be **silent**: a program sizing a pool of workers got one worker and no error. openkal 0.10 added `kal_task_parallelism`. Zero from that enquiry means *cannot say* and is reported here as a refusal rather than as a bitmap of one processor, because musl would read the latter as a fact this port had invented. | | hard links, named pipes, a bidirectional pair | `link` (`create_hard_link`), `mkfifo` and `socketpair` report `ENOSYS`. **`statvfs` (`std::filesystem::space`) is answered since 0.11.0** | openkal has no operation for the first three. `kal_fs_link_create` makes a node whose content is a name — a symbolic link — and there is no hard link; `kal_process_channel` is a pipe in one direction, so a bidirectional pair is not one of them. Volume capacity was in this row until openkal 0.10 added `kal_fs_capacity`. | | an alternate signal stack | `sigaltstack` reports `ENOSYS` since 0.10.0 | it used to report success and install nothing, and the enquiry that would have caught it answered 0 with a zeroed record. There are no signals here, so there is nothing for such a stack to be. | +| one C runtime and one C++ runtime per image | a static library compiled against the platform's own C runtime, or an object that runtime owns crossing the boundary — a `FILE*`, memory one side allocates and the other frees, `errno` — is not supported. Only handles and values may cross into a platform's own system interfaces | this package supplies the whole of a program's C library; a second one linked in beside it duplicates every piece of global state each keeps (the allocator's own arena, `errno`, locale) and the two do not agree about any of it. A vendor SDK distributed only as a static library built against another C runtime is `n/a` on this target — a design consequence and not a defect. openkal's own README states the same rule for every implementation built above it; the ecosystem design record (`.agents/docs/2026-09-17-openkal-ecosystem-cross-repo-design.md`, R1) states why it binds a package and not the specification. | +| a context a platform library creates | code reached from it has no C library state — no `errno`, no locale, no `pthread_self` — and must not rely on any, because none of it exists until `kal_task_start` builds it, and such a context was not built by it | musl's per-context state lives in thread-local storage this port's own thread start fills in. A callback from a platform's own thread pool, from COM, or from a framework's own callback thread (Windows, macOS's GCD) arrives on a context this library never started. A package reached this way confines such a callback to work that does not touch this library, or hands the work to a context of its own before it does (R2 of the design record above). | -**⭐ What carries confinement here, since a mode word does not.** A program that +**What carries confinement here, since a mode word does not.** A program that writes "only I may read this" as a mode is stating it in a vocabulary this environment does not have. What it does have is stronger and is not the program's to weaken: a program reaches only the directories the environment @@ -184,7 +186,7 @@ supplied it, and `port/src/okm_fd.c` states the rule — confinement is a proper of what was supplied, not of the program's cooperation. A caller with that requirement expresses it by being started with fewer directories. -**⭐ The permission row is a decision and not an omission.** The alternative was +**The permission row is a decision and not an omission.** The alternative was to ask the specification for a permission operation. It was declined: a FAT volume, a UEFI system partition and a Windows access-control list do not share a model, so an operation upon permissions is one that some resources of the @@ -192,7 +194,7 @@ interface can never satisfy — which is what clause 6.4 excludes. Refusing here and stating why is the answer; `.agents/docs` in the specification's repository records the reasoning. -**⭐⭐ And what to write instead, which a refusal by itself does not tell you.** +**And what to write instead, which a refusal by itself does not tell you.** A program that means *only I may read this* is stating it in a vocabulary this environment does not have: a permission presupposes an identity, and openkal models no identity for one to name. This is not a gap peculiar to openkal — it is @@ -211,13 +213,19 @@ are defending against rather than by mechanism: | another user of the machine | **the party that STARTS the program**, through the preopens it supplies and withholds. In a capability system "make this file private" is not an operation upon the file; it is a property of the directory you were granted | | a location you do not trust | encrypt the contents — where the location is untrusted no access control holds anyway | -⚠️ **The consequence for porting.** Code that hardens itself with `chmod(0600)` -after creating a file should not treat the refusal as a failure to create; the -directory it was given is either private already or was never going to be made -so from inside. The same reasoning covers `access(X_OK)`: an execute bit is a -permission judged against an ambient identity, Windows does not judge by a bit -at all, and WASI has no exec — so there is no atomic capability to map, and being -able to run something follows from having been granted the directory holding it. +**The consequence for porting.** Code that hardens itself with `chmod(0600)` +after creating a file — asking to change who may READ or WRITE it — should not +treat the refusal as a failure to create; the directory it was given is either +private already or was never going to be made so from inside. + +**`access(X_OK)` is a different question, and openkal 0.13 answers it +separately from the paragraph above.** Whether a node may be started +presupposes no principal, the way `chmod(0600)` does: a volume that records it +records it once for the node, not once per class of caller, so it is a +property rather than a permission — SPEC.md clause 11 entry 6 states the +argument and why it holds for this position and fails for a mode word. That is +what `KAL_INFO_EXECUTABLE` and `kal_fs_set_executable_at` answer, and this +port's rows above state what a program built on it observes. Two further boundaries are properties of the arrangement rather than omissions. @@ -241,7 +249,7 @@ as "the entry has already gone" and leaves the tree standing. descriptions; beyond that it is told so. Allocating the tables instead would place them on the allocator, and the allocator obtains its memory through them. -⚠️ **And a program may have started 256 programs it has not waited for.** An +**And a program may have started 256 programs it has not waited for.** An entry is taken when a program is started and released when it is waited for, which is what a process table is; a program that never waits holds entries for ever, and the next start reports `EAGAIN` — which is what POSIX says `fork` @@ -340,7 +348,7 @@ every one of those is about an object format or a naming convention rather than about a kernel — which is the shape of the result rather than a qualification of it. -**⚠️ It grew by 2294 more when the socket, datagram, readiness and image-copying +**It grew by 2294 more when the socket, datagram, readiness and image-copying routes were added, and that number deserves a reading rather than a footnote.** Roughly half of it is comment; of the code, the largest single piece is the state machine in `okm_net.c`, and what that machine bridges is one difference in @@ -355,7 +363,7 @@ was measuring in the first place, and the growth does not change its direction. ## Verification -### ⚠️⚠️ A program that needs an INTERPRETER could not be started, and it was first blamed on the emulator +### A program that needs an INTERPRETER could not be started, and it was first blamed on the emulator **Corrected in openkal-linux 0.12.0.** An earlier version of this section said that an `aarch64` build measured through `qemu-user` could not start a program of @@ -379,14 +387,14 @@ the interpreter and hands it that name *to open*, after the replacement, by whic time a close-on-exec descriptor is gone. The interpreter is told the file does not exist. -⭐ Isolated in twenty lines of ordinary C, with nothing of openkal in it: +Isolated in twenty lines of ordinary C, with nothing of openkal in it: ``` dirfd WITH O_CLOEXEC execveat -> ENOENT dirfd WITHOUT O_CLOEXEC STARTED ok ``` -⚠️ **It was never about architecture.** Two kinds of program need an interpreter, +**It was never about architecture.** Two kinds of program need an interpreter, and both were refused on every system: | program | needs an interpreter | @@ -417,7 +425,7 @@ the machine it runs on. `examples/subprocess` starts another program three ways — `fork`, `system`, `popen`. -**⭐ Those two say what they expect on the command line rather than inferring +**Those two say what they expect on the command line rather than inferring it.** `--fork` requires that duplicating the calling image work; `--no-fork` requires that it be *refused*. An environment whose backend declines `openkal.space` is not a failure, and an environment expected to provide it that @@ -451,7 +459,7 @@ interface at all. `fork` returns twice, so the second half is this port's: `space.h` describes that composition and says in terms that it belongs above the line, which is where it now is (`port/src/okm_fork.c`). -⚠️ **An earlier version of this file said `fork` was absent and would stay +**An earlier version of this file said `fork` was absent and would stay absent**, on the reading that clause 7.1 declines to duplicate an address space *and its execution state*. Half of that is right: the clause declines the **pair**. `openkal.space` supplies the first half by itself, and what was @@ -461,7 +469,7 @@ missing was never an atom. It is what every environment without the operation does, and two of the three beneath openkal are such environments. -⚠️ **This paragraph used to add that a caller cannot distinguish it. A caller +**This paragraph used to add that a caller cannot distinguish it. A caller can, and saying otherwise is what kept anyone from looking.** Three differences were known and `musl/PATCHES.md` states each. Two are now answered: a program that cannot be started (**0.10.0** — the name is asked about first, so `execvp` diff --git a/build.mcpp b/build.mcpp index cffb054..c053c23 100644 --- a/build.mcpp +++ b/build.mcpp @@ -1,7 +1,7 @@ import mcpp; import std; -// ⭐⭐ THE COMPILER'S OWN RUNTIME, NAMED ONLY WHEN IT HAS THAT NAME. +// THE COMPILER'S OWN RUNTIME, NAMED ONLY WHEN IT HAS THAT NAME. // // A compiler emits calls to routines no C library defines — a 128-bit shift, a // stack probe, a complex multiply on `long double`. They live in `libgcc.a` @@ -9,7 +9,7 @@ import std; // interchangeable names for one file: `-lgcc` on a clang link is a library that // does not exist. // -// ⚠️ THE MANIFEST CANNOT SAY THIS, WHICH IS WHY THERE IS A PROGRAM. A +// THE MANIFEST CANNOT SAY THIS, WHICH IS WHY THERE IS A PROGRAM. A // `[target.'cfg(windows)']` block describes the TARGET, and this is a question // about the toolchain that resolved. Both measurements arrived on 2026-08-22 // from the same missing distinction: @@ -23,19 +23,19 @@ import std; // // Each was the correct behaviour for the other configuration. // -// ⚠️ PE ONLY, AND `gcc_eh` WITH IT. On this format a thread-local variable is +// PE ONLY, AND `gcc_eh` WITH IT. On this format a thread-local variable is // reached through a helper rather than through an address the processor // supplies, and that helper is in the second archive rather than the first. // Where the graph brings `openkal-llvm-runtime`, compiler-rt's `emutls.c` is // the same answer and neither archive is consulted. // -// ⚠️ NOT ON ELF OR Mach-O. There the driver still links the compiler's runtime +// NOT ON ELF OR Mach-O. There the driver still links the compiler's runtime // even under `-nostdlib`, or the graph supplies it; adding a name here would be // a second source for something that already has one. -// ⭐⭐ WHICH COMPILER FAMILY RESOLVED — AND WHAT AN ABSENT ANSWER MEANS. +// WHICH COMPILER FAMILY RESOLVED — AND WHAT AN ABSENT ANSWER MEANS. // -// ⚠️ READ FROM THE ENVIRONMENT, NOT THROUGH A HELPER. A build program is +// READ FROM THE ENVIRONMENT, NOT THROUGH A HELPER. A build program is // compiled against the `mcpp` module of whichever tool RUNS it, so naming a // helper newer than the released tool makes this package require an unreleased // one. Measured 2026-08-22, on every row of three repositories at once: @@ -47,12 +47,12 @@ import std; // the FALLBACK written for the first. The environment variables are the // contract and `std::getenv` reads them on every version. // -// ⚠️ AND AN ABSENT VALUE IS NOT "NO COMPILER". Measured against mcpp 2026.8.19.1, +// AND AN ABSENT VALUE IS NOT "NO COMPILER". Measured against mcpp 2026.8.19.1, // which is what CI installs: `MCPP_TARGET_OS`, `MCPP_HOST`, `MCPP_OUT_DIR`, // `MCPP_MANIFEST_DIR`, `MCPP_TARGET` and `MCPP_TARGET_ARCH` are all set, and // `MCPP_COMPILER` and `MCPP_TOOLCHAIN_DIR` are absent. // -// ⭐ So absent means "a build tool from before the question could be asked" — +// So absent means "a build tool from before the question could be asked" — // and such a tool cannot produce the configuration the answer would change. The // clang-over-openkal cross to PE arrives on the same release as the variable. // The one configuration a tool that predates it can produce is the GCC one, so @@ -134,7 +134,7 @@ int main() { if (fam != "gcc") return 0; - // ⚠️ ELF NEEDS IT TOO, AND FOR THE SAME REASON THE PE BLOCK DID. The Linux + // ELF NEEDS IT TOO, AND FOR THE SAME REASON THE PE BLOCK DID. The Linux // block of the manifest named `-lgcc` unconditionally, and that resolved on // a Linux host because a payload with it happens to be installed there. // Measured 2026-08-23, a macOS host cross-building for `x86_64-linux-gnu`: diff --git a/examples/cross-hello/src/main.c b/examples/cross-hello/src/main.c index ac19f70..115edb8 100644 --- a/examples/cross-hello/src/main.c +++ b/examples/cross-hello/src/main.c @@ -11,7 +11,7 @@ int main(int argc, char** argv) { - /* ⚠️ UNBUFFERED, AND THAT IS PART OF THE PROBE. + /* UNBUFFERED, AND THAT IS PART OF THE PROBE. * * Output to a pipe is fully buffered, so a program that dies part way * through loses everything it had printed --- and what the machine that @@ -22,7 +22,7 @@ int main(int argc, char** argv) setvbuf(stdout, NULL, _IONBF, 0); int failures = 0; - /* ⚠️ THE CONDITION IS EVALUATED ONCE, AND THE FIRST VERSION DID NOT. + /* THE CONDITION IS EVALUATED ONCE, AND THE FIRST VERSION DID NOT. * * It read `(ok) ? "ok" : "FAIL"' and then `if (!(ok))', so an argument with * an effect happened twice --- and one of the arguments below is diff --git a/examples/identifiers/src/main.c b/examples/identifiers/src/main.c index 43f596f..f54f854 100644 --- a/examples/identifiers/src/main.c +++ b/examples/identifiers/src/main.c @@ -5,14 +5,14 @@ * header overlay defines it as an attribute and this package published the path * it is built from. * - * ⭐ THIS IS A COMPILE-TIME CRITERION AND IT IS THE WHOLE OF THE TEST. If any + * THIS IS A COMPILE-TIME CRITERION AND IT IS THE WHOLE OF THE TEST. If any * of the three below is still a macro, this file does not compile: in C they * were emptied, so `static int hidden = 7;' becomes `static int = 7;'. There * is nothing to run and nothing to compare --- the program exists so that * something LINKS and RUNS afterwards, which is what distinguishes a source * that compiles from a package that works. * - * ⚠️ `restrict' IS NOT AMONG THEM, and its absence here is deliberate rather + * `restrict' IS NOT AMONG THEM, and its absence here is deliberate rather * than an oversight. musl's PUBLIC headers write it --- declares * `fprintf(FILE *restrict, const char *restrict, ...)' --- so a C++ program * above this library needs the spelling defined and cannot have the name. That diff --git a/examples/malloc-large/src/main.c b/examples/malloc-large/src/main.c index bb3d9b8..d7af92c 100644 --- a/examples/malloc-large/src/main.c +++ b/examples/malloc-large/src/main.c @@ -8,7 +8,7 @@ * a page of the block lay past the end of what was obtained --- over the next * heap block's header. The program went on until the heap next walked there. * - * ⭐ WHAT IS OBSERVED. + * WHAT IS OBSERVED. * (1) An anonymous mapping of a length that ends inside a page: the rest of * that page reads as zero and can be written, and memory allocated beside * it keeps its contents. diff --git a/examples/net/src/main.c b/examples/net/src/main.c index b2873b8..e7e8945 100644 --- a/examples/net/src/main.c +++ b/examples/net/src/main.c @@ -1,6 +1,6 @@ /* A POSIX program that names no openkal symbol. * - * ⭐ THAT IS THE WHOLE OF THE CRITERION. This package's claim is that a program + * THAT IS THE WHOLE OF THE CRITERION. This package's claim is that a program * written for POSIX runs above openkal without being rewritten, and a probe that * called `kal_net_connect' to check that `connect' works would be checking the * wrong thing. Nothing below is conditional on which implementation is beneath; @@ -61,7 +61,7 @@ int main(void) check(bind(lis, (struct sockaddr*)&want, sizeof want) == 0, "it is bound to port zero"); check(listen(lis, 4) == 0, "it listens"); - /* ⭐ PORT ZERO ASKS THE ENVIRONMENT TO CHOOSE, and a program that must + /* PORT ZERO ASKS THE ENVIRONMENT TO CHOOSE, and a program that must * publish where it is listening has no other way to learn it. This is the * enquiry openkal.net added `kal_net_listener_local' for. */ struct sockaddr_in got; @@ -111,7 +111,7 @@ int main(void) char in[16]; memset(in, 0, sizeof in); - /* ⚠️ READ IN A LOOP. A read of a connection may report fewer bytes than + /* READ IN A LOOP. A read of a connection may report fewer bytes than * were sent, on every system --- and this port answers a readiness enquiry * by holding one byte, so a poll before a read makes the short read the * ordinary case rather than a rare one. A program that assumed otherwise @@ -169,7 +169,7 @@ int main(void) socklen_t slen = sizeof sender; memset(msg, 0, sizeof msg); const ssize_t got_n = recvfrom(a, msg, sizeof msg, 0, (struct sockaddr*)&sender, &slen); - /* ⚠️ THE MESSAGE ARRIVES WHOLE, WHICH IS THE PROPERTY THAT DISTINGUISHES A + /* THE MESSAGE ARRIVES WHOLE, WHICH IS THE PROPERTY THAT DISTINGUISHES A * DATAGRAM FROM A STREAM. A loop here would hide a port that had split it. */ check(got_n == 5 && memcmp(msg, "dgram", 5) == 0, "the whole message arrives at once"); check(sender.sin_family == AF_INET && ntohl(sender.sin_addr.s_addr) == 0x7f000001u, diff --git a/examples/posix/src/main.c b/examples/posix/src/main.c index 1ae9603..7098a30 100644 --- a/examples/posix/src/main.c +++ b/examples/posix/src/main.c @@ -147,7 +147,7 @@ int main(int argc, char **argv, char **envp) { * the two failed. It was reported as the first in openkal-linux#13 and it * was the second: `stat' resolves a directory perfectly well. * - * ⭐ SETTING IT USED TO BE ASSERTED HERE AS A REFUSAL, and this comment said + * SETTING IT USED TO BE ASSERTED HERE AS A REFUSAL, and this comment said * that if openkal gained the operation this observation would be the one to * say the row was out of date. It did its job: 0.10.0 opens a directory for * READING to stamp it --- which Linux and macOS perform and Windows does not @@ -187,7 +187,7 @@ int main(int argc, char **argv, char **envp) { check(counter == 80000, "80000 increments, none lost"); printf(" counter=%d\n", counter); - /* ⚠️⚠️ TWO WAITERS UPON ONE CONDITION VARIABLE, AND THE COUNT IS THE POINT. + /* TWO WAITERS UPON ONE CONDITION VARIABLE, AND THE COUNT IS THE POINT. * * The section above starts four contexts and contends a mutex hard, and it * passed throughout a defect that stopped this port dead: a broadcast reaches @@ -197,7 +197,7 @@ int main(int argc, char **argv, char **envp) { * nothing at all until a program had two, and then it cost everything: the * second waiter was never woken and the program did not end. * - * ⭐ SO THE OBSERVATION IS NOT "A CONDITION VARIABLE WORKS". It is that a + * SO THE OBSERVATION IS NOT "A CONDITION VARIABLE WORKS". It is that a * SECOND waiter is released, because the first one always was. Found in a * consumer's own test suite rather than here, and this is the line that would * have found it: every probe in this file was written from a list of known @@ -208,7 +208,7 @@ int main(int argc, char **argv, char **envp) { int made = 0; for (int i = 0; i < 2; i++) if (pthread_create(&w[i], NULL, cv_waiter, NULL) == 0) made++; - /* ⚠️ BOTH MUST BE WAITING BEFORE THE BROADCAST, or one is never queued + /* BOTH MUST BE WAITING BEFORE THE BROADCAST, or one is never queued * behind the other, the requeue is never requested, and the observation * below holds for a reason that has nothing to do with what it checks. */ for (int spin = 0; spin < 400; spin++) { @@ -259,7 +259,7 @@ int main(int argc, char **argv, char **envp) { failures += 2; } - /* ⭐⭐ THE DISPOSITION OF A SIGNAL IS TOUCHED, WHICH NOTHING HERE DID. + /* THE DISPOSITION OF A SIGNAL IS TOUCHED, WHICH NOTHING HERE DID. * * This file had thirty-six observations and three of them were about * `abort'. It contained no call to `signal' or `sigaction' anywhere --- so @@ -267,7 +267,7 @@ int main(int argc, char **argv, char **envp) { * may ASK what a signal is set to. A defect that killed any program doing * the second passed every one of the thirty-six. * - * ⚠️ ALL THREE FORMS, AND SIGABRT AMONG THEM. The C library takes a lock for + * ALL THREE FORMS, AND SIGABRT AMONG THEM. The C library takes a lock for * any change to that one disposition and blocks signals to take it, so * SIGABRT reaches code the others do not --- and the enquiry, which changes * nothing, reached it too. Two of the three forms below would have passed @@ -308,7 +308,7 @@ int main(int argc, char **argv, char **envp) { && strcmp(target, "okm-link-target.tmp") == 0, "a node's content reads back as it was written"); - /* ⭐ THE OBSERVATION THE PORT MOST NEEDED. Asking resolves and + /* THE OBSERVATION THE PORT MOST NEEDED. Asking resolves and * opening resolves, so the two agree; asking with the flag reports * the node itself. They disagreed, and a C++ library above reported * a link where a caller would have reached a file. */ @@ -318,7 +318,7 @@ int main(int argc, char **argv, char **envp) { check(lstat("okm-probe-link", &itself) == 0 && S_ISLNK(itself.st_mode), "lstat reports the node itself"); - /* ⭐⭐ AND THE THIRD QUESTION, WHICH IS NEITHER OF THOSE TWO. + /* AND THE THIRD QUESTION, WHICH IS NEITHER OF THOSE TWO. * * O_NOFOLLOW does not ask to open the link and does not ask to * open its target: it asks `is this name a link?' and expects @@ -326,7 +326,7 @@ int main(int argc, char **argv, char **envp) { * resolve --- by design --- so this port resolved, and for a link * to a name that is absent it answered ENOENT. * - * ⚠️ THAT IS A DIFFERENT ANSWER TO A DIFFERENT QUESTION, AND + * THAT IS A DIFFERENT ANSWER TO A DIFFERENT QUESTION, AND * NOTHING NEARBY LOOKED WRONG. Every operation above still held. * What failed was three layers up: libc++'s `remove_all' descends * by opening each entry O_DIRECTORY|O_NOFOLLOW and reads ENOENT as @@ -364,11 +364,11 @@ int main(int argc, char **argv, char **envp) { unlink("okm-link-target.tmp"); } - /* ⚠️ Two different files are two different files. `st_dev' and `st_ino' + /* Two different files are two different files. `st_dev' and `st_ino' * were constants, so every file compared equal to every other and a C++ * library's `equivalent' answered true with no error. * - * ⚠️⚠️ WHEN THIS FAILS, THE DEFECT IS USUALLY NOT IN THIS PACKAGE. This + * WHEN THIS FAILS, THE DEFECT IS USUALLY NOT IN THIS PACKAGE. This * port copies the identity out of `kal_node_info' and puts zero there when * the implementation does not report one --- which is permitted, and which * makes every node compare equal to every other. So a failure here says @@ -399,7 +399,7 @@ int main(int argc, char **argv, char **envp) { /* The page is the machine's and not the build's. * - * ⚠️⚠️ AND "POSITIVE POWER OF TWO" WAS TRUE OF THE VALUE THAT BROKE IT. + * AND "POSITIVE POWER OF TWO" WAS TRUE OF THE VALUE THAT BROKE IT. * This library took `kal_memory_granularity()' as its page size, and an * implementation for a machine with no memory management unit answers ONE * --- correctly, since nothing there needs rounding. One is positive and @@ -407,7 +407,7 @@ int main(int argc, char **argv, char **envp) { * the environment for one-byte extents and the program stopped inside the * first allocation that needed a new one. * - * ⭐ SO THE CRITERION IS WHAT THE ALLOCATOR REQUIRES, NOT WHAT THE NUMBER + * SO THE CRITERION IS WHAT THE ALLOCATOR REQUIRES, NOT WHAT THE NUMBER * LOOKS LIKE. A page smaller than this library's own quantum is not a page * this library can use, whatever openkal reports. */ { diff --git a/examples/subprocess/src/main.c b/examples/subprocess/src/main.c index a6352be..c263058 100644 --- a/examples/subprocess/src/main.c +++ b/examples/subprocess/src/main.c @@ -7,14 +7,14 @@ * system a command line handed to a shell * popen the same, with a channel back * - * ⭐ TWO OF THE FIRST THREE ALREADY WORKED AND NOTHING SAID SO. This package + * TWO OF THE FIRST THREE ALREADY WORKED AND NOTHING SAID SO. This package * replaces musl's `posix_spawn' with port/src/okm_spawn.c, and `system' and * `popen' are both written on `posix_spawn' --- so they have been available for * as long as that file has, and no test in this repository ever ran one. A * capability that works and is not asserted is a capability that will stop * working quietly. * - * ⚠️⚠️ AND THAT IS EXACTLY HOW THE REDIRECTION DEFECT SURVIVED. Every + * AND THAT IS EXACTLY HOW THE REDIRECTION DEFECT SURVIVED. Every * observation here reported that a program STARTED. None of them asked WHERE ITS * OUTPUT WENT, and the answer was: to the stream this program was started with, * whatever the caller had redirected onto. `popen' passed throughout, because @@ -23,11 +23,11 @@ * consumer as openkal-linux#13, not here, and the whole reason it was not here * is the shape of the question this file used to ask. * - * ⭐ THE PROGRAM THAT IS STARTED IS THIS ONE, WITH A MODE ON ITS COMMAND LINE. + * THE PROGRAM THAT IS STARTED IS THIS ONE, WITH A MODE ON ITS COMMAND LINE. * A probe that started `/bin/sh' would be a probe that only two of the four * systems can run, and the redirection criteria hold on all four. * - * ⚠️ WHAT IS EXPECTED IS STATED ON THE COMMAND LINE RATHER THAN INFERRED, which + * WHAT IS EXPECTED IS STATED ON THE COMMAND LINE RATHER THAN INFERRED, which * is the same arrangement examples/net uses and for the same reason. An * environment whose backend declines `openkal.space' is not a failure; an * environment that was expected to provide it and quietly does not IS one, and @@ -40,7 +40,7 @@ * how the implementation beneath expresses a program * that ended abnormally. * - * ⚠️⚠️ THREE VALUES AND NOT TWO, BECAUSE A CRITERION WHOSE ANSWER IS "SOMETHING + * THREE VALUES AND NOT TWO, BECAUSE A CRITERION WHOSE ANSWER IS "SOMETHING * UNUSUAL HAPPENED" HOLDS FOR THE DEFECT AS WELL AS FOR THE FIX. * * Before `abort' reached `kal_abort' it fell through to musl's `a_crash()' and @@ -76,7 +76,7 @@ extern char** environ; -/* ⚠️ THE FOUR REDIRECTED MARKERS SHARE A SUFFIX AND THE INHERITED ONE DOES NOT. +/* THE FOUR REDIRECTED MARKERS SHARE A SUFFIX AND THE INHERITED ONE DOES NOT. * * The criterion this probe cannot state by itself is that the started program's * bytes did NOT arrive on the stream this program was started with --- a program @@ -106,7 +106,7 @@ static int child_mode(int argc, char** argv) { for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--child-echo") == 0 && i + 1 < argc) { - /* ⚠️ `write' AND NOT `printf'. What is being measured is which + /* `write' AND NOT `printf'. What is being measured is which * stream descriptor 1 names in this program, and stdio would add a * buffer between the question and the answer. */ const char* t = argv[i + 1]; @@ -123,7 +123,7 @@ static int child_mode(int argc, char** argv) usleep((unsigned)atoi(argv[i + 1]) * 1000u); _exit(7); } - /* ⚠️⚠️ FORMS A UNIT AND THEN NAMES ONE THAT DOES NOT EXIST. + /* FORMS A UNIT AND THEN NAMES ONE THAT DOES NOT EXIST. * * This runs in a started program rather than in the probe because the * defect it observes is fatal: a `kill' that reaches the caller's OWN @@ -185,7 +185,7 @@ static int holds(const char* path, const char* marker, long* got) /* --- a redirection this program performed, and the program it starts -------- */ -/* ⚠️ NOTHING MAY BE PRINTED WHILE DESCRIPTOR 1 IS THE SINK, because this +/* NOTHING MAY BE PRINTED WHILE DESCRIPTOR 1 IS THE SINK, because this * program's own report would land in the file it is about to read back. Each * experiment therefore returns its findings and reports them afterwards. */ static int redirect_stdout(const char* path, int* saved) @@ -246,7 +246,7 @@ int main(int argc, char** argv) errno = 0; const pid_t kid = fork(); if (kid == 0) { - /* ⚠️ `_exit' AND NOT `exit'. The copy holds a copy of the parent's stdio + /* `_exit' AND NOT `exit'. The copy holds a copy of the parent's stdio * buffers, and running the exit handlers would write them a second * time --- which is a defect of the probe and looks like one of the * port. */ @@ -258,7 +258,7 @@ int main(int argc, char** argv) int status = 0; check(waitpid(kid, &status, 0) == kid, "the copy is awaited"); const int ok = WIFEXITED(status) && WEXITSTATUS(status) == 23; - /* ⚠️ THE RAW STATUS IS PRINTED WHEN IT IS WRONG, and only then. A + /* THE RAW STATUS IS PRINTED WHEN IT IS WRONG, and only then. A * line reading "it did not report the status it was written to * report" names a fault and not a place: a copy that ended on a * signal and one that returned the wrong number are different @@ -272,7 +272,7 @@ int main(int argc, char** argv) failures += 2; } } else { - /* ⭐ A REFUSAL IS THE EXPECTED ANSWER HERE AND IS CHECKED AS ONE. Clause + /* A REFUSAL IS THE EXPECTED ANSWER HERE AND IS CHECKED AS ONE. Clause * 3 permits an implementation to decline an interface in whole, clause * 6.1 makes the absence a link-time one, and okm_opt.h's rule turns it * into the defined error a POSIX caller already handles. */ @@ -282,7 +282,7 @@ int main(int argc, char** argv) /* --- the control: a program started with nothing redirected --------------- */ - /* ⭐ THIS ONE IS HERE SO THAT THE FOUR BELOW MEAN SOMETHING. Every criterion + /* THIS ONE IS HERE SO THAT THE FOUR BELOW MEAN SOMETHING. Every criterion * below asserts that a caller's redirection was carried across; without this * one they would all pass for a library that had stopped letting a started * program inherit anything at all. Its marker is the one continuous @@ -299,7 +299,7 @@ int main(int argc, char** argv) /* --- a redirection the caller performed, three ways it can be expressed ---- */ - /* ⭐ ONE ROUTE WAS FIXED AND THREE CALL SITES REACH IT. The seeding lives + /* ONE ROUTE WAS FIXED AND THREE CALL SITES REACH IT. The seeding lives * inside `__posix_spawn', so `posix_spawn', `execve' and `system' are all * answered by one change --- and one change answering three entries is not * three entries having a criterion. Each is asked separately. */ @@ -347,7 +347,7 @@ int main(int argc, char** argv) /* --- a file action that names the file itself ------------------------------ */ - /* ⭐ `addopen' WAS REFUSED AND IS EXPRESSIBLE. It is the ordinary way to send + /* `addopen' WAS REFUSED AND IS EXPRESSIBLE. It is the ordinary way to send * a started program's output to a file, and refusing it forced every caller * through `dup2' --- which is the route that did not work. */ { @@ -375,7 +375,7 @@ int main(int argc, char** argv) /* --- and one that cannot be performed --------------------------------------- */ - /* ⭐ A REFUSAL IS A CRITERION. openkal has no value meaning "no stream", and + /* A REFUSAL IS A CRITERION. openkal has no value meaning "no stream", and * the value that looks like one --- zero --- means the opposite: the stream * the caller has. So closing one of the three in the started program cannot * be done, and it used to be ACCEPTED and not done, which handed a program @@ -397,7 +397,7 @@ int main(int argc, char** argv) /* --- how a program that ended abnormally is reported ------------------------ */ - /* ⚠️⚠️ `abort' DID NOT END THE PROGRAM AND AN ILLEGAL INSTRUCTION DID. + /* `abort' DID NOT END THE PROGRAM AND AN ILLEGAL INSTRUCTION DID. * musl's `raise' is `tkill', this port had no case for it, so `abort' fell * through to the line musl's own comment calls unreachable --- `a_crash()', * which on x86_64 is `hlt' and faults as a segmentation fault. Every uncaught @@ -431,7 +431,7 @@ int main(int argc, char** argv) check(distinguishable, "an abnormal end is distinguishable from every ordinary one"); - /* ⭐ AND THE PARTICULAR END THIS SYSTEM'S IMPLEMENTATION PRODUCES, which + /* AND THE PARTICULAR END THIS SYSTEM'S IMPLEMENTATION PRODUCES, which * is what separates `abort reached kal_abort' from `abort fell through * to an illegal instruction'. The observation above cannot: a fault is * distinguishable from an ordinary end too. */ @@ -464,7 +464,7 @@ int main(int argc, char** argv) /* --- asking after a started program without waiting for it ------------------ */ - /* ⚠️ `waitpid' DISCARDED ITS OPTIONS, so the one call whose purpose is not to + /* `waitpid' DISCARDED ITS OPTIONS, so the one call whose purpose is not to * wait blocked until the program finished. `kal_timeout_wait_process' has * been in the specification since 0.8; the route was missing. */ { @@ -477,7 +477,7 @@ int main(int argc, char** argv) long spins = 0; pid_t r; while ((r = waitpid(pid, &st, WNOHANG)) == 0 && spins < 100000) spins++; - /* ⭐ THE CRITERION IS THAT THE CALLER GOT CONTROL BACK, which is + /* THE CRITERION IS THAT THE CALLER GOT CONTROL BACK, which is * what a count greater than zero states and what a count of exactly * one would not distinguish from having blocked once. */ if (spins == 0) @@ -492,7 +492,7 @@ int main(int argc, char** argv) } } - /* ⭐ AND ANY OF THEM, NOT THE FIRST ONE RECORDED. + /* AND ANY OF THEM, NOT THE FIRST ONE RECORDED. * * `waitpid(-1, …, WNOHANG)` asks after any child. Asking after the first * recorded one would report "none has finished" while a later one had, which @@ -571,13 +571,13 @@ int main(int argc, char** argv) /* --- what a copy of this image calls itself -------------------------------- */ if (expect_fork) { - /* ⚠️⚠️ EVERY CONTEXT USED TO ANSWER 1, SO A COPY REPORTED THE IDENTIFIER + /* EVERY CONTEXT USED TO ANSWER 1, SO A COPY REPORTED THE IDENTIFIER * OF THE IMAGE IT WAS COPIED FROM. Two contexts, one answer, and no way * for the copy to name itself --- a program writing its own identifier * where something else would read it (a lock file, the name of a * temporary, a line of a log) wrote a value naming something else. * - * ⭐ THE NUMBER BOTH SIDES ALREADY AGREE ON is the one `fork' returns to + * THE NUMBER BOTH SIDES ALREADY AGREE ON is the one `fork' returns to * the parent, so that is what the copy is told. It has to exist BEFORE * the copy is taken, which is why okm_fork.c reserves the table entry * above `kal_space_start' rather than recording it below. */ @@ -604,7 +604,7 @@ int main(int argc, char** argv) check(named > 0 && said == (long)named, "and it names the identifier its parent was given, not its parent's"); - /* ⚠️ THE GUARD THIS CHANGE NEEDS, AND IT IS NOT A CRITERION --- it holds + /* THE GUARD THIS CHANGE NEEDS, AND IT IS NOT A CRITERION --- it holds * before the change as well. `kill' decides "this program itself" by * comparing against the identifier, and that comparison was against the * constant 1. Carrying an identifier into a copy without moving the @@ -631,7 +631,7 @@ int main(int argc, char** argv) /* --- a program that CANNOT be started ------------------------------------- */ - /* ⭐⭐ THE QUESTION THIS FILE NEVER ASKED, AND THE ONE A CONSUMER LOST NINE + /* THE QUESTION THIS FILE NEVER ASKED, AND THE ONE A CONSUMER LOST NINE * TESTS TO. * * Every observation above starts a program that is there. None asked what @@ -641,7 +641,7 @@ int main(int argc, char** argv) * reported success. `execve' waited for the duplicate, read 127, and ENDED * THE CALLING PROGRAM with it. * - * ⚠️ WHICH BREAKS EVERY SEARCH FOR A PROGRAM BY NAME. musl's `execvp' + * WHICH BREAKS EVERY SEARCH FOR A PROGRAM BY NAME. musl's `execvp' * issues one `execve' per PATH entry and needs it to RETURN so it can try * the next; here it did not return at all, so the first entry that missed * was the end. A consumer measured `bwrap' --- installed at /usr/bin/bwrap @@ -670,7 +670,7 @@ int main(int argc, char** argv) if (e == 0) { int s; waitpid(dir_pid, &s, 0); failures++; } } - /* ⚠️ CALLED IN THIS PROGRAM AND NOT IN A COPY, DELIBERATELY. What is being + /* CALLED IN THIS PROGRAM AND NOT IN A COPY, DELIBERATELY. What is being * observed is that `execve' RETURNS; a version that does not return ends * this program at 127, and the probe runner reports a program that stopped * without a count of failures --- which is the loudest reading available and @@ -689,7 +689,7 @@ int main(int argc, char** argv) if (expect_shell) { /* The first entry misses. That is the whole point: the search has to * survive it, and until now it could not. */ - /* ⚠️ COPIED WITH A LENGTH THAT IS THE VALUE'S RATHER THAN A BUFFER'S. + /* COPIED WITH A LENGTH THAT IS THE VALUE'S RATHER THAN A BUFFER'S. * `getenv' answers a pointer INTO the environment and `setenv' below may * move it, so the old value has to be kept somewhere --- and a fixed * buffer would silently truncate on a machine whose PATH is long, which @@ -736,7 +736,7 @@ int main(int argc, char** argv) else unsetenv("PATH"); } - /* ⚠️⚠️ WHAT ONE `read' RETURNS AFTER A `poll', AND THE SIZE IS THE WHOLE + /* WHAT ONE `read' RETURNS AFTER A `poll', AND THE SIZE IS THE WHOLE * OBSERVATION. * * openkal has no readiness enquiry, so `poll' here performs a bounded @@ -746,7 +746,7 @@ int main(int argc, char** argv) * back to `poll', which keeps another, so a stream arrived one byte per * iteration for ever. * - * ⭐ EVERY BYTE WAS DELIVERED AND IN ORDER, WHICH IS WHY IT SURVIVED. A caller + * EVERY BYTE WAS DELIVERED AND IN ORDER, WHICH IS WHY IT SURVIVED. A caller * that concatenates sees exactly the right bytes; only a caller that looks at * the BOUNDARIES sees anything wrong, and then it sees a lot --- a reader * scanning each arrival for a word finds none, because `two.' arrives as `t' @@ -800,7 +800,7 @@ int main(int argc, char** argv) /* --- a program that needs an INTERPRETER --------------------------------- * - * ⚠️⚠️ A WHOLE CLASS OF PROGRAMS COULD NOT BE STARTED, AND NOTHING HERE + * A WHOLE CLASS OF PROGRAMS COULD NOT BE STARTED, AND NOTHING HERE * LOOKED. `execveat' with a directory descriptor and a relative name gives * the kernel the program's name as `/dev/fd//'. For an ordinary * executable that spelling never surfaces --- the kernel holds the file open @@ -809,16 +809,16 @@ int main(int argc, char** argv) * which time a close-on-exec descriptor is gone. The interpreter is told the * script does not exist. * - * ⭐ Two kinds of program need one, and they are the same defect: + * Two kinds of program need one, and they are the same defect: * a `#!' script --- on every architecture * a binary of another architecture --- through `binfmt_misc' * - * ⚠️ IT WAS FOUND ON aarch64 AND FIRST BLAMED ON THE EMULATOR, because there + * IT WAS FOUND ON aarch64 AND FIRST BLAMED ON THE EMULATOR, because there * every foreign binary needs the binfmt interpreter and so every start * failed at once. It reproduces natively with a script, which is what a * consumer meets on an ordinary machine. * - * ⚠️ The script is made executable by the SHELL and not by this program: + * The script is made executable by the SHELL and not by this program: * `chmod' is refused here, so a script this program wrote would be refused * for its mode and the observation would hold for the wrong reason. */ if (expect_shell) { @@ -856,9 +856,187 @@ int main(int argc, char** argv) unlink(script); } + /* --- a name that exists, may be started, and is not a program ------------- + * + * ENOEXEC WAS EIO. Before openkal 0.13's `kal_err_not_program', + * `okm_errno' had no case for the condition every environment reports + * natively, so the one answer this port could not honestly give --- "a + * device failed" --- was the one it gave. Measured by a consumer: + * `posix_spawn' of such a file returned EIO above this port and ENOEXEC on + * the host directly (issue #28). + * + * WHAT MAKES SUCH A FILE, on every row this runs on, is chmod: this port + * refused every `chmod' until 0.13's `kal_fs_set_executable_at' gave it one + * bit it can honestly grant. A plain-text file this program just wrote is + * readable and has no recognisable format, so making it executable is what + * turns "cannot be found" into "cannot be started" --- the distinction this + * probe exists to observe. On a row with no execute-bit concept at all + * (Windows) `chmod' is expected to answer `ENOSYS' and the `.txt' name + * alone is what a start there fails to recognise. */ + { + const char* path = "not-a-program.txt"; + FILE* f = fopen(path, "w"); + check(f != NULL, "a plain text file can be written for the ENOEXEC probe"); + if (f) { + fputs("this is not a program, and has no #! either\n", f); + fclose(f); + + /* THE ROUND TRIP THE CHMOD ROW BELOW ASSERTS ON ITS OWN, DONE + * HERE FOR THE ONE REASON A START NEEDS IT: the file has to be + * executable for the kernel to get as far as reading its format. + * A refusal here (`ENOSYS') is expected wherever `chmod' cannot + * grant the bit, and the spawn below is still asked — a system + * with no execute bit at all still has to refuse this name. */ + errno = 0; + const int ce = chmod(path, 0777); + if (ce != 0 && errno != ENOSYS) + printf(" chmod(0777) on a fresh file: errno=%d (expected 0 or ENOSYS)\n", + errno); + check(ce == 0 || errno == ENOSYS, + "the file this probe wrote is made executable, or the system says it cannot be"); + + pid_t pid = -1; + char* av[] = { (char*)path, NULL }; + errno = 0; + const int e = posix_spawn(&pid, path, NULL, NULL, av, environ); + if (e != ENOEXEC) + printf(" posix_spawn of a non-program file: e=%d (expected ENOEXEC=%d)\n", + e, ENOEXEC); + check(e == ENOEXEC, + "starting a name that exists, may be started, and is not a program" + " reports ENOEXEC --- not success, and not EIO"); + if (e == 0) { int s; waitpid(pid, &s, 0); failures++; } + } + unlink(path); + } + + /* --- setting whether a file may be started, and reading it back ------------ + * + * THE JUDGEMENT, AND WHY IT IS NARROW. `kal_node_info' carries one + * `writable' boolean and, since 0.13, one `executable' boolean --- neither + * is per class of caller, so the mode this port can ever REPORT is one of + * four: 0444, 0555, 0666 or 0777. `chmod' is granted only a request that + * asks for one of those four *and* changes nothing this port cannot + * change: every bit outside the three execute positions must already + * match what `stat' reports, and the three execute bits must come out + * either all clear or all set to match the read bits (which are always + * all three, above). A real per-class mode --- the ordinary 0644 a + * program creates a file with --- fails the first test against every one + * of the four and is refused precisely as it always was: this row is + * about the one bit 0.13 added a route for, not a general chmod. */ + { + const char* path = "chmod-roundtrip.tmp"; + FILE* f = fopen(path, "w"); + check(f != NULL, "a file exists for the chmod probe"); + if (f) { + fputs("x", f); + fclose(f); + + struct stat before; + check(stat(path, &before) == 0, "its mode can be read before any chmod"); + /* The control: what this port reports for a freshly written, + * writable file is 0666 --- both read and write, uniformly, + * and no execute bit --- which is the starting point every + * assertion below depends on. */ + const int base_ok = (before.st_mode & 07777) == 0666; + if (!base_ok) + printf(" fresh file mode: 0%o (expected 0666)\n", + (unsigned)(before.st_mode & 07777)); + check(base_ok, + "a freshly written file is reported readable and writable" + " and not executable, which is the control the rest needs"); + + /* --- accepted, where the volume claims the property ---------- * + * + * THIS HALF IS ASKED AND NOT ASSUMED. `KAL_FS_PROP_EXECUTABLE` + * is a property of the volume (fs.h), and one CI row's backend + * does not claim it for any volume today: openkal-windows maps + * `ERROR_BAD_EXE_FORMAT` to `kal_err_not_program` (so the ENOEXEC + * probe above holds there) without declaring the property NTFS + * has no bit for. A row without it refuses every change and the + * assertions below follow the refusal instead of the grant --- + * still exercising the same call, and the same read-back. */ + errno = 0; + const int e1 = chmod(path, 0777); + const int volume_has_it = (e1 == 0); + if (!volume_has_it && errno != ENOSYS) + printf(" chmod(0777) from 0666: rc=%d errno=%d" + " (expected 0, or -1/ENOSYS where the volume declines)\n", + e1, errno); + check(e1 == 0 || errno == ENOSYS, + "chmod grants exec where every class already reads," + " or says the volume does not store the bit"); + + struct stat mid; + check(stat(path, &mid) == 0, "the mode can be read back after that call"); + if (volume_has_it) { + check((mid.st_mode & 07777) == 0777, + "and a granted request round-trips through stat"); + check(access(path, X_OK) == 0, + "and access(X_OK) agrees once the bit is set"); + } else { + check((mid.st_mode & 07777) == 0666, + "and a request the volume declined left the mode unchanged"); + check(access(path, X_OK) == 0, + "and access(X_OK) still answers yes for anything that exists," + " which is this port's documented answer where the position" + " is never filled"); + } + + /* --- turning it back off, or the same no-op, is the same call - */ + errno = 0; + const int e2 = chmod(path, 0666); + check(e2 == 0, volume_has_it + ? "chmod clearing exec back to none succeeds" + : "chmod equal to the mode already reported succeeds" + " even where the volume declines the property"); + struct stat off; + check(stat(path, &off) == 0 && (off.st_mode & 07777) == 0666, + "and it round-trips through stat"); + if (volume_has_it) + check(access(path, X_OK) != 0 && errno == EACCES, + "and access(X_OK) agrees once the bit is clear"); + + /* --- refused: a request outside the two shapes above ---------- */ + errno = 0; + const int e3 = chmod(path, 0644); + if (e3 != -1 || errno != ENOSYS) + printf(" chmod(0644) from 0666: rc=%d errno=%d (expected -1, ENOSYS)\n", + e3, errno); + check(e3 == -1 && errno == ENOSYS, + "a request this port cannot honestly grant is refused with ENOSYS" + " --- not rounded to the nearest mode it can report"); + struct stat after; + check(stat(path, &after) == 0 && (after.st_mode & 07777) == 0666, + "and the mode is exactly as the refused call left it"); + + /* --- a no-op equal to the current mode always succeeds -------- */ + errno = 0; + check(chmod(path, 0666) == 0, + "a request equal to the mode already reported succeeds," + " asking nothing of the volume"); + } + unlink(path); + + /* fchmod has no name to work with: this port keeps none for an open + * file (only a directory's is remembered, for resolving a name that + * ascends out of it), so it is ENOSYS unconditionally. */ + FILE* g = fopen("fchmod-probe.tmp", "w"); + if (g) { + errno = 0; + const int fe = fchmod(fileno(g), 0777); + check(fe == -1 && errno == ENOSYS, + "fchmod has no name to hand kal_fs_set_executable_at, and says so"); + fclose(g); + unlink("fchmod-probe.tmp"); + } else { + check(0, "a file exists for the fchmod probe"); + } + } + /* --- units, which 0.12.0 added and no probe here ever looked at ------------ * - * ⚠️⚠️ THE WHOLE OF `setpgid'/`kill(-n)' SHIPPED WITH ITS ONLY WITNESS IN + * THE WHOLE OF `setpgid'/`kill(-n)' SHIPPED WITH ITS ONLY WITNESS IN * SOMEBODY ELSE'S TEST SUITE. It was written against a consumer, measured * against that consumer, and released --- and the checks below are the ones * that would have been run here had this file been extended at the time. @@ -873,13 +1051,13 @@ int main(int argc, char** argv) check(formed == 0 || errno == ENOSYS, "a program forms a unit of its own, or says the environment has none"); - /* ⭐⭐ AND NAMING A UNIT THAT DOES NOT EXIST IS REFUSED. + /* AND NAMING A UNIT THAT DOES NOT EXIST IS REFUSED. * * Signal zero is the enquiry form --- it changes nothing and answers * whether the target is there --- so this asks about a unit this program * has no way to name and must be told there is none. * - * ⚠️ ON 0.12.0 IT ANSWERED SUCCESS, and the reason is worth stating + * ON 0.12.0 IT ANSWERED SUCCESS, and the reason is worth stating * because it is not a slip in one line. `kill(-n)' fell back to the unit * THIS program holds whenever `n' matched no child, so every negative * identifier named the caller's own unit --- an enquiry about anything @@ -909,16 +1087,16 @@ int main(int argc, char** argv) } } - /* ⭐ WHAT A UNIT IS FOR: REACHING A PROGRAM THE CALLER NEVER HELD. + /* WHAT A UNIT IS FOR: REACHING A PROGRAM THE CALLER NEVER HELD. * * The shell exits at once and the work it backgrounded outlives it, so a * caller holding only the shell's handle has nothing to terminate. This is * the case `kal_spawn.job' was added for in 0.11, and it is the one a * timeout in a consumer has to be able to reach. * - * ⚠️ Needs a shell, so the row without one does not run it. */ + * Needs a shell, so the row without one does not run it. */ if (expect_shell) { - /* ⚠️ ABSOLUTE, BECAUSE THE WRITER IS A SHELL AND NOT THIS PROGRAM. A + /* ABSOLUTE, BECAUSE THE WRITER IS A SHELL AND NOT THIS PROGRAM. A * relative name would be resolved against whatever directory the shell * runs in, and "the file was not written" is what this check reads as * success --- so a name that missed would pass it for the wrong reason. */ @@ -932,7 +1110,7 @@ int main(int argc, char** argv) snprintf(script, sizeof script, "( sleep 3; echo x > %s ) & echo started; exit 0", marker); - /* ⚠️⚠️ THE CONTROL COMES FIRST, AND WITHOUT IT THE OBSERVATION BELOW IS + /* THE CONTROL COMES FIRST, AND WITHOUT IT THE OBSERVATION BELOW IS * WORTHLESS. What that one reads as success is a file that is ABSENT --- * which is also what a missing shell, a mistyped script, a marker written * somewhere else and a background job that never ran all produce. So this diff --git a/examples/surface/src/main.c b/examples/surface/src/main.c index 79373d2..738e410 100644 --- a/examples/surface/src/main.c +++ b/examples/surface/src/main.c @@ -1,7 +1,7 @@ /* The surface a program reaches for that is neither a file nor another program, * asked by a program that names no openkal symbol. * - * ⭐⭐ WHY THIS FILE EXISTS, AND IT IS THE WHOLE POINT OF IT. + * WHY THIS FILE EXISTS, AND IT IS THE WHOLE POINT OF IT. * * Every probe in this repository until now asked whether something WORKED. None * of them asked whether an answer was TRUE. Those are different questions, and @@ -21,13 +21,13 @@ * answered 0 --- a bound this library sets itself and was * refusing to state * - * ⚠️ NOT ONE of those is a missing operation, so not one of them appears in + * NOT ONE of those is a missing operation, so not one of them appears in * `OPENKAL_MUSL_TRACE=enosys'. They are operations that are PRESENT AND ANSWER * WRONGLY, which no diagnostic here can see and no probe here was asking about. * They were found by writing this file and comparing every answer against the * host's, and that comparison is what the file preserves. * - * ⚠️ SO EVERY OBSERVATION BELOW STATES THE ANSWER IT EXPECTS, and a refusal is + * SO EVERY OBSERVATION BELOW STATES THE ANSWER IT EXPECTS, and a refusal is * an expected answer wherever a refusal is the truth. "It returned" is not an * observation this file makes. * @@ -63,7 +63,7 @@ static void check(int ok, const char* what) /* A call that must fail, and must fail WITH A PARTICULAR REASON. * - * ⚠️ THE REASON IS HALF THE OBSERVATION. `setsid' answering ENOSYS and `setsid' + * THE REASON IS HALF THE OBSERVATION. `setsid' answering ENOSYS and `setsid' * answering EPERM are both refusals and are not the same answer: every * daemonising library in existence handles the second, because the * `fork'-then-`setsid' dance exists for it, and none handles the first. */ @@ -90,7 +90,7 @@ int main(int argc, char** argv) /* --- an identifier is a number, and never a negated error -------------- */ - /* ⚠️⚠️ THE FAMILY AND NOT THE MEMBER. `getpgrp' had exactly this defect and + /* THE FAMILY AND NOT THE MEMBER. `getpgrp' had exactly this defect and * was fixed; `getppid', three lines away in the same dispatch, was not * looked for and had it too. Both are written in musl WITHOUT * `__syscall_ret' --- deliberately, because POSIX says they cannot fail --- @@ -118,7 +118,7 @@ int main(int argc, char** argv) const long open_max = sysconf(_SC_OPEN_MAX); check(open_max > 0, "the greatest number of descriptors is answered"); - /* ⚠️⚠️ IT ANSWERED 1, SILENTLY. `hardware_concurrency()' reads this, so a + /* IT ANSWERED 1, SILENTLY. `hardware_concurrency()' reads this, so a * program sizing a pool of workers got one worker and no error. openkal 0.10 * added the enquiry beneath it. This asserts only that it is a real count * and not the fallback, because the number itself is the machine's. */ @@ -133,7 +133,7 @@ int main(int argc, char** argv) /* --- a lock that is not taken is not reported as taken ----------------- */ - /* ⚠️⚠️ THESE THREE ANSWERED 0 AND DID NOTHING. Measured against the host: + /* THESE THREE ANSWERED 0 AND DID NOTHING. Measured against the host: * two programs took one exclusive lock and BOTH were told they had it. * * 0.10.0 refused them and said the refusal was TEMPORARY in a way `chmod' @@ -142,7 +142,7 @@ int main(int argc, char** argv) * observation was the one that would change when the word arrived, and that * it should change to "a second holder is refused" rather than be deleted. * - * ⭐ openkal 0.10 IS THAT WORD, so it changed, and this is now the + * openkal 0.10 IS THAT WORD, so it changed, and this is now the * observation it said it would become. `kal_fs_lock' states the holder as * the open FILE, which is why a SECOND open file of one name is refused * here --- the older process-held form would have granted it, and a library @@ -158,7 +158,7 @@ int main(int argc, char** argv) errno = 0; check(fcntl(fd, F_SETLK, &fl) == 0, "an exclusive lock is taken"); - /* ⭐ THE OBSERVATION THAT TELLS THE TWO FORMS APART, and it needs no + /* THE OBSERVATION THAT TELLS THE TWO FORMS APART, and it needs no * second program: a SECOND OPEN FILE of the same name, here. The * process-held form grants this, because the holder is the process * and the process already holds it. The open-file form refuses it, @@ -181,7 +181,7 @@ int main(int argc, char** argv) un.l_type = F_UNLCK; un.l_whence = SEEK_SET; un.l_start = 0; un.l_len = 0; check(fcntl(fd, F_SETLK, &un) == 0, "the lock is released"); - /* ⚠️ AND THE ENQUIRY IS STILL REFUSED, WHICH IS NOT AN OVERSIGHT. + /* AND THE ENQUIRY IS STILL REFUSED, WHICH IS NOT AN OVERSIGHT. * `F_GETLK' asks whether a lock WOULD block without taking one, and * openkal has no operation that answers a question without * performing it. Taking the lock and releasing it would answer, and @@ -215,7 +215,7 @@ int main(int argc, char** argv) /* --- a group of one is a group this program is already in --------------- */ - /* ⚠️ BOTH OF THESE USED TO ANSWER ENOSYS, on the ground that "making a group + /* BOTH OF THESE USED TO ANSWER ENOSYS, on the ground that "making a group * is not the same as being in one" --- which answers a question neither of * them asks. `setpgid(0, 0)' asks for the calling program to be in a group * of its own, and `getpgid(0) == getpid()' above says it already is. */ diff --git a/examples/threads-cxx/src/main.cpp b/examples/threads-cxx/src/main.cpp index e8f86cf..fa2006a 100644 --- a/examples/threads-cxx/src/main.cpp +++ b/examples/threads-cxx/src/main.cpp @@ -7,7 +7,7 @@ * through the truncated value. libc++'s std::thread stores exactly this type, * so every std::thread on that system ended in an access violation when joined. * - * ⭐ THE static_assert IS THE CRITERION, and it is a compile-time one: before the + * THE static_assert IS THE CRITERION, and it is a compile-time one: before the * change this file does not compile for x86_64-windows-gnu. The run afterwards * shows that the value survives the round trip through a started context. */ diff --git a/examples/threads-detached/src/main.c b/examples/threads-detached/src/main.c index 121ed16..c2e3ae6 100644 --- a/examples/threads-detached/src/main.c +++ b/examples/threads-detached/src/main.c @@ -12,7 +12,7 @@ * overwritten and jumped into it: every program whose detached thread ended * stopped with an access violation. * - * ⭐ WHAT IS OBSERVED. Detached threads that end one after another, then the + * WHAT IS OBSERVED. Detached threads that end one after another, then the * two tables beneath the shared stack in use: a key with a destructor, and a * joinable thread's own error value, which is reached through the context table. */ diff --git a/mcpp.toml b/mcpp.toml index 9f98be4..2525d46 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-musl" -version = "0.13.5" +version = "0.14.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" @@ -15,7 +15,7 @@ authors = ["mcpplibs"] repo = "https://github.com/mcpplibs/openkal-musl" [dependencies] -openkal = "0.12.0" +openkal = "0.13.0" # An ordinary consumer of openkal declares the specification and leaves the # choice of implementation to whoever builds the program, which is what the @@ -30,12 +30,12 @@ openkal = "0.12.0" # # The consequence for a program is that it names this package and nothing else. [target.'cfg(os = "linux")'.dependencies] -openkal-linux = { version = "0.12.0", features = ["standalone"] } +openkal-linux = { version = "0.13.0", features = ["standalone"] } [target.'cfg(os = "macos")'.dependencies] -openkal-macos = { version = "0.9.1", features = ["standalone"] } +openkal-macos = { version = "0.10.0", features = ["standalone"] } -# ⚠️ FIRST STEP TOWARD A BARE MACHINE, AND NOT THE WHOLE OF IT. +# FIRST STEP TOWARD A BARE MACHINE, AND NOT THE WHOLE OF IT. # # An environment with no operating system reaches openkal through firmware. The # architecture directory and the generated headers for it are now here --- they @@ -46,7 +46,7 @@ openkal-macos = { version = "0.9.1", features = ["standalone"] } [target.'cfg(os = "none")'.dependencies] openkal-opensbi = { version = "0.6.0", features = ["standalone"] } -# ⭐ WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH IS EXPECTED TO PROVIDE. +# WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH IS EXPECTED TO PROVIDE. # # A machine with no operating system has no storage to open a file on, no second # image to start, and no scheduler to give a context to. openkal's answer to @@ -54,7 +54,7 @@ openkal-opensbi = { version = "0.6.0", features = ["standalone"] } # from providing one whose operations always fail — and the answer for this C # library is that it must not be built calling them. # -# ⚠️ IT IS BUILT CALLING THEM UNLESS TOLD OTHERWISE, AND THE WAY THAT SHOWS IS A +# IT IS BUILT CALLING THEM UNLESS TOLD OTHERWISE, AND THE WAY THAT SHOWS IS A # LINK FAILURE IN A PROGRAM THAT NEVER OPENS A FILE. Measured 2026-08-23 on # riscv64-none-elf: a program whose whole text was `import std;` and one line of # output failed on fifteen names it can never reach, because `fclose` is on the @@ -62,14 +62,14 @@ openkal-opensbi = { version = "0.6.0", features = ["standalone"] } # holds. port/src/okm.h records what these three change and why answering with # an error is right for a C library and wrong for an openkal implementation. # -# ⚠️ THE TARGET IS A PROXY FOR THE IMPLEMENTATION, AND AN IMPERFECT ONE. A board +# THE TARGET IS A PROXY FOR THE IMPLEMENTATION, AND AN IMPERFECT ONE. A board # CAN carry a real filesystem. Each macro is `#ifndef`-guarded, so a project # with one turns it back on from its own build without editing this file. [target.'cfg(os = "none")'.build] defines = ["OKM_HAS_FS=0", "OKM_HAS_PROCESS=0", "OKM_HAS_TASK=0"] [target.'cfg(windows)'.dependencies] -openkal-windows = { version = "0.7.4", features = ["standalone"] } +openkal-windows = { version = "0.8.0", features = ["standalone"] } # The feature macros musl's own build establishes. # @@ -120,7 +120,7 @@ sources = [ # calls needs far more --- it overwrote the context table on macOS. "!musl/src/thread/__unmapself.c", "!musl/src/process/posix_spawn.c", - # ⭐ AND ITS SIBLING, WHICH IS EXCLUDED BECAUSE THE ONE ABOVE IS. + # AND ITS SIBLING, WHICH IS EXCLUDED BECAUSE THE ONE ABOVE IS. # musl's posix_spawnp does not search a PATH: it stores `__execvpe' in the # attributes and lets posix_spawn call it IN THE DUPLICATE. This port has no # duplicate to run it in, so the field was read by nobody and a name without @@ -130,7 +130,7 @@ sources = [ "!musl/src/mman/mmap.c", "!musl/src/internal/syscall_ret.c", "!musl/src/unistd/getcwd.c", - # ⚠️⚠️ THE THIRD SOURCE OF ONE KIND, AND IT WAS UNREACHABLE UNTIL 0.11.0. + # THE THIRD SOURCE OF ONE KIND, AND IT WAS UNREACHABLE UNTIL 0.11.0. # musl's `fcntl' reads its variable argument as an `unsigned long', which holds # a pointer on every system musl was written for and thirty-two bits on one # this port builds for --- so a `struct flock *' arrived with its top half @@ -141,7 +141,7 @@ sources = [ # Replaced by port/src/okm_phdr.c: musl answers this from the auxiliary # vector, which the replaced __libc_start_main does not read. "!musl/src/ldso/dl_iterate_phdr.c", - # ⭐ THE ONE SOURCE WHOSE vDSO DEPENDENCY SURVIVES AN ARCHITECTURE HEADER THAT + # THE ONE SOURCE WHOSE vDSO DEPENDENCY SURVIVES AN ARCHITECTURE HEADER THAT # DOES NOT OPT IN. # # There is no vDSO above openkal — that is a facility one system supplies to @@ -152,13 +152,13 @@ sources = [ # VDSO_* guard that is likewise never defined. So `vdso.c` compiles to an # empty object and nothing misses it. # - # ⚠️ Except this one. `cache.c` guards its vDSO use on SYS_riscv_flush_icache + # Except this one. `cache.c` guards its vDSO use on SYS_riscv_flush_icache # — a SYSCALL NUMBER, which the generated bits/syscall.h does define — rather # than on a VDSO_* macro. So on riscv64 it alone compiles a call to a # definition that was compiled away, and the link reports # `undefined hidden symbol: __vdsosym`. # - # ⚠️ It was invisible until 2026-08-23, and not because it is new: on the + # It was invisible until 2026-08-23, and not because it is new: on the # other architectures the guard is false and the file is empty, and riscv64's # first COMPLETE link is this one. A latent defect of the port, found by the # target that has nothing to fall back on — which is the reason §8.2 makes @@ -180,7 +180,7 @@ sources = [ # own index --- built successfully against this port and failed at its # twenty-seventh test, which is what a run-time refusal costs. # - # ⚠️⚠️ THE LIST IS MUCH SHORTER THAN THE PRINCIPLE, AND THE REASON IS MEASURED + # THE LIST IS MUCH SHORTER THAN THE PRINCIPLE, AND THE REASON IS MEASURED # RATHER THAN CONSERVATIVE. # # Withholding a family only works when nothing else in musl reaches it. Each @@ -196,7 +196,7 @@ sources = [ # closure at the password functions. Withholding either takes an essential or # an ordinary facility with it, so neither is withheld. # - # ⭐ THREE OF THOSE FOUR ROWS ARE NOW MOOT, AND FOR THE RIGHT REASON: the + # THREE OF THOSE FOUR ROWS ARE NOW MOOT, AND FOR THE RIGHT REASON: the # facilities arrived rather than the exclusions being relaxed. openkal 0.8 # added `net', `datagram', `timeout' and `space'; port/src/okm_net.c, # okm_poll.c and okm_fork.c route them, so `select', the network family and @@ -205,7 +205,7 @@ sources = [ # withheld, because a readiness SET held by the environment is a facility of # one kernel rather than a capability, and openkal has none to express. # - # ⚠️ AND THE MECHANISM HAS A PRECONDITION THIS PACKAGE MEETS ON ONE TARGET OF + # AND THE MECHANISM HAS A PRECONDITION THIS PACKAGE MEETS ON ONE TARGET OF # FOUR. `-ffunction-sections` with `-Wl,--gc-sections` is what makes an # unreferenced definition cost nothing, and only `cfg(os = "linux")` sets those # ldflags. On the mingw target the objects are kept, so `daemon` and `wordexp` @@ -213,14 +213,14 @@ sources = [ # # musl/src/legacy/daemon.c:18: undefined reference to `fork' # - # ⭐ SO WITHHOLDING THE NETWORK FAMILY IS NOT REFUSED, IT IS NOT YET POSSIBLE. + # SO WITHHOLDING THE NETWORK FAMILY IS NOT REFUSED, IT IS NOT YET POSSIBLE. # It becomes possible once every target collects unreferenced sections --- # `--gc-sections` for the ELF and PE linkers, `-dead_strip` for this system's # --- and that is a change to the link line of three targets, testable on its # own, rather than something to attach here. # # What remains withheld are the five whose closure is empty on every target. - # ⚠️ THEY GO TOGETHER WITH THEIR REASON: a descriptor that is created and never + # THEY GO TOGETHER WITH THEIR REASON: a descriptor that is created and never # becomes ready is worse than one that cannot be created, because a program # waiting on it waits for ever with nothing to report. "!musl/src/linux/timerfd.c", @@ -243,7 +243,7 @@ sources = [ # headers: this package is built from the same include path it publishes, and # the alternative --- two paths --- is a second place where the set of headers # is configured. -# ⭐ THE ONE THING THIS PACKAGE KNOWS THAT A CONSUMER DOES NOT: THAT IT IS THIS +# THE ONE THING THIS PACKAGE KNOWS THAT A CONSUMER DOES NOT: THAT IT IS THIS # PACKAGE DOING THE COMPILING. # # musl's internal header overlay defines `weak`, `hidden` and `weak_alias` as @@ -252,14 +252,14 @@ sources = [ # consumers break by it was found one at a time: a C++ one on `restrict`, then # on linkage; a C one (compiler-rt) on `weak`, which it writes itself. # -# ⚠️ The discriminator used to be the LANGUAGE of the translation unit, and that +# The discriminator used to be the LANGUAGE of the translation unit, and that # was wrong in one direction: a C++ unit is certainly not musl's, but a C unit # is not certainly musl's. Authorship is the property, and it is expressible — # `defines` is applied to this package's own sources and is not carried to a # consumer, which publicUsage's include directories and flags are. # port/include/features.h records the measurement and what it cost. # -# ⚠️ A CONSUMER THAT WAS BUILT BEFORE THIS LINE EXISTED DOES NOT SEE IT UNTIL ITS +# A CONSUMER THAT WAS BUILT BEFORE THIS LINE EXISTED DOES NOT SEE IT UNTIL ITS # BUILD DIRECTORY IS CLEARED. Measured 2026-08-23: adding a flag here changed # nothing in an example that depends on this package by path — not after a # rebuild, and not after touching the sources either. The generated build file @@ -277,7 +277,7 @@ include_dirs = [ "musl/include", ] -# ⭐⭐ THE FIRST-BEST REMEDY, WHICH port/include/features.h SAID WAS NOT +# THE FIRST-BEST REMEDY, WHICH port/include/features.h SAID WAS NOT # AVAILABLE AND NOW IS. # # That file records the measurement of 2026-08-22: "The first would be for a @@ -289,14 +289,14 @@ include_dirs = [ # stop at this package's boundary, so musl's internal overlay reaches musl's # own sources and reaches no consumer. # -# ⚠️ A SUBSET OF THE LIST ABOVE RATHER THAN A LIST OF ITS OWN, and that is the +# A SUBSET OF THE LIST ABOVE RATHER THAN A LIST OF ITS OWN, and that is the # point: the ORDER is load-bearing. `port/include` must precede the # architecture's directory, and the internal overlay must precede # `musl/include` — moving these three to the end makes musl's own build find # the public first and fail with `unknown type name hidden`. # The one ordered list stays the one ordered list. # -# ⚠️ THIS DOES NOT REPLACE `OKM_MUSL_INTERNAL`. That answers a different +# THIS DOES NOT REPLACE `OKM_MUSL_INTERNAL`. That answers a different # question — "is this package the one compiling this unit" — and it is what # keeps the macros inert for a C consumer that IS built with these directories # on its line (compiler-rt, when a board builds it here). The two are the @@ -351,7 +351,7 @@ cxx_runtime = "host-coupled" [target.'cfg(os = "linux")'.build] ldflags = ["-nostdlib", "-static", "-Wl,--no-dynamic-linker", "-Wl,--gc-sections"] -# ⚠️ `-lgcc` was here; it is now named by build.mcpp, and only when the compiler +# `-lgcc` was here; it is now named by build.mcpp, and only when the compiler # is the one whose runtime has that name. See that file. # The link line on this system. @@ -377,7 +377,7 @@ ldflags = ["-nostdlib", "-static", "-Wl,--no-dynamic-linker", # this format has no program headers. sources = [ "!port/src/okm_phdr.c", - # ⭐ One file this format needs that no other does — the loader here runs + # One file this format needs that no other does — the loader here runs # constructors before the entry point, so the library has to be able to come # up from a constructor too. `port/src/*.c` is a package-wide glob, so the # file lives in a subdirectory it does not match. @@ -395,7 +395,7 @@ sources = [ # 1". This system's own linker is the one the format was designed alongside and # resolves them. # -# ⚠️ The flag's scope is narrower than it reads, and the difference matters for +# The flag's scope is narrower than it reads, and the difference matters for # cross builds. Remeasured 2026-08-22 with the open-source toolchain: the same # 1356 objects produce ZERO indirect symbols, for both architectures and for two # compiler versions, and ld64.lld links them on Linux. The linker's limitation @@ -408,14 +408,14 @@ sources = [ # reach the construct, does not need this system's linker, and therefore does # not need any file this system supplies. musl/PATCHES.md records both # measurements and the two names a program does still need. -# ⚠️ `--ld-path` IS GONE, AND THE COMMENT ABOVE ALREADY SAID WHY IT WOULD BE. +# `--ld-path` IS GONE, AND THE COMMENT ABOVE ALREADY SAID WHY IT WOULD BE. # # It named this system's own linker, to work around a construct that the # compiler this package was FIRST built with produced. The remeasurement above # is the whole of the reason it can go: with the open-source toolchain, the same # objects produce ZERO indirect symbols, and `ld64.lld` links them. # -# ⚠️ AND IT HAD TO GO, because mcpp's conditions are on the TARGET and not on the +# AND IT HAD TO GO, because mcpp's conditions are on the TARGET and not on the # host. `cfg(os = "macos")` is true when this library is BUILT ON macOS and also # when it is BUILT FOR macOS from somewhere else — and the second case is a Linux # machine, where `/usr/bin/ld` is GNU ld and cannot link a Mach-O: @@ -442,7 +442,7 @@ include_dirs = ["musl-generated/x86_64", "musl/arch/x86_64", "musl/arch/generic" [target.'cfg(all(arch = "x86_64", windows))'.build] include_dirs = ["musl-generated/x86_64-windows", "musl/arch/x86_64", "musl/arch/generic"] -# ⚠️ TWO ROWS FOR ONE ARCHITECTURE, AND MUTUALLY EXCLUSIVE — the same shape +# TWO ROWS FOR ONE ARCHITECTURE, AND MUTUALLY EXCLUSIVE — the same shape # x86_64 already has for Windows, and for a narrower version of the same reason. # # musl states its data model in three lines of bits/alltypes.h. Windows needed a @@ -450,7 +450,7 @@ include_dirs = ["musl-generated/x86_64-windows", "musl/arch/x86_64", "musl/arch/ # LP64 — a `long` holds a pointer — and still differs in one of the three: # `int64_t` is spelled `long long` there and `long` on Linux. # -# ⚠️ One spelling, and it is not cosmetic. Measured 2026-08-23 on a macOS cross: +# One spelling, and it is not cosmetic. Measured 2026-08-23 on a macOS cross: # # okm_syscall.c:439: incompatible pointer types passing 'uint64_t *' # (aka 'unsigned long *') to parameter of type 'kal_u64 *' @@ -477,9 +477,9 @@ include_dirs = ["musl-generated/riscv64", "musl/arch/riscv64", "musl/arch/generi # this format has no program headers. sources = [ "!port/src/okm_phdr.c", - # ⭐ AND ONE THIS FORMAT NEEDS THAT NO OTHER DOES. + # AND ONE THIS FORMAT NEEDS THAT NO OTHER DOES. # - # ⚠️ IN A SUBDIRECTORY, AND THAT IS THE POINT. `port/src/*.c` in the + # IN A SUBDIRECTORY, AND THAT IS THE POINT. `port/src/*.c` in the # package-wide list is a glob, so a file placed beside its neighbours would be # compiled for EVERY target — and `__main` is a name only this format's # compiler emits a call to. `port/src/win/` is not matched by that glob, which @@ -487,7 +487,7 @@ sources = [ # that would have to win against an inclusion. "port/src/win/*.c", ] -# ⚠️ `-lgcc` AND `-lgcc_eh` WERE HERE AND ARE NOT ANY MORE. +# `-lgcc` AND `-lgcc_eh` WERE HERE AND ARE NOT ANY MORE. # # They named GCC's runtime: the routines a compiler emits calls to and no C # library defines, plus one more this format needs — the helper a thread-local @@ -496,12 +496,12 @@ sources = [ # # Both statements were true. What was false is that this port should name them: # -# 1. ⭐ THE macOS BLOCK ABOVE NAMES NEITHER, and it is the same C library on the +# 1. THE macOS BLOCK ABOVE NAMES NEITHER, and it is the same C library on the # same compiler. Builtins come from the graph there — `openkal-llvm-runtime` # builds compiler-rt for the target — and the thread-local helper is # `__emutls_get_address` from that same build. This block was the outlier. # -# 2. ⚠️ AND IT ONLY EVER WORKED BY ACCIDENT. `libgcc.a` and `libgcc_eh.a` are +# 2. AND IT ONLY EVER WORKED BY ACCIDENT. `libgcc.a` and `libgcc_eh.a` are # mingw's, so a cross build resolved them out of `/usr/x86_64-w64-mingw32/lib` # — present on a machine with mingw installed and nowhere else. Measured # 2026-08-22 on a clean CI runner, alongside the four import libraries that diff --git a/musl-generated/README.md b/musl-generated/README.md index b15b144..3ceecdb 100644 --- a/musl-generated/README.md +++ b/musl-generated/README.md @@ -56,7 +56,7 @@ Apple's platforms are LP64, exactly as musl's architectures are — a `long` holds a pointer — so `_Addr` and `_Reg` are unchanged. What differs is only which 64-bit type `int64_t` is spelled as: `long long` there, `long` on Linux. -⚠️ Same width, different type, and the two do not convert. Measured +Same width, different type, and the two do not convert. Measured 2026-08-23, cross-compiling this port for `arm64-apple-macos` from Linux: ``` diff --git a/musl/PATCHES.md b/musl/PATCHES.md index 26a292b..3b40c46 100644 --- a/musl/PATCHES.md +++ b/musl/PATCHES.md @@ -108,7 +108,7 @@ the working directory, and `posix_spawnp("sh", …)` started `./sh`, failed, and `__posix_spawn` per entry, by musl's own rules — and `__posix_spawn` now refuses an attribute function it does not recognise rather than ignoring one. -⚠️ **Including musl's entry separator, which is a colon on every target and is +**Including musl's entry separator, which is a colon on every target and is the wrong one for exactly one of them.** One environment separates its own PATH with a semicolon and begins each entry with a volume letter and a colon, so reading that PATH on a colon produces entries that are not names. It is still a @@ -117,7 +117,7 @@ one in musl's own source, and having the two ways of searching for one program disagree with each other is worse for a caller than having both wrong the same way. Nothing on that target searches a PATH today: its CI row declares no shell. -⚠️⚠️ `src/thread/__unmapself.c` moves to a 256-byte stack shared by every exiting +`src/thread/__unmapself.c` moves to a 256-byte stack shared by every exiting thread before it makes the two calls that end a detached one. On Linux the thread stands on the mapping it is about to release, and two raw system calls fit in 256 bytes. Here the thread stands on the stack `kal_task_start` supplied, and @@ -137,7 +137,7 @@ pointer: the value never becomes an integer at all. That is correct on every system musl was written for and is not correct on one that writes a volume first, and openkal does not say which a system does. -⚠️⚠️ `src/fcntl/fcntl.c` is the THIRD of that kind, and it survived three releases +`src/fcntl/fcntl.c` is the THIRD of that kind, and it survived three releases of a port that already names the kind twice. It reads its variable argument as an `unsigned long`: @@ -150,14 +150,14 @@ which holds a pointer on every system musl was written for and thirty-two bits on one this port builds for. A caller passing a `struct flock *` had the top half of it discarded **before this port saw it**. -⭐ **It was unreachable until 0.11.0**, which is why it survived. Every command +**It was unreachable until 0.11.0**, which is why it survived. Every command this library answered took an integer, or took a pointer it never followed: `F_SETLK` returned 0 and did nothing, and then reported `ENOSYS`. A truncated pointer that nothing dereferences is a truncated pointer nothing reports. openkal 0.10 gave this port a real lock, `F_SETLK` began following the pointer, and it faulted on the first attempt. -⚠️ The register file names the type rather than the symptom: +The register file names the type rather than the symptom: page fault on read access to 0x00000000fe2ffec2 rax:00000000fe2ffec0 rsp:00007ffffe2fc7a0 @@ -166,7 +166,7 @@ faulted on the first attempt. `rax` is the caller's pointer with its top thirty-two bits gone, and the offset it faults at — two — is `l_whence`, the first field this port reads. -⚠️ And the vararg TYPE is part of the calling convention rather than a detail: +And the vararg TYPE is part of the calling convention rather than a detail: `va_arg(ap, unsigned long)` and `va_arg(ap, uintptr_t)` read different numbers of bytes where the two differ, so this is not a cast applied afterwards. Reading it as the narrower type has already lost the half by then. @@ -206,7 +206,7 @@ library expresses it as starting the program, waiting for it, and ending with the status it ended with. It is the arrangement every environment without the operation uses, and two of the three beneath openkal are such environments. -⚠️⚠️ **This paragraph used to say that a caller cannot distinguish that. It can, +**This paragraph used to say that a caller cannot distinguish that. It can, and the claim is what kept anyone from looking.** Three differences are known, and the first two were found by a consumer rather than here: @@ -227,14 +227,14 @@ and the first two were found by a consumer rather than here: ran to completion, unsupervised. Measured, with the host as control: identical status words, opposite outcomes. - ⚠️ **This entry used to end "Not answered here", and it is answered now.** + **This entry used to end "Not answered here", and it is answered now.** openkal had no way to say "this program's lifetime is bound to mine", and `kal_process_terminate` was right to terminate only what it was given — so what was missing was a word, not a mechanism. openkal 0.10 added `kal_process_spawn_bound`, and **since 0.11.0 `execve` asks for it**. `posix_spawn` does not and must not: a POSIX child outlives its parent. - ⚠️ A backend may decline the binding — openkal-macos has no primitive that + A backend may decline the binding — openkal-macos has no primitive that arms it from inside the started image, and openkal-windows has not measured its own. There this falls back to the unbound spawn rather than refusing to start the program at all, and the divergence is the one this entry used to @@ -265,7 +265,7 @@ interface at all. `fork` returns twice, so the second half is this port's --- `space.h` describes that composition and says in terms that it belongs above the line; `port/src/okm_fork.c` is where it is. -⚠️ **A caller can tell two things.** The copy's per-context identity is not +**A caller can tell two things.** The copy's per-context identity is not necessarily the original's --- `kal_task_current()` promises uniqueness among contexts running at the same moment and says nothing about a copy, and the two implementations answer differently --- so this port rebinds the copy's slot @@ -426,14 +426,14 @@ exits 139 with a core dumped. Every uncaught exception, `assert`, segmentation fault. They now reach `kal_abort`, which raises the signal on openkal-linux and ends with a distinguished status on the other two. -⚠️ Three numbers are musl's own and must never terminate anything: +Three numbers are musl's own and must never terminate anything: `pthread_impl.h` reserves 32, 33 and 34 for the timer thread, cancellation and `synccall`, each sent with this same call. `pthread_cancel` is `pthread_kill(t, SIGCANCEL)`, so a table making 33 terminating would end the program the first time anything cancelled a thread. They are refused, which is what they already got and what musl already handles. -⚠️ And the target is deliberately not examined. A terminating signal's default +And the target is deliberately not examined. A terminating signal's default action ends the process rather than the context that was named --- which is true on Linux too --- so which context a caller aimed at makes no difference. Comparing the identifier against `kal_task_current()` would have been worse than @@ -452,7 +452,7 @@ up to one millisecond. Where the environment provides no `openkal.timeout`, ### And one defect of the criteria rather than of the library -⭐ `tools/run-probe.sh` chose the program to run with `find target … | head -1`. +`tools/run-probe.sh` chose the program to run with `find target … | head -1`. `target/` accumulates one directory per configuration --- per toolchain, per target, and per version of a dependency, because the version is part of the fingerprint --- so after the version moved from 0.5.0 to 0.6.0 the search @@ -484,7 +484,7 @@ not a defect this library can fix, and a refusal is one a caller can act upon. ## Seven more, added with the exec search — and what they have in common -⭐⭐ **Every one of these is an operation that was PRESENT AND ANSWERED WRONGLY, +**Every one of these is an operation that was PRESENT AND ANSWERED WRONGLY, which is a different failure from an operation that is missing — and it is the reason none of them was found by the diagnostic added for the last set.** @@ -499,7 +499,7 @@ programs took one exclusive lock and both were told they had it. `F_GETLK` was worse — POSIX writes `F_UNLCK` into `l_type` when nothing would block, and leaving the caller's word untouched returns the `F_WRLCK` the caller conventionally put there, so the answer read "somebody holds this" for ever and -a loop waiting for a lock never left it. All three now report `ENOSYS`. ⭐ The +a loop waiting for a lock never left it. All three now report `ENOSYS`. The refusal is temporary in a way the permission one is not: every environment beneath openkal can lock a byte range, and what is missing is a word in the specification. Composing one here from `KAL_OPEN_EXCLUSIVE` is not an option — @@ -508,7 +508,7 @@ nothing would release it when its holder died. **`getppid` returned a negated error value as an identifier.** musl writes it without `__syscall_ret`, deliberately, because POSIX says it cannot fail; the default arm answered `-ENOSYS` and a caller was told its parent was -38, with -`errno` untouched. ⚠️ **This is the defect `getpgrp` had and that was fixed one +`errno` untouched. **This is the defect `getpgrp` had and that was fixed one release earlier, three lines away in the same dispatch, and it was not looked for.** It answers 0 now — "no parent this environment can name" — and `examples/surface` asks the whole family rather than the member. @@ -516,7 +516,7 @@ for.** It answers 0 now — "no parent this environment can name" — and **A copy of the calling image reported its parent's identifier.** `getpid` answered the constant 1 in every context, so `fork` produced two images that gave one answer and the copy had no way to name itself. The identifier is now -settled before the copy is taken and carried into it. ⚠️ The comparison `kill` +settled before the copy is taken and carried into it. The comparison `kill` makes to decide "this program itself" moved with it; against the constant it would have made `raise`, and therefore `abort`, report `ESRCH` in every copy. @@ -537,7 +537,7 @@ openkal reports no such limits and inventing one is the shape this port avoids. **`utimensat` could not set a directory's time.** It asked for `KAL_OPEN_READ | KAL_OPEN_WRITE` unconditionally and a directory refuses that. -It now asks what the name refers to and opens a directory for reading only. ⚠️ +It now asks what the name refers to and opens a directory for reading only. That is outside what `fs.h` states — the interface requires `KAL_OPEN_WRITE` and has no `kal_dir` form of `kal_fs_set_modified`, so there is no stated route to a directory's time at all. A file still asks for exactly what is required, diff --git a/port/include/features.h b/port/include/features.h index 1d02eac..0dd7d43 100644 --- a/port/include/features.h +++ b/port/include/features.h @@ -62,7 +62,7 @@ #include_next -/* ⚠️ THE INTERNAL HEADERS ARE MUSL'S, AND A CONSUMER IS NOT MUSL. +/* THE INTERNAL HEADERS ARE MUSL'S, AND A CONSUMER IS NOT MUSL. * * musl's build reaches its own declarations through src/include, whose headers * add the hidden entries the public ones do not have. This package publishes @@ -80,7 +80,7 @@ * not be lost, and it was not: mcpp 2026.8.27.1 added * `[build] private_include_dirs`, and this package's manifest now uses it. * - * ⚠️ SO WHY IS THE BLOCK BELOW STILL HERE? Because the two answer different + * SO WHY IS THE BLOCK BELOW STILL HERE? Because the two answer different * questions and only one of them is about visibility. * * private_include_dirs --- WHO SEES the internal overlay. A consumer no @@ -95,14 +95,14 @@ * use of undeclared identifier __weak__'. Removing either axis brings back a * defect that has already been paid for once. * - * ⚠️ The rejected alternative is also kept: moving the two directories into + * The rejected alternative is also kept: moving the two directories into * per-glob flags places them AFTER include_dirs on the command line, and musl's * own build then finds the public before the internal one and * fails with `unknown type name hidden'. That is why `private_include_dirs` is * a SUBSET of `include_dirs` rather than a second list --- the order is the * thing that cannot be given up. * - * ⭐⭐ THE DISCRIMINATOR WAS WRONG ONCE, AND THE WRONG ONE HELD FOR A DAY. + * THE DISCRIMINATOR WAS WRONG ONCE, AND THE WRONG ONE HELD FOR A DAY. * * It used to be `#ifdef __cplusplus', with this reasoning written beside it: * musl's own sources are C and are never compiled as anything else, so a @@ -137,7 +137,7 @@ * both named `restrict'. Given the spelling every compiler * accepts in both languages. * - * hidden ⭐ IN C++, GIVEN C LINKAGE rather than emptied. Every + * hidden IN C++, GIVEN C LINKAGE rather than emptied. Every * declaration the internal overlay adds begins with it --- that * is what it is for --- so one spelling gives all of them the * linkage they were written with. The overlay's headers carry no @@ -149,7 +149,7 @@ * ___errno_location'. In C there is no linkage to restore, so it * is emptied. * - * weak an attribute musl spells as a bare word. Emptied. ⚠️ This is + * weak an attribute musl spells as a bare word. Emptied. This is * the one that bit compiler-rt: it writes * `__attribute__((weak))' of its own, which became * `__attribute__((__attribute__((__weak__))))'. @@ -161,7 +161,7 @@ * libunwind does, in fifteen places, and reported * `use of undeclared identifier __weak__'. * - * ⚠️⚠️ THE FOUR DO NOT ALL COME FROM THE SAME PLACE, WHICH IS WHY THEY ARE NO + * THE FOUR DO NOT ALL COME FROM THE SAME PLACE, WHICH IS WHY THEY ARE NO * LONGER TREATED THE SAME. Reported as mcpplibs/openkal-musl#13: a program * above this library could not declare `static int hidden = 7;'. * @@ -183,7 +183,7 @@ * included" --- rather than the question it used to ask, which was "is this * unit musl's" and which answered the wrong way for every consumer. * - * ⚠️ THE COMPILER-RT CASE STILL HOLDS, and it is the reason the block is scoped + * THE COMPILER-RT CASE STILL HOLDS, and it is the reason the block is scoped * rather than deleted. A board building compiler-rt here compiles C that is not * musl's WITH the overlay on its line --- `private_include_dirs' does not reach * it, because it is not a consumer of this package's public interface but a diff --git a/port/include/sys/random.h b/port/include/sys/random.h index 2d12764..823fbae 100644 --- a/port/include/sys/random.h +++ b/port/include/sys/random.h @@ -1,6 +1,6 @@ /* musl's , plus the declaration a consumer expects to find here. * - * ⭐ THE SAME QUESTION EVERY OTHER OVERLAY IN THIS ECOSYSTEM ANSWERS: consumers + * THE SAME QUESTION EVERY OTHER OVERLAY IN THIS ECOSYSTEM ANSWERS: consumers * ask "which OPERATING SYSTEM is this" and assume that system's C library. Here * the C library is musl, whatever the system underneath. * @@ -13,7 +13,7 @@ * * random.cpp:52:14: error: use of undeclared identifier 'getentropy' * - * ⚠️ THIS ADDS A DECLARATION AND NOTHING ELSE. The definition is musl's, in + * THIS ADDS A DECLARATION AND NOTHING ELSE. The definition is musl's, in * `src/misc/getentropy.c`, unchanged; what this file corrects is where a * consumer looks for its name. Declaring it in both places is what glibc does * and what the consumer was written against. diff --git a/port/include/sys/stat.h b/port/include/sys/stat.h index f273a1f..efd9597 100644 --- a/port/include/sys/stat.h +++ b/port/include/sys/stat.h @@ -1,7 +1,7 @@ /* musl's , plus the field names this object format's consumers * expect. * - * ⭐ THE SAME QUESTION EVERY OTHER OVERLAY IN THIS ECOSYSTEM ANSWERS: consumers + * THE SAME QUESTION EVERY OTHER OVERLAY IN THIS ECOSYSTEM ANSWERS: consumers * ask "which OPERATING SYSTEM is this" and assume that system's C library. * Here the C library is musl, whatever the object format. * @@ -14,12 +14,12 @@ * time_utils.h:277: no member named 'st_mtimespec' in 'stat' * time_utils.h:278: no member named 'st_atimespec' in 'stat' * - * ⚠️ ALIASES, NOT A SECOND SET OF FIELDS. There is one `struct stat` and one + * ALIASES, NOT A SECOND SET OF FIELDS. There is one `struct stat` and one * layout — musl's — and this only gives two of its members a second name. A * consumer that writes either spelling reaches the same bytes, which is what * makes this safe: nothing here changes what crosses an interface. * - * ⚠️ SCOPED TO THE FORMAT THAT ASKS. On ELF and PE nothing spells them this + * SCOPED TO THE FORMAT THAT ASKS. On ELF and PE nothing spells them this * way, and defining the names there would put two identifiers into every * translation unit for no reason. * diff --git a/port/src/mach/early_init.c b/port/src/mach/early_init.c index 37e9638..5b700c3 100644 --- a/port/src/mach/early_init.c +++ b/port/src/mach/early_init.c @@ -1,6 +1,6 @@ /* Bringing this library up before the C++ runtime's static initialisers. * - * ⭐ WHY THIS FILE EXISTS ONLY FOR THIS OBJECT FORMAT. The dynamic loader here + * WHY THIS FILE EXISTS ONLY FOR THIS OBJECT FORMAT. The dynamic loader here * runs an image's constructors BEFORE it transfers control to the entry point, * which is the opposite of the order every other format this port serves uses. * `port/src/okm_start.c` records the measurement: libc++ constructs its @@ -10,7 +10,7 @@ * ⇒ A constructor of our own, so that the library is up before any other * constructor can reach it. * - * ⚠️ ORDER AMONG CONSTRUCTORS IS LINK ORDER, AND THAT IS WHAT MAKES THIS WORK + * ORDER AMONG CONSTRUCTORS IS LINK ORDER, AND THAT IS WHAT MAKES THIS WORK * RATHER THAN THE PRIORITY. A priority argument is honoured within one * translation unit and not across archives on this format. What does hold is * that this package is a DEPENDENCY of the C++ runtime, so its objects precede @@ -18,7 +18,7 @@ * Measured: the ten module initialisers of `openkal` ran first, then this * library's, then libc++'s. * - * ⚠️ AND THE GUARD IS WHAT MAKES IT SAFE RATHER THAN THE ORDER. `okm_start` + * AND THE GUARD IS WHAT MAKES IT SAFE RATHER THAN THE ORDER. `okm_start` * still calls the same function; whichever arrives first does the work and the * second returns immediately. A fix that depended on the order being right * would be a fix that fails silently when it is not. diff --git a/port/src/okm.h b/port/src/okm.h index c380f08..be409ff 100644 --- a/port/src/okm.h +++ b/port/src/okm.h @@ -21,14 +21,14 @@ #include #include -/* ⭐ WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH PROVIDES. +/* WHICH OPENKAL INTERFACES THE IMPLEMENTATION BENEATH PROVIDES. * * openkal is composable. An implementation provides an interface in whole or * not at all (clause 6.1), and one that cannot provide `fs' does not provide * it, because clause 6.2 forbids the alternative in terms: an operation that is * present and always fails is a defect. * - * ⚠️ THE CONSEQUENCE FOR A C LIBRARY, WHICH IS WHY THIS BLOCK EXISTS. + * THE CONSEQUENCE FOR A C LIBRARY, WHICH IS WHY THIS BLOCK EXISTS. * * A C library calls those interfaces. If it is BUILT calling one the * implementation beneath does not provide, the link fails --- and it fails for @@ -40,7 +40,7 @@ * openkal's core set: a program whose entire text was `import std;' and one * line of output failed to link on fifteen names it can never reach. * - * ⭐ SO THE C LIBRARY IS CONFIGURED AND AN OPENKAL IMPLEMENTATION IS NOT. THAT + * SO THE C LIBRARY IS CONFIGURED AND AN OPENKAL IMPLEMENTATION IS NOT. THAT * LOOKS LIKE A CONTRADICTION AND IS NOT ONE. * * An openkal implementation may not answer "unsupported", because openkal's @@ -54,14 +54,14 @@ * place where it would go wrong is a library that returned SUCCESS having done * nothing. * - * ⭐ THREE MACROS HERE AND ALL THE CONSEQUENCES IN ONE OTHER FILE. This block + * THREE MACROS HERE AND ALL THE CONSEQUENCES IN ONE OTHER FILE. This block * says what the environment HAS; okm_opt.h says what follows, and it is the * only file in the port that reads these. A source that calls an interface * which need not be there names `okm_fs_*' rather than `kal_fs_*', and never * asks the question itself --- because the same question asked in forty places * is answered wrongly in the forty-first. * - * ⚠️ THE TARGET IS A PROXY FOR THE IMPLEMENTATION, AND AN IMPERFECT ONE. + * THE TARGET IS A PROXY FOR THE IMPLEMENTATION, AND AN IMPERFECT ONE. * * The manifest clears these for cfg(os="none"), because a target with no * operating system is the case where an implementation providing storage is @@ -127,7 +127,7 @@ struct okm_desc { int pending_kind; char pending_name[256]; int sock; /* OKM_SOCKET: the slot in okm_net.c, else -1 */ - /* ⭐ ONE BYTE READ AHEAD, WHICH IS HOW A READINESS ENQUIRY IS ANSWERED. + /* ONE BYTE READ AHEAD, WHICH IS HOW A READINESS ENQUIRY IS ANSWERED. * * openkal has no operation that reports whether a transfer would proceed. * `openkal.timeout' bounds the transfer itself, and clause 6.3 records @@ -142,7 +142,7 @@ struct okm_desc { * moment it was made. okm_desc already holds a directory entry for the same * reason and by the same means. * - * ⚠️ ONE BYTE AND NOT A BUFFER. A short read is a result every caller of + * ONE BYTE AND NOT A BUFFER. A short read is a result every caller of * `read' already handles, and a larger read-ahead would turn this into a * second layer of buffering underneath stdio's. */ int ahead; /* a byte is held */ @@ -232,7 +232,7 @@ kal_uintptr okm_open_flags(int open_flags); /* --- sockets, in okm_net.c ------------------------------------------------- * - * ⚠️ BSD SEPARATES `socket' FROM `connect' AND `bind'; openkal DOES NOT. + * BSD SEPARATES `socket' FROM `connect' AND `bind'; openkal DOES NOT. * `kal_net_connect' produces a connection and there is no unbound socket to * produce first. So a descriptor made by `socket' holds nothing but the three * numbers it was given, and the openkal operation happens later --- at diff --git a/port/src/okm_fcntl.c b/port/src/okm_fcntl.c index 1f1e22e..fc1e342 100644 --- a/port/src/okm_fcntl.c +++ b/port/src/okm_fcntl.c @@ -1,6 +1,6 @@ /* Operations upon an open file description. * - * ⚠️⚠️ THE ELEVENTH SOURCE THIS PORT REPLACES, AND IT IS THE THIRD OF ONE KIND: + * THE ELEVENTH SOURCE THIS PORT REPLACES, AND IT IS THE THIRD OF ONE KIND: * A MACHINE WORD CARRIED THROUGH A VARIABLE DECLARED `long'. * * musl's own reads its variable argument as an `unsigned long': @@ -15,7 +15,7 @@ * thirty-two bits and a pointer holds sixty-four --- so a caller passing a * `struct flock *' had the top half of it discarded before the port saw it. * - * ⭐⭐ AND IT WAS UNREACHABLE UNTIL 0.11.0, WHICH IS WHY IT SURVIVED THREE + * AND IT WAS UNREACHABLE UNTIL 0.11.0, WHICH IS WHY IT SURVIVED THREE * RELEASES OF A PORT THAT ALREADY NAMED THIS DEFECT TWICE. * * Every command this library answered before took an integer, or took a pointer @@ -24,7 +24,7 @@ * reports. openkal 0.10 gave this port a real lock, `F_SETLK' began following * the pointer, and the defect became a fault on the first attempt. * - * ⚠️ MEASURED, and the register file names the type rather than the symptom: + * MEASURED, and the register file names the type rather than the symptom: * * page fault on read access to 0x00000000fe2ffec2 * rax:00000000fe2ffec0 rsp:00007ffffe2fc7a0 @@ -39,7 +39,7 @@ * integer at all. `uintptr_t' is the type that holds a pointer on every target, * and it is what `va_arg' is asked for. * - * ⚠️ THE VARARG TYPE IS PART OF THE CALLING CONVENTION AND NOT A DETAIL. + * THE VARARG TYPE IS PART OF THE CALLING CONVENTION AND NOT A DETAIL. * `va_arg(ap, unsigned long)' and `va_arg(ap, uintptr_t)' read different numbers * of bytes where the two types differ, so this is not a cast applied afterwards * --- reading it as the narrower type has already lost the half by then. diff --git a/port/src/okm_fd.c b/port/src/okm_fd.c index a16e24f..a076807 100644 --- a/port/src/okm_fd.c +++ b/port/src/okm_fd.c @@ -24,7 +24,7 @@ #include "okm.h" /* For kal_process_channel_close: a channel end is owned and is released here. * - * ⚠️ WEAK, for the reason okm_syscall.c states beside the same pair: a backend + * WEAK, for the reason okm_syscall.c states beside the same pair: a backend * that provides no `openkal.process' provides neither, and a strong reference * would make an interface clause 6.1 permits a backend to decline into one every * program must have. A descriptor of this kind cannot exist without the @@ -57,17 +57,26 @@ int okm_errno(int e) case kal_err_not_empty: return ENOTEMPTY; case kal_err_is_directory: return EISDIR; case kal_err_not_directory: return ENOTDIR; + /* Version 0.13. The name exists, may be started, and is not in a form this + * environment can start --- ENOEXEC is the native condition on every host + * this port targets, and folding it into EIO would report a medium failure + * for a name that read perfectly well. openkal SPEC.md 5.2 states the + * argument; issue 28 is the consumer that measured the absence of it. + * musl's own `execvp' has no `/bin/sh' fallback for ENOEXEC + * (musl/src/process/execvp.c) and this port does not add one --- see + * musl/PATCHES.md, which already declines the analogous case for `:'. */ + case kal_err_not_program: return ENOEXEC; default: return EIO; } } /* How a program's open(2) flags are said in openkal's vocabulary. * - * ⭐ ONE DECISION IN ONE PLACE. Two callers reach it --- `open' itself and the + * ONE DECISION IN ONE PLACE. Two callers reach it --- `open' itself and the * file action a spawn may carry --- and a second derivation of the same table * would agree with this one until one of them was extended. * - * ⚠️ The mode a caller supplies is not among the inputs, and its absence is not + * The mode a caller supplies is not among the inputs, and its absence is not * an oversight: `kal_fs_open' takes what a file is opened FOR and not who may * later reach it. README.md records what a program observes as a result. */ kal_uintptr okm_open_flags(int flags) @@ -104,7 +113,7 @@ static struct { int desc; int cloexec; } g_fd[OKM_MAX_FD]; static char g_dirpath[OKM_MAX_DIRS][OKM_DIR_PATH]; static int g_dirpath_used[OKM_MAX_DIRS]; -/* ⭐ THE THREE STREAMS THE PROGRAM WAS STARTED WITH, KEPT BECAUSE A LATER +/* THE THREE STREAMS THE PROGRAM WAS STARTED WITH, KEPT BECAUSE A LATER * QUESTION IS ABOUT HISTORY RATHER THAN ABOUT THE PRESENT. * * When this library starts another program it must say what that program's @@ -147,7 +156,7 @@ static void desc_release(int d) struct okm_desc* p = &g_desc[d]; if (p->kind == OKM_FILE) okm_fs_close_file(p->file); else if (p->kind == OKM_DIR) okm_fs_close_dir(p->dir); - /* ⚠️ A CHANNEL END IS OWNED AND A STREAM IS NOT, WHICH IS WHY THEY ARE TWO + /* A CHANNEL END IS OWNED AND A STREAM IS NOT, WHICH IS WHY THEY ARE TWO * KINDS. openkal draws the same division: the three standard streams are * borrowed and have no release, while a channel end is obtained and must be * given back. @@ -525,7 +534,7 @@ int okm_absolute(int dirfd, const char* path, char* out, size_t cap) /* Every stream this image holds, released --- used by the one place that has an * image with nothing left to do. * - * ⚠️⚠️ IT EXISTS BECAUSE `execve' HERE LEAVES AN IMAGE BEHIND. A replacement + * IT EXISTS BECAUSE `execve' HERE LEAVES AN IMAGE BEHIND. A replacement * leaves one image; this library composes it as start, wait, end, which leaves * two --- and the second still holds every file description the caller had. A pipe * reports the end of input when the LAST writer lets go, so a waiter holding the @@ -533,7 +542,7 @@ int okm_absolute(int dirfd, const char* path, char* out, size_t cap) * already ended. Measured through a consumer: a server that exits mid-request was * reported as a timeout rather than as a closed connection. * - * ⚠️ NOT A GENERAL `close everything'. The standard streams are released too, + * NOT A GENERAL `close everything'. The standard streams are released too, * which is right HERE and wrong anywhere else: this image writes nothing more. * The one caller is the `execve' composition, immediately before it waits. */ void __okm_close_all_for_exec(void) diff --git a/port/src/okm_fork.c b/port/src/okm_fork.c index 37fef32..8ffcff1 100644 --- a/port/src/okm_fork.c +++ b/port/src/okm_fork.c @@ -1,6 +1,6 @@ /* fork(2), composed above openkal.space. * - * ⭐ THE SPECIFICATION DESCRIBES THIS COMPOSITION AND DECLINES TO PERFORM IT, + * THE SPECIFICATION DESCRIBES THIS COMPOSITION AND DECLINES TO PERFORM IT, * WHICH IS WHY THE CODE IS HERE AND NOT BENEATH. openkal/include/openkal/space.h * says so in terms: * @@ -15,7 +15,7 @@ * carries in okm_setjmp.S, and the restoring is `longjmp' in the started * context. Nothing beneath changes and the specification does not move. * - * ⚠️ AN EARLIER READING OF openkal-linux#13 CONCLUDED THAT THE SPECIFICATION HAD + * AN EARLIER READING OF openkal-linux#13 CONCLUDED THAT THE SPECIFICATION HAD * DELIBERATELY DECLINED fork, on the ground that duplicating an address space * AND ITS EXECUTION STATE cannot be required of every environment. Half of that * is right and the conclusion drawn from it was wrong: clause 7.1 declines the @@ -68,19 +68,19 @@ void __okm_set_self_pid(int pid); /* The context the started child resumes into. * - * ⚠️ ONE, AND THE PORT'S OWN LOCK AROUND IT. Two contexts forking at once would + * ONE, AND THE PORT'S OWN LOCK AROUND IT. Two contexts forking at once would * otherwise record over each other and the second child would resume into the * first one's frame. This port's table lock is the one taken rather than a lock * of this file's, and holding it does a second job worth having: the copy is * taken while no other context is part-way through a change to the descriptor * table, so the child begins with a table that is whole. * - * ⚠️ THE CHILD RELEASES ITS COPY OF THE LOCK. It was taken before the copy, so + * THE CHILD RELEASES ITS COPY OF THE LOCK. It was taken before the copy, so * the copy holds it too, and a child that did not release it would stop at the * first descriptor it touched. */ static jmp_buf g_resume; -/* ⚠️⚠️ WHAT THE COPY CARRIES AND WHAT IT DOES NOT: THE IDENTITY IS NOT CARRIED. +/* WHAT THE COPY CARRIES AND WHAT IT DOES NOT: THE IDENTITY IS NOT CARRIED. * * okm_context.c keeps this library's per-context state --- its error value, its * locale, its thread record --- in a table keyed on `kal_task_current()'. The @@ -98,7 +98,7 @@ static jmp_buf g_resume; * interface asks, and the assumption that a copy keeps its identity was this * port's. * - * ⭐ MEASURED, AND THE PORT'S OWN DIAGNOSTIC NAMED IT. On the macOS row the + * MEASURED, AND THE PORT'S OWN DIAGNOSTIC NAMED IT. On the macOS row the * copy stopped with * * openkal-musl: this execution context has no per-context state --- the @@ -116,7 +116,7 @@ static jmp_buf g_resume; static volatile uintptr_t g_carried_tp; static void* volatile g_carried_self; -/* ⭐ AND THE IDENTIFIER, WHICH IS CARRIED FOR THE SAME REASON AND BY THE SAME +/* AND THE IDENTIFIER, WHICH IS CARRIED FOR THE SAME REASON AND BY THE SAME * MEANS. `getpid' answered the constant 1 in every context, so a copy reported * the identifier of the image it was copied from: two contexts, one answer, and * no way for the copy to name itself. The number the copy should give is the one @@ -145,12 +145,12 @@ syscall_arg_t __okm_fork(void) g_carried_tp = __okm_get_tp(); g_carried_self = __okm_get_self(); - /* ⚠️ THE ENTRY IS TAKEN BEFORE THE CONTEXT EXISTS, so that the identifier + /* THE ENTRY IS TAKEN BEFORE THE CONTEXT EXISTS, so that the identifier * settled here is the one the copy reads out of its own copy of this global. * The lock this function already holds is the table's, which is what makes * reserving here safe and what makes a second acquisition wrong. * - * ⚠️ `volatile', although both are written BEFORE the `setjmp' below and are + * `volatile', although both are written BEFORE the `setjmp' below and are * read only on the path that does not resume through it. That is enough to * be correct and is not enough to be obviously correct: this file's rule is * that a local live across that call says so, and a reader checking the rule @@ -161,12 +161,12 @@ syscall_arg_t __okm_fork(void) const volatile int reserved_pid = scratch; g_carried_pid = reserved_pid; - /* ⚠️ NOTHING BELOW THIS LINE MAY READ A LOCAL VARIABLE THAT WAS WRITTEN + /* NOTHING BELOW THIS LINE MAY READ A LOCAL VARIABLE THAT WAS WRITTEN * AFTER IT. A variable modified between `setjmp' and `longjmp' and not * declared volatile is indeterminate in the resumed context; the child path * therefore reads nothing but the two globals above and returns a constant. */ if (setjmp(g_resume) != 0) { - /* ⚠️ THIS IS THE FIRST THING THE COPY DOES, AND THE ORDER IS THE POINT. + /* THIS IS THE FIRST THING THE COPY DOES, AND THE ORDER IS THE POINT. * `okm_unlock' is an atomic store and touches no per-context state; * everything after it does. */ __okm_set_tp(g_carried_tp); @@ -175,7 +175,7 @@ syscall_arg_t __okm_fork(void) * and POSIX is explicit that a duplicate has no children. okm_syscall.c * records what keeping them would cost. */ __okm_forget_children(); - /* ⭐ AND THE COPY NAMES ITSELF. Read from this file's own global, which + /* AND THE COPY NAMES ITSELF. Read from this file's own global, which * the copy carries because it was written before the copy was taken; * `__okm_forget_children' above cleared the TABLE and not this. */ __okm_set_self_pid((int)g_carried_pid); @@ -186,7 +186,7 @@ syscall_arg_t __okm_fork(void) struct kal_process child; const int e = kal_space_start(child_entry, 0, g_entry_stack + sizeof g_entry_stack, &child); - /* ⚠️ THE ENTRY WAS TAKEN BEFORE THE CONTEXT WAS STARTED, so a start that + /* THE ENTRY WAS TAKEN BEFORE THE CONTEXT WAS STARTED, so a start that * failed has to give it back --- otherwise a program whose every `fork' * fails would exhaust the table and begin reporting EAGAIN for a reason * that has nothing to do with how many children it has. */ diff --git a/port/src/okm_net.c b/port/src/okm_net.c index 5fd4b35..8e1a22d 100644 --- a/port/src/okm_net.c +++ b/port/src/okm_net.c @@ -6,7 +6,7 @@ * dispatcher had no case for. Reported as mcpplibs/openkal-linux#13. The gap * was in this port and not in musl's sources and not in the specification. * - * ⚠️⚠️ THE ONE STRUCTURAL DIFFERENCE, AND EVERYTHING ELSE FOLLOWS FROM IT. + * THE ONE STRUCTURAL DIFFERENCE, AND EVERYTHING ELSE FOLLOWS FROM IT. * * BSD makes a socket first and decides what it is afterwards: `socket' yields * an object, and `connect' or `bind'+`listen' then gives it a role. openkal @@ -26,7 +26,7 @@ * every error a real kernel would report at `bind' this port reports at * `listen', which is one call later and carries the same value. * - * ⚠️⚠️ EVERY REFERENCE TO EITHER INTERFACE IS WEAK, AND THE RULE IS NOT + * EVERY REFERENCE TO EITHER INTERFACE IS WEAK, AND THE RULE IS NOT * OPTIONAL. Clause 6.1 expresses an interface an implementation does not * provide as the ABSENCE of its definitions, and openkal-macos, openkal-windows, * openkal-opensbi and openkal-uefi all decline these two today. A strong @@ -35,7 +35,7 @@ * added for `kal_process_channel' and `kal_random_fill' to avoid --- twice * already, in this same port. * - * ⭐ ONE TEST PER INTERFACE AND NOT ONE PER OPERATION. Clause 3 requires an + * ONE TEST PER INTERFACE AND NOT ONE PER OPERATION. Clause 3 requires an * implementation to provide an interface in whole or not at all, so whether * `kal_net_connect' is present answers for all eleven names. Every name is * still DECLARED weak --- that is what keeps the link from requiring it --- and @@ -164,7 +164,7 @@ void okm_sock_release(int slot) if (s->pend_conn && kal_net_close) kal_net_close(s->pend); s->state = OKM_SOCK_FREE; s->pend_conn = 0; s->pend_msg = 0; - /* ⚠️ THE BUFFER IS KEPT AND THE SLOT IS NOT. Freeing it here would return + /* THE BUFFER IS KEPT AND THE SLOT IS NOT. Freeing it here would return * memory to an allocator this library also implements, from a path a * program may reach while holding the table's lock. It is at most * OKM_DGRAM_MAX per slot, the slot count is fixed, and the next socket to @@ -178,7 +178,7 @@ void okm_sock_release(int slot) * written here rather than taken from `inet_pton' because it is a conversion * between two structures and not a parse of text. * - * ⚠️ A LENGTH OR A FAMILY THIS PORT DOES NOT KNOW IS REFUSED RATHER THAN READ + * A LENGTH OR A FAMILY THIS PORT DOES NOT KNOW IS REFUSED RATHER THAN READ * AS ONE IT DOES, which is the rule the specification states for the same * conversion in the other direction: an implementation that ignored the field * would misread every address a later revision defines, silently. */ @@ -278,7 +278,7 @@ int okm_sock_open(int domain, int type, int protocol) if (base == SOCK_STREAM && !OKM_HAVE_NET) return -EAFNOSUPPORT; if (base == SOCK_DGRAM && !OKM_HAVE_DGRAM) return -EAFNOSUPPORT; - /* ⚠️ REFUSED HERE RATHER THAN IGNORED. A descriptor that was asked to be + /* REFUSED HERE RATHER THAN IGNORED. A descriptor that was asked to be * non-blocking and is not would make every subsequent operation block where * the caller arranged not to, and it would do so silently. */ if ((type & SOCK_NONBLOCK) && !okm_can_bound()) return -ENOSYS; @@ -316,7 +316,7 @@ int okm_sock_bind(int fd, const void* addr, unsigned len) const int r = to_endpoint(addr, len, &ep); if (r) return r; - /* ⭐ RECORDED AND NOT PERFORMED, which is the deferral this file exists for. + /* RECORDED AND NOT PERFORMED, which is the deferral this file exists for. * A datagram socket spends it at the first send or receive; a stream socket * spends it at `listen'. Neither loses an error: openkal reports at the * operation what a kernel reports at the bind, and the value is the same @@ -451,7 +451,7 @@ int okm_sock_connect(int fd, const void* addr, unsigned len) if (s->state != OKM_SOCK_NEW) return -EINVAL; if (!OKM_HAVE_NET) return -ENOSYS; - /* ⚠️ THE CONNECTION IS ESTABLISHED BEFORE THIS RETURNS, EVEN ON A + /* THE CONNECTION IS ESTABLISHED BEFORE THIS RETURNS, EVEN ON A * NON-BLOCKING DESCRIPTOR, and a caller cannot be told otherwise honestly. * * `kal_net_connect' completes or fails; openkal has no form that begins a @@ -646,7 +646,7 @@ long okm_sock_recv(int fd, void* buf, unsigned long len, int flags, /* --- options ---------------------------------------------------------------- * - * ⚠️ AN OPTION THIS PORT CANNOT HONOUR IS REFUSED. Accepting one and ignoring it + * AN OPTION THIS PORT CANNOT HONOUR IS REFUSED. Accepting one and ignoring it * is the single failure this whole port is written to avoid: a caller that set * SO_BROADCAST and was told it succeeded would send to a broadcast address and * be told the send succeeded too, and nothing would ever arrive. ENOPROTOOPT is @@ -675,7 +675,7 @@ int okm_sock_setopt(int fd, int level, int opt, const void* val, unsigned len) switch (opt) { case SO_REUSEADDR: - /* ⭐ ACCEPTED BECAUSE IT IS ALREADY IN EFFECT, not because it is + /* ACCEPTED BECAUSE IT IS ALREADY IN EFFECT, not because it is * harmless. openkal's `kal_net_listen' sets this on the listener it * makes --- openkal-linux/src/net.cpp says so and gives the reason: a * program restarted within the kernel's lingering interval is the @@ -709,7 +709,7 @@ int okm_sock_getopt(int fd, int level, int opt, void* val, unsigned* len) *len = sizeof(int); return 0; case SO_ERROR: - /* ⭐ ALWAYS ZERO, AND IT IS AN ACCURATE ANSWER RATHER THAN A STAND-IN. + /* ALWAYS ZERO, AND IT IS AN ACCURATE ANSWER RATHER THAN A STAND-IN. * This option reports an error that arrived after the call that would * have reported it returned. Every operation here completes before it * returns --- see the note at `connect' --- so there is never one @@ -765,7 +765,7 @@ int okm_sock_wait_in(struct okm_desc* d, kal_u64 ns) if (s->state == OKM_SOCK_DGRAM) { if (s->pend_msg) return 1; if (!kal_timeout_recv_from) return -ENOSYS; - /* ⚠️ THE BUFFER IS WHY A READINESS ENQUIRY IS NOT DESTRUCTIVE HERE. + /* THE BUFFER IS WHY A READINESS ENQUIRY IS NOT DESTRUCTIVE HERE. * openkal reports a message by delivering it, so the only way to learn * that one has arrived is to take it, and the only way to keep the * enquiry honest is to hold it until the receive that follows. */ diff --git a/port/src/okm_opt.h b/port/src/okm_opt.h index a990ec9..3244287 100644 --- a/port/src/okm_opt.h +++ b/port/src/okm_opt.h @@ -1,7 +1,7 @@ /* The seam between the C library and the openkal interfaces that need not be * there. * - * ⚠️ WHY A SEAM RATHER THAN A GUARD AT EACH CALL. + * WHY A SEAM RATHER THAN A GUARD AT EACH CALL. * * `openkal.fs' and `openkal.process' are called from about forty places in * okm_syscall.c, spread through a switch on the system call number. Guarding @@ -13,7 +13,7 @@ * `okm_process_*'. Where the interface is present these are the interface, with * no code generated for the indirection. Where it is not, they are the answer. * - * ⭐ AND THE ANSWER IS AN ERROR, WHICH IS ALLOWED HERE AND FORBIDDEN BELOW. + * AND THE ANSWER IS AN ERROR, WHICH IS ALLOWED HERE AND FORBIDDEN BELOW. * * openkal clause 6.2 says an operation that is present and always fails is a * defect, and that the remedy is that its absence be expressed by its absence. @@ -28,7 +28,7 @@ * absence becomes, one layer up, an absence expressed by a defined error, and * the program that never opens a file never notices either. * - * ⚠️ THE ONE WAY THIS COULD GO WRONG IS NOT PRESENT: nothing below reports + * THE ONE WAY THIS COULD GO WRONG IS NOT PRESENT: nothing below reports * SUCCESS having done nothing. Every operation that could have done something * fails. The exceptions are marked where they appear and each is an operation * that is COMPLETE when there is nothing to do --- releasing a handle that @@ -60,13 +60,14 @@ #define okm_fs_file_info kal_fs_file_info #define okm_fs_set_modified kal_fs_set_modified #define okm_fs_set_modified_at kal_fs_set_modified_at +#define okm_fs_set_executable_at kal_fs_set_executable_at #define okm_fs_lock kal_fs_lock #define okm_fs_unlock kal_fs_unlock #define okm_fs_capacity kal_fs_capacity #define okm_fs_list_begin kal_fs_list_begin #define okm_fs_list_next kal_fs_list_next -/* ⚠️ WEAK, BECAUSE A VOLUME MAY HAVE NO SUCH NODES AND AN IMPLEMENTATION MAY +/* WEAK, BECAUSE A VOLUME MAY HAVE NO SUCH NODES AND AN IMPLEMENTATION MAY * NOT BE ABLE TO MAKE ONE. These are operations of `openkal.fs' and are present * wherever it is, so the weakness is not about the interface --- it is about a * backend built before they existed. `kal_fs_props' answers, per directory, @@ -74,7 +75,7 @@ extern __typeof(kal_fs_link_create) kal_fs_link_create __attribute__((__weak__)); extern __typeof(kal_fs_link_read) kal_fs_link_read __attribute__((__weak__)); -/* ⚠️⚠️ AND THEY GO THROUGH THE SEAM LIKE EVERYTHING ELSE, WHICH THEY DID NOT. +/* AND THEY GO THROUGH THE SEAM LIKE EVERYTHING ELSE, WHICH THEY DID NOT. * * The two call sites named `kal_fs_link_*' directly and tested the weak symbol * themselves. That is correct where `openkal.fs' is present and is not a @@ -85,7 +86,7 @@ extern __typeof(kal_fs_link_read) kal_fs_link_read __attribute__((__weak__)) * ld.lld: error: undefined symbol: kal_fs_link_read * >>> referenced by okm_syscall.c:340 ... (do_readlink) * - * ⭐ WHICH IS THE FAILURE THIS FILE'S OWN OPENING COMMENT PREDICTS, IN THE + * WHICH IS THE FAILURE THIS FILE'S OWN OPENING COMMENT PREDICTS, IN THE * WORDS IT PREDICTS IT IN: "a forty-first added later would be the one that was * missed --- and missed silently, because the way it shows is a link failure on * a target nobody was building at the time". It was found by openkal-opensbi's @@ -124,7 +125,7 @@ static inline int okm_fs_open(struct kal_dir, const char*, kal_uintptr, kal_uint * release and nothing to report --- the openkal operations are void too. */ static inline void okm_fs_close_dir(struct kal_dir) {} static inline void okm_fs_close_file(struct kal_file) {} -/* ⚠️ Zero, which is not a valid stream, and it is only ever reached with a file +/* Zero, which is not a valid stream, and it is only ever reached with a file * handle no operation above can have produced. A caller that reaches here has * already ignored a failure, and zero is what it then passes to * `kal_stream_write', which refuses it. */ @@ -155,6 +156,8 @@ static inline int okm_fs_file_info(struct kal_file, kal_u32, static inline int okm_fs_set_modified(struct kal_file, kal_u64) { return kal_err_not_supported; } static inline int okm_fs_set_modified_at(struct kal_dir, const char*, kal_uintptr, kal_u64) { return kal_err_not_supported; } +static inline int okm_fs_set_executable_at(struct kal_dir, const char*, kal_uintptr, + int) { return kal_err_not_supported; } static inline int okm_fs_lock(struct kal_file, kal_u64, kal_u64, kal_uintptr) { return kal_err_not_supported; } static inline int okm_fs_unlock(struct kal_file, kal_u64, @@ -176,7 +179,7 @@ static inline int okm_fs_list_next(struct kal_dir, kal_uintptr*, char*, kal_uint #define okm_process_terminate kal_process_terminate #define okm_process_close kal_process_close -/* ⭐⭐ THIS WAS THE ONE WEAK REFERENCE THAT NEEDED A DIFFERENT SPELLING, AND +/* THIS WAS THE ONE WEAK REFERENCE THAT NEEDED A DIFFERENT SPELLING, AND * openkal 0.9 REMOVED THE REASON. * * `kal_process_props' was an OBJECT. Every other weak reference in this port @@ -224,7 +227,7 @@ static inline void okm_process_close(struct kal_process) {} #define okm_task_wait kal_task_wait #define okm_task_wake kal_task_wake -/* ⭐ WEAK, BECAUSE openkal 0.10 ADDED IT AND A BACKEND MAY NOT HAVE FOLLOWED. +/* WEAK, BECAUSE openkal 0.10 ADDED IT AND A BACKEND MAY NOT HAVE FOLLOWED. * Every route in this port that reaches an optional operation tests the * reference before calling; this one is reached from `sched_getaffinity', which * a program asks once at startup and must not fault in. */ @@ -251,7 +254,7 @@ static inline int okm_task_join(struct kal_task) { return kal_ok; } * accurate rather than a stand-in: the caller asked to let something else run, * everything else has already run, and control comes back. */ static inline void okm_task_yield(void) {} -/* ⚠️ THE ONE ANSWER HERE THAT WOULD HANG RATHER THAN FAIL IF IT WERE WRONG. +/* THE ONE ANSWER HERE THAT WOULD HANG RATHER THAN FAIL IF IT WERE WRONG. * * musl waits like this: `while (*addr == val) futex(WAIT)'. With one execution * context nothing can change *addr while this call is in progress, so a caller @@ -261,7 +264,7 @@ static inline void okm_task_yield(void) {} * wait did not happen. */ static inline int okm_task_wait(const kal_u32*, kal_u32, kal_u64) { return kal_err_not_supported; } -/* ⚠️ SUCCESS, AND IT IS THE THIRD AND LAST OPERATION HERE THAT REPORTS ANY. +/* SUCCESS, AND IT IS THE THIRD AND LAST OPERATION HERE THAT REPORTS ANY. * * Waking every waiter is complete when there are none, and zero is the count * rather than a refusal --- a wake with no waiter is an ordinary outcome on diff --git a/port/src/okm_phdr.c b/port/src/okm_phdr.c index 25d96f9..db176bf 100644 --- a/port/src/okm_phdr.c +++ b/port/src/okm_phdr.c @@ -7,7 +7,7 @@ * have no such thing at all. The replacement therefore left the enquiry with * nothing to report. * - * ⚠️ IT REPORTED THE ABSENCE AS AN ANSWER, WHICH IS THE FAILURE THIS FILE IS + * IT REPORTED THE ABSENCE AS AN ANSWER, WHICH IS THE FAILURE THIS FILE IS * ABOUT. `dl_iterate_phdr' returned ONE object with `dlpi_phnum == 0': a caller * cannot distinguish that from a program that genuinely has no segments, so it * concludes there is nothing to find rather than that it was not told. diff --git a/port/src/okm_poll.c b/port/src/okm_poll.c index 8d0eb6c..8c7b5a5 100644 --- a/port/src/okm_poll.c +++ b/port/src/okm_poll.c @@ -1,6 +1,6 @@ /* Readiness, and transfers bounded in time. * - * ⚠️⚠️ openkal HAS NO OPERATION THAT REPORTS WHETHER A TRANSFER WOULD PROCEED, + * openkal HAS NO OPERATION THAT REPORTS WHETHER A TRANSFER WOULD PROCEED, * AND THAT IS DELIBERATE. Clause 6.3 records readiness notification among the * mechanisms considered and NOT adopted: an interface reporting readiness * obliges every implementation of it to maintain a set and a context of its @@ -10,7 +10,7 @@ * line: "a bounded read is a bounded wait for readiness followed by the * ordinary read". * - * ⭐ SO `poll' IS ANSWERED BY ATTEMPTING THE TRANSFER AND KEEPING WHAT IT + * SO `poll' IS ANSWERED BY ATTEMPTING THE TRANSFER AND KEEPING WHAT IT * PRODUCED. One byte from a stream, one connection from a listener, one message * from a datagram endpoint --- held in the descriptor and delivered to the * operation that follows. POLLIN asserts that a read will not block, and a byte @@ -22,7 +22,7 @@ * openkal has no operation that returns an entry to an iterator, so an entry * that does not fit the caller's buffer is held rather than lost. * - * ⚠️ WHAT IS NOT FAITHFUL, STATED HERE RATHER THAN DISCOVERED. + * WHAT IS NOT FAITHFUL, STATED HERE RATHER THAN DISCOVERED. * * POLLOUT is reported for every descriptor that may be written, without an * enquiry, because there is nothing to enquire of. A bounded write bounds the @@ -164,7 +164,7 @@ long okm_poll(void* p, unsigned long n, int timeout_ms) const int forever = timeout_ms < 0; kal_u64 left = forever ? 0 : (kal_u64)timeout_ms * 1000000ull; - /* ⭐ A SET OF ONE IS THE CASE WORTH SEPARATING, AND IT IS THE COMMON ONE. + /* A SET OF ONE IS THE CASE WORTH SEPARATING, AND IT IS THE COMMON ONE. * With a single descriptor the caller's whole bound can be handed to the * one bounded operation, so the wait happens in the environment rather than * in this loop. Only a larger set has to be revisited. */ @@ -192,7 +192,7 @@ long okm_poll(void* p, unsigned long n, int timeout_ms) } if ((fds[i].events & POLLIN) && has_input_route(d)) { - /* ⚠️⚠️ ZERO MEANS THE OPPOSITE IN THE TWO INTERFACES, AND THE + /* ZERO MEANS THE OPPOSITE IN THE TWO INTERFACES, AND THE * COLLISION IS SILENT. * * `poll' spells "do not wait" as a timeout of zero; openkal @@ -231,7 +231,7 @@ long okm_poll(void* p, unsigned long n, int timeout_ms) } if (ready) return ready; - /* ⚠️ A FAILURE IS REPORTED ONLY WHEN NOTHING WAS READY. A descriptor + /* A FAILURE IS REPORTED ONLY WHEN NOTHING WAS READY. A descriptor * whose environment refused the enquiry must not hide the readiness of * the others in the same set. */ if (failure) return failure; diff --git a/port/src/okm_spawn.c b/port/src/okm_spawn.c index 6b604a4..10e0e85 100644 --- a/port/src/okm_spawn.c +++ b/port/src/okm_spawn.c @@ -22,7 +22,7 @@ * * --- what the started program's three streams are ------------------------- * - * ⚠️⚠️ THIS FILE USED TO PASS `{0, 0, 0}' AND LET THE FILE ACTIONS OVERWRITE IT, + * THIS FILE USED TO PASS `{0, 0, 0}' AND LET THE FILE ACTIONS OVERWRITE IT, * WHICH LOST EVERY REDIRECTION A CALLER HAD ALREADY PERFORMED. * * openkal spells "the stream its parent has" as a handle of zero (process.h), @@ -38,12 +38,12 @@ * openkal-linux#13; measured by a consumer, not here, because the probe in * examples/subprocess started programs and never redirected first. * - * ⭐ THE ATOM WAS ALREADY PRESENT. `kal_fs_stream' is a required operation of + * THE ATOM WAS ALREADY PRESENT. `kal_fs_stream' is a required operation of * `openkal.fs' and this port has stored its result in every file description * since okm_syscall.c opened one. What was missing was the route from the table * to the spawn, which is the same shape as the defect that report began with. * - * ⭐ AND ZERO IS STILL PASSED WHERE NOTHING WAS REDIRECTED, WHICH IS LOAD-BEARING. + * AND ZERO IS STILL PASSED WHERE NOTHING WAS REDIRECTED, WHICH IS LOAD-BEARING. * `KAL_PROCESS_PROP_STREAM_PASSING' exists because an environment may be able to * start a program that inherits and unable to place a stream of the caller's * choosing. Seeding with zero for an untouched descriptor asks for that capability @@ -86,7 +86,7 @@ static int count(char* const* v) { int n = 0; while (v && v[n]) n++; return n; } /* --- can this name be started at all? -------------------------------------- * - * ⚠️⚠️ ASKED HERE BECAUSE THE ANSWER DOES NOT COME BACK FROM BENEATH, AND A + * ASKED HERE BECAUSE THE ANSWER DOES NOT COME BACK FROM BENEATH, AND A * CALLER OF `execvp' CANNOT PROCEED WITHOUT IT. * * An implementation starts a program by duplicating itself and replacing the @@ -104,12 +104,12 @@ static int count(char* const* v) { int n = 0; while (v && v[n]) n++; return n; } * openkal-linux#13 and measured by a consumer, who also measured that * `bwrap' --- present at /usr/bin/bwrap --- was reported as not installed. * - * ⭐ THE ENQUIRY IS ALREADY REQUIRED OF EVERY IMPLEMENTATION. `kal_fs_info' is + * THE ENQUIRY IS ALREADY REQUIRED OF EVERY IMPLEMENTATION. `kal_fs_info' is * an operation of `openkal.fs' and this file already resolves the name through * `okm_resolve'; asking what the name refers to is one more call on a path that * is about to start a program anyway. * - * ⚠️ AND IT ANSWERS TWO OF THE THREE QUESTIONS, WHICH IS WHY A3 IS STILL OPEN. + * AND IT ANSWERS TWO OF THE THREE QUESTIONS, WHICH IS WHY A3 IS STILL OPEN. * `ENOENT' and `ENOTDIR' are what a PATH search needs and are what this * settles. Whether an existing file may be EXECUTED is not something openkal * reports --- `kal_node_info' carries `writable' and no other permission --- so @@ -122,7 +122,7 @@ static int startable(struct kal_dir base, const char* rel) struct kal_node_info info = { .self_size = sizeof info }; /* Resolves, because starting resolves. */ const int e = okm_fs_info(base, rel, slen(rel), 0, KAL_INFO_KIND, &info); - /* ⚠️ AN ENQUIRY THAT CANNOT BE MADE IS NOT AN ANSWER OF `NO'. A build + /* AN ENQUIRY THAT CANNOT BE MADE IS NOT AN ANSWER OF `NO'. A build * configured without `openkal.fs' --- OKM_HAS_FS=0, which a machine with no * storage is built with --- answers `not supported' here, and turning that * into a refusal would stop a spawn this port would otherwise have @@ -179,7 +179,7 @@ static int startable_name(struct okm_at* at) * normalised back to zero: asking a backend for a capability it does not need * would refuse a spawn that can be performed. * - * ⚠️⚠️ AND ZERO IS BOTH A SENTINEL AND A VALID HANDLE, WHICH IS NOT THIS FILE'S + * AND ZERO IS BOTH A SENTINEL AND A VALID HANDLE, WHICH IS NOT THIS FILE'S * DOING AND IS THIS FILE'S PROBLEM. * * `kal_spawn_streams' spells inheritance as a handle of zero. `kal_stream' has @@ -210,7 +210,7 @@ static int place(struct kal_spawn_streams* s, int pos, kal_uintptr stream, /* The stream a descriptor carries, or a reason it carries none. * - * ⚠️ THE ANSWER IS DECIDED BY THE KIND AND NOT BY THE VALUE, and that distinction + * THE ANSWER IS DECIDED BY THE KIND AND NOT BY THE VALUE, and that distinction * is the whole of it: a stream handle of zero is a perfectly ordinary handle on * an implementation whose streams are its own descriptors, so "the handle is * zero" cannot be read as "there is no stream". A directory has no stream @@ -242,7 +242,7 @@ static int stream_for_spawn(int fd, kal_uintptr* out) /* The three positions before any file action is applied. * - * ⭐ THIS IS THE WHOLE OF THE FIX FOR openkal-linux#13, AND IT IS THREE LINES OF + * THIS IS THE WHOLE OF THE FIX FOR openkal-linux#13, AND IT IS THREE LINES OF * DECISION rather than a mechanism: a descriptor that still names what it named * when the program began is inheritance and is spelled zero; one that names * something else is a redirection the caller performed and is carried across; @@ -258,7 +258,7 @@ static int seed(struct kal_spawn_streams* s, int* placed) return 0; } -/* ⭐⭐ WHETHER THE STARTED PROGRAM MAY OUTLIVE THIS ONE, WHICH IS THE THING +/* WHETHER THE STARTED PROGRAM MAY OUTLIVE THIS ONE, WHICH IS THE THING * `execve' MEANS AND NOTHING ELSE HERE DOES. * * `execve' is composed as starting a program and ending with its status, so @@ -272,7 +272,7 @@ static int seed(struct kal_spawn_streams* s, int* placed) * not be said. It is asked for ONLY where `execve' is meant, because an ordinary * `posix_spawn' means the opposite: POSIX children outlive their parents. * - * ⚠️ AND A BACKEND MAY DECLINE IT, in which case this falls back to the + * AND A BACKEND MAY DECLINE IT, in which case this falls back to the * unbound spawn rather than refusing to start the program at all. A caller of * `execve' that gets an unbound program is where this port has always been; a * caller that gets no program is worse. The difference is recorded in @@ -288,7 +288,7 @@ static int start_program(int bound, struct kal_job* unit, struct kal_dir work, struct kal_spawn how; how.base = at->base; - /* ⭐ THE DIRECTORY THE PROGRAM RUNS IN, WHICH IS THIS LIBRARY'S OWN AND NOT + /* THE DIRECTORY THE PROGRAM RUNS IN, WHICH IS THIS LIBRARY'S OWN AND NOT * `base'. `base' is whichever preopen the program's NAME resolved under --- * for `/usr/bin/sh' that is the root --- and before openkal 0.11 it was the * only directory a spawn carried, so a started program ran wherever the @@ -296,7 +296,7 @@ static int start_program(int bound, struct kal_job* unit, struct kal_dir work, * names against and nothing else, so a caller that chdir'd and then started a * program was the one who found out. */ how.work = work; - /* ⭐ THE UNIT, WHICH IS WHAT `POSIX_SPAWN_SETPGROUP' MEANS HERE. A caller + /* THE UNIT, WHICH IS WHAT `POSIX_SPAWN_SETPGROUP' MEANS HERE. A caller * that asked for it gets a `kal_job' whose identity the start establishes; * a backend that does not claim the position is given no unit at all rather * than a refusal, for the same reason the binding below is optional. */ @@ -305,7 +305,7 @@ static int start_program(int bound, struct kal_job* unit, struct kal_dir work, how.grant_count = 0; how.flags = 0; - /* ⚠️ ASKED FOR ONLY WHERE IT IS MEANT, AND ONLY WHERE IT IS ANSWERED. + /* ASKED FOR ONLY WHERE IT IS MEANT, AND ONLY WHERE IT IS ANSWERED. * * `execve' is composed as starting a program and ending with its status, so * the binding is what makes the composition behave like the operation. An @@ -346,7 +346,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, int bound) { if (!res || !path) return EINVAL; - /* ⭐ `POSIX_SPAWN_SETPGROUP' IS HONOURED SINCE 0.12, AND ONLY IN THE ONE FORM + /* `POSIX_SPAWN_SETPGROUP' IS HONOURED SINCE 0.12, AND ONLY IN THE ONE FORM * THIS PORT CAN MEAN. * * The attribute carries a group to join, and a caller that asks to join @@ -356,14 +356,14 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, * "a group of your own", is answered; a named group is still refused rather * than silently turned into a different one. * - * ⚠️ That is exactly the case the consumer writes: `setpgid(0, 0)' in the + * That is exactly the case the consumer writes: `setpgid(0, 0)' in the * child so that a timeout can kill the whole tree. */ if (attr && (attr->__flags & ~(POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETPGROUP))) return ENOSYS; if (attr && (attr->__flags & POSIX_SPAWN_SETPGROUP) && attr->__pgrp != 0) return ENOSYS; - /* ⚠️ musl carries the PATH SEARCH in this field: `posix_spawnp' stores + /* musl carries the PATH SEARCH in this field: `posix_spawnp' stores * `__execvpe' there and its `posix_spawn' calls it in the duplicate instead * of `execve'. This file replaces that duplicate, so the field was read by * nobody --- and `posix_spawnp("sh", …)' therefore started `./sh', failed, @@ -410,7 +410,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, * caller --- the caller asked for a program started upon them, not for a * file --- so each is released once the spawn has been performed. * - * ⚠️ THE BOUND IS NOT THREE. Three positions can be placed, and a caller may + * THE BOUND IS NOT THREE. Three positions can be placed, and a caller may * name one of them more than once; POSIX says the last such action decides, * and the earlier file is still open and still has to be released. The bound * is stated rather than derived, and a sequence that exceeds it is refused @@ -430,7 +430,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, static struct okm_at fa_at; if (!refused && fa) { - /* ⚠️⚠️ IN THE ORDER THEY WERE ADDED, WHICH IS NOT THE ORDER THE LIST IS + /* IN THE ORDER THEY WERE ADDED, WHICH IS NOT THE ORDER THE LIST IS * IN. musl's `posix_spawn_file_actions_add*' PREPEND, so `__actions' * names the most recent one; musl's own `posix_spawn' walks to the tail * and then follows `prev'. This file followed `next' and therefore @@ -453,7 +453,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, break; } case FDOP_OPEN: { - /* ⭐ EXPRESSIBLE, AND IT WAS REFUSED. `kal_fs_open' produces the + /* EXPRESSIBLE, AND IT WAS REFUSED. `kal_fs_open' produces the * file and `kal_fs_stream' produces the stream to place, both of * them required operations this port already calls elsewhere. * Refusing it forced every caller wanting a program's output in @@ -464,7 +464,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, const long r = okm_resolve(AT_FDCWD, op->path, &fa_at, 0); if (r) { refused = (int)-r; break; } struct kal_file f; - /* ⚠️ `op->mode' is not among the inputs, exactly as it is not for + /* `op->mode' is not among the inputs, exactly as it is not for * `open'. README.md's divergence table records what a program * observes; a mode accepted here and dropped beneath would be * this file reporting success having done something else. */ @@ -476,13 +476,13 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, break; } case FDOP_CLOSE: - /* ⭐ TWO ANSWERS, DECIDED BY WHICH DESCRIPTOR IS NAMED. The + /* TWO ANSWERS, DECIDED BY WHICH DESCRIPTOR IS NAMED. The * comment this replaces gave one of them for both, on the ground * that "nothing is inherited that was not asked for" --- which is * true above position two and false at or below it, because the * three positions are inherited by construction. * - * ⚠️ So closing one of the three is an action that cannot be + * So closing one of the three is an action that cannot be * performed: openkal has no value meaning "no stream", and the * value that looks like one means the opposite. Accepting it and * doing nothing would hand a program the standard input its @@ -491,7 +491,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, if (op->fd > 2) break; refused = ENOSYS; break; - /* ⭐⭐ ANSWERED SINCE 0.12, BECAUSE openkal 0.11 GAVE A SPAWN A SECOND + /* ANSWERED SINCE 0.12, BECAUSE openkal 0.11 GAVE A SPAWN A SECOND * DIRECTORY. Both of these say the same thing --- run the program * HERE --- and until there was a place to put it they were refused * along with everything else this file could not express. */ @@ -529,7 +529,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, } } - /* ⭐ THE CAPABILITY IS REQUIRED ONLY WHERE IT IS USED. + /* THE CAPABILITY IS REQUIRED ONLY WHERE IT IS USED. * * `KAL_PROCESS_PROP_STREAM_PASSING' exists because an environment may be * able to start a program that inherits and unable to give it a stream of @@ -552,12 +552,12 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, * started program reads its own name through kal_env_arg(0), so a caller * that did not supply it could not predict what the program would read. */ struct kal_process child; - /* ⚠️ THE UNIT IS THIS CALL'S, AND IT IS NOT KEPT ANYWHERE AFTERWARDS --- see + /* THE UNIT IS THIS CALL'S, AND IT IS NOT KEPT ANYWHERE AFTERWARDS --- see * the note at the end of this function. */ struct kal_job unit = { 0 }; struct kal_job* want_unit = (attr && (attr->__flags & POSIX_SPAWN_SETPGROUP)) ? &unit : 0; - /* ⭐ THE WORKING DIRECTORY IS THIS LIBRARY'S, AND THAT IS THE WHOLE FIX. + /* THE WORKING DIRECTORY IS THIS LIBRARY'S, AND THAT IS THE WHOLE FIX. * * `chdir' here moves `okm_cwd_dir' and nothing else, because openkal has no * operation that moves a running program's. Before 0.11 a spawn carried one @@ -570,7 +570,7 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, &at, a_ptr, a_len, argc, e_ptr, e_len, envc, &streams, &child); - /* ⭐ THE ONE ENVIRONMENT THAT SPELLS A PROGRAM WITH A SUFFIX IS ANSWERED + /* THE ONE ENVIRONMENT THAT SPELLS A PROGRAM WITH A SUFFIX IS ANSWERED * BEFORE THIS POINT AND NOT AFTER IT. * * A program there is a file whose name ends in a particular suffix, and a @@ -578,14 +578,14 @@ int __okm_spawn_common(pid_t* restrict res, const char* restrict path, * resolved here rather than beneath, because openkal is deliberately * literal about names and does not know that a program is a kind of file. * - * ⚠️ It used to be resolved by RETRYING the spawn on `kal_err_not_found'. + * It used to be resolved by RETRYING the spawn on `kal_err_not_found'. * That cannot stay: the enquiry added above refuses an absent name before * the spawn is reached, so the retry would never run and every suffixless * name on that environment would be refused. `startable_name' therefore * owns the choice, keeps the same order --- the bare name first --- and * rewrites `at.rel' so that what was asked about is what is started. */ - /* ⭐ THE FILES A FILE ACTION OPENED ARE RELEASED HERE, AND THE STARTED + /* THE FILES A FILE ACTION OPENED ARE RELEASED HERE, AND THE STARTED * PROGRAM KEEPS ITS STREAM. * * That the second survives the first is what `kal_process_channel' already @@ -633,7 +633,7 @@ weak_alias(__posix_spawn, posix_spawn); * reported only if nothing is found; and any other error ends the search at * once, because it is not evidence about the next entry. * - * ⚠️ INCLUDING THE ENTRY SEPARATOR, WHICH IS A COLON ON EVERY TARGET AND IS THE + * INCLUDING THE ENTRY SEPARATOR, WHICH IS A COLON ON EVERY TARGET AND IS THE * WRONG ONE FOR EXACTLY ONE OF THEM. * * One environment separates its own PATH with a semicolon and begins each entry @@ -664,7 +664,7 @@ int __posix_spawnp(pid_t* restrict res, const char* restrict file, const size_t k = slen(file); if (k > NAME_MAX) return ENAMETOOLONG; - /* ⚠️ ON THE STACK, WHICH IS WHAT musl DOES TOO (a variable-length array of + /* ON THE STACK, WHICH IS WHAT musl DOES TOO (a variable-length array of * the same bound). The static buffers above are under this file's lock and * this function runs before it is taken --- a static here would be a race * between two contexts searching at once, which is worse than a frame. */ diff --git a/port/src/okm_start.c b/port/src/okm_start.c index a22ec4f..ea1a08d 100644 --- a/port/src/okm_start.c +++ b/port/src/okm_start.c @@ -119,7 +119,7 @@ static char* dup_counted(const char* s, size_t n) * other way. */ void __okm_init_env(void) { - /* ⭐ EACH VALUE IS COPIED, AND THE LENGTH REPORTED IS THE VALUE'S OWN. It + /* EACH VALUE IS COPIED, AND THE LENGTH REPORTED IS THE VALUE'S OWN. It * was answered with a pointer into the implementation's storage, which is * meaningful only while the implementation shares this address space --- and * this library is precisely the consumer that must not depend on which way @@ -237,14 +237,14 @@ static void fill_random(void) } } -/* ⭐⭐ BRINGING THE LIBRARY UP, SEPARATED FROM BEING ENTERED — BECAUSE ON ONE +/* BRINGING THE LIBRARY UP, SEPARATED FROM BEING ENTERED — BECAUSE ON ONE * OBJECT FORMAT THOSE ARE NOT THE SAME MOMENT. * * On ELF this function runs from the entry point, before anything else, and * `__libc_start_init` then walks the constructors. The order is: library, then * constructors, then `main`. * - * ⚠️ Mach-O INVERTS IT. The dynamic loader runs an image's constructors BEFORE + * Mach-O INVERTS IT. The dynamic loader runs an image's constructors BEFORE * transferring control to its entry point, so a C++ runtime's static * initialisers execute while this library has not been initialised at all. * Measured 2026-08-23, an arm64 image built on Linux and run on a real Mac: @@ -262,7 +262,7 @@ static void fill_random(void) * platform that needs it earlier calls it earlier (`port/src/mach/`). Calling * it twice is not a hazard; calling it late is. * - * ⚠️ NOT A LOCK. This runs before the thread layer exists — `__init_tls` is one + * NOT A LOCK. This runs before the thread layer exists — `__init_tls` is one * of the things it does — so a mutex here would be using what it is installing. * A plain flag is correct because there is exactly one execution context at * this point on every format: the loader has not started any, and neither have @@ -274,7 +274,7 @@ void __okm_libc_init(void) if (g_libc_up) return; g_libc_up = 1; - /* ⚠️⚠️ THE PAGE WAS FIXED WHEN THIS LIBRARY WAS BUILT AND IS NOW ASKED FOR. + /* THE PAGE WAS FIXED WHEN THIS LIBRARY WAS BUILT AND IS NOW ASKED FOR. * * It was the constant 4096 here and again in the auxiliary vector below, * and it is what this library reports as `sysconf(_SC_PAGESIZE)', what it @@ -285,7 +285,7 @@ void __okm_libc_init(void) * * openkal 0.9 carries the value because it is a property of the machine the * program RUNS on and not of the machine it was built for. */ - /* ⚠️⚠️ AND IT IS NOT THE SAME QUANTITY AS THIS LIBRARY'S PAGE SIZE, WHICH + /* AND IT IS NOT THE SAME QUANTITY AS THIS LIBRARY'S PAGE SIZE, WHICH * IS WHAT ASSIGNING IT DIRECTLY ASSUMED. * * openkal's granularity is the coarsest quantum a caller must respect. An @@ -299,7 +299,7 @@ void __okm_libc_init(void) * one-byte extents and the program stopped inside the first allocation * large enough to need a new one. * - * ⭐ MEASURED, AND ONLY ON THE MACHINE THAT ANSWERS THAT WAY. Over + * MEASURED, AND ONLY ON THE MACHINE THAT ANSWERS THAT WAY. Over * openkal-opensbi the same-source example printed three of its four lines * and stopped --- containers, exceptions and unwinding all held, and the * fourth line was the first to format a string. Over openkal-linux, whose diff --git a/port/src/okm_syscall.c b/port/src/okm_syscall.c index d6e6890..2e84b60 100644 --- a/port/src/okm_syscall.c +++ b/port/src/okm_syscall.c @@ -28,7 +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 -/* ⚠️⚠️ WEAK, OR AN INTERFACE A BACKEND MAY DECLINE BECOMES ONE IT MUST PROVIDE. +/* 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 * absence of a definition, and a bare-metal backend provides no `openkal.process' @@ -39,7 +39,7 @@ * ld.lld: error: undefined symbol: kal_process_channel * >>> referenced by okm_syscall.c:407 * - * ⚠️ Measured on openkal-opensbi through openkal-llvm-runtime's bare-metal row, + * Measured on openkal-opensbi through openkal-llvm-runtime's bare-metal row, * which is the row that has nothing to fall back on. The same rule is already * applied to `kal_random_fill' below, and it is the second time this port has * had to learn it. */ @@ -49,7 +49,7 @@ 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__)); -/* ⭐ WHAT `WNOHANG' IS EXPRESSED AS, AND IT WAS ALREADY IN THE SPECIFICATION. +/* 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 * finished BLOCKED until it did --- the one thing `WNOHANG' exists to prevent. @@ -126,7 +126,7 @@ static syscall_arg_t do_write(int fd, const void* buf, size_t len) struct okm_desc* d = okm_desc_of(fd); if (d->flags & O_NONBLOCK) return okm_timed_write(s, buf, len, OKM_NOW_NS); struct kal_stream st; st.h = s; - /* ⭐ ONE SIGNED WORD, AND THIS IS THE CODE THAT ARGUED FOR IT. openkal + /* ONE SIGNED WORD, AND THIS IS THE CODE THAT ARGUED FOR IT. openkal * returned a count and a condition, and every consumer of that pair --- all * of them here --- collapsed it by hand and by the same rule: report what * moved, or the condition when nothing did. Version 0.9 returns that. */ @@ -142,14 +142,14 @@ static syscall_arg_t do_read(int fd, void* buf, size_t len) if (len == 0) return 0; struct okm_desc* d = okm_desc_of(fd); - /* ⭐ WHAT A READINESS ENQUIRY TOOK IS DELIVERED HERE, and delivering it is + /* WHAT A READINESS ENQUIRY TOOK IS DELIVERED HERE, and delivering it is * what made that enquiry's answer true rather than momentary. okm_poll.c * states why openkal leaves no other way to answer one. A short read is a * result every caller of `read' already handles. */ const long held = okm_take_ahead(d, buf, len); if (held == OKM_AHEAD_EOF) return 0; if (held) { - /* ⚠️⚠️ AND WHATEVER ELSE IS ALREADY THERE, BECAUSE ONE BYTE ON ITS OWN + /* AND WHATEVER ELSE IS ALREADY THERE, BECAUSE ONE BYTE ON ITS OWN * TURNED EVERY POLLED READ INTO A POLLED READ OF ONE BYTE. * * The enquiry takes a byte to make its answer true. Returning only that @@ -161,14 +161,14 @@ static syscall_arg_t do_read(int fd, void* buf, size_t len) * here "o" "n" "e" "." "t" "w" "o" "." eight chunks * host "one." "two." two * - * ⚠️ Every byte is delivered and in order, so a caller that concatenates + * Every byte is delivered and in order, so a caller that concatenates * sees the right bytes --- which is why this survived: the defect is * invisible to anyone who does not look at the BOUNDARIES. A caller that * scans a chunk for a word finds none, because `two' arrives as `t' and * `wo'. openkal-linux#13's first report said the program `only output * one byte', and this is that. * - * ⭐ THE REMEDY IS THE OPERATION THE ENQUIRY ITSELF IS BUILT ON. A bound + * THE REMEDY IS THE OPERATION THE ENQUIRY ITSELF IS BUILT ON. A bound * of `now' asks for whatever has already arrived and does not wait, so * the byte and the rest of what is there come back together. Where the * environment beneath declines `openkal.timeout' there is no such @@ -197,7 +197,7 @@ static syscall_arg_t do_openat(int dirfd, const char* path, int flags, int mode) if (r) return r; const size_t n = slen(at.rel); - /* ⭐⭐ O_NOFOLLOW ON A LINK IS `ELOOP', AND ANSWERING `ENOENT' MADE A + /* O_NOFOLLOW ON A LINK IS `ELOOP', AND ANSWERING `ENOENT' MADE A * DIRECTORY UNREMOVABLE. * * openkal states that opening RESOLVES and offers no form that declines @@ -205,12 +205,12 @@ static syscall_arg_t do_openat(int dirfd, const char* path, int flags, int mode) * bytes is asking what `kal_fs_link_read' answers. So this reached the * link's target, and for a link whose target is absent that is `ENOENT'. * - * ⚠️ WHICH IS A DIFFERENT ANSWER TO A DIFFERENT QUESTION. POSIX says + * WHICH IS A DIFFERENT ANSWER TO A DIFFERENT QUESTION. POSIX says * ELOOP: `the name is a link and you said not to follow one'. ENOENT * says `there is no such name', and the two are acted upon differently * by exactly the caller that passes this flag. * - * ⭐ MEASURED THROUGH THREE LAYERS. libc++'s `remove_all' descends by + * MEASURED THROUGH THREE LAYERS. libc++'s `remove_all' descends by * opening each entry O_DIRECTORY|O_NOFOLLOW: on ELOOP or ENOTDIR it * unlinks the entry, on ENOENT it concludes the entry has already gone * and moves on. Against this port it moved on, unlinked nothing, and @@ -292,7 +292,18 @@ static void fill_kstat(const struct kal_node_info* in, struct kstat* out) } mode |= 0444u; if (in->writable) mode |= 0222u; + /* A directory is always reported traversable: openkal answers + * KAL_INFO_EXECUTABLE for a file only (fs.h, version 0.13), and a directory + * a program can list at all has always been reported as one it can enter. + * Everything else follows the enquiry: the execute bit is set for exactly + * the classes that have the read bit above --- every one of them, since + * `writable' is this port's only per-class distinction --- when the + * enquiry reports KAL_INFO_EXECUTABLE set, and cleared when it reports the + * position clear or does not answer it at all. "Does not answer" reads as + * "not executable", which is the same reading `access(X_OK)' gives it + * below and is documented in README.md. */ if (in->kind == kal_node_directory) mode |= 0111u; + else if ((in->present & KAL_INFO_EXECUTABLE) && in->executable) mode |= 0111u; out->st_mode = mode; out->st_mtime_sec = (kal_i64)(in->modified_ns / 1000000000u); out->st_mtime_nsec = (kal_i64)(in->modified_ns % 1000000000u); @@ -302,7 +313,7 @@ static void fill_kstat(const struct kal_node_info* in, struct kstat* out) out->st_ctime_nsec = out->st_mtime_nsec; out->st_uid = 1000; out->st_gid = 1000; - /* ⚠️⚠️ THE IDENTITY WAS A CONSTANT, SO EVERY FILE WAS THE SAME FILE. + /* THE IDENTITY WAS A CONSTANT, SO EVERY FILE WAS THE SAME FILE. * * `st_ino' was 0 and `st_dev' was 1 for every node, and nothing reported * that they were not answers. Measured through the C++ library above: @@ -393,7 +404,7 @@ static syscall_arg_t do_readlink(int dirfd, const char* path, char* out, size_t /* Makes a node whose content is another name. * - * ⚠️ THE CONTENT IS NOT A NAME THIS INTERFACE RESOLVES. It is stored and read + * THE CONTENT IS NOT A NAME THIS INTERFACE RESOLVES. It is stored and read * later by whoever follows it, so it is not put through the resolution that * would refuse one that ascends --- and one that ascends is the ordinary case * for a relative target. Only the name being CREATED is resolved. */ @@ -414,7 +425,7 @@ static syscall_arg_t do_fstatat(int dirfd, const char* path, struct kstat* st, i struct okm_at at; const syscall_arg_t r = okm_resolve(dirfd, path, &at, 0); if (r) return r; - /* ⭐⭐ `stat' AND `lstat' ARE TWO QUESTIONS AND THIS ANSWERED ONE OF THEM + /* `stat' AND `lstat' ARE TWO QUESTIONS AND THIS ANSWERED ONE OF THEM * TWICE. The flag was ignored, so both asked about the name itself while * `open' resolved --- a program was told a name referred to a link when * opening it would have reached a file. Through the C++ library above: @@ -433,6 +444,79 @@ static syscall_arg_t do_fstatat(int dirfd, const char* path, struct kstat* st, i return 0; } +/* --- whether a node may be started, expressed as a permission bit ---------- + * + * openkal 0.13 gives this port one word to answer `chmod' with: + * `kal_fs_set_executable_at' sets or clears the execute property for every + * class of caller at once (fs.h states why it is one property and not a + * permission). musl's mode word has three read, three write and three + * execute bits, one triple per class, and this port's own report of that word + * --- `fill_kstat', just above --- can only ever hold the same bit in all + * three positions of a triple: `writable' is one boolean and the read bit is + * unconditional, so the read and write triples are always 000 or 111, never a + * mixture. That is the shape a request is judged against. + * + * A REQUEST THIS PORT CANNOT HONESTLY GRANT IS REFUSED, NOT ROUNDED. + * `chmod' reporting success for a mode it did not set is exactly the + * simulation clause 3.1 forbids --- SPEC.md's own `kal_err_exists' argument + * and entry 6's chmod paragraph make the same point about a mode word a + * volume does not store. So a request is granted only when + * + * (a) every bit outside the three execute positions already equals what + * this port would report today, and + * (b) the three execute bits are exactly one of the two shapes the single + * `kal_fs_set_executable_at' property can produce: all clear, or set in + * exactly the classes that have a read bit (which, per the previous + * paragraph, is either none of the three or all of them). + * + * A request equal to today's reported mode always satisfies both --- the + * current mode is itself one of the two shapes --- and is short-circuited + * before the underlying call, so it succeeds even where the volume does not + * claim KAL_FS_PROP_EXECUTABLE: nothing is being asked of that property, and + * refusing a caller that asked for nothing to change is not this port's + * decision to make. A request that asks for a change goes to + * `kal_fs_set_executable_at', which is where an unsupported volume's + * `kal_err_not_supported' --- ENOSYS --- comes from. + * + * Everything else --- a real 0644-style mode, where the read and write bits + * differ between classes --- fails rule (a) against EVERY reachable current + * mode, because this port never reports one. It is refused as ENOSYS, the + * same answer this port has always given `chmod', for the same reason: a + * mode word is not what a capability-shaped volume stores. README.md's limits + * table gives the four reachable modes and worked examples. */ +static syscall_arg_t do_chmod(int dirfd, const char* path, mode_t mode) +{ + struct okm_at at; + const syscall_arg_t r = okm_resolve(dirfd, path, &at, 0); + if (r) return r; + + struct kal_node_info info = { .self_size = sizeof info }; + const int qe = okm_fs_info(at.base, at.rel, slen(at.rel), 0, + KAL_INFO_KIND | KAL_INFO_WRITABLE + | KAL_INFO_EXECUTABLE, &info); + if (qe != kal_ok) return -okm_errno(qe); + if (info.kind == kal_node_absent) return -ENOENT; + + /* The mode `fill_kstat' would report for this node today. */ + unsigned current = 0444u; + if (info.writable) current |= 0222u; + if (info.kind == kal_node_directory) current |= 0111u; + else if ((info.present & KAL_INFO_EXECUTABLE) && info.executable) current |= 0111u; + + const unsigned requested = (unsigned)mode & 07777u; + if (requested == current) return 0; /* nothing to change; see above */ + + const unsigned none = current & ~0111u; + const unsigned full = none | ((current & 0444u) >> 2); + int executable; + if (requested == none) executable = 0; + else if (requested == full) executable = 1; + else return -ENOSYS; + + const int e = okm_fs_set_executable_at(at.base, at.rel, slen(at.rel), executable); + return e == kal_ok ? 0 : -okm_errno(e); +} + /* --- directory enumeration -------------------------------------------------- */ struct linux_dirent64 { @@ -511,7 +595,7 @@ static syscall_arg_t do_getdents(int fd, void* buf, size_t cap) /* --- memory ---------------------------------------------------------------- */ -/* ⚠️ A MAPPING IS WHOLE PAGES, AND THE CALLER USES ALL OF THEM. +/* A MAPPING IS WHOLE PAGES, AND THE CALLER USES ALL OF THEM. * * `mmap' maps every page the length touches, so the bytes from the length to * the end of its last page are the caller's too, and they read as zero. musl's @@ -525,7 +609,7 @@ static syscall_arg_t do_getdents(int fd, void* buf, size_t cap) * next block, and the program stopped at a later free or allocation with an * access violation, or ended without a word. * - * ⭐ Measured under Wine: a std::string grown by push_back past 196,607 bytes + * Measured under Wine: a std::string grown by push_back past 196,607 bytes * ended the program, and it passes with the length rounded up here and at * SYS_munmap, whose length is the caller's as well. On windows-2022 the same * fault stopped lsp-mcpp's conformance runner while it read a build tree. */ @@ -567,7 +651,7 @@ static void to_timespec(kal_duration ns, struct timespec* ts) /* --- processes -------------------------------------------------------------- */ -/* ⚠️ THE BOUND IS PART OF THE CONTRACT, AND IT WAS NOT. +/* THE BOUND IS PART OF THE CONTRACT, AND IT WAS NOT. * * An entry is taken when a program is started and released when it is waited * for, which is what a process table is; a program that starts programs and @@ -575,7 +659,7 @@ static void to_timespec(kal_duration ns, struct timespec* ts) * EAGAIN. That is what POSIX says `fork' does when the table is full, so the * behaviour is right --- what was wrong is that the number was invisible. * - * ⭐ Measured: the sixty-fifth `posix_spawn' failed with `Resource temporarily + * Measured: the sixty-fifth `posix_spawn' failed with `Resource temporarily * unavailable' on a program that had started sixty-four and waited for none, * and on one that polled each with WNOHANG once and did not come back --- and a * caller meeting that has an error on an operation with no evident relation to @@ -587,7 +671,7 @@ static void to_timespec(kal_duration ns, struct timespec* ts) * others --- which is the kind of program that meets it --- and because each * entry is three words. */ #define OKM_MAX_CHILD 256 -/* ⭐ AND THE UNIT THAT START FORMED, WHICH IS THE ONLY PLACE IT CAN LIVE. +/* AND THE UNIT THAT START FORMED, WHICH IS THE ONLY PLACE IT CAN LIVE. * A caller ends a group by naming a negative identifier; openkal names a unit by * a handle whose meaning is the implementation's. The two are related here and * nowhere else --- openkal deliberately offers no way to recover a unit from a @@ -598,7 +682,7 @@ static void to_timespec(kal_duration ns, struct timespec* ts) static struct { int used; int pid; struct kal_process h; struct kal_job job; int has_job; } g_child[OKM_MAX_CHILD]; static int g_next_pid = 1000; -/* ⭐ WHAT THIS PROGRAM ANSWERS WHEN ASKED WHO IT IS. +/* WHAT THIS PROGRAM ANSWERS WHEN ASKED WHO IT IS. * * It was the constant 1 for every context, so a copy made by `fork' reported * the identifier of the image it was copied from --- two contexts, one answer, @@ -611,7 +695,7 @@ static int g_next_pid = 1000; * `fork' returned it. */ static int g_self_pid = 1; -/* The unit this program formed, if it did. ⚠️ KEPT BECAUSE THE NUMBER A CALLER +/* The unit this program formed, if it did. KEPT BECAUSE THE NUMBER A CALLER * WILL LATER USE IS NOT THE HANDLE: `kill(-n)' names a group by a POSIX * identifier, and openkal's unit is a handle whose meaning belongs to the * implementation. One per program, which is what one `setpgid(0, 0)' forms, so @@ -624,12 +708,12 @@ void __okm_set_self_pid(int pid) { g_self_pid = pid; } /* Takes an entry and settles its identifier WITHOUT a resource to put in it. * - * ⚠️ THE IDENTIFIER HAS TO EXIST BEFORE THE CONTEXT DOES. `fork' copies the + * THE IDENTIFIER HAS TO EXIST BEFORE THE CONTEXT DOES. `fork' copies the * address space at `kal_space_start', so anything the copy is to know must be * written before that call --- and the identifier used to be assigned after it, * from the handle it returned. Reserving first is what lets the copy be told. * - * ⚠️ THE CALLER HOLDS THE LOCK. `__okm_child_record' takes it and this does + * THE CALLER HOLDS THE LOCK. `__okm_child_record' takes it and this does * not, because `__okm_fork' is already inside it when it reserves: the copy has * to be taken while no other context is part-way through a change to the table. * A second acquisition would not nest. */ @@ -666,7 +750,7 @@ void __okm_child_job(int slot, struct kal_job j) void __okm_child_release(int slot) { - /* ⚠️ THE UNIT IS NOT RELEASED WITH THE PROGRAM, AND THAT IS THE POINT OF IT. + /* THE UNIT IS NOT RELEASED WITH THE PROGRAM, AND THAT IS THE POINT OF IT. * A group outlives the program that formed it for exactly as long as it has * members --- which is the case a caller uses a unit FOR: the shell exits * immediately and the work it put in the background is what a timeout has to @@ -697,7 +781,7 @@ int __okm_child_record(struct kal_process h) return __okm_child_record_job(h, none); } -/* ⚠️ A COPY OF THE CALLING IMAGE INHERITS THIS TABLE AND MUST NOT KEEP IT. +/* A COPY OF THE CALLING IMAGE INHERITS THIS TABLE AND MUST NOT KEEP IT. * * The entries name programs the ORIGINAL started, and POSIX is explicit that a * duplicate has no children. Left in place they are worse than useless: a copy @@ -731,7 +815,7 @@ static int job_index(int pid) /* --- what a signal aimed at this program means ------------------------------ * - * ⚠️⚠️ `abort' DID NOT END THE PROGRAM, AND WHAT ENDED IT WAS AN ILLEGAL + * `abort' DID NOT END THE PROGRAM, AND WHAT ENDED IT WAS AN ILLEGAL * INSTRUCTION. * * musl's `raise' is one line --- `syscall(SYS_tkill, self->tid, sig)' --- and @@ -749,14 +833,14 @@ static int job_index(int pid) * openkal-linux#13; the report reasoned from the wrong half of it, because 139 * looked like the null jump it had also seen. * - * ⭐ THE ANSWER IS NOT SYNTHESISED. openkal-linux's `kal_abort' IS + * THE ANSWER IS NOT SYNTHESISED. openkal-linux's `kal_abort' IS * `tgkill(pid, tid, SIGABRT)', so routing `abort' onto it gives a real signal * death: a parent reads WIFSIGNALED and WTERMSIG == SIGABRT, and a core is * written, which is what `abort' means everywhere else. Choosing * `kal_exit(134)' instead would have produced a number that looks the same to a * shell and answers `WIFEXITED' to a program. * - * ⭐⭐ AND THE TARGET IS DELIBERATELY NOT EXAMINED, WHICH IS THE OPPOSITE OF + * AND THE TARGET IS DELIBERATELY NOT EXAMINED, WHICH IS THE OPPOSITE OF * WHAT IT LOOKS LIKE. * * The default action of a terminating signal ends the PROCESS and not the @@ -767,7 +851,7 @@ static int job_index(int pid) * identity, so the comparison would have failed for every context but the * first, and `abort' from a thread would have gone back to `hlt'. * - * ⚠️⚠️ THREE NUMBERS ARE MUSL'S OWN AND MUST NOT TERMINATE ANYTHING. + * THREE NUMBERS ARE MUSL'S OWN AND MUST NOT TERMINATE ANYTHING. * pthread_impl.h reserves 32, 33 and 34 for the timer thread, cancellation and * `synccall', and each is sent with this same call. `pthread_cancel' is * `pthread_kill(t, SIGCANCEL)' --- so a table that made 33 a terminating signal @@ -825,12 +909,12 @@ static syscall_arg_t do_wait4(int pid, int* status, int options, void* rusage) int st = 0, terminated = 0; int e; if (options & WNOHANG) { - /* ⚠️ NOT ZERO. openkal spells "no bound" as zero (timeout.h), so a + /* NOT ZERO. openkal spells "no bound" as zero (timeout.h), so a * caller asking not to wait must ask for the smallest bound there is * and not for none. `OKM_NOW_NS' is that bound and is what this port * already passes for a non-blocking read. * - * ⚠️ An implementation rounds a bound up to its own granularity, so + * An implementation rounds a bound up to its own granularity, so * `WNOHANG' here waits at most one polling interval of the environment * beneath rather than not at all --- a millisecond on openkal-linux, * which has no bounded wait for a child and polls. Recorded in @@ -838,7 +922,7 @@ static syscall_arg_t do_wait4(int pid, int* status, int options, void* rusage) * what the environment distinguishes is a promise it cannot keep. */ if (!kal_timeout_wait_process) return -ENOSYS; - /* ⚠️ EVERY CHILD, WHERE THE CALLER NAMED NONE. `waitpid(-1, …, WNOHANG)' + /* EVERY CHILD, WHERE THE CALLER NAMED NONE. `waitpid(-1, …, WNOHANG)' * asks after ANY child, and asking after the first recorded one would * report "none has finished" while a later one had --- the reading a * caller draining its children in a loop acts upon. The blocking form @@ -874,7 +958,7 @@ static syscall_arg_t do_wait4(int pid, int* status, int options, void* rusage) /* --- the report of an operation this library does not have ------------------ * - * ⭐ THE DEFAULT ARM ANSWERS ENOSYS IN SILENCE, AND A CONSUMER CANNOT ACT ON A + * THE DEFAULT ARM ANSWERS ENOSYS IN SILENCE, AND A CONSUMER CANNOT ACT ON A * SILENCE. * * The answer itself is right --- POSIX has a word for a facility that is not @@ -883,12 +967,12 @@ static syscall_arg_t do_wait4(int pid, int* status, int options, void* rusage) * rounds of openkal-linux#13 were spent on exactly that question, and reading a * dispatcher is not a thing a consumer of a C library should have to do. * - * ⚠️ NOT ON BY DEFAULT, AND NOT A BUILD OPTION EITHER. A consumer meets this on + * NOT ON BY DEFAULT, AND NOT A BUILD OPTION EITHER. A consumer meets this on * a binary it already has; rebuilding the C library to find out what the binary * needed is the cost this is here to remove. So it is a variable of the * environment, read once. * - * ⚠️ AND ONLY THIS ARM. `mprotect' and `rt_sigreturn' answer ENOSYS from cases + * AND ONLY THIS ARM. `mprotect' and `rt_sigreturn' answer ENOSYS from cases * of their own, and each is a decision with a reason recorded beside it rather * than a gap. Tracing those would report a facility as missing that this port * deliberately does not have, which is a different sentence. @@ -957,7 +1041,7 @@ static void trace_absent(syscall_arg_t n) { if (!trace_wanted()) return; - /* ⚠️ EACH NUMBER ONCE, AND THE DOCUMENTATION SAYS SO. A program that retries + /* EACH NUMBER ONCE, AND THE DOCUMENTATION SAYS SO. A program that retries * a refused operation in a loop would otherwise bury the report in copies of * itself, and a reader counting the lines would conclude it happened once. */ if (n >= 0 && n < 8192) { @@ -966,7 +1050,7 @@ static void trace_absent(syscall_arg_t n) if (__atomic_fetch_or(&seen[n >> 3], bit, __ATOMIC_ACQ_REL) & bit) return; } - /* ⚠️ WRITTEN TO THE STREAM DIRECTLY, NOT THROUGH THIS LIBRARY'S OWN OUTPUT. + /* WRITTEN TO THE STREAM DIRECTLY, NOT THROUGH THIS LIBRARY'S OWN OUTPUT. * What failed may be the operation that stdio was about to perform, and a * report that reaches stdio from inside the failure of stdio is a report * that arrives as a second failure. Nothing here allocates either. */ @@ -1085,13 +1169,13 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * offsets, and a caller copying sequentially passes none. */ /* pipe and pipe2, upon kal_process_channel. * - * ⭐ THIS BECAME POSSIBLE IN openkal 0.8 AND WAS NOT BEFORE. A pipe is a + * THIS BECAME POSSIBLE IN openkal 0.8 AND WAS NOT BEFORE. A pipe is a * pair of streams of which one end is meant to cross a spawn, which is * exactly what that interface provides and what openkal previously had no * way to express. Until then `pipe` belonged with the facilities the port * withholds; now it is supplied like any other. * - * ⚠️ AND THE CLOSURE SAID SO BEFORE THE REASONING DID. Withholding it broke + * AND THE CLOSURE SAID SO BEFORE THE REASONING DID. Withholding it broke * `faccessat`, which forks and reports its answer back through a pipe: * * ld64.lld: error: undefined symbol: pipe2 @@ -1102,7 +1186,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * * The two ends are bound as ordinary stream descriptors, so read, write, * close, dup and poll reach them through the paths they already take. */ - /* ⚠️ `SYS_pipe` DOES NOT EXIST EVERYWHERE. The architectures that gained + /* `SYS_pipe` DOES NOT EXIST EVERYWHERE. The architectures that gained * their numbering after pipe2 have only the later call, so naming the older * one unconditionally does not compile there: * @@ -1121,7 +1205,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * have. Refusing is the honest answer; silently ignoring it would give * a caller a byte stream where it asked for messages. */ if (flags & ~(O_CLOEXEC | O_NONBLOCK)) return -EINVAL; - /* ⚠️ AND `O_NONBLOCK' USED TO BE ACCEPTED AND CARRIED NO FURTHER. The + /* AND `O_NONBLOCK' USED TO BE ACCEPTED AND CARRIED NO FURTHER. The * flag was stored in the description and nothing read it, so a caller * asked for a pipe that would not wait, was told it had one, and waited. * That is the one shape the head of this file forbids. It is expressed @@ -1275,7 +1359,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, const long r = okm_resolve((int)a1, (const char*)a2, &at, 0); if (r) return r; } - /* ⚠️⚠️ A DIRECTORY COULD NOT HAVE ITS TIME SET, AND IT IS AN ORDINARY + /* A DIRECTORY COULD NOT HAVE ITS TIME SET, AND IT IS AN ORDINARY * THING TO WANT. * * `KAL_OPEN_READ | KAL_OPEN_WRITE' was asked for unconditionally, and @@ -1286,19 +1370,19 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * one name, so the failure read as though READING the time had failed. * Reading it was never broken. * - * ⭐ AND THE OPERATION IS PERFORMABLE. Measured directly against + * AND THE OPERATION IS PERFORMABLE. Measured directly against * openkal-linux: opening the directory with KAL_OPEN_READ succeeds and * `kal_fs_set_modified' upon it succeeds and the directory's time * really changes. * - * ⚠️ WHICH IS OUTSIDE WHAT `fs.h' STATES, AND IS RECORDED RATHER THAN + * WHICH IS OUTSIDE WHAT `fs.h' STATES, AND IS RECORDED RATHER THAN * CONCEALED. The interface says the file "shall have been opened with * KAL_OPEN_WRITE", and names `kal_fs_open_dir' --- which yields a * `kal_dir' --- as the way to open a directory, while * `kal_fs_set_modified' takes a `kal_file' and has no `kal_dir' form. * So there is no stated route to a directory's time at all. * - * ⭐⭐ ASKED OF THE SPECIFICATION, AND openkal 0.10 ANSWERED IT. + * ASKED OF THE SPECIFICATION, AND openkal 0.10 ANSWERED IT. * `kal_fs_set_modified_at' takes a NAME, so a directory is now reached by * a stated route rather than by opening it for reading and hoping. The * older way is kept below for a backend that has not followed yet, and is @@ -1306,7 +1390,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, struct kal_node_info kind = { .self_size = sizeof kind }; const int ke = okm_fs_info(at.base, at.rel, slen(at.rel), 0, KAL_INFO_KIND, &kind); - /* ⚠️ AN ENQUIRY THAT CANNOT BE MADE IS NOT AN ANSWER OF `NO', so a build + /* AN ENQUIRY THAT CANNOT BE MADE IS NOT AN ANSWER OF `NO', so a build * without `openkal.fs' asks for what the interface requires and lets the * open answer, exactly as it did before this enquiry was added. */ if (ke != kal_ok && ke != kal_err_not_supported) return -okm_errno(ke); @@ -1513,10 +1597,19 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, if (r) return r; struct kal_node_info info = { .self_size = sizeof info }; const int e = okm_fs_info(at.base, at.rel, slen(at.rel), 0, - KAL_INFO_KIND | KAL_INFO_WRITABLE, &info); + KAL_INFO_KIND | KAL_INFO_WRITABLE + | KAL_INFO_EXECUTABLE, &info); if (e != kal_ok) return -okm_errno(e); if (info.kind == kal_node_absent) return -ENOENT; if (((int)a3 & W_OK) && !info.writable) return -EACCES; + /* Version 0.13: X_OK follows the same field `fstat' now reports. + * Before it, and still where the enquiry does not answer the + * position (a directory, or a volume that does not claim + * KAL_FS_PROP_EXECUTABLE), this answers yes for anything that + * exists --- README.md records both halves. */ + if (((int)a3 & X_OK) && info.kind != kal_node_directory + && (info.present & KAL_INFO_EXECUTABLE) && !info.executable) + return -EACCES; return 0; } #ifdef SYS_access @@ -1526,13 +1619,39 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, if (r) return r; struct kal_node_info info = { .self_size = sizeof info }; const int e = okm_fs_info(at.base, at.rel, slen(at.rel), 0, - KAL_INFO_KIND | KAL_INFO_WRITABLE, &info); + KAL_INFO_KIND | KAL_INFO_WRITABLE + | KAL_INFO_EXECUTABLE, &info); if (e != kal_ok) return -okm_errno(e); if (info.kind == kal_node_absent) return -ENOENT; if (((int)a2 & W_OK) && !info.writable) return -EACCES; + if (((int)a2 & X_OK) && info.kind != kal_node_directory + && (info.present & KAL_INFO_EXECUTABLE) && !info.executable) + return -EACCES; return 0; } #endif + + /* Version 0.13. `chmod' and `fchmodat' both name a path, and `do_chmod' + * above states the judgement. `fchmod' names a descriptor instead, and + * this port has no name for one: a regular file's own name is not kept + * anywhere in `struct okm_desc' (only a directory's is, for resolving a + * name that ascends out of it --- `okm_dir_remember'), so there is + * nothing to pass `kal_fs_set_executable_at'. ENOSYS, as this port has + * always answered `fchmod'. + * + * THIS IS WHY musl's OWN `fchmod' NEVER TRIES A NAME EITHER. Its + * fallback path re-reads `/proc/self/fd/' only when the direct call + * reports EBADF (musl/src/stat/fchmod.c); ENOSYS reports here instead, + * so the fallback --- which this port could not serve anyway, having no + * `/proc' --- is never reached. */ +#ifdef SYS_fchmod + case SYS_fchmod: return -ENOSYS; +#endif +#ifdef SYS_chmod + case SYS_chmod: return do_chmod(AT_FDCWD, (const char*)a1, (mode_t)a2); +#endif + case SYS_fchmodat: return do_chmod((int)a1, (const char*)a2, (mode_t)a3); + case SYS_getcwd: { const char* p = okm_cwd_path(); const size_t n = slen(p) + 1; @@ -1544,10 +1663,10 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case SYS_chdir: return okm_chdir(AT_FDCWD, (const char*)a1); case SYS_fchdir: return okm_chdir((int)a1, 0); - /* ⭐ NODES WHOSE CONTENT IS ANOTHER NAME. openkal 0.9 carries the two + /* NODES WHOSE CONTENT IS ANOTHER NAME. openkal 0.9 carries the two * operations, so these answer rather than refusing. * - * ⚠️ THE ENQUIRY IS ASKED FIRST AND IT TAKES THE DIRECTORY, which is the + * THE ENQUIRY IS ASKED FIRST AND IT TAKES THE DIRECTORY, which is the * whole reason these can be operations of `openkal.fs' at all: the same * implementation succeeds on one volume and fails on another, so a caller * that could not ask would be left to discover it by the attempt. */ @@ -1594,7 +1713,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case F_GETFL: return d->flags; case F_SETFL: { const int want = (int)a3; - /* ⚠️ REFUSED WHERE IT CANNOT BE HONOURED, AND ONLY WHEN IT IS BEING + /* REFUSED WHERE IT CANNOT BE HONOURED, AND ONLY WHEN IT IS BEING * ASKED FOR. A descriptor that was asked to be non-blocking and is * not would make every subsequent transfer wait where the caller * arranged not to. `O_NONBLOCK' is expressed here as the smallest @@ -1607,7 +1726,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, | (want & (O_APPEND | O_NONBLOCK)); return 0; } - /* ⚠️⚠️ THESE THREE ANSWERED `0' AND DID NOTHING, SO EVERY LOCK WAS + /* THESE THREE ANSWERED `0' AND DID NOTHING, SO EVERY LOCK WAS * GRANTED AND NO LOCK EXISTED. Measured with the host as control: two * programs took one exclusive lock and BOTH were told they had it. * @@ -1615,7 +1734,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * permission one is not --- every environment beneath openkal can lock a * byte range, and what was missing was a word in the specification. * - * ⭐ openkal 0.10 IS THAT WORD. `kal_fs_lock' states the holder as the + * openkal 0.10 IS THAT WORD. `kal_fs_lock' states the holder as the * open FILE and requires release when the program ends however it ends, * which is the half a caller could never have built for itself. */ case F_SETLK: case F_SETLKW: { @@ -1639,7 +1758,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * size that may already have changed. */ return -EINVAL; } - /* ⭐ ZERO MEANS `TO THE END, HOWEVER FAR THAT COMES TO BE' IN BOTH, + /* ZERO MEANS `TO THE END, HOWEVER FAR THAT COMES TO BE' IN BOTH, * so it is passed rather than translated. */ const kal_u64 len = (kal_u64)fl->l_len; @@ -1653,7 +1772,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, const int e = okm_fs_lock(d->file, start, len, mode); return e == kal_ok ? 0 : -okm_errno(e); } - /* ⚠️ AND THE ENQUIRY IS STILL REFUSED, WHICH IS NOT AN OVERSIGHT. + /* AND THE ENQUIRY IS STILL REFUSED, WHICH IS NOT AN OVERSIGHT. * * `F_GETLK' asks whether a lock WOULD block without taking one, and * openkal has no operation that answers a question without performing @@ -1662,7 +1781,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * the caller did not ask for, hand a spurious `no' to a caller that * already holds one, and be stale the moment it returned. * - * ⭐ A refusal is what a caller can act upon; `F_SETLK' answers the + * A refusal is what a caller can act upon; `F_SETLK' answers the * question `F_GETLK' is usually asked in order to answer. */ case F_GETLK: return -ENOSYS; default: return -EINVAL; @@ -1682,7 +1801,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * terminal can be asked to do beyond that is not an operation * openkal has. * - * ⚠️⚠️ AND THE REQUEST IT ASKS IT WITH IS NOT THE ONE THIS BRANCH + * AND THE REQUEST IT ASKS IT WITH IS NOT THE ONE THIS BRANCH * FIRST RECOGNISED. `TCGETS' is the request a C library uses to * READ a terminal's settings; the one it uses to ASK WHETHER * something is a terminal is musl's own `isatty': @@ -1703,7 +1822,7 @@ 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 + * 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. @@ -1786,14 +1905,14 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, /* --- execution contexts ------------------------------------------------ */ case SYS_futex: return __okm_futex((const int*)a1, (int)a2, (int)a3, (const struct timespec*)a4); - /* ⭐ THROUGH THE INTERFACE, NOT THROUGH THE PLATFORM. + /* THROUGH THE INTERFACE, NOT THROUGH THE PLATFORM. * * musl's own `src/linux/getrandom.c` issues SYS_getrandom directly, which * is right where a Linux kernel is underneath and wrong here: this port * exists so that every request reaches the environment through openkal. * The call below is the whole difference. * - * ⚠️ AND IT IS WHY `openkal.random` HAD TO EXIST. Entropy is not derivable + * AND IT IS WHY `openkal.random` HAD TO EXIST. Entropy is not derivable * from the other interfaces --- a clock reading is unpredictable to a * reader of the source and not to an adversary, which the AT_RANDOM note * in okm_start.c already says about the bytes it derives, and openkal.fs @@ -1804,7 +1923,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * `kal_random_fill` has no partial success to report. An environment that * blocks says so in `kal_random_props`. * - * ⭐⭐ AND THE REFERENCE IS WEAK, BECAUSE THE INTERFACE IS OPTIONAL. + * AND THE REFERENCE IS WEAK, BECAUSE THE INTERFACE IS OPTIONAL. * * `openkal.random` is optional, and clause 6.1 expresses an implementation * that does not provide it as the absence of a link-time definition. This @@ -1818,7 +1937,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * measured on a bare-metal program over openkal-opensbi, which provides * eight interfaces and not this one. * - * ⚠️ A WEAK REFERENCE IS NOT THE RUN-TIME REFUSAL CLAUSE 6.1 FORBIDS. That + * A WEAK REFERENCE IS NOT THE RUN-TIME REFUSAL CLAUSE 6.1 FORBIDS. That * clause governs an IMPLEMENTATION of openkal: one shall not offer an * interface whose operations report a lack of support while running. What * happens below is on the other side of the layer --- this file implements @@ -1838,7 +1957,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case SYS_gettid: return (syscall_arg_t)OKM_CONTEXT_ID(); case SYS_getpid: return (syscall_arg_t)g_self_pid; - /* ⚠️⚠️ THE SAME DEFECT `getpgrp' HAD, IN THE SAME FAMILY, MISSED ONCE. + /* THE SAME DEFECT `getpgrp' HAD, IN THE SAME FAMILY, MISSED ONCE. * * musl's `getppid' is `return __syscall(SYS_getppid);' WITHOUT * `__syscall_ret', deliberately, because POSIX says the call cannot fail. @@ -1849,7 +1968,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * was not looked for. The criterion added with this change sweeps the whole * family rather than this member of it. * - * ⭐ ZERO RATHER THAN ONE. openkal names nothing that started this program, + * ZERO RATHER THAN ONE. openkal names nothing that started this program, * so there is no identifier to give. Zero is what the first process of a * system answers on the environment this library's callers come from, and * it means what is true here: there is no parent to name. One would be a @@ -1886,7 +2005,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * recorded in musl/PATCHES.md. */ case SYS_execve: { pid_t child = 0; - /* ⭐ BOUND, WHICH IS THE THING `execve' MEANS. The started program + /* BOUND, WHICH IS THE THING `execve' MEANS. The started program * stands in for this one, so it does not outlive it --- and until * openkal 0.10 there was no way to say so, which is why a `kill' aimed * at this image reached the copy that waits and left the program @@ -1895,7 +2014,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, (char* const*)a2, (char* const*)a3, 1); if (e) return -e; - /* ⚠️⚠️ THE WAITER LETS GO OF EVERY STREAM IT HOLDS, AND WITHOUT THIS THE + /* THE WAITER LETS GO OF EVERY STREAM IT HOLDS, AND WITHOUT THIS THE * FAR END OF A PIPE NEVER SAW THE END OF INPUT. * * A replacement leaves ONE image. This composition leaves two, and the @@ -1906,14 +2025,14 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * the other side saw a stream that was still open, from a program that * had already ended. * - * ⭐ Measured through a consumer: an MCP server that exits while a + * Measured through a consumer: an MCP server that exits while a * request is in flight should be reported as "Connection closed", and was * reported as "Timed out after 1000ms" --- the client waited its full * deadline for an end of input that this image was holding shut. The * server was long gone; nobody was writing; the pipe stayed open because * of a waiter neither side knew existed. * - * ⚠️ THIS IS THE 0.10 DEFECT'S THIRD FACE. `kal_process_spawn_bound' was + * THIS IS THE 0.10 DEFECT'S THIRD FACE. `kal_process_spawn_bound' was * added because a SIGNAL reached the middle image; this is the middle * image holding a RESOURCE. Both come from the same fact --- the * composition has an image the interface never told anyone about --- and @@ -1936,7 +2055,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, #ifdef SYS_kill case SYS_kill: { const int pid = (int)a1, sig = (int)a2; - /* ⭐⭐ A NEGATIVE IDENTIFIER NAMES A UNIT, AND THIS IS THE OTHER HALF OF + /* A NEGATIVE IDENTIFIER NAMES A UNIT, AND THIS IS THE OTHER HALF OF * `setpgid(0, 0)' ABOVE. * * `kill(-n)' is how a caller ends a group, and the identifier it uses is @@ -1944,11 +2063,11 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * copy formed the unit. Both reach the unit this library holds, because * a program forms at most one and a copy carries its parent's. * - * ⚠️ WITHOUT THIS THE FORMING WOULD BE INVISIBLE. That is the shape of + * WITHOUT THIS THE FORMING WOULD BE INVISIBLE. That is the shape of * the two defects before it: a call that succeeds and changes nothing * observable is worse than one that refuses, because the caller proceeds. * `kill(-n)' answered ESRCH here while the unit existed. */ - /* ⚠️ `pid != INT_MIN' IS NOT DEFENSIVENESS. Negating it is undefined --- it + /* `pid != INT_MIN' IS NOT DEFENSIVENESS. Negating it is undefined --- it * has no positive counterpart in the type --- and both this block and the * comparison at its end negate. A caller reaching here with that value * names no unit either way, so it takes the same route as any other @@ -1960,7 +2079,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, const int e = okm_process_job_terminate(g_child[gi].job); return e == kal_ok ? 0 : -okm_errno(e); } - /* ⚠️⚠️ AND THIS PROGRAM'S OWN UNIT IS REACHED ONLY WHEN THE CALLER + /* AND THIS PROGRAM'S OWN UNIT IS REACHED ONLY WHEN THE CALLER * NAMED IT, WHICH IS THE WHOLE OF THE 0.12.0 DEFECT. * * The condition used to be `g_job_held' alone. Every negative @@ -1971,7 +2090,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * library --- a program that formed a unit and called * `kill(-99999, SIGKILL)' ended on signal 9. * - * ⭐ IT WAS WRITTEN TO SERVE `fork(); setpgid(0, 0); exec', AND IT + * IT WAS WRITTEN TO SERVE `fork(); setpgid(0, 0); exec', AND IT * CANNOT. There the unit belongs to the copy, and its handle is the * copy's: openkal handles do not cross a spawn boundary --- clause * 6.7 constructs them from an index into the holder's own table --- so @@ -1989,7 +2108,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, } const int i = child_index(pid); if (i >= 0) { - /* ⚠️ SIGNAL ZERO IS AN ENQUIRY AND USED TO TERMINATE THE CHILD. + /* SIGNAL ZERO IS AN ENQUIRY AND USED TO TERMINATE THE CHILD. * Every value reached `kal_process_terminate', so the one form of * `kill' whose whole purpose is to change nothing --- the test that * a program is still there --- killed it. */ @@ -2002,7 +2121,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * reported ESRCH --- including for SIGABRT. Zero and minus one name * groups that contain this program, and it is the only member this * library can reach. */ - /* ⚠️ AND IT IS `g_self_pid' RATHER THAN THE CONSTANT. A copy made by + /* AND IT IS `g_self_pid' RATHER THAN THE CONSTANT. A copy made by * `fork' answers the identifier its parent recorded, so comparing * against 1 would make `raise' and `abort' report ESRCH in every copy. */ if (pid == g_self_pid || pid == 0 || pid == -1) return signal_self(sig); @@ -2022,13 +2141,13 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, #endif /* --- duplicating the calling image -------------------------------------- */ - /* ⭐ `fork' IS COMPOSED ABOVE `openkal.space' AND THE SPECIFICATION SAYS SO. + /* `fork' IS COMPOSED ABOVE `openkal.space' AND THE SPECIFICATION SAYS SO. * okm_fork.c carries the composition and the header it quotes. What reaches * here is musl's `_Fork', which issues this call with a termination signal * and no stack; every other shape asks for a context that SHARES the * caller's address space, which is `openkal.task' and reaches this library * through `__clone' rather than through this seam. */ - /* ⚠️ TWO NUMBERS AND NOT ONE, AND THE SECOND IS THE ONE THAT MATTERED. + /* TWO NUMBERS AND NOT ONE, AND THE SECOND IS THE ONE THAT MATTERED. * musl's `_Fork' issues `SYS_fork' where the architecture has it and * `SYS_clone' where it does not, so an implementation of the second alone * is reached on aarch64 and riscv64 and never on x86_64. Measured: the @@ -2162,7 +2281,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, fd_set* ex = (fd_set*)a4; if (nfds < 0 || nfds > FD_SETSIZE) return -EINVAL; - /* ⚠️ A BOUND ON THE SET, STATED RATHER THAN SILENT. Each descriptor in + /* A BOUND ON THE SET, STATED RATHER THAN SILENT. Each descriptor in * the set costs one bounded operation per round, and the set has to be * held somewhere while that happens. A larger one is refused; it is not * truncated, because a `select' that watched some of what it was given @@ -2185,7 +2304,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, watched++; } - /* ⚠️ THE TWO CALLS STATE THE BOUND IN DIFFERENT STRUCTURES, and which + /* THE TWO CALLS STATE THE BOUND IN DIFFERENT STRUCTURES, and which * one was written is decided by the number rather than by the machine: * `select' passes a `timeval' and `pselect6' a `timespec'. Reading one * as the other would misread the fractional field by a factor of a @@ -2230,7 +2349,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case SYS_getuid: case SYS_geteuid: case SYS_getgid: case SYS_getegid: return 1000; - /* ⚠️ `getpgrp' HANDED A NEGATED ERROR TO ITS CALLER AS A PROCESS GROUP. + /* `getpgrp' HANDED A NEGATED ERROR TO ITS CALLER AS A PROCESS GROUP. * * There was no case for this number, so the default arm answered -ENOSYS * --- and musl's `getpgrp' is `return __syscall(SYS_getpgid, 0);' WITHOUT @@ -2245,7 +2364,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * program's is a group this environment has no way to name, and is * refused. * - * ⚠️⚠️ THIS PARAGRAPH USED TO END "`setpgid' AND `setsid' REMAIN REFUSED: + * THIS PARAGRAPH USED TO END "`setpgid' AND `setsid' REMAIN REFUSED: * MAKING A GROUP IS NOT THE SAME AS BEING IN ONE", AND THAT ANSWERED A * QUESTION NEITHER OF THEM ASKS. * @@ -2260,7 +2379,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * already a process group leader. `getpgid(0) == getpid()' is that * assertion, so EPERM is the true answer rather than a polite one. * - * ⭐ AND THE DIFFERENCE IS NOT COSMETIC. The `fork'-then-`setsid' dance + * AND THE DIFFERENCE IS NOT COSMETIC. The `fork'-then-`setsid' dance * exists BECAUSE of EPERM, so every daemonising library handles it; not one * handles ENOSYS. A written-down failure is one a caller can act on. */ case SYS_getpgid: @@ -2271,7 +2390,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case SYS_getsid: return (a1 == 0 || a1 == g_self_pid) ? g_self_pid : -ESRCH; - /* ⭐⭐ A REAL GROUP SINCE 0.12, AND IT IS THE CALL A SHELL RUNNER MAKES. + /* A REAL GROUP SINCE 0.12, AND IT IS THE CALL A SHELL RUNNER MAKES. * * `setpgid(0, 0)' asks that THIS program lead a unit of its own, which * openkal 0.11 spells `kal_process_job_enter'. It used to answer 0 and form @@ -2279,7 +2398,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * trivially alone --- and the caller's next act, `kill(-pid)', found nothing * to kill. * - * ⚠️ THE UNIT IS KEPT, BECAUSE THE NUMBER THE CALLER WILL USE IS NOT ENOUGH. + * THE UNIT IS KEPT, BECAUSE THE NUMBER THE CALLER WILL USE IS NOT ENOUGH. * A caller ends a group by naming a negative identifier, and openkal's unit * is a handle whose meaning is the implementation's --- a process group's * identifier on one system, a job object on another. `g_job' is where this @@ -2304,7 +2423,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, case SYS_setsid: return -EPERM; /* already a process group leader; see above */ - /* ⭐ THE BOUND IS THIS LIBRARY'S OWN AND IT WAS REFUSING TO STATE IT. + /* THE BOUND IS THIS LIBRARY'S OWN AND IT WAS REFUSING TO STATE IT. * * musl answers `sysconf(_SC_OPEN_MAX)' from this call, so with no case here * the answer was ZERO --- and a program sizing a set of descriptors, or @@ -2319,7 +2438,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * * musl reaches this through `prlimit64' first and falls back to * `getrlimit' only on ENOSYS, so answering this one answers both. */ - /* ⭐⭐ IT ANSWERED 1, SILENTLY, AND A POOL OF WORKERS WAS SIZED AGAINST IT. + /* IT ANSWERED 1, SILENTLY, AND A POOL OF WORKERS WAS SIZED AGAINST IT. * * musl's `sysconf(_SC_NPROCESSORS_ONLN)' reaches this, and with no case it * fell back to 1 --- so `std::thread::hardware_concurrency()' answered 1 @@ -2328,7 +2447,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * openkal 0.10 adds the enquiry, and it reports the set THIS context may * run on rather than the set the machine has. * - * ⚠️ ZERO IS `CANNOT SAY' AND IS NOT ONE, so it is reported as a refusal + * ZERO IS `CANNOT SAY' AND IS NOT ONE, so it is reported as a refusal * rather than as a bitmap of one processor: musl would read the latter as a * fact and this port would be inventing it. */ #ifdef SYS_sched_getaffinity @@ -2403,7 +2522,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, } #endif - /* ⭐ REFUSED FROM A CASE OF ITS OWN RATHER THAN FROM THE DEFAULT ARM, SO + /* REFUSED FROM A CASE OF ITS OWN RATHER THAN FROM THE DEFAULT ARM, SO * THAT THE TRACE DOES NOT REPORT IT. * * musl's `pthread_create' calls `__membarrier_init' the first time a @@ -2412,7 +2531,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * optimisation and failing it costs nothing. Nothing in this port's musl * ever calls `__membarrier' itself. * - * ⚠️ It reached the default arm, so `OPENKAL_MUSL_TRACE=enosys' reported it + * It reached the default arm, so `OPENKAL_MUSL_TRACE=enosys' reported it * beside five operations that a program actually wanted, and the first * consumer to use that switch had to work out which of the six mattered. * A trace whose reader must filter it is a trace that costs its reader @@ -2470,7 +2589,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * accepted and can never run is exactly the silent wrongness clause 3.1 * names, so the request is refused and the program learns it. */ case SYS_rt_sigprocmask: { - /* ⚠️⚠️ THE SIZE IS THE CALLER'S, AND TAKING IT FROM THE TYPE INSTEAD + /* THE SIZE IS THE CALLER'S, AND TAKING IT FROM THE TYPE INSTEAD * DESTROYED THE CALLER'S RETURN ADDRESS. * * This wrote `sizeof(sigset_t)' --- 128 bytes --- into whatever `a3' @@ -2485,7 +2604,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * -0x20 in a frame of 0x30, so 120 bytes past it lay the saved frame * pointer and the return address, and `__sigaction' returned to zero. * - * ⭐ MEASURED, and the whole of the reproduction is three lines: + * MEASURED, and the whole of the reproduction is three lines: * * int main(void) { signal(SIGABRT, h); return 0; } * @@ -2495,7 +2614,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, * a null pointer, and reads as the second. Every other signal number * returns SIG_ERR and exits 0. * - * ⚠️ AND IT WAS NOT ONLY INSTALLING A HANDLER. `signal(SIGABRT, SIG_IGN)' + * AND IT WAS NOT ONLY INSTALLING A HANDLER. `signal(SIGABRT, SIG_IGN)' * and a plain enquiry, `sigaction(SIGABRT, NULL, &old)', died the same * way: musl takes the lock for any change to that disposition, and * blocking signals around it is how it takes it. So a program that only @@ -2509,7 +2628,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, return 0; } case SYS_rt_sigaction: { - /* ⚠️ THE SAME DEFECT'S OTHER HALF, IN THE OTHER DIRECTION. The + /* THE SAME DEFECT'S OTHER HALF, IN THE OTHER DIRECTION. The * old-action was cleared for the size of a structure declared HERE --- * three fields --- while `struct k_sigaction' is four, so eight bytes * of the caller's structure were left holding whatever the stack held @@ -2532,7 +2651,7 @@ syscall_arg_t __okm_syscall(syscall_arg_t n, syscall_arg_t a1, syscall_arg_t a2, return -ENOSYS; } #ifdef SYS_sigaltstack - /* ⚠️ IT ANSWERED `0' AND INSTALLED NOTHING, AND THE ENQUIRY LIED TOO. + /* IT ANSWERED `0' AND INSTALLED NOTHING, AND THE ENQUIRY LIED TOO. * Measured: install a stack, ask for it back, and the answer is a zeroed * record --- reported as success, with `ss_sp' and `ss_size' both zero, * rather than as "none is installed". An alternate stack is where a signal diff --git a/port/src/okm_thread.c b/port/src/okm_thread.c index 39a0bf4..fbaa87c 100644 --- a/port/src/okm_thread.c +++ b/port/src/okm_thread.c @@ -154,7 +154,7 @@ syscall_arg_t __okm_task_exit(int code) return 0; } -/* ⚠️⚠️ A DETACHED CONTEXT RELEASES ITS MAPPING FROM WHERE IT STANDS, AND THE +/* A DETACHED CONTEXT RELEASES ITS MAPPING FROM WHERE IT STANDS, AND THE * STACK MUSL MOVED TO FIRST WAS OVERRUN FIFTY TIMES OVER. * * A detached thread releases its own mapping as the last thing it does. On Linux @@ -206,7 +206,7 @@ syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespe if (e != kal_ok) return -okm_errno(e); return (long)woken; } - /* ⚠️⚠️ WOKEN HERE RATHER THAN MOVED THERE, AND WITHOUT THIS CASE A CONDITION + /* WOKEN HERE RATHER THAN MOVED THERE, AND WITHOUT THIS CASE A CONDITION * VARIABLE WITH TWO WAITERS STOPPED FOR EVER. * * `FUTEX_REQUEUE' asks for `val' waiters upon `addr' to be woken and a @@ -216,7 +216,7 @@ syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespe * nothing that moves a waiter between two addresses --- so this used to reach * the arm below and answer ENOSYS. * - * ⚠️ AND MUSL DOES NOT CHECK. `unlock_requeue' in pthread_cond_timedwait.c + * AND MUSL DOES NOT CHECK. `unlock_requeue' in pthread_cond_timedwait.c * releases the barrier and then makes this request; when it fails there is no * remaining path that wakes anyone, so the next waiter in the list sleeps * until the program is killed: @@ -226,7 +226,7 @@ syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespe * else __syscall(SYS_futex, l, FUTEX_REQUEUE|FUTEX_PRIVATE, 0, 1, r) != -ENOSYS * || __syscall(SYS_futex, l, FUTEX_REQUEUE, 0, 1, r); * - * ⭐ IT TAKES TWO WAITERS, WHICH IS WHY IT SURVIVED. That call is reached only + * IT TAKES TWO WAITERS, WHICH IS WHY IT SURVIVED. That call is reached only * when `node.prev' is set --- when a second context is queued behind the one * being released. One waiter upon a condition variable never reaches it, and * one waiter is what almost every program has. Measured against a host: a @@ -240,7 +240,7 @@ syscall_arg_t __okm_futex(const int* addr, int op, int val, const struct timespe * upon the mutex, where a move would have left it blocked once. That is a cost * in scheduling and not in correctness, and it is the whole difference. * - * ⚠️ `val2' arrives in the argument this port declares as a deadline, because + * `val2' arrives in the argument this port declares as a deadline, because * that is the register the operation puts it in. It is a count here. */ case FUTEX_REQUEUE: { const kal_intptr move = (kal_intptr)t; diff --git a/port/src/win/main_stub.c b/port/src/win/main_stub.c index a21b175..12a0e8d 100644 --- a/port/src/win/main_stub.c +++ b/port/src/win/main_stub.c @@ -1,7 +1,7 @@ /* `__main`, which this format's compiler emits a call to and this C library has * already made unnecessary. * - * ⭐ WHAT IT IS. On a PE target the compiler emits `call __main` as the first + * WHAT IT IS. On a PE target the compiler emits `call __main` as the first * thing `main` does. In the arrangement that name comes from, that call runs the * program's global constructors: the C runtime's startup does not, and the * responsibility sits at the top of `main` instead. @@ -11,7 +11,7 @@ * of this port already uses. So the constructors are run exactly once, before * `main`, and `__main` has nothing left to do. * - * ⚠️ IT CANNOT SIMPLY BE ABSENT. The call is in the object whether or not + * IT CANNOT SIMPLY BE ABSENT. The call is in the object whether or not * anything defines the name, so leaving it undefined is a link error rather * than a saving: * @@ -21,7 +21,7 @@ * archive had been supplying it, together with `___chkstk_ms`, and neither is a * routine this port should be taking from a different compiler's runtime. * - * ⚠️ AND IT MUST NOT RUN THE CONSTRUCTORS ITSELF. Defining it as a second walk + * AND IT MUST NOT RUN THE CONSTRUCTORS ITSELF. Defining it as a second walk * of `.init_array` would run every one of them twice, which is not a failure * the linker can report. */ diff --git a/tools/cross-build-macos.sh b/tools/cross-build-macos.sh index f5e13cd..32c785e 100755 --- a/tools/cross-build-macos.sh +++ b/tools/cross-build-macos.sh @@ -41,7 +41,7 @@ inc=(-Iport/include -Imusl/src/include -Imusl/src/internal -Imusl-generated/internal -Imusl-generated/"$arch" -Imusl/arch/"$arch" -Imusl/arch/generic -Imusl/include -I"$here"/../openkal/include) -# ⚠️ `-DOKM_MUSL_INTERNAL=1` IS LOAD-BEARING AND WAS ADDED AFTER THIS LIST WAS +# `-DOKM_MUSL_INTERNAL=1` IS LOAD-BEARING AND WAS ADDED AFTER THIS LIST WAS # WRITTEN, WHICH IS THE POINT. # # It says the unit being compiled is one of musl's own, which is what @@ -50,7 +50,7 @@ inc=(-Iport/include -Imusl/src/include -Imusl/src/internal # # crypt_r.c:23: type specifier missing | weak_alias(__crypt_r, crypt_r); # -# ⭐ This list is a SECOND COPY of the manifest's, and the comment below already +# This list is a SECOND COPY of the manifest's, and the comment below already # says keeping it in step is what makes the answer the configured one. It went # out of step the first time the manifest gained a flag, and continuous # integration is what said so. Two places for one decision, and this is the diff --git a/tools/one-artifact.sh b/tools/one-artifact.sh index 910d87e..58f3bc3 100755 --- a/tools/one-artifact.sh +++ b/tools/one-artifact.sh @@ -5,7 +5,7 @@ # # one-artifact.sh # from the directory holding target/ # -# ⚠️⚠️ WHY THIS IS A SCRIPT AND NOT `find | head -1`. +# WHY THIS IS A SCRIPT AND NOT `find | head -1`. # # `target/` accumulates ONE DIRECTORY PER CONFIGURATION --- per toolchain, per # target, and per version of a dependency, because the version is part of the @@ -14,13 +14,13 @@ # built, and the difference is invisible: an old program runs, prints, and # reports success. # -# ⭐ MEASURED WHILE THIS PACKAGE WAS BEING CHANGED, twice in one session. The +# MEASURED WHILE THIS PACKAGE WAS BEING CHANGED, twice in one session. The # version moved from 0.5.0 to 0.6.0, `examples/subprocess/target` grew a second # fingerprint directory, and two newly added observations did not appear in the # output --- of a run that reported `-- failures: 0 --`. The criteria had not # failed; they had not run, and nothing said so. # -# ⚠️ IT DOES NOT BITE IN CONTINUOUS INTEGRATION, which is the reason it survives: +# IT DOES NOT BITE IN CONTINUOUS INTEGRATION, which is the reason it survives: # a fresh checkout builds one configuration and there is nothing to choose # between. It bites on the machine where the change is being written, which is # where a criterion is trusted most. @@ -43,7 +43,7 @@ if [ "$fps" != 1 ]; then exit 1 fi -# ⚠️ BOTH SPELLINGS. One of the three systems appends a suffix, and a search for +# BOTH SPELLINGS. One of the three systems appends a suffix, and a search for # the bare name there finds nothing and reports it as a build that did not # happen. binary="$(find target -type f \( -name "$name" -o -name "$name.exe" \) | head -1)" diff --git a/tools/probe-cross-macos.sh b/tools/probe-cross-macos.sh index 3f4e98f..33e7446 100755 --- a/tools/probe-cross-macos.sh +++ b/tools/probe-cross-macos.sh @@ -12,7 +12,7 @@ # recorded is two, and they are listed. A third would mean this port has # acquired a dependency on that system that nobody decided to acquire. # -# ⚠️ That is not the same as the set the LINK needs. `dyld_stub_binder' is +# That is not the same as the set the LINK needs. `dyld_stub_binder' is # referenced by the linker for its own lazy binding and by no source here, # and whether it is referenced at all depends on the linker's version: # ld64.lld 22 does not, ld64.lld 18 does. So the enumeration below asks @@ -50,7 +50,7 @@ inc=(-Iport/include -Imusl/src/include -Imusl/src/internal -Imusl-generated/internal -Imusl-generated/"$arch" -Imusl/arch/"$arch" -Imusl/arch/generic -Imusl/include -I"$here"/../openkal/include) -# ⚠️ `-DOKM_MUSL_INTERNAL=1` IS LOAD-BEARING AND WAS ADDED AFTER THIS LIST WAS +# `-DOKM_MUSL_INTERNAL=1` IS LOAD-BEARING AND WAS ADDED AFTER THIS LIST WAS # WRITTEN, WHICH IS THE POINT. # # It says the unit being compiled is one of musl's own, which is what @@ -59,7 +59,7 @@ inc=(-Iport/include -Imusl/src/include -Imusl/src/internal # # crypt_r.c:23: type specifier missing | weak_alias(__crypt_r, crypt_r); # -# ⭐ This list is a SECOND COPY of the manifest's, and the comment below already +# This list is a SECOND COPY of the manifest's, and the comment below already # says keeping it in step is what makes the answer the configured one. It went # out of step the first time the manifest gained a flag, and continuous # integration is what said so. Two places for one decision, and this is the @@ -75,14 +75,14 @@ cd "$here" # minus the one this system's build excludes. Keeping this list in step with # mcpp.toml is what makes question 2's answer the configured one. # -# ⚠️⚠️ AND `posix_spawnp' IS NAMED SEPARATELY, WHICH IS NOT REDUNDANT. The match +# AND `posix_spawnp' IS NAMED SEPARATELY, WHICH IS NOT REDUNDANT. The match # is anchored on the whole basename, so `posix_spawn' does NOT cover # `posix_spawnp.c' --- and when that source became the tenth this port replaces, # this list said nothing and the link reported # # ld64.lld: error: duplicate symbol: _posix_spawnp # -# ⭐ Which is the whole reason this list carries the warning it does: it is a +# Which is the whole reason this list carries the warning it does: it is a # SECOND statement of what mcpp.toml already states, and a second statement is # a thing that falls behind the first. It fell behind on the release that added # the tenth entry, and it is this job that said so. diff --git a/tools/run-probe.sh b/tools/run-probe.sh index ebf5e73..e8b6366 100644 --- a/tools/run-probe.sh +++ b/tools/run-probe.sh @@ -6,7 +6,7 @@ # # with the target, where the row needs one, in `MCPP_TARGET`. # -# ⚠️ WHY THIS IS A SCRIPT AND NOT A STEP. There are five probes now and each +# WHY THIS IS A SCRIPT AND NOT A STEP. There are five probes now and each # needs the same three things: a watchdog, because a program that does not # return is as much a failure as one that returns wrongly and the job would # otherwise spend its whole timeout finding out; a report of where a program @@ -19,7 +19,7 @@ set -euo pipefail dir="${1:?the example directory}" -# ⚠️ NO APOSTROPHE IN THIS MESSAGE, AND THAT IS NOT STYLE. Bash parses `${2:?...}` +# NO APOSTROPHE IN THIS MESSAGE, AND THAT IS NOT STYLE. Bash parses `${2:?...}` # with quoting active, so "the program's name" opens a single quote that never # closes --- and the report arrives thirty lines later as # @@ -31,7 +31,7 @@ dir="${1:?the example directory}" name="${2:?the name of the program}" shift 2 -# ⚠️ RESOLVED BEFORE THE `cd' BELOW. `BASH_SOURCE' is the path this script was +# RESOLVED BEFORE THE `cd' BELOW. `BASH_SOURCE' is the path this script was # invoked by, which is relative in every caller here, and a relative path stops # naming this directory the moment the working directory moves. here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -43,7 +43,7 @@ extra='' # shellcheck disable=SC2086 mcpp build $extra -# ⚠️ THE ARTEFACT OF THE BUILD THAT JUST RAN. tools/one-artifact.sh records what +# THE ARTEFACT OF THE BUILD THAT JUST RAN. tools/one-artifact.sh records what # a search across an accumulating `target/` answers instead, and what it cost. binary="$(bash "$here/one-artifact.sh" "$name")" @@ -91,7 +91,7 @@ else exit 1 fi -# ⚠️ BOTH DIRECTIONS. That the program reported, and that nothing it observed +# BOTH DIRECTIONS. That the program reported, and that nothing it observed # failed to hold. The first alone would pass for a program that printed its # failures; the second alone would pass for a program that printed nothing. grep -qE '^-- failures: 0 --$' run.log \ diff --git a/tools/working-trees.sh b/tools/working-trees.sh index d989ef5..34dc82d 100755 --- a/tools/working-trees.sh +++ b/tools/working-trees.sh @@ -56,7 +56,7 @@ fetch() { fetch openkal fetch "$implementation" -# ⭐ AND WHAT WAS FETCHED HAS TO BE WHAT THE MANIFEST ASKED FOR. +# AND WHAT WAS FETCHED HAS TO BE WHAT THE MANIFEST ASKED FOR. # # Substituting a working tree for a version removes the one check that would # otherwise happen: the resolver never sees a version requirement, so a tree of @@ -65,7 +65,7 @@ fetch "$implementation" # have no counterpart — but it means a change that spans two repositories is # built against whichever half happens to be on `main`. # -# ⚠️ THAT FALLBACK ONCE PRODUCED A FAILURE THAT NAMED THE WRONG THING. This +# THAT FALLBACK ONCE PRODUCED A FAILURE THAT NAMED THE WRONG THING. This # package's branch was `feat/getrandom-through-openkal` while the # specification's was `feat/openkal-random`, so the fallback supplied openkal # 0.6.0 to a manifest asking for 0.7.0, and five jobs reported: @@ -129,7 +129,7 @@ native() { } specification="$(native "$beside/openkal")" -# ⚠️ THE REWRITE IS PERMANENT AND IS MEANT TO BE. This runs in a checkout that +# THE REWRITE IS PERMANENT AND IS MEANT TO BE. This runs in a checkout that # is thrown away, and the manifests must keep naming the working trees for the # rest of the job, so there is no trap restoring them. # From 229ac2386825237a442ca93b8db741ba7a01238c Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 17 Sep 2026 17:05:50 +0800 Subject: [PATCH 2/3] docs: two more limits, measured on Windows and not yet recorded Name resolution by name fails there: musl's resolver reads /etc/resolv.conf through this port's filesystem layer, and openkal defines no name resolution (SPEC.md clause 3.4, clause 11 item 5) for a nameserver list Windows keeps where this port cannot read it. getaddrinfo reports EAI_AGAIN rather than guessing one; a numeric address is unaffected. Measured under Wine with openkal-windows 0.7.4 / this package's 0.13.5. And a C program linking this package directly for x86_64-windows-gnu fails to link on three compiler-rt builtins this package carries no archive for; a C++ program through openkal-llvm-runtime receives them with the rest of the runtime it names. Measured with mcpp 2026.9.17.2, including the fix for mcpp#662. Neither is addressed in this change. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1e6a777..e207c68 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,8 @@ answer that leaves a program wrong without telling it. | an alternate signal stack | `sigaltstack` reports `ENOSYS` since 0.10.0 | it used to report success and install nothing, and the enquiry that would have caught it answered 0 with a zeroed record. There are no signals here, so there is nothing for such a stack to be. | | one C runtime and one C++ runtime per image | a static library compiled against the platform's own C runtime, or an object that runtime owns crossing the boundary — a `FILE*`, memory one side allocates and the other frees, `errno` — is not supported. Only handles and values may cross into a platform's own system interfaces | this package supplies the whole of a program's C library; a second one linked in beside it duplicates every piece of global state each keeps (the allocator's own arena, `errno`, locale) and the two do not agree about any of it. A vendor SDK distributed only as a static library built against another C runtime is `n/a` on this target — a design consequence and not a defect. openkal's own README states the same rule for every implementation built above it; the ecosystem design record (`.agents/docs/2026-09-17-openkal-ecosystem-cross-repo-design.md`, R1) states why it binds a package and not the specification. | | a context a platform library creates | code reached from it has no C library state — no `errno`, no locale, no `pthread_self` — and must not rely on any, because none of it exists until `kal_task_start` builds it, and such a context was not built by it | musl's per-context state lives in thread-local storage this port's own thread start fills in. A callback from a platform's own thread pool, from COM, or from a framework's own callback thread (Windows, macOS's GCD) arrives on a context this library never started. A package reached this way confines such a callback to work that does not touch this library, or hands the work to a context of its own before it does (R2 of the design record above). | +| resolving a host name by name, on Windows | `getaddrinfo` reports `EAI_AGAIN`; a numeric address is unaffected | musl's resolver reads `/etc/resolv.conf` for a nameserver list, through this port's own filesystem layer. openkal defines no name resolution (SPEC.md clause 3.4, clause 11 item 5); the nameserver list is the environment's, and on Windows it is kept where this port cannot read it — there is no `/etc/resolv.conf` for any layer here to find. Answering with a guessed nameserver would be silently wrong, so this reports that the name could not be resolved rather than inventing one. A program that needs names on Windows supplies its own `resolv.conf` in a directory it was given (a preopen), or resolves by another means. Measured under Wine, `openkal-windows` 0.7.4 and this package's 0.13.5: `getaddrinfo("httpbin.org")` returns `-3` ("Try again") and no `resolv.conf` is present; the same program on Linux resolves the name and connects. | +| the compiler-rt builtins a C program needs, on `x86_64-windows-gnu` | a C program that names this package directly fails to link: `___chkstk_ms`, `__muldc3` and `__mulxc3` are undefined | this package supplies no C++ runtime and carries no builtins archive of its own. `openkal-llvm-runtime` builds compiler-rt for the target, and a C++ program names it for the runtime and receives these three along with everything else it provides; a C program that names only this package does not. Measured with mcpp 2026.9.17.2, including the fix for mcpp#662. Not addressed here: a C program on this target names `openkal-llvm-runtime` as well, or supplies the three symbols itself. | **What carries confinement here, since a mode word does not.** A program that writes "only I may read this" as a mode is stating it in a vocabulary this From 5d97c7c84b7390188276e449978ee784c7abc099 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 17 Sep 2026 17:47:11 +0800 Subject: [PATCH 3/3] Review: a directory's execute bits, the two readings of an unanswered position, and the install lines A chmod that would change a directory's execute bits reached kal_fs_set_executable_at, which refuses a directory, and reported EISDIR; it now reports ENOSYS like every other change this port cannot express, and the subprocess probe asserts it. The comment in fill_kstat claimed stat and access(X_OK) read an unanswered position the same way; they do not, and did not before 0.14.0 (stat: no execute bits; access: yes), which the comment and the README row now state. README install lines name 0.14.0 and runtime 0.10.0, and the carried-by table gains the 0.14.0 row. --- README.md | 9 +++++---- examples/subprocess/src/main.c | 14 ++++++++++++++ port/src/okm_syscall.c | 11 +++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e207c68..baef5ce 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ the claim can be checked rather than repeated. ```toml [dependencies] -openkal-musl = "0.11.0" +openkal-musl = "0.14.0" ``` It names no implementation and no platform: a C library is the one consumer that @@ -31,7 +31,7 @@ error: dependency 'mcpplibs.openkal-musl' has irreconcilable versions: ```toml [dependencies] -openkal-llvm-runtime = "0.6.0" # carries openkal-musl 0.10.0 +openkal-llvm-runtime = "0.10.0" # carries openkal-musl 0.14.0 ``` Recorded here because it was got wrong by the people who maintain this @@ -41,7 +41,8 @@ consumer needs to answer it without asking. | this package | is carried by | | --- | --- | -| 0.13.5 | `openkal-llvm-runtime = "0.9.6"` | +| 0.14.0 | `openkal-llvm-runtime = "0.10.0"` | +| 0.13.5 | `openkal-llvm-runtime = "0.9.6"`, `"0.9.7"` | | 0.13.4 | `openkal-llvm-runtime = "0.9.5"` | | 0.13.3 | `openkal-llvm-runtime = "0.9.4"` | | 0.13.2 | `openkal-llvm-runtime = "0.9.3"` | @@ -167,7 +168,7 @@ answer that leaves a program wrong without telling it. | ~~a version a program can read~~ | **answered since 0.9.0.** `uname`'s `release` field is this package's version, and `OPENKAL_MUSL_TRACE=enosys` names it on the error stream once per process before the program runs | It was the string literal `0.5.0` through every release after 0.5.0, so a program that asked was not left without an answer -- it was given a false one. It therefore MOVES AT EVERY RELEASE: nothing here or in musl reads it (`gethostname` and `getdomainname` are musl's only consumers of `uname` and both read `nodename`), but a program above it that compares the field against a fixed string will see it change. `sysname` is `openkal` and not `Linux`, so nothing can have been reading it as a kernel version. | | ~~**setting** the modification time of a directory~~ | **answered everywhere since 0.11.0.** 0.10.0 answered it only where the implementation could open a directory --- Linux and macOS could and Windows could not, because its `kal_fs_open` names `FILE_NON_DIRECTORY_FILE`. **Reading** it was never affected | 0.10.0 reached a directory's time by opening the directory for READING and stamping that, which worked and was **outside anything `fs.h` stated** --- there was no route to a directory's time at all. openkal 0.10 added `kal_fs_set_modified_at`, which takes a NAME, and every implementation answers it: the Windows one opens for the attribute alone, which reaches a directory where opening a FILE cannot. The divergence this row recorded was caused by a missing declaration, and it went away when the declaration arrived. | | ~~a lock on a file~~ | **`fcntl(F_SETLK)` and `F_SETLKW` take a real lock since 0.11.0.** They used to answer 0 and do nothing, so **two programs took one exclusive lock and both were told they had it**. The holder is the **open file**, not the program: a SECOND open file of one name is refused here, where the older process-held form would have granted it and a library that opened one file twice would have destroyed its own lock. `F_GETLK` is still refused, and `flock` has no case | 0.10.0 refused all three and said the refusal was **temporary** in a way the permission row is not — every environment beneath openkal can lock a byte range and what was missing was a word. openkal 0.10 added `kal_fs_lock` with `KAL_FS_PROP_LOCKS`, and this is what that record said would happen. `F_GETLK` remains refused because it asks whether a lock **would** block without taking one, and openkal has no operation that answers a question without performing it — the absence clause 6.3 records for readiness. | -| whether a file may be executed | **Answered on both halves since openkal 0.13.** `access(path, X_OK)` and `stat`'s execute bits follow `KAL_INFO_EXECUTABLE` when the enquiry reports the position filled (a file, on a volume that claims `KAL_FS_PROP_EXECUTABLE`, since 0.14.0 also settable — see the row above) and answer **yes** where it does not — a directory, or a volume that does not claim the property. Starting a name that exists and cannot be run no longer ends the caller at 127 either: `posix_spawn` reports a reason and returns no pid — `ENOENT`, `EACCES`, `EISDIR`, and, since openkal 0.13's `kal_err_not_program`, **`ENOEXEC`** for a name that is there, may be started, and is not in a form this environment recognises | `kal_node_info` carried `writable` and no other permission before 0.13, so "it is there" was the whole of what this port could answer, and a start that could not happen was folded into `kal_err_io` — reporting a device failure when none occurred, and giving a C library above no way to map the failure back to `ENOEXEC`, which every environment this port targets has natively. **Versioned, because the three implementations did not move together.** openkal-linux has reported the reason for a start that did not happen since 0.8.0, and reports `kal_err_not_program` for it since 0.13.0. openkal-macos reported `kal_ok` and a handle for **every** name that could not be started — including one that does not exist at all — until 0.10.0; a program above an older openkal-macos still meets 127, indistinguishable from a program that ran and returned that status. openkal-windows fails synchronously in the caller (`CreateProcessW` needs no pipeline for it) and maps the condition to `kal_err_not_program` since 0.8.0. | +| whether a file may be executed | **Answered on both halves since openkal 0.13.** `access(path, X_OK)` and `stat`'s execute bits follow `KAL_INFO_EXECUTABLE` when the enquiry reports the position filled (a file, on a volume that claims `KAL_FS_PROP_EXECUTABLE`, since 0.14.0 also settable — see the row above). Where it does not, the two read the absence differently, as they did before 0.14.0: `stat` reports a file without execute bits, and `access(X_OK)` answers **yes**, so a program that asks before starting a name still attempts the start and is told the reason. A directory is reported traversable by both, and a `chmod` that would change a directory's execute bits reports `ENOSYS`. Starting a name that exists and cannot be run no longer ends the caller at 127 either: `posix_spawn` reports a reason and returns no pid — `ENOENT`, `EACCES`, `EISDIR`, and, since openkal 0.13's `kal_err_not_program`, **`ENOEXEC`** for a name that is there, may be started, and is not in a form this environment recognises | `kal_node_info` carried `writable` and no other permission before 0.13, so "it is there" was the whole of what this port could answer, and a start that could not happen was folded into `kal_err_io` — reporting a device failure when none occurred, and giving a C library above no way to map the failure back to `ENOEXEC`, which every environment this port targets has natively. **Versioned, because the three implementations did not move together.** openkal-linux has reported the reason for a start that did not happen since 0.8.0, and reports `kal_err_not_program` for it since 0.13.0. openkal-macos reported `kal_ok` and a handle for **every** name that could not be started — including one that does not exist at all — until 0.10.0; a program above an older openkal-macos still meets 127, indistinguishable from a program that ran and returned that status. openkal-windows fails synchronously in the caller (`CreateProcessW` needs no pipeline for it) and maps the condition to `kal_err_not_program` since 0.8.0. | | descriptors above 2 crossing into a started program | a started program receives standard input, output and error and **nothing else**; a non-close-on-exec descriptor 4 is not there, and `fcntl(F_SETFD, 0)` upon one therefore changes nothing | `kal_spawn_streams` has exactly three positions and openkal has no general form for placing a stream at position *n*. `posix_spawn_file_actions_adddup2` above position two is already refused rather than accepted, so the two agree; only implicit inheritance is lost. A general form has been asked for. | | ~~**the working directory a started program runs in**~~ | **answered since 0.12.0**, together with `posix_spawn_file_actions_addchdir_np` and `addfchdir_np`. openkal 0.11 gave `kal_spawn` a second directory — `base` resolves the program's NAME, `work` is where it RUNS — and a copy that called `chdir` carries its own into the `execve` it then performs. The rest of this row is what it said before | this one was silent, and it is the one a consumer is most likely to meet: the call reported success, the caller's own paths followed it, and only the started program disagreed. `kal_process_spawn`'s `base` is what the program's NAME resolves against, and resolving a name is not entering a directory — so naming a program and naming where it runs are two directories and openkal 0.10 carried one. It could not be composed here either: `chdir` in a copy before starting would need openkal to have an operation that moves a running program's working directory, and it deliberately has none. The route was a declaration rather than a composition, which is the same shape as the directory-time row above. | this one is silent, and it is the one a consumer is most likely to meet: the call reports success, the caller's own paths follow it, and only the started program disagrees. `kal_process_spawn`'s `base` is what the program's NAME resolves against, and resolving a name is not entering a directory — so naming a program and naming where it runs are two directories and openkal 0.10 carries one. It cannot be composed here either: `chdir` in a copy before starting would need openkal to have an operation that moves a running program's working directory, and it deliberately has none. A per-spawn form has been asked for; see `.agents/docs/2026-08-30-openkal-0.11-start-design.md`. | | ~~**terminating what a started program itself started**~~ | **answered since 0.12.0 for the form that works**, which is `posix_spawn` with `POSIX_SPAWN_SETPGROUP` and a zero group: the started program forms a unit, what it starts inherits the unit, and `kill(-n)` upon the identifier `posix_spawn` returned reaches all of it — including a shell's backgrounded work, which is the case a timeout has to reach. `setpgid(0, 0)` in **this** program forms a unit too. Naming another program's group is still `EPERM`, and `setsid` is still `EPERM` | openkal 0.11 gave `kal_spawn` a `job`, which is where a unit is established: the caller says which unit, and the implementation performs the placement. What this costs is stated in the row below, because it is one composition and not a general gap. | diff --git a/examples/subprocess/src/main.c b/examples/subprocess/src/main.c index c263058..956330b 100644 --- a/examples/subprocess/src/main.c +++ b/examples/subprocess/src/main.c @@ -1018,6 +1018,20 @@ int main(int argc, char** argv) } unlink(path); + /* A directory's execute bits are its traversal, which openkal does not + * record; a change to them is refused rather than routed to an + * operation that refuses directories with a different error. */ + if (mkdir("chmod-dir-probe.tmp", 0777) == 0) { + struct stat ds; + if (stat("chmod-dir-probe.tmp", &ds) == 0) { + errno = 0; + const int de = chmod("chmod-dir-probe.tmp", (ds.st_mode & 07777) & ~0111u); + check(de == -1 && errno == ENOSYS, + "a chmod that would change a directory's execute bits reports ENOSYS"); + } + rmdir("chmod-dir-probe.tmp"); + } + /* fchmod has no name to work with: this port keeps none for an open * file (only a directory's is remembered, for resolving a name that * ascends out of it), so it is ENOSYS unconditionally. */ diff --git a/port/src/okm_syscall.c b/port/src/okm_syscall.c index 2e84b60..75135bf 100644 --- a/port/src/okm_syscall.c +++ b/port/src/okm_syscall.c @@ -300,8 +300,11 @@ static void fill_kstat(const struct kal_node_info* in, struct kstat* out) * `writable' is this port's only per-class distinction --- when the * enquiry reports KAL_INFO_EXECUTABLE set, and cleared when it reports the * position clear or does not answer it at all. "Does not answer" reads as - * "not executable", which is the same reading `access(X_OK)' gives it - * below and is documented in README.md. */ + * "not executable" here, as `stat' reported every file before 0.14.0. + * `access(X_OK)' below reads the same absence as "yes", as it did before: + * a program that asks before starting a name still attempts the start, + * and the start reports the reason. The two readings of an unanswered + * position differ, and README.md states both. */ if (in->kind == kal_node_directory) mode |= 0111u; else if ((in->present & KAL_INFO_EXECUTABLE) && in->executable) mode |= 0111u; out->st_mode = mode; @@ -505,6 +508,10 @@ static syscall_arg_t do_chmod(int dirfd, const char* path, mode_t mode) const unsigned requested = (unsigned)mode & 07777u; if (requested == current) return 0; /* nothing to change; see above */ + /* A directory's execute bits are its traversal, which openkal does not + * record and this port always reports. A change to them is a change this + * port cannot express, and it is refused the way every other one is. */ + if (info.kind == kal_node_directory) return -ENOSYS; const unsigned none = current & ~0111u; const unsigned full = none | ((current & 0444u) >> 2);