//go:build linux package operatorlaunch import ( "bytes" "context" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "io" "os" "os/exec" "path/filepath" "strings" "testing" "time" "amadan.net/carlos/platform.git/launchctx" "golang.org/x/sys/unix" ) func TestStartupChild(t *testing.T) { if os.Getenv("HOME_STARTUP_FIXTURE") != "1" { return } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() c, raw, err := ReadOperator(ctx, os.Getenv("HOME_STARTUP_DATABASE")) closed := true for _, fd := range []uintptr{0, 3, 4} { var st unix.Stat_t flags, e := unix.FcntlInt(fd, unix.F_GETFL, 0) if e != nil || flags&unix.O_ACCMODE != unix.O_RDONLY || unix.Fstat(int(fd), &st) != nil || st.Mode&unix.S_IFMT != unix.S_IFCHR || unix.Major(st.Rdev) != 1 || unix.Minor(st.Rdev) != 3 { closed = false } } if err != nil && (err != ErrRefused || len(raw) != 0 || c.Version != 0) { os.Exit(3) } if err == nil { file, e := os.Open(os.Getenv("HOME_STARTUP_DATABASE")) if e != nil || file.Fd() <= 4 { os.Exit(4) } if os.Stdin.Close() != nil { os.Exit(5) } if _, e := file.Stat(); e != nil || file.Close() != nil { os.Exit(6) } } json.NewEncoder(os.Stdout).Encode(map[string]any{"accepted": err == nil, "closed": closed, "input_bytes": len(raw)}) clear(raw) os.Exit(0) } func TestStartupTransportInSubprocess(t *testing.T) { image, e := os.ReadFile("/proc/self/exe") if e != nil { t.Fatal(e) } imageSum := sha256.Sum256(image) for _, mode := range []string{"valid", "runtime-default", "context", "context-file", "context-overflow", "input", "input-file", "input-overflow", "selector", "artifact-hash", "artifact-identity", "artifact-flags", "artifact-path", "expired", "checker", "stalled-context", "stalled-input"} { t.Run(mode, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "image") if e := os.WriteFile(path, image, 0700); e != nil { t.Fatal(e) } database := filepath.Join(dir, "home.db") marker := []byte("untouched database sentinel") if e := os.WriteFile(database, marker, 0600); e != nil { t.Fatal(e) } input := []byte(`{"private":"` + strings.Repeat("x", 60000) + `"}`) inputSum := sha256.Sum256(input) id := base64.RawURLEncoding.EncodeToString(make([]byte, 32)) digest := strings.Repeat("a", 64) c := launchctx.LaunchContext{Version: 1, Account: "bds", App: "home", Host: "home.example", RequestID: id, AttemptID: id, RequestDigest: digest, RecordIdentity: digest, ArtifactDigest: hex.EncodeToString(imageSum[:]), EnvironmentEnvelopeDigest: digest, GuardGeneration: 1, InputSHA256: hex.EncodeToString(inputSum[:]), DatabasePath: database, DatabaseDevice: 1, DatabaseInode: 1, ExecutionDeadline: time.Now().Add(10 * time.Second).UTC().Format(time.RFC3339Nano)} selector := database artifactPath, artifactFlags := path, os.O_RDONLY switch mode { case "input": input[20] = 'y' case "input-overflow": input = bytes.Repeat([]byte("x"), inputLimit+1) case "selector": selector += ".other" case "artifact-hash": c.ArtifactDigest = digest case "artifact-path": artifactFlags = unix.O_PATH case "artifact-identity", "artifact-flags": artifactPath = filepath.Join(dir, "other") if e := os.WriteFile(artifactPath, image, 0600); e != nil { t.Fatal(e) } if mode == "artifact-flags" { artifactFlags = os.O_RDWR } case "expired": c.ExecutionDeadline = time.Now().Add(-time.Second).UTC().Format(time.RFC3339Nano) case "checker": c.Version = 2 c.ExecutionKind = "compatibility-check" c.OperationID = base64.RawURLEncoding.EncodeToString(bytes.Repeat([]byte{1}, 32)) c.ExpectedStateSHA256 = digest c.ClosedCheckpoint = &launchctx.ClosedCheckpoint{Version: 1, PreparationID: id, CompletedAt: time.Now().UTC().Format(time.RFC3339Nano), MainLength: 1, MainSHA256: digest} c.ClosedCheckpoint.EvidenceSHA256, e = c.CheckpointDigest() if e != nil { t.Fatal(e) } } raw, e := launchctx.EncodeLaunch(c) if e != nil { t.Fatal(e) } switch mode { case "context": raw = []byte(`{"version":1}`) case "context-overflow": raw = bytes.Repeat([]byte(" "), launchctx.MaxLaunchBytes+1) } pin, e := os.OpenFile(artifactPath, artifactFlags, 0) if e != nil { t.Fatal(e) } defer pin.Close() inR, inW, e := os.Pipe() if e != nil { t.Fatal(e) } defer inR.Close() defer inW.Close() contextR, contextW, e := os.Pipe() if e != nil { t.Fatal(e) } defer contextR.Close() defer contextW.Close() ctx, cancel := context.WithTimeout(t.Context(), 8*time.Second) defer cancel() cmd := exec.CommandContext(ctx, path, "-test.run=^TestStartupChild$") cmd.Dir = "/" // Keep the fixture's descriptor inventory independent of runtime CPU detection. cmd.Env = []string{"HOME_STARTUP_FIXTURE=1", "HOME_STARTUP_DATABASE=" + selector, "GODEBUG=containermaxprocs=0,updatemaxprocs=0"} if mode == "runtime-default" { cmd.Env = cmd.Env[:2] } cmd.Stdin = inR cmd.ExtraFiles = []*os.File{contextR, pin} if mode == "input-file" || mode == "context-file" { content := input if mode == "context-file" { content = raw } filePath := filepath.Join(dir, "invalid-transport") if e := os.WriteFile(filePath, content, 0600); e != nil { t.Fatal(e) } file, e := os.Open(filePath) if e != nil { t.Fatal(e) } defer file.Close() if mode == "input-file" { cmd.Stdin = file } else { cmd.ExtraFiles[0] = file } } var output bytes.Buffer cmd.Stdout = &output if e := cmd.Start(); e != nil { t.Fatal(e) } inR.Close() contextR.Close() writes := make(chan struct{}, 2) for _, item := range []struct { fd *os.File raw []byte stall bool }{{inW, input, mode == "stalled-input"}, {contextW, raw, mode == "stalled-context"}} { go func() { if !item.stall { io.Copy(item.fd, bytes.NewReader(item.raw)) item.fd.Close() } writes <- struct{}{} }() } if e := cmd.Wait(); e != nil { t.Fatal("startup child failed", e) } <-writes <-writes var result struct { Accepted bool `json:"accepted"` Closed bool `json:"closed"` InputBytes int `json:"input_bytes"` } wantAccepted := mode == "valid" || mode == "runtime-default" if json.Unmarshal(output.Bytes(), &result) != nil || !result.Closed || result.Accepted != wantAccepted { t.Fatal("wrong startup result", output.String()) } if wantAccepted && result.InputBytes != len(input) { t.Fatal("private input changed") } unchanged, e := os.ReadFile(database) if e != nil || !bytes.Equal(unchanged, marker) { t.Fatal("startup touched database") } }) } }