//go:build linux package maintenanceexec import ( "context" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "io" "os" "os/exec" "path/filepath" "reflect" "strconv" "strings" "sync" "syscall" "testing" "time" "github.com/carlosframework/platform/internal/maintenance" "golang.org/x/sys/unix" ) // This fixture exercises actual exec and pipe behavior. Its process-group // cleanup is a test substitute, not native cgroup containment evidence. type fixtureGroup struct { cmd *exec.Cmd starts, kills int startErr, killErr error before func() } func (g *fixtureGroup) Start(c *exec.Cmd) error { g.starts++ g.cmd = c if g.before != nil { g.before() } if g.startErr != nil { return g.startErr } c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} return c.Start() } func (g *fixtureGroup) Populated() (bool, error) { if g.cmd == nil || g.cmd.Process == nil { return false, nil } entries, err := os.ReadDir("/proc") if err != nil { return false, err } for _, entry := range entries { if _, err := strconv.Atoi(entry.Name()); err != nil { continue } raw, err := os.ReadFile("/proc/" + entry.Name() + "/stat") if err != nil { continue } fields := strings.Fields(string(raw)[strings.LastIndexByte(string(raw), ')')+1:]) if len(fields) >= 3 && fields[2] == strconv.Itoa(g.cmd.Process.Pid) && fields[0] != "Z" { return true, nil } } return false, nil } func (g *fixtureGroup) KillAndEmpty(ctx context.Context) error { if ctx.Err() != nil { return ctx.Err() } g.kills++ if g.cmd != nil && g.cmd.Process != nil { unix.Kill(-g.cmd.Process.Pid, unix.SIGKILL) } return g.killErr } type fixtureLease struct { mu sync.Mutex until time.Time lost chan struct{} err error } func (l *fixtureLease) SafeUntil() (time.Time, error) { l.mu.Lock() defer l.mu.Unlock() return l.until, l.err } func (l *fixtureLease) Lost() <-chan struct{} { return l.lost } func (l *fixtureLease) lose() { l.mu.Lock(); defer l.mu.Unlock(); l.err = ErrRefused; close(l.lost) } func TestChildFixture(t *testing.T) { mode := os.Getenv("MAINTENANCE_EXEC_FIXTURE") if mode == "" { return } if len(os.Environ()) != 2 || os.Getenv("GODEBUG") != "containermaxprocs=0,updatemaxprocs=0" { os.Exit(15) } if cwd, err := os.Getwd(); err != nil || cwd != "/" { os.Exit(16) } stderr, err := os.Stderr.Stat() null, nullErr := os.Stat(os.DevNull) if err != nil || nullErr != nil || !os.SameFile(stderr, null) { os.Exit(17) } entries, err := os.ReadDir("/proc/self/fd") if err != nil { os.Exit(18) } for _, entry := range entries { fd, err := strconv.Atoi(entry.Name()) if err != nil || fd <= 4 { continue } target, err := os.Readlink("/proc/self/fd/" + entry.Name()) if errors.Is(err, os.ErrNotExist) || strings.HasPrefix(target, "anon_inode:") { continue } os.Exit(19) } if mode == "blocked" { time.Sleep(time.Minute) return } if mode == "overflow" { os.Stdout.Write([]byte(strings.Repeat("x", maintenance.MaxResultBytes+1))) time.Sleep(time.Minute) return } if mode == "nonzero" { os.Exit(7) } if mode == "descendant" { child := exec.Command("/proc/self/exe", "-test.run=^TestChildFixture$") child.Env = []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=blocked"} child.Dir = "/" child.Stdout = os.Stdout if child.Start() != nil { os.Exit(14) } } c, err := maintenance.ReadLaunch(os.NewFile(3, "context")) if err != nil { os.Exit(8) } pin := os.NewFile(4, "artifact") info, err := pin.Stat() if err != nil { os.Exit(9) } self, err := os.Stat("/proc/self/exe") if err != nil || !os.SameFile(info, self) { os.Exit(10) } flags, err := unix.FcntlInt(pin.Fd(), unix.F_GETFL, 0) if err != nil || flags&unix.O_ACCMODE != unix.O_RDONLY { os.Exit(11) } raw, err := io.ReadAll(os.Stdin) if err != nil { os.Exit(12) } sum := sha256.Sum256(raw) if hex.EncodeToString(sum[:]) != c.InputSHA256 { os.Exit(13) } json.NewEncoder(os.Stdout).Encode(map[string]any{"input_bytes": len(raw), "bound_image": true}) os.Exit(0) } func executionFixture(t *testing.T) (*os.File, maintenance.LaunchContext, maintenance.Input, *fixtureGroup, *fixtureLease) { t.Helper() t.Setenv("MAINTENANCE_PARENT_ONLY", "must-not-reach-child") src, err := os.Open("/proc/self/exe") if err != nil { t.Fatal(err) } defer src.Close() path := filepath.Join(t.TempDir(), "image") dst, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0700) if err != nil { t.Fatal(err) } hash := sha256.New() if _, err := io.Copy(io.MultiWriter(dst, hash), src); err != nil { t.Fatal(err) } if err := dst.Close(); err != nil { t.Fatal(err) } pin, err := os.Open(path) if err != nil { t.Fatal(err) } t.Cleanup(func() { pin.Close() }) input := maintenance.Input{Arguments: []string{"-test.run=^TestChildFixture$"}, Stdin: []byte(`{"test":"` + strings.Repeat("a", 60000) + `"}`)} sum := sha256.Sum256(input.Stdin) id := base64.RawURLEncoding.EncodeToString(make([]byte, 32)) digest := strings.Repeat("a", 64) c := maintenance.LaunchContext{Version: 1, Account: "bds", App: "home", Host: "home.bds.oncarlos.com", RequestID: id, AttemptID: id, RequestDigest: digest, RecordIdentity: digest, ArtifactDigest: hex.EncodeToString(hash.Sum(nil)), EnvironmentEnvelopeDigest: digest, GuardGeneration: 1, InputSHA256: hex.EncodeToString(sum[:]), DatabasePath: "/synthetic/home.db", DatabaseDevice: 1, DatabaseInode: 2, ExecutionDeadline: time.Now().Add(8 * time.Second).UTC().Format(time.RFC3339Nano)} return pin, c, input, &fixtureGroup{}, &fixtureLease{until: time.Now().Add(10 * time.Second), lost: make(chan struct{})} } func TestPinnedImageAndExactPrivateInput(t *testing.T) { pin, c, input, g, l := executionFixture(t) g.before = func() { if err := os.Rename(pin.Name(), pin.Name()+".old"); err != nil { t.Fatal(err) } if err := os.WriteFile(pin.Name(), []byte("not an executable"), 0700); err != nil { t.Fatal(err) } } out, err := run(t.Context(), g, l, pin, c, input, []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=success"}) if err != nil { t.Fatal(err, g.cmd.ProcessState) } var result struct { InputBytes int `json:"input_bytes"` BoundImage bool `json:"bound_image"` } if json.Unmarshal(out, &result) != nil || result.InputBytes != len(input.Stdin) || !result.BoundImage { t.Fatal("wrong child result") } if g.starts != 1 || g.kills != 1 { t.Fatal("wrong containment lifecycle", g.starts, g.kills) } } func TestBlockedIOOverflowFailureAndLeaseLoss(t *testing.T) { for _, mode := range []string{"blocked", "overflow", "nonzero", "lease-loss"} { t.Run(mode, func(t *testing.T) { pin, c, input, g, l := executionFixture(t) ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) defer cancel() fixtureMode := mode if mode == "lease-loss" { fixtureMode = "blocked" g.before = func() { go func() { time.Sleep(50 * time.Millisecond); l.lose() }() } } started := time.Now() out, err := run(ctx, g, l, pin, c, input, []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=" + fixtureMode}) if err == nil || len(out) != 0 || g.starts != 1 || g.kills == 0 { t.Fatal("failure became success", err, g.starts, g.kills) } if time.Since(started) > 3*time.Second { t.Fatal("supervisor blocked") } }) } } func TestDescendantHoldingOutputPipe(t *testing.T) { pin, c, input, g, l := executionFixture(t) ctx, cancel := context.WithTimeout(t.Context(), 4*time.Second) defer cancel() started := time.Now() out, err := run(ctx, g, l, pin, c, input, []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=descendant"}) if err != ErrRefused || len(out) != 0 || g.kills != 1 { t.Fatal("surviving descendant was accepted", err, g.kills) } if time.Since(started) > 3*time.Second { t.Fatal("waited for descendant instead of terminating it") } } func TestExpiredLeaseStillTerminatesProcess(t *testing.T) { pin, c, input, g, l := executionFixture(t) l.until = time.Now().Add(2200 * time.Millisecond) g.before = func() { time.Sleep(time.Until(l.until) + 10*time.Millisecond) } out, err := run(t.Context(), g, l, pin, c, input, []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=blocked"}) if err != ErrUncertain || len(out) != 0 || g.kills == 0 { t.Fatal("expired lease skipped termination", err, g.kills) } } func descriptorSnapshot(t *testing.T) map[string]string { t.Helper() entries, err := os.ReadDir("/proc/self/fd") if err != nil { t.Fatal(err) } out := map[string]string{} for _, entry := range entries { target, err := os.Readlink("/proc/self/fd/" + entry.Name()) if errors.Is(err, os.ErrNotExist) || strings.HasPrefix(target, "anon_inode:") { continue } if err != nil { t.Fatal(err) } out[entry.Name()] = target } return out } func TestParentDescriptorsClose(t *testing.T) { for _, mode := range []string{"success", "blocked", "overflow", "nonzero", "descendant"} { t.Run(mode, func(t *testing.T) { pin, c, input, g, l := executionFixture(t) before := descriptorSnapshot(t) ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() _, err := run(ctx, g, l, pin, c, input, []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=" + mode}) if (err == nil) != (mode == "success") { t.Fatal("wrong result", err) } if !reflect.DeepEqual(before, descriptorSnapshot(t)) { t.Fatal("parent descriptor set changed after execution") } }) } } func TestInputRemainsBoundAfterPreflight(t *testing.T) { pin, c, input, g, l := executionFixture(t) g.before = func() { input.Stdin[10] = 'b' } out, err := run(t.Context(), g, l, pin, c, input, []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=success"}) if err != nil || len(out) == 0 { t.Fatal("child received input changed after validation", err) } } func TestPreflightRefusesWithoutSpawn(t *testing.T) { for _, mode := range []string{"input", "artifact", "deadline", "checker", "lease"} { t.Run(mode, func(t *testing.T) { pin, c, input, g, l := executionFixture(t) switch mode { case "input": input.Stdin = []byte(`{}`) case "artifact": c.ArtifactDigest = strings.Repeat("b", 64) case "deadline": c.ExecutionDeadline = time.Now().Add(-time.Second).UTC().Format(time.RFC3339Nano) case "checker": c.Version = 2 case "lease": l.lose() } if _, err := run(t.Context(), g, l, pin, c, input, nil); err == nil || g.starts != 0 { t.Fatal("invalid launch spawned") } }) } } func TestFailedStartAndUncertainCleanup(t *testing.T) { pin, c, input, g, l := executionFixture(t) g.startErr = errors.New("synthetic private start detail") if _, err := run(t.Context(), g, l, pin, c, input, nil); err != ErrRefused || g.starts != 1 || g.kills != 1 { t.Fatal("start failure lost cleanup", err) } pin, c, input, g, l = executionFixture(t) g.killErr = errors.New("synthetic private cleanup detail") out, err := run(t.Context(), g, l, pin, c, input, []string{"GODEBUG=containermaxprocs=0,updatemaxprocs=0", "MAINTENANCE_EXEC_FIXTURE=success"}) if err != ErrUncertain || len(out) != 0 { t.Fatal("uncertain cleanup accepted", err) } }