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 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/rootstate"
15 "golang.org/x/sys/unix"
16)
17
18const claimUnitRebootDir = "/var/lib/carlos-e51-unit-reboot"
19
20func claimUnitRebootFixture(t *testing.T, create bool) (claimUnitMaterializer, claimUnitBinding, claimUnitFiles) {
21 t.Helper()
22 dir := claimUnitRebootDir
23 if create {
24 if err := os.Mkdir(dir, 0755); err != nil {
25 t.Fatal(err)
26 }
27 }
28 state, err := rootstate.Open(filepath.Join(dir, "records"), create, false)
29 if err != nil {
30 t.Fatal(err)
31 }
32 t.Cleanup(func() { state.Close() })
33 if create {
34 if err := os.Mkdir(filepath.Join(dir, "units"), 0755); err != nil {
35 t.Fatal(err)
36 }
37 }
38 files := claimUnitFiles{dir: filepath.Join(dir, "units")}
39 m := claimUnitMaterializer{state: state, read: files.read, create: files.create, replace: files.replace, idle: func(string) error { return nil }}
40 _, in, s := plan14UnitBindingFixture(t, "plan14-reboot.service", "", "")
41 s.Program = strings.Repeat("a", 64)
42 b := claimUnitBinding{Schema: 1, Owner: unitBindingOwner(in), Shape: s}
43 return m, b, files
44}
45
46func TestRootRebootClaimUnitDevicePhase1(t *testing.T) {
47 m, b, _ := claimUnitRebootFixture(t, true)
48 if err := m.install(t.Context(), b); err != nil {
49 t.Fatal(err)
50 }
51 r, err := m.load(b.Owner.Host)
52 if err != nil || r.Identity.Handle == "" {
53 t.Fatal("persistent unit identity", r, err)
54 }
55 raw, _ := json.Marshal(r.Identity)
56 if err := os.WriteFile(filepath.Join(claimUnitRebootDir, "before.json"), raw, 0600); err != nil {
57 t.Fatal(err)
58 }
59 dir, err := os.Open(filepath.Join(claimUnitRebootDir, "units"))
60 if err != nil {
61 t.Fatal(err)
62 }
63 treeProof, err := rootstate.PersistentIdentity(dir)
64 dir.Close()
65 if err != nil {
66 t.Fatal(err)
67 }
68 binding := release.GenerationBinding{Path: filepath.Join(claimUnitRebootDir, "units", b.Shape.Unit), Tree: filepath.Join(claimUnitRebootDir, "units"), Executable: rootstate.FileIdentity{Device: r.Identity.Device, Inode: r.Identity.Inode}, TreeIdentity: rootstate.FileIdentity{Device: treeProof.Device, Inode: treeProof.Inode}, ExecutableProof: r.Identity, TreeProof: treeProof, Target: release.GenerationOwner{BootID: r.Identity.Boot}}
69 raw, _ = json.Marshal(binding)
70 if err := os.WriteFile(filepath.Join(claimUnitRebootDir, "generation.json"), raw, 0600); err != nil {
71 t.Fatal(err)
72 }
73 t.Logf("before boot=%s device=%d mount=%d", r.Identity.Boot, r.Identity.Device, r.Identity.Mount)
74 claimRebootPredecessorPhase1(t)
75}
76
77func TestRootRebootClaimUnitDevicePhase2(t *testing.T) {
78 m, b, files := claimUnitRebootFixture(t, false)
79 t.Cleanup(func() { os.RemoveAll(claimUnitRebootDir) })
80 var before rootstate.FileIdentity
81 raw, err := os.ReadFile(filepath.Join(claimUnitRebootDir, "before.json"))
82 if err != nil || json.Unmarshal(raw, &before) != nil {
83 t.Fatal(err)
84 }
85 _, current, err := files.read(b.Shape.Unit)
86 if err != nil || before.Boot == current.Boot {
87 t.Fatal("actual reboot required", err)
88 }
89 t.Logf("after boot=%s device=%d mount=%d", current.Boot, current.Device, current.Mount)
90 claimRebootPredecessorPhase2(t)
91 if err = m.verify(b); err != nil {
92 t.Fatal("persistent unit rejected after reboot", err)
93 }
94 r, err := m.load(b.Owner.Host)
95 if err != nil {
96 t.Fatal(err)
97 }
98 // Force numerical drift too, even if this reboot reused a number.
99 r.Identity.Device = current.Device + 1
100 r.Identity.Mount = current.Mount + 1
101 if err = m.save(r); err != nil {
102 t.Fatal(err)
103 }
104 if err = m.verify(b); err != nil {
105 t.Fatal("boot-local numbers treated as persistent", err)
106 }
107 var binding release.GenerationBinding
108 raw, err = os.ReadFile(filepath.Join(claimUnitRebootDir, "generation.json"))
109 if err != nil || json.Unmarshal(raw, &binding) != nil {
110 t.Fatal(err)
111 }
112 binding.Executable.Device = current.Device + 1
113 binding.TreeIdentity.Device = current.Device + 1
114 binding.ExecutableProof.Device = current.Device + 1
115 binding.TreeProof.Device = current.Device + 1
116 local, err := generationCurrentFiles(binding, rootstate.OpenTrustedFile)
117 if err != nil || local.Executable.Device != current.Device || local.TreeIdentity.Device != current.Device {
118 t.Fatal("generation remapping", local, err)
119 }
120 if _, err = generationTree(t.Context(), local); err != nil {
121 t.Fatal("generation tree census", err)
122 }
123 mounts, err := os.ReadFile("/proc/self/mountinfo")
124 if err != nil {
125 t.Fatal(err)
126 }
127 if err = generationMounts(mounts, local); err != nil {
128 t.Fatal("generation mount census", err)
129 }
130 // Same filesystem, same inode through a substituted bind mount still refuses.
131 unitPath := filepath.Join(files.dir, b.Shape.Unit)
132 if err = unix.Mount(unitPath, unitPath, "", unix.MS_BIND, ""); err != nil {
133 t.Fatal(err)
134 }
135 rejected := m.verify(b)
136 _, generationRejected := generationCurrentFiles(binding, rootstate.OpenTrustedFile)
137 if generationRejected == nil {
138 t.Error("generation accepted substituted mount")
139 }
140 unmount := unix.Unmount(unitPath, 0)
141 if rejected == nil || unmount != nil {
142 t.Fatal("substituted mount", rejected, unmount)
143 }
144 // A different filesystem cannot use identical file bytes as identity.
145 original, _, err := files.read(b.Shape.Unit)
146 if err != nil {
147 t.Fatal(err)
148 }
149 if err = unix.Mount("tmpfs", files.dir, "tmpfs", 0, "mode=0755"); err != nil {
150 t.Fatal(err)
151 }
152 writeErr := os.WriteFile(unitPath, original, 0644)
153 rejected = m.verify(b)
154 unmount = unix.Unmount(files.dir, 0)
155 if writeErr != nil || rejected == nil || unmount != nil {
156 t.Fatal("substituted filesystem", writeErr, rejected, unmount)
157 }
158 if err = m.verify(b); err != nil {
159 t.Fatal("restored unit", err)
160 }
161 if err = m.retire(t.Context(), b, func() error { return nil }); err != nil {
162 t.Fatal("cross-boot retirement", err)
163 }
164 old := b
165 old.Retiring = true
166 old.Retired = true
167 next := b
168 next.Owner.Stamp++
169 if err = m.handover(t.Context(), old, next); err != nil {
170 t.Fatal("successor handover", err)
171 }
172 if err = m.verify(next); err != nil {
173 t.Fatal(err)
174 }
175 if err = m.retire(context.Background(), b, func() error { return nil }); err == nil {
176 t.Fatal("old owner retired successor")
177 }
178}
179