| 1 | //go:build linux && rootrunner |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/hostbox" |
| 11 | "github.com/carlosframework/platform/internal/hostclaim" |
| 12 | ) |
| 13 | |
| 14 | func TestPlan14FirstInstallWithVendorUnitsRootRunner(t *testing.T) { |
| 15 | servingRootRunner(t) |
| 16 | uid, _ := generationRunnerRegistryUser(t) |
| 17 | units, err := os.MkdirTemp("/var/lib", "plan14-vendor-census-") |
| 18 | lifecycleCheck(t, "units", err) |
| 19 | defer os.RemoveAll(units) |
| 20 | lifecycleCheck(t, "public units ancestry", os.Chmod(units, 0755)) |
| 21 | f, root, q := plan14InstallRegistration(t, hostclaim.KindServing, "") |
| 22 | root.Roots["units"] = units |
| 23 | selected := hostbox.RecordSelection{Host: q.Target.Host, Key: q.RecordKey, ETag: q.RecordETag, Incarnation: q.Target.Incarnation, Family: "instance"} |
| 24 | check := func() error { return root.Reconcile.Census.CheckLocal(t.Context(), f.read(), selected) } |
| 25 | lifecycleCheck(t, "empty census", check()) |
| 26 | target := "/lib/systemd/system/systemd-journald.service" |
| 27 | _, err = os.Stat(target) |
| 28 | lifecycleCheck(t, "vendor unit", err) |
| 29 | wants := filepath.Join(units, "multi-user.target.wants") |
| 30 | lifecycleCheck(t, "wants", os.Mkdir(wants, 0755)) |
| 31 | link := filepath.Join(wants, "systemd-journald.service") |
| 32 | lifecycleCheck(t, "vendor link", os.Symlink(target, link)) |
| 33 | lifecycleCheck(t, "template wants", os.Mkdir(filepath.Join(units, "sshd@.service.wants"), 0755)) |
| 34 | lifecycleCheck(t, "template wants link", os.Symlink(target, filepath.Join(units, "sshd@.service.wants", "sshd-keygen.service"))) |
| 35 | lifecycleCheck(t, "template link", os.Symlink("/lib/systemd/system/getty@.service", filepath.Join(wants, "getty@tty1.service"))) |
| 36 | lifecycleCheck(t, "first installation with OS unit", check()) |
| 37 | lifecycleCheck(t, "migration with OS unit", hostbox.CheckMigrationRoots(t.Context(), root.Roots, root.Reconcile.Census.SharedPaths, nil)) |
| 38 | for _, attack := range []string{"service owner", "foreign target", "host alias", "carlos alias", "regular file", "writable ancestor"} { |
| 39 | t.Run(attack, func(t *testing.T) { |
| 40 | bad := filepath.Join(wants, "unexpected.service") |
| 41 | switch attack { |
| 42 | case "service owner": |
| 43 | lifecycleCheck(t, "owner", os.Lchown(link, uid, -1)) |
| 44 | defer os.Lchown(link, 0, 0) |
| 45 | case "writable ancestor": |
| 46 | lifecycleCheck(t, "mode", os.Chmod(wants, 0777)) |
| 47 | defer os.Chmod(wants, 0755) |
| 48 | case "foreign target": |
| 49 | lifecycleCheck(t, "link", os.Symlink("/etc/passwd", bad)) |
| 50 | defer os.Remove(bad) |
| 51 | case "host alias": |
| 52 | bad = filepath.Join(wants, q.Target.Host+".service") |
| 53 | lifecycleCheck(t, "link", os.Symlink(target, bad)) |
| 54 | defer os.Remove(bad) |
| 55 | case "carlos alias": |
| 56 | bad = filepath.Join(wants, "carlos-instance@other.service") |
| 57 | lifecycleCheck(t, "link", os.Symlink(target, bad)) |
| 58 | defer os.Remove(bad) |
| 59 | case "regular file": |
| 60 | lifecycleCheck(t, "file", os.WriteFile(bad, []byte("[Service]\nExecStart=/bin/true\n"), 0644)) |
| 61 | defer os.Remove(bad) |
| 62 | } |
| 63 | if err := check(); err == nil { |
| 64 | t.Fatal("unsafe unit inventory accepted") |
| 65 | } |
| 66 | }) |
| 67 | } |
| 68 | _, err = root.commitRootInventory(t.Context(), q) |
| 69 | lifecycleCheck(t, "first production registration", err) |
| 70 | } |
| 71 | |