mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
fix(proc): 关闭时等 stage 收尾,修每次 shutdown 必现的 SIGSEGV
连续两次部署都被验证门拦下(「近 2 分钟出现 5 条 panic/fatal」)。
查出这不是新版本崩了,而是**旧进程关闭时**必崩 —— 每次 shutdown 都触发:
fatal error: fault
[signal SIGSEGV: segmentation violation ...]
proc.(*Segment).descOffset
proc.(*Segment).setDesc
proc.(*Segment).writeLocal.func1 shmcodec.go:104
proc.(*Segment).writeLocal
proc.(*Segment).WriteAll
proc.(*stageCoordinator).enter host.go:310
proc.(*Host).beginStage
proc.(*coreHandler).runStage
agent/core.(*StageHost).RunStage
## 根因:Close 只处理了「事件环」一路写者,漏了「stage 数据面」
Host.Close 里那段长注释把事件环写得非常清楚(订阅 handler 会
ring.WritePush 写共享内存,unmap 后必炸,而 recover 捕不到 runtime
fatal),也**确实**在 freeShm 前退订了。
但共享段有**两条独立写者**。beginStage 的首进者会持 stageMu 调
Segment.WriteAll 写同一块段 —— 它**不在事件环订阅里**,退订阅根本管不到它。
于是 Close 只等问题 A 就 freeShm,问题 B 继续写已 unmap 的内存。
## 修法:用 stageMu 作为「没有 stage 在写段」的判据
stageMu 的既有语义已经就是我们要等的东西(首进者取、最后离开者放,
见 beginStage/endStage 注释),不必再造一个会与它失去同步的并行状态。
`Close` 现在 unmap 前独占获取它。
另加 `closing atomic.Bool`:光「等正在进行的 stage」还不够 —— Close
释放 stageMu 之后仍可能有新 stage 进场(agent 收尾路径),那时它照写已
释放的段。beginStage 现在看到一个明确的错误,而不是撞 SIGSEGV。
## 为何这件事必须修,而不只是「已知缺陷」
它让部署验证门失效:门报的「5 条 panic/fatal」分不清是新版本坏了,
还是旧版本关闭时的既有噪声。我这两次都手工核对了 PID 才敢继续 ——
下一个部署的人不该被迫做这种考古。修掉之后,这个门才真的能拦回归。
## 验证(判据可杀变异,逐个实测)
- `TestHost_CloseWaitsForInflightStage`:stage 持锁时 Close 必须阻塞。
删掉 `closing.Store` + `stageMu.Lock` → FAIL「未等 stage 收尾就返回」
- `TestHost_BeginStageAfterCloseRejected`:Close 后 beginStage 必须报错。
把检查改成 `if false` → **测试进程真的 SIGSEGV 被杀**(与生产同栈)
- `go test -race ./internal/plugin/proc/` 通过
- 全仓 `go test ./... -count=1` → 48 包 ok / 0 FAIL
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package proc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@ -51,6 +52,11 @@ type Host struct {
|
||||
coordMu sync.Mutex
|
||||
coord *stageCoordinator
|
||||
sup *Supervisor
|
||||
|
||||
// closing 置真后 beginStage 直接拒绝新 stage(Close 与新 stage 进场的赛跑:
|
||||
// unmap 后若又进来一个 stage,它照样会去写已释放的段)。
|
||||
// 用 atomic 是为了让 beginStage 无需额外加锁就能读到。
|
||||
closing atomic.Bool
|
||||
}
|
||||
|
||||
// NewHost 创建共享段(平台层 allocShm + 布局初始化)。
|
||||
@ -164,13 +170,40 @@ func (h *Host) Close() error {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
// ★ 等正在进行的 stage 收尾(2026-10-08 修 shutdown use-after-free)。
|
||||
//
|
||||
// 上面的注释只处理了**事件环**一路写者,但漏了 **stage 数据面**:
|
||||
//
|
||||
// 上面的注释只处理了**事件环**一路写者,但漏了 **stage 数据面**:
|
||||
// beginStage 的首进者会在 stageMu 保护下调 Segment.WriteAll(写共享段),
|
||||
// 而这条路径**不在**事件环订阅里,退订阅根本不影响它。
|
||||
//
|
||||
// 实测后果:每次关闭都 SIGSEGV(栈:stageCoordinator.enter →
|
||||
// Segment.WriteAll → writeLocal → descOffset),
|
||||
// 即首次进入的新进程日志里必然带一份“fatal error: fault”。
|
||||
// 这还使部署验证门形同虚设——分不清“新版本崩了”还是“旧版本关闭时崩”。
|
||||
//
|
||||
// 为何用 stageMu 而不是另加 done 通道:stageMu 的语义已经是
|
||||
// 「本轮 stage 独占共享段」(首进者取、最后离开者放,见 beginStage/
|
||||
// endStage)。取到它就意味着“此刻没有 stage 在写段”,正是我们要等的东西,
|
||||
// 不必再造一个会与它失去同步的并行状态。
|
||||
//
|
||||
// 锁序:stageMu 是这组锁的最后一级(beginStage 是 coordMu → stageMu,
|
||||
// endStage 不取锁),此处独占获取不会成环。
|
||||
// 注意 StageHost 的 handler 已由 StopAll/上层停过,不会无限持锁。
|
||||
h.closing.Store(true) // 先挡住新 stage,再等旧 stage 退出
|
||||
h.stageMu.Lock()
|
||||
|
||||
var firstErr error
|
||||
if h.data != nil {
|
||||
// 先让段不可再写(置 nil 后 segdata 已失效),
|
||||
// 再真正 unmap——完成写入的 stage 已在上面等完。
|
||||
if err := freeShm(h.memfd, h.data); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
h.data, h.memfd, h.unified = nil, nil, nil
|
||||
h.data, h.memfd, h.unified, h.seg = nil, nil, nil, nil
|
||||
}
|
||||
h.stageMu.Unlock()
|
||||
if h.evtfd != nil {
|
||||
h.evtfd.Close()
|
||||
h.evtfd = nil
|
||||
@ -192,6 +225,11 @@ func (h *Host) Close() error {
|
||||
//
|
||||
// 锁序:stageMu → coordMu。endStage 只解锁 stageMu、不获取,所以无环。
|
||||
func (h *Host) beginStage(sc *pubsdk.StageContext) (*stageCoordinator, error) {
|
||||
// Close 已开始:共享段即将/已被 unmap,此时再写段就是 use-after-free
|
||||
// (runtime fatal,recover 捕不到)。直接拒绝,让上层得到明确错误。
|
||||
if h.closing.Load() {
|
||||
return nil, errors.New("proc: 内核正在关闭,拒绝新 stage")
|
||||
}
|
||||
h.coordMu.Lock()
|
||||
if h.coord == nil {
|
||||
// 首个进入者:独占共享段直到本次 stage 全部插件离开。
|
||||
|
||||
Reference in New Issue
Block a user