| 1 | //go:build linux && rootrunner |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "crypto/sha256" |
| 8 | "encoding/hex" |
| 9 | "os" |
| 10 | "path/filepath" |
| 11 | "sort" |
| 12 | "strings" |
| 13 | "syscall" |
| 14 | "testing" |
| 15 | |
| 16 | "golang.org/x/sys/unix" |
| 17 | ) |
| 18 | |
| 19 | // consoleUpgradeFixture is a protected console directory installed by |
| 20 | // install-console with build A, and source files for builds A to D. |
| 21 | type consoleUpgradeFixture struct { |
| 22 | t *testing.T |
| 23 | d, dir string |
| 24 | src map[string]string // build name to source path |
| 25 | body map[string][]byte |
| 26 | sha map[string]string |
| 27 | } |
| 28 | |
| 29 | func newConsoleUpgradeFixture(t *testing.T) *consoleUpgradeFixture { |
| 30 | t.Helper() |
| 31 | d := activationRootFixture(t) |
| 32 | f := &consoleUpgradeFixture{t: t, d: d, dir: filepath.Join(d, "opt", "system", "console"), src: map[string]string{}, body: map[string][]byte{}, sha: map[string]string{}} |
| 33 | f.check(os.Chmod(d, 0o755)) |
| 34 | f.check(os.MkdirAll(filepath.Join(d, "src"), 0o755)) |
| 35 | for _, name := range []string{"A", "B", "C", "D"} { |
| 36 | f.body[name] = []byte("console build " + name) |
| 37 | f.sha[name] = artifactSHA(f.body[name]) |
| 38 | f.src[name] = filepath.Join(d, "src", name) |
| 39 | f.check(os.WriteFile(f.src[name], f.body[name], 0o755)) |
| 40 | } |
| 41 | if got, err := installProtectedConsole(f.dir, f.src["A"], f.sha["A"], false); err != nil || got.Already { |
| 42 | t.Fatal("first install", got, err) |
| 43 | } |
| 44 | f.want("A", "A") |
| 45 | return f |
| 46 | } |
| 47 | |
| 48 | func (f *consoleUpgradeFixture) check(err error) { |
| 49 | f.t.Helper() |
| 50 | if err != nil { |
| 51 | f.t.Fatal(err) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | func (f *consoleUpgradeFixture) path(name string) string { return filepath.Join(f.dir, name) } |
| 56 | |
| 57 | // slot requires name to be a root:root 0755 one-link regular file holding |
| 58 | // build b. |
| 59 | func (f *consoleUpgradeFixture) slot(name, b string) { |
| 60 | f.t.Helper() |
| 61 | got, err := os.ReadFile(f.path(name)) |
| 62 | st, serr := os.Lstat(f.path(name)) |
| 63 | if err != nil || serr != nil || !bytes.Equal(got, f.body[b]) { |
| 64 | f.t.Fatalf("%s: want build %s, got %q %v %v", name, b, got, err, serr) |
| 65 | } |
| 66 | sys := st.Sys().(*syscall.Stat_t) |
| 67 | if st.Mode() != 0o755 || sys.Uid != 0 || sys.Gid != 0 || sys.Nlink != 1 { |
| 68 | f.t.Fatalf("%s mode %v uid %d gid %d links %d", name, st.Mode(), sys.Uid, sys.Gid, sys.Nlink) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | // entries lists the directory's names. |
| 73 | func (f *consoleUpgradeFixture) entries() string { |
| 74 | f.t.Helper() |
| 75 | list, err := os.ReadDir(f.dir) |
| 76 | f.check(err) |
| 77 | var names []string |
| 78 | for _, e := range list { |
| 79 | names = append(names, e.Name()) |
| 80 | } |
| 81 | sort.Strings(names) |
| 82 | return strings.Join(names, ",") |
| 83 | } |
| 84 | |
| 85 | // want requires console to hold current, console.prev to hold prev and |
| 86 | // nothing else in the directory. |
| 87 | func (f *consoleUpgradeFixture) want(current, prev string) { |
| 88 | f.t.Helper() |
| 89 | f.slot("console", current) |
| 90 | f.slot("console.prev", prev) |
| 91 | if got := f.entries(); got != "console,console.prev" { |
| 92 | f.t.Fatal("directory holds", got) |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | // upgrade runs the upgrade to build to, from the sha of build from. |
| 97 | func (f *consoleUpgradeFixture) upgrade(to, from string, dry bool) (consoleInstalled, error) { |
| 98 | return upgradeProtectedConsole(f.dir, f.src[to], f.sha[to], f.sha[from], dry) |
| 99 | } |
| 100 | |
| 101 | // refused requires the upgrade to build to from build from to refuse, dry |
| 102 | // and not, and the directory listing to be unchanged by either. |
| 103 | func (f *consoleUpgradeFixture) refused(what, to, from string) { |
| 104 | f.t.Helper() |
| 105 | before := f.entries() |
| 106 | for _, dry := range []bool{true, false} { |
| 107 | if got, err := f.upgrade(to, from, dry); err == nil { |
| 108 | f.t.Fatal("accepted", what, dry, got) |
| 109 | } |
| 110 | if after := f.entries(); after != before { |
| 111 | f.t.Fatal(what, "changed the directory", before, after) |
| 112 | } |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | // TestRootActivationConsoleUpgrade is install-console --upgrade as real |
| 117 | // root: the swap, its refusals, crash recovery, a leftover temp file, the |
| 118 | // Already rerun and the dry run. |
| 119 | func TestRootActivationConsoleUpgrade(t *testing.T) { |
| 120 | f := newConsoleUpgradeFixture(t) |
| 121 | |
| 122 | // A wrong --from refuses and changes nothing; so does a malformed one. |
| 123 | f.refused("wrong from", "B", "C") |
| 124 | f.want("A", "A") |
| 125 | for _, from := range []string{strings.ToUpper(f.sha["A"]), f.sha["A"][:63], ""} { |
| 126 | for _, dry := range []bool{true, false} { |
| 127 | if _, err := upgradeProtectedConsole(f.dir, f.src["B"], f.sha["B"], from, dry); err == nil { |
| 128 | t.Fatalf("accepted from %q", from) |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | // A source that does not hash to the target refuses. |
| 133 | if _, err := upgradeProtectedConsole(f.dir, f.src["C"], f.sha["B"], f.sha["A"], false); err == nil { |
| 134 | t.Fatal("accepted another source") |
| 135 | } |
| 136 | f.want("A", "A") |
| 137 | |
| 138 | // The dry run checks and reports, and changes nothing. |
| 139 | got, err := f.upgrade("B", "A", true) |
| 140 | if err != nil || got.Already || got.From != f.sha["A"] || got.PreviousSHA != f.sha["A"] || got.Path != f.path("console") || got.Previous != f.path("console.prev") { |
| 141 | t.Fatal("dry run", got, err) |
| 142 | } |
| 143 | f.want("A", "A") |
| 144 | |
| 145 | // Upgrade A to B: prev is A, current is B, no temp file left. |
| 146 | got, err = f.upgrade("B", "A", false) |
| 147 | if err != nil || got.Already || got.From != f.sha["A"] || got.PreviousSHA != f.sha["A"] { |
| 148 | t.Fatal("upgrade", got, err) |
| 149 | } |
| 150 | f.want("B", "A") |
| 151 | |
| 152 | // A rerun is Already and changes nothing, dry or not. |
| 153 | for _, dry := range []bool{true, false} { |
| 154 | got, err = f.upgrade("B", "A", dry) |
| 155 | if err != nil || !got.Already || got.PreviousSHA != f.sha["A"] { |
| 156 | t.Fatal("rerun", dry, got, err) |
| 157 | } |
| 158 | f.want("B", "A") |
| 159 | } |
| 160 | |
| 161 | // A malformed --from refuses even when the console is already current. |
| 162 | for _, from := range []string{strings.ToUpper(f.sha["A"]), f.sha["A"][:63], ""} { |
| 163 | for _, dry := range []bool{true, false} { |
| 164 | if _, err := upgradeProtectedConsole(f.dir, f.src["B"], f.sha["B"], from, dry); err == nil { |
| 165 | t.Fatalf("Already with from %q", from) |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | // Without --upgrade an occupied slot still refuses, as before. |
| 171 | for _, b := range []string{"B", "C"} { |
| 172 | for _, dry := range []bool{true, false} { |
| 173 | if _, err := installProtectedConsole(f.dir, f.src[b], f.sha[b], dry); err == nil { |
| 174 | t.Fatal("first install over an installed console", b, dry) |
| 175 | } |
| 176 | } |
| 177 | } |
| 178 | f.want("B", "A") |
| 179 | |
| 180 | // The current console's shape: each refuses, nothing changes. |
| 181 | cur := f.path("console") |
| 182 | shape := func(what string, setup, undo func()) { |
| 183 | t.Helper() |
| 184 | setup() |
| 185 | f.refused(what, "C", "B") |
| 186 | undo() |
| 187 | f.want("B", "A") |
| 188 | } |
| 189 | shape("current symlink", func() { f.check(os.Rename(cur, cur+".real")); f.check(os.Symlink(cur+".real", cur)) }, func() { f.check(os.Remove(cur)); f.check(os.Rename(cur+".real", cur)) }) |
| 190 | shape("current not root's", func() { f.check(os.Chown(cur, 1000, 1000)) }, func() { f.check(os.Chown(cur, 0, 0)) }) |
| 191 | shape("current owner not root", func() { f.check(os.Chown(cur, 1000, 0)) }, func() { f.check(os.Chown(cur, 0, 0)) }) |
| 192 | shape("current group not root", func() { f.check(os.Chown(cur, 0, 1000)) }, func() { f.check(os.Chown(cur, 0, 0)) }) |
| 193 | shape("current two links", func() { f.check(os.Link(cur, cur+".link")) }, func() { f.check(os.Remove(cur + ".link")) }) |
| 194 | shape("current group writable", func() { f.check(os.Chmod(cur, 0o775)) }, func() { f.check(os.Chmod(cur, 0o755)) }) |
| 195 | shape("current not executable by others", func() { f.check(os.Chmod(cur, 0o750)) }, func() { f.check(os.Chmod(cur, 0o755)) }) |
| 196 | shape("current directory", func() { f.check(os.Rename(cur, cur+".real")); f.check(os.Mkdir(cur, 0o755)) }, func() { f.check(os.Remove(cur)); f.check(os.Rename(cur+".real", cur)) }) |
| 197 | |
| 198 | // console.prev's shape: a symlink, a directory or the wrong owner each |
| 199 | // refuses. |
| 200 | prev := f.path("console.prev") |
| 201 | shape("prev symlink", func() { f.check(os.Rename(prev, prev+".real")); f.check(os.Symlink(prev+".real", prev)) }, func() { f.check(os.Remove(prev)); f.check(os.Rename(prev+".real", prev)) }) |
| 202 | shape("prev directory", func() { f.check(os.Rename(prev, prev+".real")); f.check(os.Mkdir(prev, 0o755)) }, func() { f.check(os.Remove(prev)); f.check(os.Rename(prev+".real", prev)) }) |
| 203 | shape("prev not root's", func() { f.check(os.Chown(prev, 1000, 1000)) }, func() { f.check(os.Chown(prev, 0, 0)) }) |
| 204 | shape("prev owner not root", func() { f.check(os.Chown(prev, 1000, 0)) }, func() { f.check(os.Chown(prev, 0, 0)) }) |
| 205 | shape("prev two links", func() { f.check(os.Link(prev, prev+".link")) }, func() { f.check(os.Remove(prev + ".link")) }) |
| 206 | shape("prev group writable", func() { f.check(os.Chmod(prev, 0o775)) }, func() { f.check(os.Chmod(prev, 0o755)) }) |
| 207 | |
| 208 | // console.prev's bytes are not checked: it is about to be replaced. |
| 209 | f.check(os.WriteFile(prev+".x", []byte("an older build"), 0o755)) |
| 210 | f.check(os.Rename(prev+".x", prev)) |
| 211 | if got, err = f.upgrade("C", "B", false); err != nil || got.Already || got.PreviousSHA != f.sha["B"] { |
| 212 | t.Fatal("upgrade over any prev", got, err) |
| 213 | } |
| 214 | f.want("C", "B") |
| 215 | |
| 216 | // A run cut off after renaming console to console.prev: console is |
| 217 | // absent, console.prev holds the old build and console.next the new. |
| 218 | f.check(os.WriteFile(f.path(consoleUpgradeTemp), f.body["D"], 0o755)) |
| 219 | f.check(os.Rename(cur, prev)) |
| 220 | if got := f.entries(); got != "console.next,console.prev" { |
| 221 | t.Fatal("crash state", got) |
| 222 | } |
| 223 | f.refused("crash state, wrong from", "D", "B") |
| 224 | f.refused("crash state, from the new build", "D", "D") |
| 225 | if got, err = f.upgrade("D", "C", true); err != nil || got.Already || got.PreviousSHA != f.sha["C"] { |
| 226 | t.Fatal("crash state dry run", got, err) |
| 227 | } |
| 228 | if got := f.entries(); got != "console.next,console.prev" { |
| 229 | t.Fatal("crash state dry run changed", got) |
| 230 | } |
| 231 | if got, err = f.upgrade("D", "C", false); err != nil || got.Already || got.From != f.sha["C"] || got.PreviousSHA != f.sha["C"] { |
| 232 | t.Fatal("crash recovery", got, err) |
| 233 | } |
| 234 | f.want("D", "C") |
| 235 | |
| 236 | // With both slots gone there is nothing to upgrade from. |
| 237 | f.check(os.Rename(cur, cur+".keep")) |
| 238 | f.check(os.Rename(prev, prev+".keep")) |
| 239 | f.refused("no console", "A", "D") |
| 240 | f.refused("no console, from prev", "A", "C") |
| 241 | f.check(os.Rename(cur+".keep", cur)) |
| 242 | f.check(os.Rename(prev+".keep", prev)) |
| 243 | |
| 244 | // A run cut off before its renames leaves a partial console.next: the |
| 245 | // dry run leaves it, a rerun removes it and completes. |
| 246 | f.check(os.WriteFile(f.path(consoleUpgradeTemp), []byte("partial"), 0o600)) |
| 247 | if _, err = f.upgrade("A", "D", true); err != nil { |
| 248 | t.Fatal("dry run over a leftover", err) |
| 249 | } |
| 250 | if b, _ := os.ReadFile(f.path(consoleUpgradeTemp)); string(b) != "partial" { |
| 251 | t.Fatal("dry run touched the leftover") |
| 252 | } |
| 253 | if got, err = f.upgrade("A", "D", false); err != nil || got.Already || got.PreviousSHA != f.sha["D"] { |
| 254 | t.Fatal("upgrade over a leftover", got, err) |
| 255 | } |
| 256 | f.want("A", "D") |
| 257 | |
| 258 | // A console.next that is not a regular file refuses and is left. |
| 259 | f.check(os.Mkdir(f.path(consoleUpgradeTemp), 0o700)) |
| 260 | f.refused("console.next directory", "B", "A") |
| 261 | f.check(os.Remove(f.path(consoleUpgradeTemp))) |
| 262 | f.check(os.Symlink(f.src["B"], f.path(consoleUpgradeTemp))) |
| 263 | f.refused("console.next symlink", "B", "A") |
| 264 | f.check(os.Remove(f.path(consoleUpgradeTemp))) |
| 265 | f.want("A", "D") |
| 266 | |
| 267 | // An upgrade never creates the directory. |
| 268 | missing := filepath.Join(f.d, "opt", "system", "other") |
| 269 | for _, dry := range []bool{true, false} { |
| 270 | if _, err := upgradeProtectedConsole(missing, f.src["B"], f.sha["B"], f.sha["A"], dry); err == nil { |
| 271 | t.Fatal("upgraded a missing directory", dry) |
| 272 | } |
| 273 | } |
| 274 | if _, err := os.Lstat(missing); !os.IsNotExist(err) { |
| 275 | t.Fatal("upgrade created the directory", err) |
| 276 | } |
| 277 | |
| 278 | // A group-writable directory refuses, as for the first install. |
| 279 | f.check(os.Chmod(f.dir, 0o775)) |
| 280 | f.refused("group-writable directory", "B", "A") |
| 281 | f.check(os.Chmod(f.dir, 0o755)) |
| 282 | f.want("A", "D") |
| 283 | } |
| 284 | |
| 285 | // TestRootActivationConsoleUpgradeIdentity runs the upgrade through |
| 286 | // runInstallConsole: the staged identity must be the published one and |
| 287 | // name the running build, or nothing changes. |
| 288 | func TestRootActivationConsoleUpgradeIdentity(t *testing.T) { |
| 289 | f := newConsoleUpgradeFixture(t) |
| 290 | w := newArtifactWorld(t) |
| 291 | identity := w.run(t, w.identityPlan(t)).Output |
| 292 | idPath := filepath.Join(f.d, "identity.json") |
| 293 | f.check(os.WriteFile(idPath, identity, 0o644)) |
| 294 | f.body["I"] = []byte("console build") |
| 295 | f.sha["I"] = artifactSHA(f.body["I"]) |
| 296 | f.src["I"] = filepath.Join(f.d, "src", "I") |
| 297 | f.check(os.WriteFile(f.src["I"], f.body["I"], 0o755)) |
| 298 | |
| 299 | for _, build := range []string{"v0.28.1", "v0.28.3", ""} { |
| 300 | for _, dry := range []bool{true, false} { |
| 301 | if _, _, got, err := runInstallConsole(t.Context(), w.store, w.pub, idPath, f.src["I"], f.dir, build, "linux-arm64", f.sha["A"], dry); err == nil { |
| 302 | t.Fatal("accepted build", build, got) |
| 303 | } |
| 304 | } |
| 305 | } |
| 306 | for _, dry := range []bool{true, false} { |
| 307 | if _, _, _, err := runInstallConsole(t.Context(), w.store, w.pub, idPath, f.src["I"], f.dir, "v0.28.2", "linux-amd64", f.sha["A"], dry); err == nil { |
| 308 | t.Fatal("accepted another platform") |
| 309 | } |
| 310 | } |
| 311 | f.want("A", "A") |
| 312 | |
| 313 | tag, sha, got, err := runInstallConsole(t.Context(), w.store, w.pub, idPath, f.src["I"], f.dir, "v0.28.2", "linux-arm64", f.sha["A"], true) |
| 314 | if err != nil || tag != "v0.28.2" || sha != f.sha["I"] || got.Already || got.PreviousSHA != f.sha["A"] { |
| 315 | t.Fatal("dry run", tag, sha, got, err) |
| 316 | } |
| 317 | f.want("A", "A") |
| 318 | if _, _, got, err = runInstallConsole(t.Context(), w.store, w.pub, idPath, f.src["I"], f.dir, "v0.28.2", "linux-arm64", f.sha["A"], false); err != nil || got.Already { |
| 319 | t.Fatal("upgrade", got, err) |
| 320 | } |
| 321 | f.want("I", "A") |
| 322 | if _, _, got, err = runInstallConsole(t.Context(), w.store, w.pub, idPath, f.src["I"], f.dir, "v0.28.2", "linux-arm64", f.sha["A"], false); err != nil || !got.Already { |
| 323 | t.Fatal("rerun", got, err) |
| 324 | } |
| 325 | // Without --upgrade the installed console is still refused. |
| 326 | if _, _, _, err = runInstallConsole(t.Context(), w.store, w.pub, idPath, f.src["I"], f.dir, "v0.28.2", "linux-arm64", "", false); err == nil { |
| 327 | t.Fatal("first install over an upgraded console") |
| 328 | } |
| 329 | f.want("I", "A") |
| 330 | } |
| 331 | |
| 332 | // TestRootActivationConsoleUpgradeSourceAlias: an upgrade never takes its |
| 333 | // source from its own slots or temp file, whether named by a path inside |
| 334 | // the directory or reached through a bind mount of it (same device and |
| 335 | // inode, another path). Each refuses with nothing changed. |
| 336 | func TestRootActivationConsoleUpgradeSourceAlias(t *testing.T) { |
| 337 | f := newConsoleUpgradeFixture(t) |
| 338 | // console = A, console.prev = B: the target B is in console.prev. |
| 339 | prev := f.path("console.prev") |
| 340 | f.check(os.WriteFile(prev+".x", f.body["B"], 0o755)) |
| 341 | f.check(os.Rename(prev+".x", prev)) |
| 342 | alias := filepath.Join(f.d, "alias") |
| 343 | f.check(os.Mkdir(alias, 0o755)) |
| 344 | f.check(unix.Mount(f.dir, alias, "", unix.MS_BIND, "")) |
| 345 | t.Cleanup(func() { unix.Unmount(alias, unix.MNT_DETACH) }) |
| 346 | |
| 347 | refuse := func(what, source, to, from string) { |
| 348 | t.Helper() |
| 349 | before := f.entries() |
| 350 | for _, dry := range []bool{true, false} { |
| 351 | if got, err := upgradeProtectedConsole(f.dir, source, f.sha[to], f.sha[from], dry); err == nil { |
| 352 | t.Fatal("accepted", what, dry, got) |
| 353 | } |
| 354 | if after := f.entries(); after != before { |
| 355 | t.Fatal(what, "changed the directory", before, after) |
| 356 | } |
| 357 | } |
| 358 | } |
| 359 | refuse("console.prev by path", prev, "B", "A") |
| 360 | refuse("console.prev through a bind mount", filepath.Join(alias, "console.prev"), "B", "A") |
| 361 | refuse("console by path", f.path("console"), "A", "B") |
| 362 | refuse("console through a bind mount", filepath.Join(alias, "console"), "A", "B") |
| 363 | // Another file inside the directory, not a slot, is refused by path. |
| 364 | f.check(os.WriteFile(f.path("console.src"), f.body["C"], 0o755)) |
| 365 | refuse("a file in the directory", f.path("console.src"), "C", "A") |
| 366 | f.check(os.Remove(f.path("console.src"))) |
| 367 | |
| 368 | // A leftover console.next holding the target. |
| 369 | f.check(os.WriteFile(f.path(consoleUpgradeTemp), f.body["C"], 0o755)) |
| 370 | refuse("console.next by path", f.path(consoleUpgradeTemp), "C", "A") |
| 371 | refuse("console.next through a bind mount", filepath.Join(alias, consoleUpgradeTemp), "C", "A") |
| 372 | if b, _ := os.ReadFile(f.path(consoleUpgradeTemp)); !bytes.Equal(b, f.body["C"]) { |
| 373 | t.Fatal("console.next changed") |
| 374 | } |
| 375 | f.check(os.Remove(f.path(consoleUpgradeTemp))) |
| 376 | f.slot("console", "A") |
| 377 | f.slot("console.prev", "B") |
| 378 | |
| 379 | // The same target from a source outside the directory upgrades. |
| 380 | if got, err := f.upgrade("B", "A", false); err != nil || got.Already { |
| 381 | t.Fatal("upgrade from an outside source", got, err) |
| 382 | } |
| 383 | f.want("B", "A") |
| 384 | } |
| 385 | |
| 386 | // TestRootActivationConsoleUpgradeAlreadySyncs: a run cut off after its |
| 387 | // second rename, before the directory fsync, leaves console = target; the |
| 388 | // rerun is Already and fsyncs the directory first, and a failed fsync is |
| 389 | // the rerun's error. A dry rerun does not sync. |
| 390 | func TestRootActivationConsoleUpgradeAlreadySyncs(t *testing.T) { |
| 391 | f := newConsoleUpgradeFixture(t) |
| 392 | var dirSt unix.Stat_t |
| 393 | f.check(unix.Stat(f.dir, &dirSt)) |
| 394 | var calls int |
| 395 | var fail error |
| 396 | saved := consoleUpgradeSync |
| 397 | t.Cleanup(func() { consoleUpgradeSync = saved }) |
| 398 | consoleUpgradeSync = func(fd int) error { |
| 399 | var st unix.Stat_t |
| 400 | if err := unix.Fstat(fd, &st); err != nil || st.Dev != dirSt.Dev || st.Ino != dirSt.Ino { |
| 401 | t.Fatal("synced something other than the directory", err) |
| 402 | } |
| 403 | calls++ |
| 404 | if fail != nil { |
| 405 | return fail |
| 406 | } |
| 407 | return saved(fd) |
| 408 | } |
| 409 | |
| 410 | // The state just before the final fsync. |
| 411 | f.check(os.WriteFile(f.path(consoleUpgradeTemp), f.body["B"], 0o755)) |
| 412 | f.check(os.Rename(f.path("console"), f.path("console.prev"))) |
| 413 | f.check(os.Rename(f.path(consoleUpgradeTemp), f.path("console"))) |
| 414 | f.want("B", "A") |
| 415 | |
| 416 | if got, err := f.upgrade("B", "A", true); err != nil || !got.Already || calls != 0 { |
| 417 | t.Fatal("dry rerun", got, err, calls) |
| 418 | } |
| 419 | if got, err := f.upgrade("B", "A", false); err != nil || !got.Already || calls != 1 { |
| 420 | t.Fatal("rerun", got, err, calls) |
| 421 | } |
| 422 | fail = syscall.EIO |
| 423 | if got, err := f.upgrade("B", "A", false); err != syscall.EIO || calls != 2 { |
| 424 | t.Fatal("rerun with a failing fsync", got, err, calls) |
| 425 | } |
| 426 | fail = nil |
| 427 | f.want("B", "A") |
| 428 | |
| 429 | // A full upgrade syncs once, after its renames. |
| 430 | if got, err := f.upgrade("C", "B", false); err != nil || got.Already || calls != 3 { |
| 431 | t.Fatal("upgrade", got, err, calls) |
| 432 | } |
| 433 | f.want("C", "B") |
| 434 | } |
| 435 | |
| 436 | // TestRootActivationConsoleUpgradeOversizedSlot: a slot larger than |
| 437 | // consoleMaxBytes is refused, not hashed by its prefix, so --from naming |
| 438 | // the prefix's digest cannot match it. |
| 439 | func TestRootActivationConsoleUpgradeOversizedSlot(t *testing.T) { |
| 440 | f := newConsoleUpgradeFixture(t) |
| 441 | cur := f.path("console") |
| 442 | big := cur + ".big" |
| 443 | f.check(os.WriteFile(big, nil, 0o755)) |
| 444 | f.check(os.Truncate(big, consoleMaxBytes+2)) // sparse: zeros |
| 445 | f.check(os.Rename(big, cur)) |
| 446 | prefix := zeroDigest(t, consoleMaxBytes+1) // what a LimitReader(max+1) hashes |
| 447 | f.sha["prefix"] = prefix |
| 448 | f.refused("oversized console", "B", "prefix") |
| 449 | if _, err := os.Lstat(cur); err != nil { |
| 450 | t.Fatal(err) |
| 451 | } |
| 452 | // As console.prev in the crash state it is refused too. |
| 453 | f.check(os.Rename(cur, f.path("console.prev"))) |
| 454 | f.refused("oversized console.prev", "B", "prefix") |
| 455 | } |
| 456 | |
| 457 | // zeroDigest is the sha256 of n zero bytes. |
| 458 | func zeroDigest(t *testing.T, n int64) string { |
| 459 | t.Helper() |
| 460 | h := sha256.New() |
| 461 | buf := make([]byte, 1<<20) |
| 462 | for n > 0 { |
| 463 | k := int64(len(buf)) |
| 464 | if n < k { |
| 465 | k = n |
| 466 | } |
| 467 | h.Write(buf[:k]) |
| 468 | n -= k |
| 469 | } |
| 470 | return hex.EncodeToString(h.Sum(nil)) |
| 471 | } |
| 472 | |