diff --git a/internal/tools/exec_backend_test.go b/internal/tools/exec_backend_test.go index 41006f3..9974ce7 100644 --- a/internal/tools/exec_backend_test.go +++ b/internal/tools/exec_backend_test.go @@ -13,6 +13,7 @@ import ( "os/exec" "path/filepath" "strings" + "syscall" "testing" "time" ) @@ -87,6 +88,11 @@ func requireBwrap(t *testing.T) { // sandboxRun 经 sandbox 后端执行一段 bash 脚本,返回合并输出与退出描述。 func sandboxRun(t *testing.T, script string, timeout time.Duration) (string, string) { + out, state, _ := sandboxRunWithPath(t, script, timeout) + return out, state +} + +func sandboxRunWithPath(t *testing.T, script string, timeout time.Duration) (string, string, string) { t.Helper() dir := t.TempDir() p := filepath.Join(dir, "s.sh") @@ -117,7 +123,7 @@ func sandboxRun(t *testing.T, script string, timeout time.Duration) (string, str if ctx.Err() == context.DeadlineExceeded { state = "exit=timeout" } - return out, state + return out, state, p } func TestSandboxHomeHidden(t *testing.T) { @@ -207,17 +213,84 @@ func TestSandboxForkBombContained(t *testing.T) { func TestSandboxOrphansDieWithNamespace(t *testing.T) { requireBwrap(t) - out, state := sandboxRun(t, ":(){ :|:& };:; echo gone", 30*time.Second) + out, state, scriptPath := sandboxRunWithPath(t, ":(){ :|:& };:; echo gone", 30*time.Second) if state != "exit=0" || !strings.Contains(out, "gone") { t.Fatalf("state=%s out=%q", state, out) } time.Sleep(500 * time.Millisecond) // 给孤儿进程留存活窗口 - hb, err := exec.Command("bash", "-c", "ps -eo args | grep -c '[s]\\.sh' || true").Output() + count, err := sandboxScriptProcessCount(scriptPath) if err != nil { t.Skipf("host ps probe failed: %v", err) } - if strings.TrimSpace(string(hb)) != "0" { - t.Fatalf("orphaned bomb survived outside the namespace: %s", hb) + if count != 0 { + t.Fatalf("sandbox script still has %d host processes: %s", count, scriptPath) + } +} + +func sandboxScriptProcessCount(scriptPath string) (int, error) { + out, err := exec.Command("ps", "-eo", "args").Output() + if err != nil { + return 0, err + } + count := 0 + for _, args := range strings.Split(string(out), "\n") { + if strings.Contains(args, scriptPath) { + count++ + } + } + return count, nil +} + +func TestSandboxOrphanProbeScopesScript(t *testing.T) { + dir := t.TempDir() + actual := filepath.Join(dir, "actual", "s.sh") + unrelated := filepath.Join(dir, "unrelated", "s.sh") + for _, path := range []string{actual, unrelated} { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("sleep 10 & wait\n"), 0o600); err != nil { + t.Fatal(err) + } + } + start := func(path string) { + proc := exec.Command("bash", path) + proc.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + if err := proc.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = syscall.Kill(-proc.Process.Pid, syscall.SIGKILL); _ = proc.Wait() }) + } + start(unrelated) + for deadline := time.Now().Add(time.Second); ; { + count, err := sandboxScriptProcessCount(unrelated) + if err != nil { + t.Fatal(err) + } + if count > 0 { + break + } + if time.Now().After(deadline) { + t.Fatal("unrelated control script did not start") + } + time.Sleep(time.Millisecond) + } + if count, err := sandboxScriptProcessCount(actual); err != nil || count != 0 { + t.Fatalf("unrelated process counted as this sandbox script: count=%d err=%v", count, err) + } + start(actual) + for deadline := time.Now().Add(time.Second); ; { + count, err := sandboxScriptProcessCount(actual) + if err != nil { + t.Fatal(err) + } + if count > 0 { + break + } + if time.Now().After(deadline) { + t.Fatal("own sandbox script was not detected") + } + time.Sleep(time.Millisecond) } }