독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
208 lines
5.8 KiB
Go
208 lines
5.8 KiB
Go
package agentworkspace
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/go/agenttask"
|
|
)
|
|
|
|
func TestConfinementPlatformFailsWithTypedUnavailableError(t *testing.T) {
|
|
identity, err := platformConfinementRevision()
|
|
if err != nil {
|
|
if !errors.Is(err, ErrConfinementUnavailable) {
|
|
t.Fatalf("platform error = %v, want ErrConfinementUnavailable", err)
|
|
}
|
|
return
|
|
}
|
|
if identity == "" {
|
|
t.Fatal("available confinement returned an empty platform identity")
|
|
}
|
|
}
|
|
|
|
func TestConfinementLinuxUsesMetadataCompleteMountNamespacePolicy(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("Linux-only confinement policy")
|
|
}
|
|
identity, err := platformConfinementRevision()
|
|
if err != nil {
|
|
if errors.Is(err, ErrConfinementUnavailable) {
|
|
return
|
|
}
|
|
t.Fatalf("platform confinement revision: %v", err)
|
|
}
|
|
if !strings.Contains(identity, "mount-namespace-metadata") || strings.Contains(identity, "landlock") {
|
|
t.Fatalf("Linux confinement identity = %q, want metadata-complete mount namespace policy", identity)
|
|
}
|
|
}
|
|
|
|
func TestConfinementProofRejectsTamperedBinding(t *testing.T) {
|
|
baseRoot, localRoot := newWorkspaceFixture(t)
|
|
writeFile(t, filepath.Join(baseRoot, "input.txt"), "base\n", 0o600)
|
|
backend := newTestBackend(t, localRoot, baseRoot)
|
|
prepared, err := backend.Prepare(
|
|
context.Background(),
|
|
testIsolationRequest("task", "task#1"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Prepare: %v", err)
|
|
}
|
|
proof, ok := prepared.Confinement.(*ConfinementProof)
|
|
if !ok {
|
|
t.Fatalf("confinement proof type = %T", prepared.Confinement)
|
|
}
|
|
expected := proof.Binding()
|
|
expected.ProfileRevision = "profile-r2"
|
|
if err := proof.Validate(expected); err == nil {
|
|
t.Fatal("profile rebinding passed confinement validation")
|
|
}
|
|
|
|
tampered := *proof
|
|
tampered.binding = proof.Binding()
|
|
tampered.binding.BaseRoot = filepath.Dir(baseRoot)
|
|
if _, err := tampered.Start(
|
|
context.Background(),
|
|
agenttask.ConfinementCommand{Name: "true"},
|
|
); err == nil {
|
|
t.Fatal("tampered proof produced a child command")
|
|
}
|
|
}
|
|
|
|
func TestConfinementStartCreatesProofOwnedPipes(t *testing.T) {
|
|
started, err := startConfinementCommand(exec.Command(
|
|
"sh",
|
|
"-c",
|
|
`read line; printf 'stdout:%s\n' "$line"; printf 'stderr:%s\n' "$line" >&2`,
|
|
))
|
|
if err != nil {
|
|
t.Fatalf("startConfinementCommand: %v", err)
|
|
}
|
|
if started.Child() == nil ||
|
|
started.Stdin() == nil ||
|
|
started.Stdout() == nil ||
|
|
started.Stderr() == nil {
|
|
t.Fatalf("started handle is incomplete: %#v", started)
|
|
}
|
|
type readResult struct {
|
|
content string
|
|
err error
|
|
}
|
|
stdoutResult := make(chan readResult, 1)
|
|
stderrResult := make(chan readResult, 1)
|
|
go func() {
|
|
content, err := io.ReadAll(started.Stdout())
|
|
stdoutResult <- readResult{content: string(content), err: err}
|
|
}()
|
|
go func() {
|
|
content, err := io.ReadAll(started.Stderr())
|
|
stderrResult <- readResult{content: string(content), err: err}
|
|
}()
|
|
if _, err := io.WriteString(started.Stdin(), "owned\n"); err != nil {
|
|
t.Fatalf("write proof-owned stdin: %v", err)
|
|
}
|
|
if err := started.Stdin().Close(); err != nil {
|
|
t.Fatalf("close proof-owned stdin: %v", err)
|
|
}
|
|
if err := started.Child().Wait(); err != nil {
|
|
t.Fatalf("wait confined child: %v", err)
|
|
}
|
|
stdout := <-stdoutResult
|
|
stderr := <-stderrResult
|
|
if stdout.err != nil || stdout.content != "stdout:owned\n" {
|
|
t.Fatalf("stdout = %q, %v", stdout.content, stdout.err)
|
|
}
|
|
if stderr.err != nil || stderr.content != "stderr:owned\n" {
|
|
t.Fatalf("stderr = %q, %v", stderr.content, stderr.err)
|
|
}
|
|
if err := started.Abort(); err != nil {
|
|
t.Fatalf("cleanup completed handle: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConfinementStartCleansPipeAndProcessFailures(t *testing.T) {
|
|
t.Run("pipe setup", func(t *testing.T) {
|
|
var opened []*os.File
|
|
calls := 0
|
|
factory := func() (*os.File, *os.File, error) {
|
|
calls++
|
|
if calls == 3 {
|
|
return nil, nil, errors.New("injected pipe failure")
|
|
}
|
|
reader, writer, err := os.Pipe()
|
|
if err == nil {
|
|
opened = append(opened, reader, writer)
|
|
}
|
|
return reader, writer, err
|
|
}
|
|
if _, err := startConfinementCommandWithPipes(
|
|
exec.Command("true"),
|
|
factory,
|
|
); err == nil || !strings.Contains(err.Error(), "stderr pipe") {
|
|
t.Fatalf("pipe setup error = %v", err)
|
|
}
|
|
assertClosedFiles(t, opened)
|
|
})
|
|
|
|
t.Run("process start", func(t *testing.T) {
|
|
var opened []*os.File
|
|
factory := func() (*os.File, *os.File, error) {
|
|
reader, writer, err := os.Pipe()
|
|
if err == nil {
|
|
opened = append(opened, reader, writer)
|
|
}
|
|
return reader, writer, err
|
|
}
|
|
missing := filepath.Join(t.TempDir(), "missing-command")
|
|
if _, err := startConfinementCommandWithPipes(
|
|
exec.Command(missing),
|
|
factory,
|
|
); err == nil || !strings.Contains(err.Error(), "start confined child") {
|
|
t.Fatalf("process start error = %v", err)
|
|
}
|
|
assertClosedFiles(t, opened)
|
|
})
|
|
}
|
|
|
|
func TestConfinementAbortClosesPipesAndReapsChild(t *testing.T) {
|
|
started, err := startConfinementCommand(exec.Command("sleep", "5"))
|
|
if err != nil {
|
|
t.Fatalf("startConfinementCommand: %v", err)
|
|
}
|
|
stdin := started.Stdin()
|
|
stdout := started.Stdout()
|
|
stderr := started.Stderr()
|
|
if err := started.Abort(); err != nil {
|
|
t.Fatalf("Abort: %v", err)
|
|
}
|
|
if started.Child().ProcessState == nil {
|
|
t.Fatal("Abort did not reap the child")
|
|
}
|
|
if _, err := stdin.Write([]byte("leak")); err == nil {
|
|
t.Fatal("Abort left stdin open")
|
|
}
|
|
if _, err := stdout.Read(make([]byte, 1)); err == nil {
|
|
t.Fatal("Abort left stdout open")
|
|
}
|
|
if _, err := stderr.Read(make([]byte, 1)); err == nil {
|
|
t.Fatal("Abort left stderr open")
|
|
}
|
|
if err := started.Abort(); err != nil {
|
|
t.Fatalf("second Abort: %v", err)
|
|
}
|
|
}
|
|
|
|
func assertClosedFiles(t *testing.T, files []*os.File) {
|
|
t.Helper()
|
|
for _, file := range files {
|
|
if _, err := file.Stat(); err == nil {
|
|
t.Fatalf("pipe endpoint %d remained open", file.Fd())
|
|
}
|
|
}
|
|
}
|