# 进程替换后，会话审批在 exec 之后还有效吗？

会话审批应跟随经过验证的执行身份，而不是操作系统进程标识符。进程调用 `exec` 时，内核可能保留原来的 PID，但代码、参数、环境以及进程实际承担的目的通常都会被替换。让旧审批在替换后继续有效，就等于把用户授予其他对象的权限交给未经审查的新代码。

我会采用一条清晰的规则：如果替换后的可执行文件身份不同，必须重新请求审批，之后才能执行需要凭据的操作。只有在严格验证为同一可执行文件镜像重新 exec 时，才保留连续性。这项政策会让合法工作流偶尔多出一次提示，却能堵住更严重的漏洞。在代理、包装器、更新程序或遭入侵的依赖可以决定下一步运行什么的场景中，这个漏洞可能出现在任何工作流里。

## 审批绑定的是执行者，不是 PID

PID 标识的是内核用于记账的一个位置，而审批需要标识能够使用人类决定所授予权限的程序。这是两项不同的工作。把它们当成同一个对象，代码一旦启动其他代码，就会立刻带来问题。

Sallyport 将每会话授权描述为对新代理进程的审批，授权持续到本次运行结束。这是一条对用户很实用的规则，但实现时需要更精确的内部定义：获批的运行必须始终由获批的可执行文件执行，而不能只是保留同一个 PID。

这一差异很重要，因为秘密保存在网关中，代理收到的是结果，而不是凭据材料。这种设计避免了子进程从环境变量中读取令牌的常见灾难。但它并不能阻止被替换的进程请求网关使用已存储的凭据调用 API 或运行 SSH。如果替换后的进程继承了审批，它不需要看到秘密，也能造成同样的外部损害。

明确审批的主体。它应包含稳定的可执行文件身份、进程创建事件或会话随机数、向用户展示的代码签名机构，以及足以说明进程为何存在的启动上下文。PID 可以作为审计属性和排障时的有用线索，但不要把它当作持有权限的凭据。

这个区分也能让撤销逻辑保持准确。如果用户撤销了一次运行，即使该运行重新 exec，网关也应拒绝它之后发出的调用。如果替换后的程序获得了新的审批，它应拥有一条新的会话记录，用户可以单独撤销它。

## 即使 PID 不变，Exec 也会改变可执行文件

当 `exec` 加载不同程序时，它应重置授权，因为它替换了程序镜像，即使操作系统可能保留 PID。POSIX 对 exec 函数的描述是“替换当前进程镜像”。这句话很简短，却准确指出了关键：连续性属于管理层面的延续，不代表仍然是同一个执行者。

Shell 会让这个问题不容易被察觉。开发者启动了一个获批代理，代理调用辅助程序，辅助程序再调用 `exec`，避免留下额外的父进程。进程列表可能在替换前后显示相同的 PID。如果你的会话查询逻辑写成了“pid 4127 已获批”，那么新的辅助程序就会获得旧代理的权限。

可执行文件的变化还可能以 PID 无法表达的方式改变风险。替换后的程序可能连接另一台主机，解释不受信任的仓库文件，加载扩展，接受标准输入中的数据，或者只是专门转发操作请求。这些事实都不会出现在进程编号里。

不要根据新的命令行看起来是否熟悉来决定。命令参数可以提供有用上下文，但程序能够重写参数，包装器也可以让看似无害的命令启动无关的目标。应从操作系统实际启动的镜像中确定可执行文件身份，同时保留启动时观察到的路径和参数，供审查请求的人参考。

`posix_spawn` 也应纳入同一套设计讨论，但结果不同。它创建子进程，而不是替换调用者的镜像。默认情况下，子进程不应拥有会话审批。fork 后再 exec 也应遵循同一原则：新进程应针对最终运行的镜像请求审批。

## 新的可执行文件需要新的审批

网关应要求新的审批，之后不同的可执行文件才能通过已经授权的会话执行任何操作。应使用经过验证的镜像身份定义“不同”，而不是文件名或显示名称。

一份实用的身份记录可以包含平台提供的不可变可执行文件标识符、签名代码的摘要（如果有）、签名机构，以及启动时观察到的具体可执行文件路径。前两个字段回答镜像是否发生变化。签名者告诉审查者谁为代码负责。路径告诉审查者程序从哪里启动。每个字段回答的问题都不同，不要把它们压缩成一个字符串。

这就是让宽松继承看起来很有吸引力、直到为时已晚的漏洞。一个获批的编程代理调用仓库辅助程序。辅助程序发现某个环境设置，然后把自己替换为预期路径下本地构建的工具。这个工具不需要提取 API 凭据，它只需请求一次 HTTP 操作，就可能删除部署、修改账单设置或发布版本。网关看到原来的 PID，于是接受调用。用户审批的是编程代理，不是审批之后自行选择并运行的工具。

人们通常支持继承审批，是因为担心提示过多。这种担心确实存在，但允许任意替换并不能解决提示疲劳。它只是把决定隐藏起来，让唯一能判断替换是否合理的人失去审查机会。让正常的代理运行保持稳定，减少审批卡片的出现，然后在执行者发生变化的时刻再次请求审批。

检查必须发生在注入凭据、执行 SSH 或进行任何其他出站操作之前。它也必须发生在网关返回可能帮助替换后的程序策划操作的元数据之前。等待审批不等于处于部分授权状态。

## 同一镜像重新 exec 是狭窄的例外

如果经过验证的重新 exec 运行的是完全相同的可执行文件镜像，就可以保留会话审批，因为它没有引入新的执行者。程序可能会通过自我重新 exec 来实现干净重启、更新文件描述符，或在更新自身环境后进行有意的交接。在这种情况下强制显示新的卡片，只会增加干扰，却没有带来有意义的新决定。

这个例外必须严格限定。网关应将当前观察到的镜像与获得审批的镜像进行比较。如果身份完全一致，可以保留会话随机数，并在审计日志中写入连续性事件。如果网关无法确认身份，就应再次请求审批。存在歧义不等于替换是安全的证据。

不要把例外扩大到整个签名机构。一个签名机构可能发布代理、安装程序、诊断工具和网络工具。这些程序可能拥有完全不同的操作权限。可见的签名机构能帮助用户判断请求，但不应悄悄将审批扩展到所有带有同一签名机构的二进制文件。

也不要把例外扩大到整个路径。自我更新可以在固定路径替换字节。审批之后，符号链接可以指向其他位置。脚本可以保留文件名但改变内容。身份比较必须能应对这三种情况。

合法更新安装新版本代理时，应让它再次请求审批。这个提示传达了一个真实事实：将要执行操作的可执行文件已经改变。认为更新属于常规操作的用户可以一键批准。没有预期这次变化的用户则有机会阻止它。

## 代码签名机构能帮助用户判断，但不会授予权限范围

应醒目显示代码签名机构，因为它回答了用户真正关心的问题：谁发布了这个可执行文件？但不要把这个答案误当成完整的授权规则。

Apple 的代码签名模型让 macOS 可以识别签名代码，并根据适用的信任规则验证其完整性。这是审批卡片上的有力证据，但它并没有说明同一机构发布的所有代码都拥有相同的实际用途，也不能告诉网关父进程是否通过不受信任的仓库设置选择了这个可执行文件。

一张好的审批卡片应先显示可执行文件名称和路径，然后显示签名机构、父进程身份，以及新请求的原因。对于 exec 替换，卡片应在一句话中说明变化的两端：获批的代理已将自身替换为这个可执行文件。用户不需要通过比较两张互不相连的提示来推断发生了什么。

有签名和无签名的情况应遵守同一边界。未签名的本地开发版本可能是开发工作流中正常的一部分，而已签名的工具也可能不适合继承权限。卡片应如实描述证据，不要假装签名能把新的可执行文件变成旧的可执行文件。

避免使用“受信任进程”这样的模糊标签。它会让人们审批一个类别，而不是一个具体请求。应写出可执行文件名称，并显示它与已获批进程之间的关系。这样，审查者才有明确的对象可以识别或拒绝。

## 脚本和启动器会暴露薄弱的边界

脚本驱动的启动需要两种身份：负责执行的解释器，以及内容实际控制行为的脚本。只检查解释器时，每个 shell 脚本看起来都像同一个 shell。只检查脚本时，又可能漏掉通过 shebang 行或包装器选择的解释器。

对于 shell 脚本，应记录解释器可执行文件身份、解析后的脚本路径，以及脚本内容摘要。如果脚本通过获批的 shell 运行，而该 shell 随后 exec 了另一个二进制文件，那么这个二进制文件的替换仍然需要新的审批。脚本不应成为穿过会话边界的通道。

启动器会带来类似问题。已获批的启动器可能检查配置文件，从 `PATH` 中发现工具，下载辅助程序，或选择某个版本目录。人们常常审批启动器，并把它选择的目标视为同一运行的一部分。当启动器是在用户审批之后才做出安全相关选择时，这条规则就是错误的。

可以采用两种方式之一。如果启动器在第一次调用网关之前就知道目标，让卡片显示最终目标，并审批该执行身份。如果启动器稍后才做出选择，就让它在没有操作权限的情况下运行，直到选中的目标第一次请求执行操作时再要求审批。第二种方式能留下更诚实的审计记录。

同一规则也适用于运行时插件和嵌入式解释器。原生宿主程序可能保持不变，却从项目目录加载代码。如果加载的代码能够构造网关请求，仅凭宿主镜像身份就无法说明实际执行者。网关无法安全地检查每一种运行时，因此更安全的默认做法是将会话审批限制在定义好的代理可执行文件上，并在它把操作控制权交给外部程序时要求新的决定。

## 子进程和 exec 属于不同的委托情况

子进程不应仅仅因为父进程拥有会话审批就继承该审批。创建进程会引入新的执行者，而 exec 会替换当前执行者。当不同的可执行文件将要调用网关时，这两种情况都需要新的审批，但审计关系不同。

对于子进程，应创建一条带有父会话引用的新会话候选记录。在审批卡片中显示父进程，因为它能提供有用上下文，而不是因为它授予权限。如果用户批准子进程，就为它分配自己的会话随机数和独立的撤销控制柄。

对于 exec，应关闭或取代旧的可执行文件身份，并创建一条与之前会话关联的替换候选记录。如果新镜像与获批镜像完全一致，就保留连续性并记录结果。如果不同，就在闸门处停止并等待决定。这样可以避免一条巨大的会话记录包含多个互不相关的程序。

不要创建通用委托令牌，让父进程把它交给子进程或替换后的程序。一个写着“我启动的任何程序都可以执行操作”的令牌，很容易成为遭入侵代理或混乱包装器的目标。日志中的父级引用已经足以讲清楚关系，不需要把进程祖先关系变成权限。

这种分离也能改善事故审查。你可以回答父进程是否启动了子进程、它是否替换了自身，以及用户是否审批了最终执行的可执行文件。一次糟糕操作发生后，仅靠一份记录允许操作的扁平日志无法回答这些问题。

## 将替换记录为独立事件

无论网关保留审批还是再次请求审批，审计日志都应将 exec 替换显示为独立事件。没有这条事件，审查者看到的是来自同一会话的操作，可能会以为所有操作都由同一个稳定的可执行文件执行。

设计约定可以写成下面这样。这只是一个需要持久化的信息示例，不是强制性的传输格式：

```json
{
  "event": "execution_replaced",
  "session_id": "sess_8f2c",
  "previous_image": {
    "identity": "image:4f19...",
    "path": "/work/agent/bin/agent"
  },
  "current_image": {
    "identity": "image:b66a...",
    "path": "/work/agent/bin/release-helper",
    "signing_authority": "Example Development Team"
  },
  "decision": "approval_required",
  "parent_relation": "exec"
}
```

事件需要记录之前和现在的身份，不能只写 exec 已发生。它还需要记录决定结果。之后的操作记录应引用当前会话身份，这样调查人员才能将操作与允许它执行的审批关联起来。

与其他操作历史一样，应将这条记录设为只能追加。Sallyport 的加密哈希链审计日志和离线 `sp audit verify` 检查在这里尤其有用，因为替换事件及其之后的操作可以共享一条可验证的序列。验证结果只能证明保存的序列没有被更改，不能让一开始就过于宽泛的审批政策变得合理。

用户撤销会话时，应将撤销记录关联到持有该会话的可执行文件身份。如果替换后的程序获得了新的审批，就应继续单独显示。这个细节可以防止撤销控件暗示出超出实际范围的覆盖效果。

## 审批卡片需要展示变化前后的事实

exec 之后的新审批卡片应让用户一眼看清替换内容，并且容易做出安全选择。笼统的提示会训练人们直接点击。明确写出发生变化的可执行文件，只有在真正发生变化时才会促使用户停下来查看。

先显示新的可执行文件身份和它想使用的操作通道。然后说明它是由已经获批的进程启动的，并列出旧可执行文件名称和路径、新路径，以及代码签名机构。如果替换来自脚本或启动器，也应使用通俗语言说明这种关系。

使用能准确暴露用户正在审批什么的决定模型。批准替换后，应授权该可执行文件运行到退出为止，同时仍受保险库闸门以及任何设置为每次调用审批的凭据限制。它不应追溯授权兄弟进程、未来的替换，或同一路径下之后更新的文件。

可以用一个问题检查提示文案：疲惫的开发者能否分辨出预期的辅助程序和意外的下载程序？如果不能，卡片就缺少关键事实。增加更多装饰性的安全措辞无法解决这个问题。

通过保持执行稳定来减少提示次数，而不是扩大继承范围。正常代理始终使用同一镜像时，应只显示一张会话卡片。进程改变执行者时应显示卡片，因为它已经跨过了会话概念本来要守住的边界。

## 围绕真实的规避方式构建测试

应使用能够保留表面标识符的替换场景来测试授权边界。只用稳定二进制文件进行成功路径测试，找不到那些会把权限交给错误可执行文件的缺陷。

可以从这些场景开始：

- 已获批的二进制文件重新 exec 自身的精确副本。网关保留会话，并记录连续性事件。
- 二进制文件重新 exec 同一签名机构签名的不同可执行文件。用户批准新的运行之前，网关拒绝操作。
- 符号链接仍保留相同的路径字符串，但解析到不同的可执行文件。网关再次请求审批。
- 会话开始后，shell 脚本内容发生变化。下一次由脚本控制的请求不能复用之前的决定。
- 启动器启动子进程，子进程尝试执行 HTTP 或 SSH 操作。子进程需要自己的会话决定。

然后测试时序。让替换后的程序在 exec 之后立即发出操作请求。确认网关会拒绝请求或将其挂起等待，同时确认在审批结果返回前，凭据注入和 SSH 执行都没有开始。这样可以发现那些已经派发工作后才更新日志的实现。

在同一套测试中验证撤销。撤销一个已获批的运行，尝试进行同一镜像的重新 exec，并确认撤销状态优先。批准新的替换程序，再只撤销原始会话，确认两条记录的行为符合产品明确说明的控制范围。会话模型只有在边界场景中的表现与用户看到的描述一致时，才能赢得信任。

最后，像普通人一样检查审计输出。你应该能够沿着替换事件，将一次操作追溯到覆盖该可执行文件的审批卡片。如果必须关联 PID、根据时间戳猜测，或相信可能已经改变的路径，说明模型仍然存在漏洞。

进程替换正是需要严格把关的时刻。旧代码已经获得过决定，新代码需要自己的决定。
