ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

Potpie Daemon 信号终止权限收敛:ADR-0012 如何限制强制终止仅作用于直接子进程 Potpie Daemon 信号终止权限收敛ADR-0012 如何限制强制终止仅作用于直接子进程【免费下载链接】potpieContext Graph for AI Native SDLC项目地址: https://gitcode.com/GitHub_Trending/po/potpie导读本文围绕 Potpie 的架构决策记录 ADR-0012Restrict 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 分配给了另一个进程那么发现流程缺失、无效或无法认证missing, invalid, or unauthenticated discovery控制器仍然依据残留 PID 调用os.kill(pid, signal.SIGTERM)甚至SIGKILL信号实际送达的是一个与本守护进程毫无关系的同用户进程。这与已经接受的守护进程需求相冲突PID 只是诊断元数据而不是 daemon 实例身份对应 conformance 中的DAEMON-012PID remains diagnostic过期发现必须安全失败DAEMON-013Stale 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 无法认证、失败或超时时不得发送操作系统终止信号。具体落到三条规则attach 不授权信号DaemonController.attach()从运行时记录重建控制器时_owns_process被置为False该控制器对进程只有“观察”权没有“终止”权无法安全停止就返回类型化错误返回ResourceLifecycleError不声称守护进程已停止no stopped claim并保留运行时记录清理受身份约束除非清理被“精确期望的 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_processstart()通过asyncio.create_subprocess_exec直接创建子进程后立即设置self._owns_process Truecontroller.py中start()路径attach()重建控制器时明确设置self._owns_process False其 docstring 也点明这是“观察早前 CLI 调用启动的守护进程而非外部 supervisor 集成”见 controller.py。stop()中的关键分叉逻辑已简化为要点完整实现在 controller.pyif isinstance(requested, Success): try: await asyncio.wait_for(process.wait(), timeoutself._stop_timeout_s) # ... return StopResult(modetyped_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_posturesaferecommended_next_actioninspect 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 报告readyTrue但 shutdown 不可用时stop()返回StopResult(modeterminated, exit_code-int(signal.SIGTERM))test_attached_controller_refuses_signal_fallback参数化readyFalse/True先attach()再stop()断言结果为Failure、错误码为daemon_attached_shutdown_unavailable、retry_posture safe并且关键断言process.terminate_calls 0、process.kill_calls 0、process.wait_calls 0——即 attach 控制器在认证不可用时对进程句柄零信号、零等待。身份约束的清理remove_daemon_runtime_records的双重匹配运行时记录的清理位于 potpie/daemon/discovery.py 的remove_daemon_runtime_records()。清理只删除三类 canonical 记录discovery.json、daemon.credential、daemon.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_id与pid同时等于调用方期望值时才执行删除。这正好落实了 DAEMON-056identity-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_pidpid)在持锁状态下执行身份约束的删除。这样一次并发的 stop 无法擦除正在进行的 replacement boot 的记录凭证发布与所有清理都“串行化通过运行时所有权锁”即 DAEMON-054 与 DAEMON-055 的实现基础。行为约束落地DAEMON-052 至 DAEMON-056daemon 模块规格 中新增的五个行为 ID 与 ADR-0012 一一对应行为 ID约束内容依据 spec/modules/daemon.mdDAEMON-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保留纯 PID 有界终止限时等待只能缩短危险窗口不能建立目标身份PID 复用仍可能终止无关的同用户进程认证一次后对记录 PID 发信号认证只证明了某个瞬间的端点无法把后续信号绑定到同一个 OS 进程化身——认证与发信号之间守护进程可能退出、PID 可能被复用持久化跨平台进程化身令牌Linux pidfd 或进程启动时间start time理论上支持更强的重挂载但可移植的身份契约与恢复策略需要单独决策P1 修复不依赖引入该机制匹配可执行名或命令行名称与命令行既不稳定、也无法作为不可伪造的进程身份同样无法关闭 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 handle—passedDAEMON-053Attached shutdown failures return typed errors without signalling or a stopped claim—passedDAEMON-054Canonical runtime-record publication occurs while the boot owns the runtime lock—passedDAEMON-055Runtime-record removal is serialized through the ownership lock—passedDAEMON-056Identity-bound cleanup matches the exact expected PID and per-boot instance—passed。验证证据包括 pinned source reviewD2-E1、完整测试根道D2-E2uv run pytest tests -m not premerge_journey -q1447 passed、架构清单D2-E3以及聚焦协议/安全/生命周期车道D2-E5含 controller、runtime client/codec/transport、detached-daemon E2E、ownership-race 等测试。DAEMON-039与DAEMON-049因“无兼容适配器、无备选运行时”而被标记为 verified-not-applicable。小结ADR-0012 是 Potpie 在“本地权威边界local authority boundary”上的一次重要安全收敛它把操作系统信号的授权从“记录中的数字 PID”收窄到“当前控制器直接创建的进程句柄”用_owns_process、RuntimeOwnershipLock与 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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进