carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux && rootrunner
2
3package host
4
5import (
6 "bytes"
7 "os"
8 "path/filepath"
9 "syscall"
10 "testing"
11)
12
13// TestRootActivationConsoleInstall is install-console's copy step as real
14// root: modes, ownership, checked ancestry, symlinks, extra links, partial
15// state, dry runs and refusals that leave everything as it was.
16func TestRootActivationConsoleInstall(t *testing.T) {
17 d := activationRootFixture(t)
18 check := func(err error) {
19 t.Helper()
20 if err != nil {
21 t.Fatal(err)
22 }
23 }
24 // The fixture root is created 0700; the service user must be able to
25 // search every ancestor of the protected copy.
26 check(os.Chmod(d, 0o755))
27 body := []byte("console build abc1234")
28 sha := artifactSHA(body)
29 source := filepath.Join(d, "src", "console")
30 check(os.MkdirAll(filepath.Dir(source), 0o755))
31 check(os.WriteFile(source, body, 0o755))
32 check(os.Chown(source, 1000, 1000)) // a carlos-owned versions tree
33 other := filepath.Join(d, "src", "other")
34 check(os.WriteFile(other, []byte("console build def5678"), 0o755))
35 absent := func(p string) {
36 t.Helper()
37 if _, err := os.Lstat(p); !os.IsNotExist(err) {
38 t.Fatal("present", p, err)
39 }
40 }
41 slot := func(p string) {
42 t.Helper()
43 got, err := os.ReadFile(p)
44 st, serr := os.Lstat(p)
45 if err != nil || serr != nil || !bytes.Equal(got, body) {
46 t.Fatal("slot", p, err, serr)
47 }
48 sys := st.Sys().(*syscall.Stat_t)
49 if st.Mode() != 0o755 || sys.Uid != 0 || sys.Gid != 0 || sys.Nlink != 1 {
50 t.Fatalf("slot %s mode %v uid %d gid %d links %d", p, st.Mode(), sys.Uid, sys.Gid, sys.Nlink)
51 }
52 }
53 dir := filepath.Join(d, "opt", "system", "console")
54
55 // Bytes that do not match, and a symlinked source, are refused before
56 // anything is created.
57 link := filepath.Join(d, "src", "link")
58 check(os.Symlink(source, link))
59 check(os.Symlink(filepath.Join(d, "src"), filepath.Join(d, "srclink")))
60 for _, src := range []string{other, link, filepath.Join(d, "srclink", "console"), "relative/console", source + "/../src/console"} {
61 for _, dry := range []bool{true, false} {
62 if _, err := installProtectedConsole(dir, src, sha, dry); err == nil {
63 t.Fatal("accepted source", src, dry)
64 }
65 }
66 }
67 absent(filepath.Join(d, "opt"))
68
69 // A dry run with nothing there writes nothing.
70 if _, err := installProtectedConsole(dir, source, sha, true); err != nil {
71 t.Fatal(err)
72 }
73 absent(filepath.Join(d, "opt"))
74
75 got, err := installProtectedConsole(dir, source, sha, false)
76 if err != nil || got.Already || got.Path != filepath.Join(dir, "console") || got.Previous != filepath.Join(dir, "console.prev") {
77 t.Fatal(got, err)
78 }
79 slot(got.Path)
80 slot(got.Previous)
81 for _, p := range []string{filepath.Join(d, "opt"), filepath.Join(d, "opt", "system"), dir} {
82 st, err := os.Stat(p)
83 if err != nil || st.Mode().Perm() != 0o755 || st.Sys().(*syscall.Stat_t).Uid != 0 {
84 t.Fatal("directory", p, st.Mode(), err)
85 }
86 }
87 entries, _ := os.ReadDir(dir)
88 if len(entries) != 2 {
89 t.Fatal("staging left behind", entries)
90 }
91 if again, err := installProtectedConsole(dir, source, sha, false); err != nil || !again.Already {
92 t.Fatal("rerun", again, err)
93 }
94 if again, err := installProtectedConsole(dir, source, sha, true); err != nil || !again.Already {
95 t.Fatal("dry rerun", again, err)
96 }
97
98 // A run cut off after .prev: the next run completes it.
99 check(os.Remove(got.Path))
100 if again, err := installProtectedConsole(dir, source, sha, false); err != nil || again.Already {
101 t.Fatal("partial", again, err)
102 }
103 slot(got.Path)
104
105 // Anything else in a slot is refused and left as it is.
106 refuse := func(name string, setup, undo func()) {
107 t.Helper()
108 setup()
109 for _, dry := range []bool{true, false} {
110 if _, err := installProtectedConsole(dir, source, sha, dry); err == nil {
111 t.Fatal("accepted", name, dry)
112 }
113 }
114 undo()
115 slot(got.Previous)
116 }
117 foreign := []byte("someone else's console")
118 refuse("other bytes", func() { check(os.WriteFile(got.Path+".x", foreign, 0o755)); check(os.Rename(got.Path+".x", got.Path)) }, func() {
119 if b, _ := os.ReadFile(got.Path); !bytes.Equal(b, foreign) {
120 t.Fatal("refusal replaced the slot")
121 }
122 check(os.Remove(got.Path))
123 })
124 refuse("symlink", func() { check(os.Symlink(source, got.Path)) }, func() { check(os.Remove(got.Path)) })
125 refuse("extra link", func() { check(os.Link(got.Previous, got.Path)) }, func() { check(os.Remove(got.Path)) })
126 refuse("group writable", func() { check(os.Chmod(got.Previous, 0o775)) }, func() { check(os.Chmod(got.Previous, 0o755)) })
127 refuse("not root's", func() { check(os.Chown(got.Previous, 1000, 1000)) }, func() { check(os.Chown(got.Previous, 0, 0)) })
128
129 // Ancestry: a writable or unsearchable directory, or a symlinked one.
130 refuse("group-writable directory", func() { check(os.Chmod(dir, 0o775)) }, func() { check(os.Chmod(dir, 0o755)) })
131 refuse("private directory", func() { check(os.Chmod(dir, 0o700)) }, func() { check(os.Chmod(dir, 0o755)) })
132 refuse("world-writable ancestor", func() { check(os.Chmod(filepath.Join(d, "opt"), 0o777)) }, func() { check(os.Chmod(filepath.Join(d, "opt"), 0o755)) })
133 refuse("carlos-owned ancestor", func() { check(os.Chown(filepath.Join(d, "opt"), 1000, 1000)) }, func() { check(os.Chown(filepath.Join(d, "opt"), 0, 0)) })
134 moved := filepath.Join(d, "opt", "system", "real")
135 refuse("symlinked directory", func() { check(os.Rename(dir, moved)); check(os.Symlink(moved, dir)) }, func() { check(os.Remove(dir)); check(os.Rename(moved, dir)) })
136 if again, err := installProtectedConsole(dir, source, sha, false); err != nil {
137 t.Fatal("restored state", again, err)
138 }
139 slot(got.Path)
140 slot(got.Previous)
141}
142