news 2026/9/17 20:56:06

Potpie Daemon 信号终止权限收敛:ADR-0012 如何限制强制终止仅作用于直接子进程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Potpie Daemon 信号终止权限收敛:ADR-0012 如何限制强制终止仅作用于直接子进程

Potpie Daemon 信号终止权限收敛:ADR-0012 如何限制强制终止仅作用于直接子进程

【免费下载链接】potpieContext Graph for AI Native SDLC项目地址: https://gitcode.com/GitHub_Trending/po/potpie

导读

本文围绕 Potpie 的架构决策记录 ADR-0012(Restrict Forceful Daemon Termination To Owned Children)展开,解析该决策如何堵住一条因 PID 复用引发的“误杀无关进程”安全漏洞:此前 CLI 在守护进程无法通过认证时,允许依据daemon.pid中记录的数值 PID 发送SIGTERM/SIGKILL,而 PID 会被操作系统复用,导致信号可能落到无关进程上。读完本文你将掌握:Potpie 本地守护进程(daemon)的终止权限模型、直接子进程与“重挂载(attach)”进程的边界、RuntimeOwnershipLock与运行时记录的串行化清理机制,以及 DAEMON-052 至 DAEMON-056 五条新行为约束的源码级实现与测试验证。

背景:ADR-0009 留下的“有界操作系统终止”回退通道

Potpie 的守护进程架构建立在 ADR-0009 定义的“类型化本地运行时执行契约”之上。该契约规定:

  • DaemonController直接创建并观察一个前台子进程,不委托给操作系统级 supervisor;
  • 正常停止必须走“认证的类型化 daemon-control 操作”(即daemon.shutdown类型的控制通道);
  • 但如果“无法认证任何存活端点”,控制器允许使用有界操作系统终止(先SIGTERM,超时后SIGKILL),然后再清理过期运行时记录。

问题恰恰出在这个回退通道上。ADR-0009 允许回退的意图是处理“守护进程已死但记录残留”或“端点无法认证”的边界场景,但它隐含了一个危险假设:daemon.pid文件中的数值 PID 足以标识守护进程实例

问题本质:PID 是诊断元数据,不是实例身份

后来的 CLI 调用会从daemon.pid中读出数字 PID,并据此重建一个进程句柄(_RecordedDaemonProcess)。如果守护进程已经崩溃退出,而操作系统恰好把该 PID 分配给了另一个进程,那么:

  1. 发现流程缺失、无效或无法认证(missing, invalid, or unauthenticated discovery);
  2. 控制器仍然依据残留 PID 调用os.kill(pid, signal.SIGTERM),甚至SIGKILL
  3. 信号实际送达的是一个与本守护进程毫无关系的同用户进程

这与已经接受的守护进程需求相冲突:

  • PID 只是诊断元数据,而不是 daemon 实例身份(对应 conformance 中的DAEMON-012:PID remains diagnostic);
  • 过期发现必须安全失败DAEMON-013:Stale discovery recovers safely);
  • 仅凭“owner-only 运行时文件”只能证明文件的控制权,不能证明持有被复用 PID 的进程就是原来的守护进程

这是一个“本地同用户进程终止”漏洞(local same-user process-termination vulnerability),正是 SPEC-CHANGE-0012 中change_type: security的由来。

决策核心:强制终止仅限“直接拥有的子进程”

ADR-0012 给出的裁决非常明确(见 ADR-0012):

有界操作系统终止仅允许通过“由当前控制器直接创建的前台子进程句柄”执行。由后续 CLI 调用重建出来的控制器,只使用认证的类型化 shutdown;当类型化 shutdown 无法认证、失败或超时时,不得发送操作系统终止信号

具体落到三条规则:

  1. attach 不授权信号DaemonController.attach()从运行时记录重建控制器时,_owns_process被置为False,该控制器对进程只有“观察”权,没有“终止”权;
  2. 无法安全停止就返回类型化错误:返回ResourceLifecycleError不声称守护进程已停止(no stopped claim),并保留运行时记录;
  3. 清理受身份约束:除非清理被“精确期望的 daemon 实例身份 + PID”双重守卫,否则不得删除记录;凭证发布与清理全部串行化在运行时所有权锁(RuntimeOwnershipLock)之后。

同时,该决策只取代 ADR-0009 中“无法认证端点时允许有界 OS 终止”这一条,不改变:直接子进程的 readiness 清理、认证类型化 shutdown、daemon 传输、bearer 认证、发现 schema 与正常重启行为。

源码级实现:DaemonController的所有权与信号边界

核心实现在 potpie/runtime/controller.py 的DaemonController类中。类内部通过_owns_process字段区分“本控制器创建的子进程”与“从记录重挂载的进程”:

@property def owns_process(self) -> bool: """Whether this controller created the currently observed process.""" return self._owns_process
  • start()通过asyncio.create_subprocess_exec直接创建子进程后立即设置self._owns_process = Truecontroller.pystart()路径);
  • attach()重建控制器时明确设置self._owns_process = False,其 docstring 也点明这是“观察早前 CLI 调用启动的守护进程,而非外部 supervisor 集成”(见 controller.py)。

stop()中的关键分叉逻辑(已简化为要点,完整实现在 controller.py):

if isinstance(requested, Success): try: await asyncio.wait_for(process.wait(), timeout=self._stop_timeout_s) # ... return StopResult(mode="typed_shutdown", ...) except TimeoutError: if not self._owns_process: return self._attached_stop_failure(...) # 不发送信号 result = await self._bounded_terminate(process) # 仅直接子进程可走此路 else: if not self._owns_process: return self._attached_stop_failure(...) # 认证不可用也不发送信号 result = await self._bounded_terminate(process)

也就是说,_bounded_terminateSIGTERM→ 超时 →SIGKILL只可能被直接拥有的子进程路径触达;而 attach 路径在“shutdown 超时”(daemon_attached_shutdown_timeout)或“shutdown 不可用”(daemon_attached_shutdown_unavailable)时一律返回Failure[ResourceLifecycleError],并携带:

  • retry_posture="safe"
  • recommended_next_action="inspect daemon status and runtime records before manual recovery"
  • 错误 details 中的pid以及可选的cause_category/cause_code

测试如何锁定该边界

test_daemon_controller.py 中有两个成对出现的测试,分别验证“直接子进程允许有界终止”和“attach 进程拒绝信号回退”:

  • test_controller_falls_back_to_bounded_signal_termination:直接启动的子进程,在 observer 报告ready=True但 shutdown 不可用时,stop()返回StopResult(mode="terminated", exit_code=-int(signal.SIGTERM))
  • test_attached_controller_refuses_signal_fallback(参数化ready=False/True):先attach()stop(),断言结果为Failure、错误码为daemon_attached_shutdown_unavailableretry_posture == "safe",并且关键断言process.terminate_calls == 0process.kill_calls == 0process.wait_calls == 0——即 attach 控制器在认证不可用时对进程句柄零信号、零等待。

身份约束的清理:remove_daemon_runtime_records的双重匹配

运行时记录的清理位于 potpie/daemon/discovery.py 的remove_daemon_runtime_records()。清理只删除三类 canonical 记录(discovery.jsondaemon.credentialdaemon.pid)以及 UDS 端点文件,且必须先通过双重身份匹配:

if expected_instance_id is not None and ( discovery is None or discovery.instance_id != expected_instance_id ): return if expected_pid is not None and ( discovery is None or discovery.pid != expected_pid ): return

只有当前discovery.json中的instance_idpid同时等于调用方期望值时,才执行删除。这正好落实了 DAEMON-056:identity-bound cleanup 必须精确匹配期望 PID 与 per-boot 实例身份,杜绝“检查后删除”竞态(check-then-unlink race)误删替换启动(replacement boot)的记录。

所有权锁:发布与清理的串行化

凭证发布与记录清理全部经由 potpie/runtime/ownership.py 的RuntimeOwnershipLock串行化。该锁是跨平台的 OS 级排他锁:POSIX 上使用fcntl.flock(LOCK_EX | LOCK_NB),Windows 上回退到msvcrt.locking(LK_NBLCK);锁文件路径要求绝对路径,父目录chmod 0o700、锁文件chmod 0o600

在 potpie/daemon/lifecycle.py 中可以看到它的两处典型用法:

  • Daemon.start()在启动前先_cleanup_runtime_records()抢锁,抢不到(daemon_ownership_conflict)则拒绝启动,防止两个 boot 并发写入记录;
  • Daemon.stop()在类型化 shutdown 成功后才调用_cleanup_runtime_records_under_lock(expected_instance_id=..., expected_pid=pid),在持锁状态下执行身份约束的删除。

这样,一次并发的 stop 无法擦除正在进行的 replacement boot 的记录,凭证发布与所有清理都“串行化通过运行时所有权锁”,即 DAEMON-054 与 DAEMON-055 的实现基础。

行为约束落地:DAEMON-052 至 DAEMON-056

daemon 模块规格 中新增的五个行为 ID 与 ADR-0012 一一对应:

行为 ID约束内容(依据 spec/modules/daemon.md)
DAEMON-052控制器只能通过自己直接创建的前台子进程句柄发送 OS 终止信号;禁止对从运行时记录 attach 的进程使用信号回退
DAEMON-053attach 进程的类型化 shutdown 无法认证 / 失败 / 超时时,控制器必须返回ResourceLifecycleError,不得发送信号,也不得声称已停止
DAEMON-054canonical PID 记录、discovery 文档与 per-boot 凭证的发布,必须仅在对应 boot 持有运行时所有权锁时进行
DAEMON-055任何删除 canonical PID / discovery / credential / owned UDS 端点的操作,必须持有或获取运行时所有权锁
DAEMON-056针对已知 daemon 身份的清理,仅当 discovery 文档同时匹配精确期望 PID 与精确期望 per-boot 实例身份时才允许删除

备选方案:为什么最终没有采用

ADR-0012 明确评审了四种替代方案,并给出了否决理由(见 ADR-0012 的 Alternatives Considered):

  1. 保留纯 PID 有界终止:限时等待只能缩短危险窗口,不能建立目标身份,PID 复用仍可能终止无关的同用户进程;
  2. 认证一次后对记录 PID 发信号:认证只证明了某个瞬间的端点,无法把后续信号绑定到同一个 OS 进程化身——认证与发信号之间守护进程可能退出、PID 可能被复用;
  3. 持久化跨平台进程化身令牌:Linux pidfd 或进程启动时间(start time)理论上支持更强的重挂载,但可移植的身份契约与恢复策略需要单独决策,P1 修复不依赖引入该机制;
  4. 匹配可执行名或命令行:名称与命令行既不稳定、也无法作为不可伪造的进程身份,同样无法关闭 PID 复用竞态。

因此该决策刻意选择了“fail closed”:宁可让无响应的守护进程需要人工恢复,也不冒险向无法证明身份的 PID 发信号。未来若引入独立的进程化身机制(process-incarnation mechanism),再另行决策。

后果与用户可见影响

ADR-0012 的后果清单(Consequences)可以归纳为对三类场景的影响:

  • 过期或被复用的 PID 记录:不再能授权SIGTERM/SIGKILL(漏洞关闭);
  • 健康守护进程:跨 CLI 调用仍通过认证的类型化daemon.shutdown正常停止,无行为回归;
  • 直接拥有的子进程:readiness 失败或类型化 shutdown 未完成时,仍可接收有界终止(terminated/killed模式);
  • 无响应的 attach 守护进程:现在失败关闭stop返回ResourceLifecycleError与非零 CLI 退出码,不会报告“已停止”,并建议人工恢复(manual recovery);
  • 清理竞态:未经身份验证的过期控制器无法删除 replacement boot 的记录。

这些语义变化通过 SPEC-CHANGE-0012 以规格变更记录的形式落地,将SPEC-DAEMON从 revision 1 推进到 revision 2。

验证与一致性:conformance 记录

daemon conformance 记录 在 PR-review 修复实现上验证了 DAEMON-001 至 DAEMON-056,其中:

  • DAEMON-052OS signals remain limited to a directly owned child process handlepassed
  • DAEMON-053Attached shutdown failures return typed errors without signalling or a stopped claimpassed
  • DAEMON-054Canonical runtime-record publication occurs while the boot owns the runtime lockpassed
  • DAEMON-055Runtime-record removal is serialized through the ownership lockpassed
  • DAEMON-056Identity-bound cleanup matches the exact expected PID and per-boot instancepassed

验证证据包括 pinned source review(D2-E1)、完整测试根道(D2-E2:uv run pytest tests -m "not premerge_journey" -q,1447 passed)、架构清单(D2-E3)以及聚焦协议/安全/生命周期车道(D2-E5,含 controller、runtime client/codec/transport、detached-daemon E2E、ownership-race 等测试)。DAEMON-039DAEMON-049因“无兼容适配器、无备选运行时”而被标记为 verified-not-applicable。

小结

ADR-0012 是 Potpie 在“本地权威边界(local authority boundary)”上的一次重要安全收敛:它把操作系统信号的授权从“记录中的数字 PID”收窄到“当前控制器直接创建的进程句柄”,用_owns_processRuntimeOwnershipLock与 PID + instance 双重身份匹配三重机制,从决策、规格、实现、测试到 conformance 记录形成完整闭环。对于理解 Potpie 守护进程生命周期(lifecycle.py)与 CLI 控制面(ADR-0009、daemon 模块规格)的读者而言,这条决策链展示了“类型化 shutdown 为主、有界信号为直接子进程保留的例外”这一清晰的权限模型。

【免费下载链接】potpieContext Graph for AI Native SDLC项目地址: https://gitcode.com/GitHub_Trending/po/potpie

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 20:51:18

ESXi虚拟机OVF/OVA导出导入工程实践指南

1. 项目概述:为什么ESXi上的虚拟机导出导入不是“点几下鼠标”那么简单在vSphere生态里,“导出虚拟机”和“导入虚拟机”这两个动作,表面看只是vSphere Client界面上几个按钮——文件 → 导出为OVF模板、文件 → 部署OVF模板。但真正做过生产…

作者头像 李华
网站建设 2026/9/17 20:46:56

智能运维AIOps平台建设:从数据底座到告警降噪与根因分析

简介:《人工智能智能运维平台建设综合解决方案》PPT 是一份面向企业IT运维团队、解决方案架构师及技术决策者的体系化方案。内容聚焦如何通过人工智能、大数据分布式处理与机器学习实现业务系统的实时监控、预测性维护和主动式风险预警,帮助企业挖掘海量…

作者头像 李华
网站建设 2026/9/17 20:46:07

Hive与HBase整合实战:外部表映射、读优化与HFile批量写入

上周有个做风控的朋友在群里问我:他们线上把用户行为明细全量写在 HBase 里,现在业务方要一张按天、按渠道的漏斗报表,是不是只能先把数据导出到 HDFS,再灌进 Hive 跑离线任务?我的答复是:不用绕这一圈&…

作者头像 李华