//go:build linux package host import ( "crypto/sha256" "encoding/hex" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/instancectl" "golang.org/x/sys/unix" ) func plan11LaunchFileID(t *testing.T, path string) hostbox.LaunchFile { t.Helper() var st unix.Stat_t if err := unix.Stat(path, &st); err != nil { t.Fatal(err) } return hostbox.LaunchFile{Device: uint64(st.Dev), Inode: st.Ino} } func TestClaimLaunchKernelVerifiesAnImmutableBuild(t *testing.T) { opt := filepath.Join(t.TempDir(), "opt") build := filepath.Join(opt, "versions", "v1", "app") if err := os.MkdirAll(filepath.Dir(build), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(build, []byte("app"), 0o555); err != nil { t.Fatal(err) } k := linuxClaimLaunchKernel{OptRoot: opt, CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc", Owner: uint32(os.Getuid())} want := plan11LaunchFileID(t, build) if err := k.Verify(build, want); err != nil { t.Fatal(err) } if err := k.Verify(build, hostbox.LaunchFile{Device: want.Device, Inode: want.Inode + 1}); err == nil { t.Fatal("other file identity accepted") } if err := (linuxClaimLaunchKernel{OptRoot: opt, Owner: uint32(os.Getuid()) + 1}).Verify(build, want); err == nil { t.Fatal("build not owned by root accepted") } link := filepath.Join(opt, "versions", "current") if err := os.Symlink("v1", link); err != nil { t.Fatal(err) } if err := k.Verify(filepath.Join(link, "app"), want); err == nil { t.Fatal("symlinked component accepted") } live := filepath.Join(opt, "live", "shop.example") if err := os.MkdirAll(filepath.Dir(live), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(live, []byte("app"), 0o555); err != nil { t.Fatal(err) } if err := k.Verify(live, plan11LaunchFileID(t, live)); err == nil { t.Fatal("retargetable live tree accepted") } if err := k.Verify(filepath.Join(t.TempDir(), "app"), want); err == nil { t.Fatal("build outside the release tree accepted") } if err := os.Chmod(build, 0o575); err != nil { t.Fatal(err) } if err := k.Verify(build, want); err == nil { t.Fatal("group-writable build accepted") } if err := os.Chmod(build, 0o555); err != nil { t.Fatal(err) } if err := os.Chmod(filepath.Dir(build), 0o777); err != nil { t.Fatal(err) } if err := k.Verify(build, want); err == nil { t.Fatal("build in a writable directory accepted") } if err := os.Chmod(filepath.Dir(build), 0o755); err != nil { t.Fatal(err) } if err := k.Verify(strings.Replace(build, "/v1/", "/v1/../v1/", 1), want); err == nil { t.Fatal("unclean path accepted") } } func TestClaimLaunchKernelObservesTheStartedProcess(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) } bound := "/bin/sleep" child := exec.Command(bound, "30") if err := child.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = child.Process.Kill(); _ = child.Wait() }) k := linuxClaimLaunchKernel{CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc", BoundApp: strings.TrimPrefix(bound, "/")} started := instancectl.ClaimUnitStarted{Unit: "u", InvocationID: "i", ControlGroup: rel, MainPID: os.Getpid()} got, err := k.Observe(started) if err != nil { t.Fatal(err) } if got.ProcessStart != procSelfStart(t) || got.Group != plan11LaunchFileID(t, filepath.Join("/sys/fs/cgroup", rel)) || got.Executable != plan11LaunchFileID(t, bound) || got.Running != got.Executable || got.TenantPID != child.Process.Pid || got.TenantStart == "" { t.Fatalf("observed=%+v", got) } started.ControlGroup = "carlos.slice/elsewhere.service" if _, err := k.Observe(started); err == nil { t.Fatal("process outside the unit group accepted") } } func TestPlan11Rev2RootHashesThePinnedExecutable(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "versions", "v1", "app") if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatal(err) } if err := os.Chmod(root, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("app"), 0555); err != nil { t.Fatal(err) } k := linuxClaimLaunchKernel{OptRoot: root, Owner: uint32(os.Getuid())} sum := sha256.Sum256([]byte("app")) digest := hex.EncodeToString(sum[:]) id := plan11LaunchFileID(t, path) if os.Getuid() != 0 { // The hash is taken only from a root-owned executable (#267), so an // unprivileged run can prove the refusal but not the acceptance. if err := k.VerifyDigest(t.Context(), path, id, digest); !errors.Is(err, hostbox.ErrChanged) { t.Fatalf("non-root-owned executable: err=%v, want ErrChanged", err) } t.Skip("accepting a pinned executable needs a root-owned fixture") } if err := k.VerifyDigest(t.Context(), path, id, digest); err != nil { t.Fatal(err) } if err := k.VerifyDigest(t.Context(), path, id, strings.Repeat("0", 64)); err == nil { t.Fatal("root accepted forged executable digest") } id.Inode++ if err := k.VerifyDigest(t.Context(), path, id, digest); err == nil { t.Fatal("root accepted another inode") } }