228 lines
6.3 KiB
Go
228 lines
6.3 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 TestConfinementDarwinUsesBoundWorkingDirectory(t *testing.T) {
|
|
if runtime.GOOS != "darwin" {
|
|
t.Skip("Darwin-only confinement command")
|
|
}
|
|
workingDirectory := t.TempDir()
|
|
command, err := platformConfinementCommand(
|
|
context.Background(),
|
|
agenttask.ConfinementBinding{WorkingDir: workingDirectory},
|
|
confinementPolicy{WritableRoots: []string{workingDirectory}},
|
|
"/usr/bin/true",
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("platformConfinementCommand: %v", err)
|
|
}
|
|
if command.Dir != workingDirectory {
|
|
t.Fatalf("confined command directory = %q", command.Dir)
|
|
}
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
}
|