//go:build linux package instancectl import ( "context" "errors" "net" "os" "path/filepath" "testing" "time" ) func claimFilesystemUnits(t *testing.T) (ClaimUnits, string) { t.Helper() root, err := os.MkdirTemp("", "cfs") if err != nil { t.Fatal(err) } t.Cleanup(func() { os.RemoveAll(root) }) return ClaimUnits{ExeRoot: filepath.Join(root, "exe"), UnitRoot: filepath.Join(root, "units"), DataRoot: filepath.Join(root, "data"), Systemctl: func(context.Context, ...string) (string, error) { return "", nil }}, "safe.example.com" } func TestClaimFilesystemRootDirRefusesParentSymlinkBeforeCreate(t *testing.T) { root := t.TempDir() victim := filepath.Join(root, "victim") if err := os.Mkdir(victim, 0700); err != nil { t.Fatal(err) } link := filepath.Join(root, "link") if err := os.Symlink(victim, link); err != nil { t.Fatal(err) } if err := claimRootDir(filepath.Join(link, "created")); err == nil { t.Fatal("accepted symlink parent") } if _, err := os.Lstat(filepath.Join(victim, "created")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("changed victim before refusal: %v", err) } } func TestClaimFilesystemClearRefusesSymlinkParents(t *testing.T) { for _, target := range []string{"source", "dropin"} { t.Run(target, func(t *testing.T) { u, host := claimFilesystemUnits(t) source, dropin, err := u.launchPaths(host) if err != nil { t.Fatal(err) } victim := t.TempDir() for _, name := range []string{"launch.json", "permit.json", "exec.sock", "60-claim-launch.conf"} { if err := os.WriteFile(filepath.Join(victim, name), []byte("victim"), 0600); err != nil { t.Fatal(err) } } linked, ordinary := source, dropin if target == "dropin" { linked, ordinary = dropin, source } if err := os.MkdirAll(filepath.Dir(linked), 0700); err != nil { t.Fatal(err) } if err := os.MkdirAll(ordinary, 0700); err != nil { t.Fatal(err) } if err := os.Symlink(victim, linked); err != nil { t.Fatal(err) } if err := u.clearLaunch(context.Background(), host); err == nil { t.Error("accepted symlink parent") } for _, name := range []string{"launch.json", "permit.json", "exec.sock", "60-claim-launch.conf"} { raw, err := os.ReadFile(filepath.Join(victim, name)) if err != nil || string(raw) != "victim" { t.Errorf("victim %s changed: %q, %v", name, raw, err) } } }) } } func TestClaimFilesystemFreezeReplacesHardlinksWithoutChangingVictim(t *testing.T) { u, host := claimFilesystemUnits(t) source, dropin, err := u.launchPaths(host) if err != nil { t.Fatal(err) } for _, dir := range []string{source, dropin} { if err := os.MkdirAll(dir, 0700); err != nil { t.Fatal(err) } } victim := filepath.Join(t.TempDir(), "victim") if err := os.WriteFile(victim, []byte("victim"), 0600); err != nil { t.Fatal(err) } for _, path := range []string{filepath.Join(source, "launch.json"), filepath.Join(dropin, "60-claim-launch.conf")} { if err := os.Link(victim, path); err != nil { t.Fatal(err) } } if err := u.freezeLaunch(context.Background(), host, Launch{Schema: LaunchSchema, Host: host}); err != nil { t.Fatal(err) } raw, err := os.ReadFile(victim) if err != nil || string(raw) != "victim" { t.Fatalf("victim changed: %q, %v", raw, err) } st, err := os.Stat(victim) if err != nil { t.Fatal(err) } if st.Mode().Perm() != 0600 { t.Fatalf("victim mode changed: %o", st.Mode().Perm()) } } func TestClaimFilesystemExecGateCanonicalSocketRetained(t *testing.T) { u, host := claimFilesystemUnits(t) if err := u.freezeLaunch(context.Background(), host, Launch{Schema: LaunchSchema, Host: host}); err != nil { t.Fatal(err) } gate, err := u.prepareExecGate(context.Background(), host, ClaimExecPermit{Reservation: "r", Inputs: "i", BootID: "b", SafeUntil: 1}) if err != nil { t.Fatal(err) } defer gate.close() conn, err := net.DialTimeout("unix", gate.path, time.Second) if err != nil { t.Fatal(err) } conn.Close() before, err := os.Lstat(gate.path) if err != nil { t.Fatal(err) } if before.Mode()&os.ModeSocket == 0 || before.Mode().Perm() != 0666 { t.Fatalf("socket mode = %v", before.Mode()) } gate.close() after, err := os.Lstat(gate.path) if err != nil || !os.SameFile(before, after) { t.Fatalf("socket inode removed or replaced: %v", err) } }