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 "context"
7 "encoding/json"
8 "fmt"
9 "io"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "syscall"
15 "testing"
16
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/rootparticipation"
20 "github.com/carlosframework/platform/internal/rootstate"
21 "golang.org/x/sys/unix"
22)
23
24func TestPlan14GenerationKernelNamespaceMountRootRunner(t *testing.T) {
25 const childKey = "PLAN14_E27_NAMESPACE_CHILD"
26 dir := os.Getenv(childKey)
27 if dir == "" {
28 dir = generationRootRunnerDir(t)
29 cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", os.Args[0], "-test.run=^TestPlan14GenerationKernelNamespaceMountRootRunner$", "-test.v", "-test.timeout=2m")
30 cmd.Dir = dir
31 cmd.Env = append(os.Environ(), childKey+"="+dir)
32 if out, err := cmd.CombinedOutput(); err != nil {
33 t.Fatalf("isolated namespace census: %v\n%s", err, out)
34 } else {
35 t.Logf("%s", out)
36 }
37 return
38 }
39 if os.Geteuid() != 0 {
40 t.Fatal("designated runner must execute as root")
41 }
42 alias := filepath.Join(dir, "hidden-alias")
43 tree := filepath.Join(dir, "versions")
44 if os.Getenv("PLAN14_E27_NAMESPACE_HOLDER") == "1" {
45 if err := unix.Mount("", "/", "", unix.MS_REC|unix.MS_PRIVATE, ""); err != nil {
46 t.Fatal(err)
47 }
48 if err := unix.Mount(tree, alias, "", unix.MS_BIND, ""); err != nil {
49 t.Fatal(err)
50 }
51 ready := os.NewFile(3, "ready")
52 hold := os.NewFile(4, "hold")
53 if _, err := ready.Write([]byte{1}); err != nil {
54 t.Fatal(err)
55 }
56 ready.Close()
57 if _, err := io.Copy(io.Discard, hold); err != nil {
58 t.Fatal(err)
59 }
60 hold.Close()
61 // Leave the bind in this private namespace for the parent's retained
62 // namespace mount. The parent removes that final reference after checking.
63 return
64 }
65 t.Setenv("CARLOS_OPT_DIR", dir)
66 for _, path := range []string{tree, alias} {
67 if err := os.Mkdir(path, 0755); err != nil {
68 t.Fatal(err)
69 }
70 }
71 if err := os.WriteFile(filepath.Join(tree, "artifact"), []byte("retained"), 0644); err != nil {
72 t.Fatal(err)
73 }
74 pins, certificate := generationNamespacePinFixture(t, dir)
75 if got, err := pins.StagedPins(t.Context(), dir); err != nil || len(got) != 0 {
76 t.Fatalf("initial quiescent pins: %v %v", got, err)
77 }
78 initial := certificate()
79 if !initial.Complete {
80 t.Fatal("initial completeness was not recorded")
81 }
82 readyR, readyW, err := os.Pipe()
83 if err != nil {
84 t.Fatal(err)
85 }
86 defer readyR.Close()
87 defer readyW.Close()
88 holdR, holdW, err := os.Pipe()
89 if err != nil {
90 t.Fatal(err)
91 }
92 defer holdR.Close()
93 defer holdW.Close()
94 child := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14GenerationKernelNamespaceMountRootRunner$", "-test.timeout=90s")
95 child.Env = append(os.Environ(), "PLAN14_E27_NAMESPACE_HOLDER=1")
96 child.Dir = dir
97 child.ExtraFiles = []*os.File{readyW, holdR}
98 child.SysProcAttr = &syscall.SysProcAttr{Cloneflags: unix.CLONE_NEWNS}
99 child.Stdout, child.Stderr = os.Stdout, os.Stderr
100 if err := child.Start(); err != nil {
101 t.Fatal(err)
102 }
103 joined := false
104 defer func() {
105 if !joined {
106 _ = child.Process.Kill()
107 _ = child.Wait()
108 }
109 }()
110 readyW.Close()
111 holdR.Close()
112 var ready [1]byte
113 if _, err := io.ReadFull(readyR, ready[:]); err != nil || ready[0] != 1 {
114 t.Fatalf("namespace holder did not establish the bind: %v", err)
115 }
116 namespace := filepath.Join(dir, "retained-namespace")
117 if err := os.WriteFile(namespace, nil, 0600); err != nil {
118 t.Fatal(err)
119 }
120 if err := unix.Mount(fmt.Sprintf("/proc/%d/ns/mnt", child.Process.Pid), namespace, "", unix.MS_BIND, ""); err != nil {
121 t.Fatal(err)
122 }
123 mounted := true
124 defer func() {
125 if mounted {
126 if err := unix.Unmount(namespace, 0); err != nil {
127 t.Error(err)
128 }
129 }
130 }()
131 holdW.Close()
132 err = child.Wait()
133 joined = true
134 if err != nil {
135 t.Fatal("namespace holder did not exit cleanly", err)
136 }
137 mounts, err := os.ReadFile("/proc/self/mountinfo")
138 if err != nil {
139 t.Fatal(err)
140 }
141 found := false
142 for _, line := range strings.Split(string(mounts), "\n") {
143 if strings.Contains(line, " "+namespace+" ") && strings.Contains(line, " - nsfs ") {
144 found = true
145 t.Logf("retained namespace after child exit: %s", line)
146 }
147 }
148 if !found {
149 t.Fatal("fixture did not retain an nsfs mount")
150 }
151 retained := exec.CommandContext(t.Context(), "nsenter", "--mount="+namespace, "/bin/cat", "/proc/self/mountinfo")
152 if out, err := retained.CombinedOutput(); err != nil || !strings.Contains(string(out), " "+alias+" ") {
153 t.Fatalf("retained namespace lost the staged bind: %v\n%s", err, out)
154 }
155 if got, err := pins.StagedPins(t.Context(), dir); err == nil || got != nil {
156 t.Fatalf("namespace retained by a mount granted pins: %v %v", got, err)
157 }
158 denied := certificate()
159 if denied.Complete || denied.Revision <= initial.Revision {
160 t.Fatal("namespace refusal left the previous certificate usable")
161 }
162 if err := unix.Unmount(namespace, 0); err != nil {
163 t.Fatal(err)
164 }
165 mounted = false
166 if got, err := pins.StagedPins(t.Context(), dir); err != nil || len(got) != 0 {
167 t.Fatalf("released namespace still refused: %v %v", got, err)
168 }
169 final := certificate()
170 if !final.Complete || final.Revision <= denied.Revision {
171 t.Fatal("released namespace did not obtain a fresh certificate")
172 }
173}
174
175func generationNamespacePinFixture(t *testing.T, dir string) (generationPinSource, func() release.GenerationCompleteness) {
176 t.Helper()
177 censusPath, fencesPath := filepath.Join(dir, "census"), filepath.Join(dir, "fences")
178 journalPath, generationsPath := filepath.Join(dir, "journals.db"), filepath.Join(dir, "generations")
179 for _, path := range []string{censusPath, fencesPath, generationsPath} {
180 state, err := rootstate.Open(path, true, false)
181 if err != nil {
182 t.Fatal(err)
183 }
184 if path == censusPath {
185 raw, err := json.Marshal(rootparticipation.Census{Schema: 1, Box: "runner", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
186 if err == nil {
187 err = state.Write("census.json", raw)
188 }
189 if err != nil {
190 state.Close()
191 t.Fatal(err)
192 }
193 }
194 state.Close()
195 }
196 journal, err := hostbox.OpenLaunchJournal(journalPath)
197 if err != nil {
198 t.Fatal(err)
199 }
200 _, err = hostbox.NewJournalService(journal)
201 journal.Close()
202 if err != nil {
203 t.Fatal(err)
204 }
205 open := func() (rootstate.Store, error) { return rootstate.Open(generationsPath, false, false) }
206 v := generationReconciler{
207 read: func(ctx context.Context) (generationInventory, error) {
208 return generationInventoryAt(ctx, censusPath, fencesPath, journalPath)
209 },
210 kernel: productionGenerationKernelCensus,
211 }
212 source := generationPinSource{open: open, retire: productionRetireGenerations, reconcile: v.reconcile}
213 return source, func() release.GenerationCompleteness {
214 t.Helper()
215 state, err := open()
216 if err != nil {
217 t.Fatal(err)
218 }
219 defer state.Close()
220 c, err := (&release.RootGenerations{Store: state}).Completeness()
221 if err != nil {
222 t.Fatal(err)
223 }
224 return c
225 }
226}
227