//go:build linux package host import ( "errors" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/instancectl" ) func TestPlan11Rev3ObservesRealInitTenant(t *testing.T) { raw, err := os.ReadFile("/proc/self/cgroup") if err != nil || !strings.HasPrefix(string(raw), "0::/") { t.Skip("no unified cgroup membership") } rel := strings.TrimSpace(strings.TrimPrefix(strings.SplitN(string(raw), "\n", 2)[0], "0::/")) if _, err := os.Stat(filepath.Join("/sys/fs/cgroup", rel)); err != nil { t.Skip("cgroup tree not visible:", err) } binary := filepath.Join(t.TempDir(), "instanceexec.test") build := exec.Command("go", "test", "-c", "-o", binary, "../instanceexec") if out, err := build.CombinedOutput(); err != nil { t.Fatalf("build init helper: %v\n%s", err, out) } cmd := exec.Command(binary, "-test.run=^TestPlan11Rev3InitObserveHelper$") cmd.Env = append(os.Environ(), "CARLOS_REV3_OBSERVE_HELPER=1") if err := cmd.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = cmd.Process.Signal(syscall.SIGTERM); _ = cmd.Wait() }) deadline := time.Now().Add(5 * time.Second) for { tasks, err := filepath.Glob(filepath.Join("/proc", strconv.Itoa(cmd.Process.Pid), "task", "*", "children")) if err != nil { t.Fatal(err) } found := false for _, path := range tasks { raw, _ := os.ReadFile(path) for _, child := range strings.Fields(string(raw)) { if st, err := os.Stat(filepath.Join("/proc", child, "exe")); err == nil { want, err := os.Stat("/bin/sleep") if err == nil && os.SameFile(st, want) { found = true } } } } if found { break } if time.Now().After(deadline) { t.Fatal("real init did not fork tenant") } time.Sleep(time.Millisecond) } k := linuxClaimLaunchKernel{CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc", BoundApp: "bin/sleep"} started := instancectl.ClaimUnitStarted{Unit: "u", InvocationID: "i", ControlGroup: rel, MainPID: cmd.Process.Pid} got, err := k.Observe(started) if err != nil { t.Fatal(err) } want := plan11LaunchFileID(t, "/bin/sleep") if got.Running != want || got.Executable != want { t.Fatalf("observed trampoline instead of tenant: got=%+v tenant=%+v", got, want) } mainStart, err := claimLaunchProcessStart("/proc", cmd.Process.Pid) if err != nil || got.ProcessStart != mainStart { t.Fatalf("main PID lifetime lost: got=%+v main=%s err=%v", got, mainStart, err) } } func rev3ChildFixture(t *testing.T) (linuxClaimLaunchKernel, instancectl.ClaimUnitStarted, func(string, string)) { t.Helper() root := t.TempDir() write := func(path, value string) { t.Helper() path = filepath.Join(root, path) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(value), 0644); err != nil { t.Fatal(err) } } stat := func(pid, ppid, start string) string { return pid + " (app) S " + ppid + strings.Repeat(" 0", 17) + " " + start } write("proc/100/stat", stat("100", "1", "main-start")) write("proc/200/stat", stat("200", "100", "child-start")) write("proc/100/cgroup", "0::/unit\n") write("proc/200/cgroup", "0::/unit\n") write("proc/100/task/100/children", "") write("proc/100/task/101/children", "200") write("proc/100/root/opt/carlos/app", "tenant") if err := os.MkdirAll(filepath.Join(root, "proc/200/root/opt/carlos"), 0755); err != nil { t.Fatal(err) } if err := os.Link(filepath.Join(root, "proc/100/root/opt/carlos/app"), filepath.Join(root, "proc/200/root/opt/carlos/app")); err != nil { t.Fatal(err) } if err := os.Link(filepath.Join(root, "proc/100/root/opt/carlos/app"), filepath.Join(root, "proc/200/exe")); err != nil { t.Fatal(err) } write("proc/100/exe", "trampoline") if err := os.MkdirAll(filepath.Join(root, "cgroup/unit"), 0755); err != nil { t.Fatal(err) } return linuxClaimLaunchKernel{ProcRoot: filepath.Join(root, "proc"), CgroupRoot: filepath.Join(root, "cgroup")}, instancectl.ClaimUnitStarted{MainPID: 100, ControlGroup: "unit"}, write } func TestPlan11Rev3ChildScope(t *testing.T) { for _, name := range []string{"valid", "other parent", "other cgroup", "ambiguous", "no tenant", "dead tenant", "other namespace bind"} { t.Run(name, func(t *testing.T) { k, s, write := rev3ChildFixture(t) switch name { case "other parent": write("proc/200/stat", "200 (app) S 99"+strings.Repeat(" 0", 17)+" child-start") case "other cgroup": write("proc/200/cgroup", "0::/other\n") case "ambiguous": write("proc/100/task/100/children", "300") case "no tenant": write("proc/100/task/101/children", "") case "dead tenant": write("proc/200/stat", "200 (app) Z 100"+strings.Repeat(" 0", 17)+" child-start") case "other namespace bind": if err := os.Remove(filepath.Join(k.ProcRoot, "200/root/opt/carlos/app")); err != nil { t.Fatal(err) } write("proc/200/root/opt/carlos/app", "other") } got, err := k.Observe(s) if name == "valid" { if err != nil || got.TenantPID != 200 || got.TenantStart != "child-start" || got.ProcessStart != "main-start" || got.Running != got.Executable { t.Fatalf("observation=%+v err=%v", got, err) } } else if name == "no tenant" { if !errors.Is(err, errClaimLaunchPending) || got.ProcessStart != "main-start" || got.TenantPID != 0 { t.Fatalf("pending=%+v err=%v", got, err) } } else if err == nil { t.Fatalf("unsafe child accepted: %+v", got) } }) } } func TestPlan11Rev3PinsTenantReadiness(t *testing.T) { for _, name := range []string{"stable", "changed pid", "reused pid", "disappeared"} { t.Run(name, func(t *testing.T) { f := newLaunchFixture(t, servingOp()) calls := 0 f.kernel.observe = func(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) { calls++ got := f.kernel.observed got.TenantPID, got.TenantStart = 4243, "child-start" if calls == 1 { return got, errClaimLaunchPending } switch name { case "changed pid": got.TenantPID++ case "reused pid": got.TenantStart = "other-start" case "disappeared": got.TenantPID, got.TenantStart = 0, "" } return got, nil } got, err := f.root.Serve(t.Context(), f.peer, f.req) if name == "stable" { if err != nil || got.Grant == nil || got.Grant.PID != 4242 || got.Grant.ProcessStart != "ps-4242" { t.Fatalf("main supervision identity changed: %+v %v", got, err) } } else if !errors.Is(err, hostbox.ErrChanged) { t.Fatalf("replacement child granted: %+v %v", got, err) } }) } }