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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions .agents/docs/2026-08-28-issue13-spawn-streams-abort-and-wait.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**,包括不做任何重定向的控制项。
Expand All @@ -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`,
Expand All @@ -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 在修复前
Expand Down Expand Up @@ -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`,读任何产物之前先
断言「恰好一个指纹目录」,三个调用点共用。

Expand Down Expand Up @@ -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()`**:两者当前等价,但"描述符是否仍
> 指着程序启动时那个流"是一条关于**历史**的判断,把它表达成一次记录而不是一次
> 重新提问,是这条规则唯一说得清的形式。

Expand All @@ -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 里报告的那类空跳,只是发生在数据上。
Expand All @@ -208,15 +208,15 @@ 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
> 分成两行,而不是把注释改一改留着。

`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。
Expand All @@ -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`,与既有七项同一个二进制,三系统同跑):
Expand All @@ -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` 恰好两处都写也会绿。
> 两个反向对照抓的是不同的东西。

---
Expand Down Expand Up @@ -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`)一致:这里做的
Expand All @@ -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`,不是"不等于某个值"。**

---
Expand All @@ -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`。
Expand Down Expand Up @@ -402,11 +402,11 @@ exit=139

两条元规则,是这批缺陷本身教出来的:

> **一处改动答复三个入口,不等于三个入口都有判据。** A 的改点只有一个
> **一处改动答复三个入口,不等于三个入口都有判据。** A 的改点只有一个
> (`__posix_spawn` 内部),但 `posix_spawn` / `execve` / `system` 是三条独立的
> 调用链,必须各测一条。
>
> ⚠️ **探针的绿必须能回答消费者问的那个问题。** `examples/subprocess` 七项全绿,
> **探针的绿必须能回答消费者问的那个问题。** `examples/subprocess` 七项全绿,
> 而 #13 报的那件事它一次都没问过。新增观察之前先检查:这条观察若被删掉,哪一条
> 缺陷会重新变成绿的?答不上来的观察不要加。

Expand Down Expand Up @@ -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"这个读数的含义就变了 —— 这两条要在修完之后**重测一遍**
> 才谈得上归因。
30 changes: 15 additions & 15 deletions .agents/docs/2026-08-30-issue13-exec-search-and-what-it-hid.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**,所有读数均取自
Expand Down Expand Up @@ -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 读码 —— 三环,每一环可复核
Expand Down Expand Up @@ -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」**。
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -257,7 +257,7 @@ waitpid = 1194981 raw status = 0x000f WIFSIGNALED=1 WTERMSIG=15
marker.txt absent <-- SIGTERM 打到了程序
```

⚠️ **状态字一模一样(`0x000f`),两边都告诉调用者「它死于 SIGTERM」。**
**状态字一模一样(`0x000f`),两边都告诉调用者「它死于 SIGTERM」。**
一边是真的,一边是假的,而调用者手上没有任何东西能把两者分开。

范围是精确的——只有 `fork`+`execve` 这条路:
Expand All @@ -282,7 +282,7 @@ posix_spawn status=0x000f program was killed

而父亲收到的是「1001 死于 SIGTERM」,因为等待者确实死于 SIGTERM。

### 3.3 ⚠️ 今天在本仓修不完,这一点要说清楚
### 3.3 今天在本仓修不完,这一点要说清楚

`kill` 发生在父亲这一侧,父亲**无法知道**自己的哪个孩子是等待者;等待者阻塞在
`kal_process_wait` 里,**收不到任何东西也跑不了代码**。openkal 今天没有任何原子
Expand All @@ -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` 用它起替身。
Expand Down Expand Up @@ -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 层:

Expand All @@ -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` 的形式**。
Expand All @@ -386,7 +386,7 @@ kal_fs_open(dir, READ) -> 0 (ok)
自己的代码;`ENOSYS` 是在说「这个环境没有这个操作」。
- 代价:一个每个身下环境都做得到的普通 POSIX 操作就此长期不可用。

⚠️ 无论选哪条,`musl/PATCHES.md:125` 那段都要补:它今天只记了「要写权限而不是
无论选哪条,`musl/PATCHES.md:125` 那段都要补:它今天只记了「要写权限而不是
要所有权」,**没有记「目录的时间根本设不了」**。

---
Expand All @@ -407,7 +407,7 @@ getpid=1 getpgid(0)=1 getsid(0)=1
`getpgid`/`getsid` 诚实地回答「就一个程序,它自成一组」。然后接着说
「`setpgid` 与 `setsid` 仍然拒绝:造一个组和身处一个组不是一回事」。

⚠️ **但这两个调用问的恰恰不是「造一个组」:**
**但这两个调用问的恰恰不是「造一个组」:**

- `setpgid(0, 0)` 请求的状态是「调用者自成一组」——按上面那三行读数,
**这个状态已经成立**。它不是要求一个不存在的效果,它要求的是已经为真的事。
Expand All @@ -423,7 +423,7 @@ getpid=1 getpgid(0)=1 getsid(0)=1

顺带:这会消掉报告者 trace 里 16 行(`setpgid` 12 + `setsid` 4)。

> ⚠️ 这条是**判断**而不是读码结论,和 §1-§4 不同级别,单独列出来等 review 否决。
> 这条是**判断**而不是读码结论,和 §1-§4 不同级别,单独列出来等 review 否决。

---

Expand Down Expand Up @@ -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。

Expand Down
Loading
Loading