//go:build linux package instancectl import ( "context" "encoding/json" "net" "os" "path/filepath" "strings" "testing" ) func TestRev3ReservedUnitChecksAtTrampolineBoundary(t *testing.T) { for _, name := range []string{"valid", "expired during setup", "wrong peer", "wrong reservation", "replacement invocation", "main PID handed over"} { t.Run(name, func(t *testing.T) { u, ctl, in, _ := claimUnitFixture(t) dir, err := os.MkdirTemp("", "rg") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) u.ExeRoot = filepath.Join(dir, "exe") u.UnitRoot = filepath.Join(dir, "unit") u.DataRoot = filepath.Join(dir, "data") p := ClaimExecPermit{Reservation: "a:main", Inputs: "hash", BootID: "boot", SafeUntil: 200, Containment: 1} setup := false checks := 0 received := make(chan error, 1) original := u.Systemctl u.Systemctl = func(ctx context.Context, args ...string) (string, error) { out, err := original(ctx, args...) if name == "main PID handed over" && args[0] == "show" && strings.Contains(strings.Join(args, ","), "MainPID") { ctl.mu.Lock() ctl.pid = os.Getpid() ctl.mu.Unlock() } if args[0] != "start" || err != nil { return out, err } setup = true ctl.mu.Lock() ctl.pid = os.Getpid() if name == "wrong peer" || name == "main PID handed over" { ctl.pid++ } ctl.mu.Unlock() source, dropin, _ := u.launchPaths(in.Host) body, e := os.ReadFile(filepath.Join(dropin, "60-claim-launch.conf")) if e != nil || !strings.Contains(string(body), "BindReadOnlyPaths=/proc/sys/kernel/random/boot_id:"+ClaimExecBootID) || !strings.Contains(string(body), "instance-exec -claim-reservation="+ClaimExecSnapshot+" run %I\n") { t.Errorf("missing trusted trampoline binds/argument: %s %v", body, e) } go func() { c, e := net.Dial("unix", filepath.Join(source, "exec.sock")) if e != nil { received <- e return } defer c.Close() q := p if name == "wrong reservation" { q.Reservation = "other" } if e = json.NewEncoder(c).Encode(q); e == nil { var got ClaimExecPermit e = json.NewDecoder(c).Decode(&got) } received <- e }() return out, nil } l := Launch{Schema: LaunchSchema, Host: in.Host, Argv: []string{LiveDir + "/" + in.Host}} started, err := u.StartReserved(t.Context(), in, l, p, func() error { checks++ if setup && name == "expired during setup" { return ErrClaimUnit } if setup && name == "replacement invocation" { ctl.mu.Lock() ctl.invocation = "replacement" ctl.mu.Unlock() } return nil }) clientErr := <-received if name == "valid" || name == "main PID handed over" { if err != nil || clientErr != nil || checks != 2 || started.MainPID != os.Getpid() { t.Fatalf("start=%v gate=%v checks=%d", err, clientErr, checks) } source, _, _ := u.launchPaths(in.Host) if _, err := os.Lstat(filepath.Join(source, "exec.sock")); err != nil { t.Fatalf("ExecStopPost bind source removed early: %v", err) } _ = u.Stop(t.Context(), in.Host) if _, err := os.Lstat(filepath.Join(source, "exec.sock")); !os.IsNotExist(err) { t.Fatalf("stopped gate source retained: %v", err) } } else if err == nil || clientErr == nil { t.Fatalf("unsafe start=%v gate=%v", err, clientErr) } if !strings.Contains(ctl.joined(), "start ") { t.Fatal("did not reach asynchronous setup") } }) } }