carlos / platform Public

Supervise pinned maintenance subprocesses and strengthen launch conformance tests

Paul Campbell pushed by paul@keymail.dev 01b82bde37694e3eb1348b383a53491eaf9003c8 parent 387908f
6 files changed, +697 −16
  • internal/maintenance/launch.go +6 −3
  • internal/maintenance/launch_test.go +112 −13
  • internal/maintenance/testdata/generate-launch-vector.py +44 −0
  • internal/maintenanceexec/pipes_linux.go +33 −0
  • internal/maintenanceexec/run_linux.go +275 −0
  • internal/maintenanceexec/run_linux_test.go +227 −0
diff --git a/internal/maintenance/launch.go b/internal/maintenance/launch.go
index a0cde583c..edc48b0d9 100644
--- a/internal/maintenance/launch.go
+++ b/internal/maintenance/launch.go
@@ -194,7 +194,7 @@ func (c LaunchContext) valid() bool {
}
p := c.ClosedCheckpoint
completed, ok := canonicalStamp(p.CompletedAt)
- if !ok || !completed.Before(deadline) || p.Version != 1 || !maintenanceguard.ValidID(p.PreparationID) || p.MainLength <= 0 || p.MainLength > 268435456 || !maintenanceguard.ValidDigest(p.MainSHA256) || !maintenanceguard.ValidDigest(p.EvidenceSHA256) {
+ if !ok || completed.After(deadline) || p.Version != 1 || !maintenanceguard.ValidID(p.PreparationID) || p.MainLength <= 0 || p.MainLength > 268435456 || !maintenanceguard.ValidDigest(p.MainSHA256) || !maintenanceguard.ValidDigest(p.EvidenceSHA256) {
return false
}
return p.EvidenceSHA256 == c.checkpointDigest()
@@ -210,9 +210,12 @@ func (c LaunchContext) CheckpointDigest() (string, error) {
}
return c.checkpointDigest(), nil
}
-func (c LaunchContext) checkpointDigest() string {
+func (c LaunchContext) checkpointBytes() []byte {
p := c.ClosedCheckpoint
raw, _ := json.Marshal([]any{"carlos/closed-checkpoint/v1", c.Account, c.App, c.Host, c.OperationID, c.RequestID, c.AttemptID, c.RequestDigest, c.RecordIdentity, c.ArtifactDigest, c.EnvironmentEnvelopeDigest, c.GuardGeneration, c.InputSHA256, c.ExpectedStateSHA256, c.DatabasePath, c.DatabaseDevice, c.DatabaseInode, c.ExecutionDeadline, p.PreparationID, p.CompletedAt, p.MainLength, p.MainSHA256})
- sum := sha256.Sum256(raw)
+ return raw
+}
+func (c LaunchContext) checkpointDigest() string {
+ sum := sha256.Sum256(c.checkpointBytes())
return hex.EncodeToString(sum[:])
}
diff --git a/internal/maintenance/launch_test.go b/internal/maintenance/launch_test.go
index 55f156c3e..51faa7c22 100644
--- a/internal/maintenance/launch_test.go
+++ b/internal/maintenance/launch_test.go
@@ -2,6 +2,8 @@ package maintenance
import (
"bytes"
+ "crypto/sha256"
+ "encoding/hex"
"encoding/json"
"io"
"os"
@@ -41,6 +43,13 @@ func TestLaunchExactIntegersAndIndependentCheckpointVector(t *testing.T) {
if !bytes.Contains(binding, []byte(`18446744073709551615`)) || !bytes.Contains(binding, []byte(`\u003chome\u003e\u0026`)) {
t.Fatal("independent vector lacks precision/escaping cases")
}
+ if !bytes.Equal(binding, c.checkpointBytes()) {
+ t.Fatal("Go binding differs from independent Python vector")
+ }
+ digest := sha256.Sum256(binding)
+ if hex.EncodeToString(digest[:]) != c.ClosedCheckpoint.EvidenceSHA256 {
+ t.Fatal("independent vector digest mismatch")
+ }
c.Version = 1
c.ExecutionKind = ""
c.OperationID = ""
@@ -97,22 +106,42 @@ func TestLaunchCheckpointCannotBeTransplanted(t *testing.T) {
raw, _ := EncodeLaunch(c)
var fields map[string]json.RawMessage
json.Unmarshal(raw, &fields)
- for key, value := range fields {
+ for key := range fields {
if key == "closed_checkpoint" || key == "version" || key == "execution_kind" {
continue
}
t.Run(key, func(t *testing.T) {
var f map[string]json.RawMessage
json.Unmarshal(raw, &f)
- var str string
- if json.Unmarshal(value, &str) == nil {
- f[key], _ = json.Marshal(str + "x")
- } else {
- f[key] = json.RawMessage("1")
+ var value any
+ switch key {
+ case "account":
+ value = "abc"
+ case "app":
+ value = "docs"
+ case "host":
+ value = "other.bds.oncarlos.com"
+ case "request_id", "attempt_id", "operation_id":
+ value = strings.Repeat("A", 43)
+ case "database_path":
+ value = "/other/home.db"
+ case "execution_deadline":
+ value = "2026-09-11T21:00:00Z"
+ case "guard_generation", "database_device", "database_inode":
+ value = 2
+ default:
+ value = strings.Repeat("a", 64)
}
+ f[key], _ = json.Marshal(value)
bad, _ := json.Marshal(f)
if _, err := DecodeLaunch(bad); err == nil {
- t.Fatal("changed binding accepted")
+ t.Fatal("changed valid binding accepted with old evidence")
+ }
+ var changed LaunchContext
+ json.Unmarshal(bad, &changed)
+ changed.ClosedCheckpoint.EvidenceSHA256 = changed.checkpointDigest()
+ if _, err := EncodeLaunch(changed); err != nil {
+ t.Fatal("mutation was not valid independent of its evidence", err)
}
})
}
@@ -145,13 +174,12 @@ func TestLaunchSizeUTF8AndSurrogates(t *testing.T) {
t.Fatal("overflow bound", err, r.Len())
}
for _, path := range []string{`"/bad\ud800"`, `"/bad\udc00"`, `"/bad\ud800\u0001"`, `"/bad` + string([]byte{0xff}) + `"`} {
- var f map[string]json.RawMessage
- json.Unmarshal(raw, &f)
- f["database_path"] = json.RawMessage(path)
- bad, e := json.Marshal(f)
- if e != nil {
- continue
+ oldPath, _ := json.Marshal(c.DatabasePath)
+ bad := bytes.Replace(raw, oldPath, []byte(path), 1)
+ if bytes.Equal(bad, raw) {
+ t.Fatal("mutation missed path")
}
+
if _, err := DecodeLaunch(bad); err == nil {
t.Fatal("invalid string accepted")
}
@@ -173,3 +201,74 @@ func TestLaunchSizeUTF8AndSurrogates(t *testing.T) {
type errReader struct{}
func (errReader) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF }
+
+func TestNestedLaunchFieldsAreClosed(t *testing.T) {
+ raw, _ := launchFixture(t)
+ var top, fields map[string]json.RawMessage
+ json.Unmarshal(raw, &top)
+ original := top["closed_checkpoint"]
+ json.Unmarshal(original, &fields)
+ refuse := func(b json.RawMessage) {
+ t.Helper()
+ top["closed_checkpoint"] = b
+ bad, _ := json.Marshal(top)
+ if _, err := DecodeLaunch(bad); err == nil {
+ t.Fatal("ambiguous checkpoint accepted")
+ }
+ }
+ for key, value := range fields {
+ t.Run(key, func(t *testing.T) {
+ var f map[string]json.RawMessage
+ json.Unmarshal(original, &f)
+ delete(f, key)
+ bad, _ := json.Marshal(f)
+ refuse(bad)
+ f[strings.ToUpper(key)] = value
+ bad, _ = json.Marshal(f)
+ refuse(bad)
+ delete(f, strings.ToUpper(key))
+ f[key] = json.RawMessage("null")
+ bad, _ = json.Marshal(f)
+ refuse(bad)
+ prefix, _ := json.Marshal(key)
+ dup := append(append(append([]byte{'{'}, prefix...), ':'), value...)
+ dup = append(dup, ',')
+ dup = append(dup, bytes.TrimSpace(original)[1:]...)
+ refuse(dup)
+ })
+ }
+ var f map[string]json.RawMessage
+ json.Unmarshal(original, &f)
+ f["extra"] = json.RawMessage("1")
+ bad, _ := json.Marshal(f)
+ refuse(bad)
+}
+func TestLaunchNumericAndTimestampLexicalForms(t *testing.T) {
+ raw, c := launchFixture(t)
+ for _, key := range []string{"version", "guard_generation", "database_device", "database_inode"} {
+ for _, value := range []string{"0", "-0", "-1", "1.0", "1e0", "18446744073709551616"} {
+ var f map[string]json.RawMessage
+ json.Unmarshal(raw, &f)
+ f[key] = json.RawMessage(value)
+ bad, _ := json.Marshal(f)
+ if _, err := DecodeLaunch(bad); err == nil {
+ t.Fatal("noncanonical integer accepted", key, value)
+ }
+ }
+ }
+ for _, stamp := range []string{"2026-09-11T20:00:00+00:00", "2026-09-11T20:00:00.120Z", "2026-09-11t20:00:00Z", "2026-09-11T20:00:00z"} {
+ v := c
+ v.ExecutionDeadline = stamp
+ v.ClosedCheckpoint = &ClosedCheckpoint{}
+ *v.ClosedCheckpoint = *c.ClosedCheckpoint
+ v.ClosedCheckpoint.EvidenceSHA256 = v.checkpointDigest()
+ if _, err := EncodeLaunch(v); err == nil {
+ t.Fatal("noncanonical timestamp accepted", stamp)
+ }
+ }
+ c.ClosedCheckpoint.CompletedAt = c.ExecutionDeadline
+ c.ClosedCheckpoint.EvidenceSHA256 = c.checkpointDigest()
+ if _, err := EncodeLaunch(c); err != nil {
+ t.Fatal("equal completion/deadline refused", err)
+ }
+}
diff --git a/internal/maintenance/testdata/generate-launch-vector.py b/internal/maintenance/testdata/generate-launch-vector.py
new file mode 100644
index 000000000..81b4481d3
--- /dev/null
+++ b/internal/maintenance/testdata/generate-launch-vector.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+"""Generate the launch binding with Python integers, independent of Go code."""
+import base64
+import hashlib
+import json
+from pathlib import Path
+
+root = Path(__file__).parent
+identity = lambda n: base64.urlsafe_b64encode(bytes([n]) * 32).decode().rstrip('=')
+c = {
+ 'version': 2, 'account': 'bds', 'app': 'home',
+ 'host': 'overnight-home.bds.oncarlos.com',
+ 'request_id': identity(1), 'attempt_id': identity(2),
+ 'request_digest': '1' * 64, 'record_identity': '2' * 64,
+ 'artifact_digest': '3' * 64, 'environment_envelope_digest': '4' * 64,
+ 'guard_generation': 9223372036854775807, 'input_sha256': '5' * 64,
+ 'database_path': '/data/<home>&/☃.db',
+ 'database_device': 18446744073709551615, 'database_inode': 9007199254740993,
+ 'execution_deadline': '2026-09-11T20:00:00.123456789Z',
+ 'execution_kind': 'compatibility-check', 'operation_id': identity(3),
+ 'expected_state_sha256': '6' * 64,
+ 'closed_checkpoint': {
+ 'version': 1, 'preparation_id': identity(4),
+ 'completed_at': '2026-09-11T19:59:59.123456789Z',
+ 'main_length': 268435456, 'main_sha256': '7' * 64,
+ 'evidence_sha256': '',
+ },
+}
+p = c['closed_checkpoint']
+values = ['carlos/closed-checkpoint/v1'] + [c[k] for k in [
+ 'account', 'app', 'host', 'operation_id', 'request_id', 'attempt_id',
+ 'request_digest', 'record_identity', 'artifact_digest',
+ 'environment_envelope_digest', 'guard_generation', 'input_sha256',
+ 'expected_state_sha256', 'database_path', 'database_device',
+ 'database_inode', 'execution_deadline',
+]] + [p[k] for k in ['preparation_id', 'completed_at', 'main_length', 'main_sha256']]
+binding = json.dumps(values, ensure_ascii=False, separators=(',', ':'))
+for char, escaped in [('<', '\\u003c'), ('>', '\\u003e'), ('&', '\\u0026'),
+ ('\u2028', '\\u2028'), ('\u2029', '\\u2029')]:
+ binding = binding.replace(char, escaped)
+binding = binding.encode()
+p['evidence_sha256'] = hashlib.sha256(binding).hexdigest()
+(root / 'launch-v2.json').write_text(json.dumps(c, ensure_ascii=False, indent=2) + '\n')
+(root / 'launch-v2-binding.json').write_bytes(binding)
diff --git a/internal/maintenanceexec/pipes_linux.go b/internal/maintenanceexec/pipes_linux.go
new file mode 100644
index 000000000..2198881bf
--- /dev/null
+++ b/internal/maintenanceexec/pipes_linux.go
@@ -0,0 +1,33 @@
+//go:build linux
+
+package maintenanceexec
+
+import "os"
+
+type processPipes struct{ inR, inW, outR, outW, contextR, contextW *os.File }
+
+func newPipes() (p *processPipes, err error) {
+ p = &processPipes{}
+ defer func() {
+ if err != nil {
+ p.close()
+ }
+ }()
+ if p.inR, p.inW, err = os.Pipe(); err != nil {
+ return p, err
+ }
+ if p.outR, p.outW, err = os.Pipe(); err != nil {
+ return p, err
+ }
+ if p.contextR, p.contextW, err = os.Pipe(); err != nil {
+ return p, err
+ }
+ return p, nil
+}
+func (p *processPipes) close() {
+ for _, f := range []*os.File{p.inR, p.inW, p.outR, p.outW, p.contextR, p.contextW} {
+ if f != nil {
+ f.Close()
+ }
+ }
+}
diff --git a/internal/maintenanceexec/run_linux.go b/internal/maintenanceexec/run_linux.go
new file mode 100644
index 000000000..b37aa4e82
--- /dev/null
+++ b/internal/maintenanceexec/run_linux.go
@@ -0,0 +1,275 @@
+//go:build linux
+
+// Package maintenanceexec supplies process and pipe mechanics. Its result is
+// not an approval, database-compatibility receipt or permission to resume.
+package maintenanceexec
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "time"
+
+ "github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/lease"
+ "github.com/carlosframework/platform/internal/maintenance"
+ "golang.org/x/sys/unix"
+)
+
+var ErrRefused = errors.New("maintenance execution: refused")
+var ErrUncertain = errors.New("maintenance execution: process cleanup uncertain")
+
+const stopMargin = 2 * time.Second
+
+type containment interface {
+ Start(*exec.Cmd) error
+ KillAndEmpty(context.Context) error
+}
+type ownership interface {
+ SafeUntil() (time.Time, error)
+ Lost() <-chan struct{}
+}
+
+// Run requires concrete retained containment and lease handles. The caller
+// must already hold the lifecycle fence, checked approval and verified release
+// and environment snapshots. This package exposes no HTTP or CLI entry point.
+// Artifact, containment and lease ownership remain with the caller on return.
+func Run(ctx context.Context, group *cgroup.Prepared, held *lease.Held, artifact *os.File, launch maintenance.LaunchContext, input maintenance.Input, environment []string) ([]byte, error) {
+ if group == nil || held == nil {
+ return nil, ErrRefused
+ }
+ return run(ctx, group, held, artifact, launch, input, environment)
+}
+
+func run(ctx context.Context, group containment, held ownership, artifact *os.File, launch maintenance.LaunchContext, input maintenance.Input, environment []string) (result []byte, err error) {
+ if ctx == nil || ctx.Err() != nil || artifact == nil || launch.Version != 1 {
+ return nil, ErrRefused
+ }
+ raw, e := maintenance.EncodeLaunch(launch)
+ if e != nil {
+ return nil, ErrRefused
+ }
+ defer clear(raw)
+ frame, e := maintenance.EncodeInput(input)
+ if e != nil {
+ return nil, ErrRefused
+ }
+ clear(frame)
+ sum := sha256.Sum256(input.Stdin)
+ if hex.EncodeToString(sum[:]) != launch.InputSHA256 {
+ return nil, ErrRefused
+ }
+ executionDeadline, _ := time.Parse(time.RFC3339Nano, launch.ExecutionDeadline)
+ until, e := held.SafeUntil()
+ if e != nil {
+ return nil, ErrRefused
+ }
+ if !time.Now().Add(stopMargin).Before(until) || !time.Now().Add(stopMargin).Before(executionDeadline) {
+ return nil, ErrRefused
+ }
+ hashDeadline := until.Add(-stopMargin)
+ if executionDeadline.Add(-stopMargin).Before(hashDeadline) {
+ hashDeadline = executionDeadline.Add(-stopMargin)
+ }
+ hashCtx, cancelHash := context.WithDeadline(ctx, hashDeadline)
+ e = checkArtifact(hashCtx, artifact, launch.ArtifactDigest)
+ cancelHash()
+ if e != nil {
+ return nil, ErrRefused
+ }
+ pipes, e := newPipes()
+ if e != nil {
+ return nil, ErrRefused
+ }
+ defer pipes.close()
+ null, e := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
+ if e != nil {
+ return nil, ErrRefused
+ }
+ defer null.Close()
+ cmd := exec.Command("/proc/self/fd/4", input.Arguments...)
+ cmd.Env = append([]string{}, environment...)
+ cmd.Dir = "/"
+ cmd.Stdin = pipes.inR
+ cmd.Stdout = pipes.outW
+ cmd.Stderr = null
+ cmd.ExtraFiles = []*os.File{pipes.contextR, artifact}
+ if until, e = held.SafeUntil(); e != nil || ctx.Err() != nil || !time.Now().Add(stopMargin).Before(until) || !time.Now().Add(stopMargin).Before(executionDeadline) {
+ return nil, ErrRefused
+ }
+ if e = group.Start(cmd); e != nil {
+ stopCtx, cancel := context.WithDeadline(context.Background(), until)
+ stopErr := group.KillAndEmpty(stopCtx)
+ cancel()
+ if stopErr != nil {
+ return nil, ErrUncertain
+ }
+ return nil, ErrRefused
+ }
+ pipes.inR.Close()
+ pipes.outW.Close()
+ pipes.contextR.Close()
+ cleanupDone := false
+ defer func() {
+ if cleanupDone {
+ return
+ }
+ pipes.close()
+ stopCtx, cancel := context.WithDeadline(context.Background(), until)
+ defer cancel()
+ if group.KillAndEmpty(stopCtx) != nil {
+ err = ErrUncertain
+ result = nil
+ }
+ }()
+ events := make(chan pipeEvent, 4)
+ go writePipe(pipes.inW, append([]byte(nil), input.Stdin...), "input", events)
+ go writePipe(pipes.contextW, append([]byte(nil), raw...), "context", events)
+ go func() {
+ b, e := io.ReadAll(io.LimitReader(pipes.outR, maintenance.MaxResultBytes+1))
+ if len(b) > maintenance.MaxResultBytes {
+ clear(b)
+ b = nil
+ e = ErrRefused
+ }
+ events <- pipeEvent{"output", b, e}
+ }()
+ go func() { events <- pipeEvent{"wait", nil, cmd.Wait()} }()
+ tick := time.NewTicker(10 * time.Millisecond)
+ defer tick.Stop()
+ left := 4
+ failed := false
+ waited := false
+ for left > 0 {
+ select {
+ case event := <-events:
+ left--
+ if event.kind == "wait" {
+ waited = true
+ }
+ if event.err != nil {
+ failed = true
+ }
+ if event.kind == "output" {
+ result = event.data
+ }
+ case <-ctx.Done():
+ failed = true
+ case <-held.Lost():
+ failed = true
+ case <-tick.C:
+ next, e := held.SafeUntil()
+ if e != nil {
+ failed = true
+ } else {
+ until = next
+ }
+ now := time.Now()
+ if !now.Add(stopMargin).Before(until) || !now.Add(stopMargin).Before(executionDeadline) {
+ failed = true
+ }
+ }
+ if failed || waited {
+ stopCtx, cancel := context.WithDeadline(context.Background(), until)
+ e := group.KillAndEmpty(stopCtx)
+ cancel()
+ if e != nil {
+ clear(result)
+ return nil, ErrUncertain
+ }
+ cleanupDone = true
+ // Closing parent pipe endpoints also releases blocked I/O after the
+ // kernel has proved the child tree empty.
+ if failed {
+ pipes.close()
+ }
+ for left > 0 {
+ select {
+ case event := <-events:
+ left--
+ if event.err != nil {
+ failed = true
+ }
+ if event.kind == "output" {
+ result = event.data
+ }
+ case <-time.After(time.Until(until)):
+ clear(result)
+ return nil, ErrUncertain
+ }
+ }
+ break
+ }
+ }
+ if !cleanupDone {
+ stopCtx, cancel := context.WithDeadline(context.Background(), until)
+ e := group.KillAndEmpty(stopCtx)
+ cancel()
+ if e != nil {
+ clear(result)
+ return nil, ErrUncertain
+ }
+ cleanupDone = true
+ }
+ if _, e = held.SafeUntil(); e != nil || ctx.Err() != nil || !time.Now().Before(executionDeadline) {
+ failed = true
+ }
+ if failed || len(result) == 0 {
+ clear(result)
+ return nil, ErrRefused
+ }
+ return result, nil
+}
+
+type pipeEvent struct {
+ kind string
+ data []byte
+ err error
+}
+
+func writePipe(f *os.File, data []byte, kind string, events chan<- pipeEvent) {
+ defer clear(data)
+ _, err := f.Write(data)
+ closeErr := f.Close()
+ if err == nil {
+ err = closeErr
+ }
+ events <- pipeEvent{kind: kind, err: err}
+}
+func checkArtifact(ctx context.Context, f *os.File, want string) error {
+ flags, e := unix.FcntlInt(f.Fd(), unix.F_GETFL, 0)
+ if e != nil || flags&unix.O_ACCMODE != unix.O_RDONLY {
+ return ErrRefused
+ }
+ st, e := f.Stat()
+ if e != nil || !st.Mode().IsRegular() || st.Size() <= 0 {
+ return ErrRefused
+ }
+ hash := sha256.New()
+ reader := io.NewSectionReader(f, 0, st.Size())
+ buf := make([]byte, 64<<10)
+ for {
+ if ctx.Err() != nil {
+ return ErrRefused
+ }
+ n, e := reader.Read(buf)
+ if n > 0 {
+ hash.Write(buf[:n])
+ }
+ if e == io.EOF {
+ break
+ }
+ if e != nil {
+ return ErrRefused
+ }
+ }
+ after, e := f.Stat()
+ if e != nil || after.Size() != st.Size() || !os.SameFile(st, after) || hex.EncodeToString(hash.Sum(nil)) != want {
+ return ErrRefused
+ }
+ return nil
+}
diff --git a/internal/maintenanceexec/run_linux_test.go b/internal/maintenanceexec/run_linux_test.go
new file mode 100644
index 000000000..67ef4e60c
--- /dev/null
+++ b/internal/maintenanceexec/run_linux_test.go
@@ -0,0 +1,227 @@
+//go:build linux
+
+package maintenanceexec
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "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) KillAndEmpty(context.Context) error {
+ 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 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)
+ }
+ 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()
+ 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{"MAINTENANCE_EXEC_FIXTURE=success"})
+ if err != nil {
+ t.Fatal(err)
+ }
+ 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{"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 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{"MAINTENANCE_EXEC_FIXTURE=success"})
+ if err != ErrUncertain || len(out) != 0 {
+ t.Fatal("uncertain cleanup accepted", err)
+ }
+}