| 1 | //go:build linux && rootrunner |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "errors" |
| 8 | "fmt" |
| 9 | "os" |
| 10 | "os/exec" |
| 11 | "path/filepath" |
| 12 | "reflect" |
| 13 | "strings" |
| 14 | "syscall" |
| 15 | "testing" |
| 16 | "time" |
| 17 | |
| 18 | "github.com/carlosframework/platform/internal/release" |
| 19 | "github.com/carlosframework/platform/internal/rootstate" |
| 20 | ) |
| 21 | |
| 22 | func TestPlan14GenerationInstalledUnitRootRunner(t *testing.T) { |
| 23 | dir := generationRootRunnerDir(t) |
| 24 | pid1, err := os.ReadFile("/proc/1/comm") |
| 25 | if err != nil || strings.TrimSpace(string(pid1)) != "systemd" { |
| 26 | t.Fatal("designated runner requires systemd as PID 1") |
| 27 | } |
| 28 | for _, path := range []string{"/run/carlos-control", "/run/carlos", "/var/lib/carlos-claims", "/var/lib/carlos-release", rootReleasePrivate} { |
| 29 | private := path == "/var/lib/carlos-claims" || path == rootReleasePrivate |
| 30 | mode := os.FileMode(0755) |
| 31 | if private { |
| 32 | mode = 0700 |
| 33 | } |
| 34 | if err := os.Mkdir(path, mode); err == nil { |
| 35 | t.Cleanup(func() { |
| 36 | if err := os.Remove(path); err != nil { |
| 37 | t.Error(err) |
| 38 | } |
| 39 | }) |
| 40 | } else if !errors.Is(err, os.ErrExist) { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | state, err := rootstate.Open(path, false, !private) |
| 44 | if err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | state.Close() |
| 48 | } |
| 49 | authority := filepath.Join(rootReleasePrivate, filepath.Base(dir)) |
| 50 | state, err := rootstate.Open(authority, true, false) |
| 51 | if err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | state.Close() |
| 55 | t.Cleanup(func() { |
| 56 | if err := os.RemoveAll(authority); err != nil { |
| 57 | t.Error(err) |
| 58 | } |
| 59 | }) |
| 60 | for _, name := range []string{"data", "opt"} { |
| 61 | if err := os.Mkdir(filepath.Join(dir, name), 0755); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | } |
| 65 | binary, err := os.Executable() |
| 66 | if err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | body, err := os.ReadFile(binary) |
| 70 | if err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | installed := filepath.Join(dir, "runner.test") |
| 74 | if err := os.WriteFile(installed, body, 0755); err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | raw, err := unitsFS.ReadFile("units/" + claimControlUnit) |
| 78 | if err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | ctl := func(t *testing.T, args ...string) string { |
| 82 | t.Helper() |
| 83 | ctx, cancel := context.WithTimeout(t.Context(), 45*time.Second) |
| 84 | defer cancel() |
| 85 | out, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput() |
| 86 | if err != nil { |
| 87 | t.Fatalf("systemctl %v: %v\n%s", args, err, out) |
| 88 | } |
| 89 | return strings.TrimSpace(string(out)) |
| 90 | } |
| 91 | for _, mode := range []string{"off", "on"} { |
| 92 | t.Run(mode, func(t *testing.T) { |
| 93 | unit := filepath.Base(dir) + "-" + mode + ".service" |
| 94 | content := claimUnitContent(claimControlUnit, raw, claimUnitSettings{enabled: mode == "on", data: filepath.Join(dir, "data"), opt: filepath.Join(dir, "opt")}) |
| 95 | var lines []string |
| 96 | for _, line := range strings.Split(string(content), "\n") { |
| 97 | if strings.HasPrefix(line, "Requires=") || strings.HasPrefix(line, "After=") || strings.HasPrefix(line, "ExecStart=") || strings.HasPrefix(line, "EnvironmentFile=") { |
| 98 | continue |
| 99 | } |
| 100 | lines = append(lines, line) |
| 101 | } |
| 102 | content = []byte(strings.Join(lines, "\n") + fmt.Sprintf("\nType=oneshot\nTimeoutStartSec=45s\nExecStart=%s -test.run=^TestPlan14GenerationUnitPayload$ -test.v -test.timeout=30s\nEnvironment=PLAN14_GENERATION_UNIT_MODE=%s PLAN14_GENERATION_UNIT_STORE=%s\nIPAddressDeny=any\n", installed, mode, authority)) |
| 103 | path := filepath.Join("/run/systemd/system", unit) |
| 104 | file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) |
| 105 | if err != nil { |
| 106 | t.Fatal(err) |
| 107 | } |
| 108 | t.Cleanup(func() { |
| 109 | for _, args := range [][]string{{"stop", unit}, {"reset-failed", unit}} { |
| 110 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| 111 | out, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput() |
| 112 | cancel() |
| 113 | if err != nil && args[0] == "stop" { |
| 114 | t.Errorf("cleanup unit: %v %s", err, out) |
| 115 | } |
| 116 | } |
| 117 | if err := os.Remove(path); err != nil { |
| 118 | t.Error(err) |
| 119 | } |
| 120 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| 121 | defer cancel() |
| 122 | if out, err := exec.CommandContext(ctx, "systemctl", "daemon-reload").CombinedOutput(); err != nil { |
| 123 | t.Errorf("cleanup reload: %v %s", err, out) |
| 124 | } |
| 125 | }) |
| 126 | _, writeErr := file.Write(content) |
| 127 | if err := errors.Join(writeErr, file.Close()); err != nil { |
| 128 | t.Fatal(err) |
| 129 | } |
| 130 | ctl(t, "daemon-reload") |
| 131 | ctl(t, "start", unit) |
| 132 | if ctl(t, "show", unit, "--property=Result", "--value") != "success" || ctl(t, "show", unit, "--property=ExecMainStatus", "--value") != "0" { |
| 133 | t.Fatal("installed unit did not complete its payload") |
| 134 | } |
| 135 | }) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func TestPlan14GenerationUnitPayload(t *testing.T) { |
| 140 | mode, path := os.Getenv("PLAN14_GENERATION_UNIT_MODE"), os.Getenv("PLAN14_GENERATION_UNIT_STORE") |
| 141 | if path == "" || (mode != "off" && mode != "on" && mode != "service") { |
| 142 | t.Fatal("invoke through TestPlan14GenerationInstalledUnitRootRunner") |
| 143 | } |
| 144 | if mode == "service" { |
| 145 | if os.Geteuid() != 65534 { |
| 146 | t.Fatal("service witness did not drop credentials") |
| 147 | } |
| 148 | if _, err := os.ReadDir(path); !errors.Is(err, os.ErrPermission) { |
| 149 | t.Fatalf("service listed private generations: %v", err) |
| 150 | } |
| 151 | if err := os.WriteFile(filepath.Join(path, "forged.json"), nil, 0600); !errors.Is(err, os.ErrPermission) { |
| 152 | t.Fatalf("service wrote private generations: %v", err) |
| 153 | } |
| 154 | return |
| 155 | } |
| 156 | if os.Geteuid() != 0 { |
| 157 | t.Fatal("installed control unit must run as root") |
| 158 | } |
| 159 | if mode == "off" { |
| 160 | if err := os.WriteFile(filepath.Join(path, "unexpected.json"), nil, 0600); !errors.Is(err, syscall.EROFS) { |
| 161 | t.Fatalf("claims-off private generation write: %v", err) |
| 162 | } |
| 163 | return |
| 164 | } |
| 165 | state, err := rootstate.Open(path, false, false) |
| 166 | if err != nil { |
| 167 | t.Fatal(err) |
| 168 | } |
| 169 | defer state.Close() |
| 170 | boot, err := (generationKernel{proc: "/proc"}).boot() |
| 171 | if err != nil { |
| 172 | t.Fatal(err) |
| 173 | } |
| 174 | start, err := claimConstructionProcessStart() |
| 175 | if err != nil { |
| 176 | t.Fatal(err) |
| 177 | } |
| 178 | binary, err := os.Executable() |
| 179 | if err != nil { |
| 180 | t.Fatal(err) |
| 181 | } |
| 182 | identity := func(path string) rootstate.FileIdentity { |
| 183 | t.Helper() |
| 184 | file, err := os.Open(path) |
| 185 | if err != nil { |
| 186 | t.Fatal(err) |
| 187 | } |
| 188 | defer file.Close() |
| 189 | id, err := rootstate.Identity(file) |
| 190 | if err != nil { |
| 191 | t.Fatal(err) |
| 192 | } |
| 193 | return id |
| 194 | } |
| 195 | // Authorization and launch facts are fixtures. This tests installed access. |
| 196 | binding := release.GenerationBinding{ |
| 197 | Binding: release.HistoryBinding{Host: "unit.example", Account: "bab", App: "shop"}, |
| 198 | Target: release.GenerationOwner{Host: "unit.example", Incarnation: "inc", Stamp: 1, Session: 1, Box: "runner", Enrollment: "en", BootID: boot, ControllerID: "controller"}, |
| 199 | Installation: "fixture", Attempt: "main", Inputs: "fixture", Invocation: "fixture", Record: "fixture", RecordETag: "fixture", |
| 200 | ControllerPID: os.Getpid(), ControllerStart: start, Version: "v1", Digest: strings.Repeat("a", 64), |
| 201 | Path: binary, Tree: filepath.Dir(binary), Executable: identity(binary), TreeIdentity: identity(filepath.Dir(binary)), |
| 202 | } |
| 203 | g := &release.RootGenerations{Store: state} |
| 204 | reserved, err := g.Reserve(binding) |
| 205 | if err != nil { |
| 206 | t.Fatal(err) |
| 207 | } |
| 208 | cmd := exec.CommandContext(t.Context(), binary, "-test.run=^TestPlan14GenerationUnitPayload$", "-test.v", "-test.timeout=10s") |
| 209 | cmd.Dir = filepath.Dir(binary) |
| 210 | cmd.Env = []string{"PLAN14_GENERATION_UNIT_MODE=service", "PLAN14_GENERATION_UNIT_STORE=" + path} |
| 211 | cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 65534, Gid: 65534, Groups: []uint32{65534}}} |
| 212 | if out, err := cmd.CombinedOutput(); err != nil { |
| 213 | t.Fatalf("service-UID witness: %v\n%s", err, out) |
| 214 | } |
| 215 | state.Close() |
| 216 | state, err = rootstate.Open(path, false, false) |
| 217 | if err != nil { |
| 218 | t.Fatal(err) |
| 219 | } |
| 220 | defer state.Close() |
| 221 | got, err := (&release.RootGenerations{Store: state}).Read(binding) |
| 222 | if err != nil || !reflect.DeepEqual(got, reserved) { |
| 223 | t.Fatalf("reopened generation changed: %+v %v", got, err) |
| 224 | } |
| 225 | } |
| 226 | |