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
2
3package hostbox
4
5import (
6 "crypto/sha256"
7 "database/sql"
8 "encoding/hex"
9 "errors"
10 "fmt"
11 "net/url"
12 "os"
13 "path/filepath"
14 "strings"
15 "syscall"
16 "testing"
17 "time"
18)
19
20// plan9Fix2Schema rewrites a legacy file's schema on one connection, as an
21// agent holding the file could.
22func plan9Fix2Schema(t *testing.T, path string, stmts ...string) {
23 t.Helper()
24 db, err := sql.Open("sqlite", "file:"+url.PathEscape(path)+"?_pragma=busy_timeout(5000)")
25 if err != nil {
26 t.Fatal(err)
27 }
28 defer db.Close()
29 db.SetMaxOpenConns(1)
30 for _, stmt := range append([]string{"PRAGMA writable_schema=ON"}, stmts...) {
31 if _, err = db.Exec(stmt); err != nil {
32 t.Fatal(stmt, err)
33 }
34 }
35}
36
37// I2-1: no expression in an agent-supplied schema is evaluated. A CHECK that
38// would allocate beyond SQLite's limit proves it: evaluating it fails with
39// "string or blob too big", so the refusal must come from the schema
40// allowlist, before integrity_check reads a row.
41func TestPlan9Fix2MigrationRefusesExpensiveCheckBeforeEvaluation(t *testing.T) {
42 s, _, in := journalFixture(t)
43 legacy := plan9Fix1Legacy(t, in)
44 plan9Fix2Schema(t, filepath.Join(legacy, "launches.db"),
45 "UPDATE sqlite_schema SET sql='CREATE TABLE launches (identity TEXT PRIMARY KEY, body BLOB NOT NULL, CHECK(length(hex(zeroblob(2000000000)))>0))' WHERE name='launches'")
46 err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept)
47 if !errors.Is(err, ErrTypedJournal) {
48 t.Fatalf("planted CHECK accepted: %v", err)
49 }
50 if strings.Contains(err.Error(), "too big") {
51 t.Fatalf("planted CHECK was evaluated before the schema was refused: %v", err)
52 }
53}
54
55// I2-1 and m-6: a table entry with root page zero is a virtual table or view
56// whatever its text says; it is refused even when the text matches.
57func TestPlan9Fix2MigrationRefusesRootpageZero(t *testing.T) {
58 s, _, in := journalFixture(t)
59 legacy := plan9Fix1Legacy(t, in)
60 plan9Fix2Schema(t, filepath.Join(legacy, "inventory.db"), "UPDATE sqlite_schema SET rootpage=0 WHERE name='journal_inventory_versions'")
61 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); !errors.Is(err, ErrTypedJournal) {
62 t.Fatalf("root page zero accepted: %v", err)
63 }
64 if got, err := s.Launches.HasHost(t.Context(), in.Target.Host); err != nil || got {
65 t.Fatalf("import committed: %v %v", got, err)
66 }
67}
68
69// I2-1: each allowed table must carry exactly the text the legacy code wrote.
70func TestPlan9Fix2MigrationRefusesAlteredTableText(t *testing.T) {
71 s, _, in := journalFixture(t)
72 legacy := plan9Fix1Legacy(t, in)
73 plan9Fix2Schema(t, filepath.Join(legacy, "launches.db"),
74 "UPDATE sqlite_schema SET sql='CREATE TABLE launches (identity TEXT PRIMARY KEY, body BLOB NOT NULL )' WHERE name='launches'")
75 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); !errors.Is(err, ErrTypedJournal) {
76 t.Fatalf("altered table text accepted: %v", err)
77 }
78}
79
80// m-8: the marker binds content. A metadata-only change (ctime, mtime) to an
81// imported legacy file needs neither quiescence nor a refusal.
82func TestPlan9Fix2MarkerIgnoresMetadataOnlyChange(t *testing.T) {
83 s, _, in := journalFixture(t)
84 legacy := plan9Fix1Legacy(t, in)
85 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); err != nil {
86 t.Fatal(err)
87 }
88 later := time.Now().Add(time.Hour)
89 for _, name := range []string{"launches.db", "inventory.db"} {
90 path := filepath.Join(legacy, name)
91 if err := os.Chtimes(path, later, later); err != nil {
92 t.Fatal(err)
93 }
94 if err := os.Chmod(path, 0640); err != nil {
95 t.Fatal(err)
96 }
97 }
98 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", nil, nil); err != nil {
99 t.Fatalf("metadata-only change refused: %v", err)
100 }
101}
102
103// m-8: a marker written before the marker had a binding (4f342ec7, files and
104// rows both empty after the column upgrade) is accepted when every legacy row
105// is one root already holds; the binding is then recorded, and later legacy
106// writes are refused as before.
107func TestPlan9Fix2MigrationAcceptsUnboundMarker(t *testing.T) {
108 s, _, in := journalFixture(t)
109 legacy := plan9Fix1Legacy(t, in)
110 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); err != nil {
111 t.Fatal(err)
112 }
113 if _, err := s.Launches.db.Exec("UPDATE journal_migration SET files='', rows=''"); err != nil {
114 t.Fatal(err)
115 }
116 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); err != nil {
117 t.Fatalf("unbound marker refused: %v", err)
118 }
119 var files, rows string
120 if err := s.Launches.db.QueryRow("SELECT files,rows FROM journal_migration").Scan(&files, &rows); err != nil || files == "" || rows == "" {
121 t.Fatalf("binding not recorded: %q %q %v", files, rows, err)
122 }
123 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", nil, nil); err != nil {
124 t.Fatalf("bound marker refused: %v", err)
125 }
126 old, err := OpenLaunchJournal(filepath.Join(legacy, "launches.db"))
127 if err != nil {
128 t.Fatal(err)
129 }
130 second := in
131 second.Target.Session = 2
132 if err = old.Create(t.Context(), second); err != nil {
133 t.Fatal(err)
134 }
135 old.Close()
136 if err = s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); !errors.Is(err, ErrChanged) {
137 t.Fatalf("post-marker legacy write accepted: %v", err)
138 }
139}
140
141// m-8: an unbound marker is not a licence to skip history root never held.
142func TestPlan9Fix2UnboundMarkerRefusesRowsRootNeverHeld(t *testing.T) {
143 s, _, in := journalFixture(t)
144 legacy := plan9Fix1Legacy(t, in)
145 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); err != nil {
146 t.Fatal(err)
147 }
148 if _, err := s.Launches.db.Exec("UPDATE journal_migration SET files='', rows=''"); err != nil {
149 t.Fatal(err)
150 }
151 old, err := OpenLaunchJournal(filepath.Join(legacy, "launches.db"))
152 if err != nil {
153 t.Fatal(err)
154 }
155 second := in
156 second.Target.Session = 2
157 if err = old.Create(t.Context(), second); err != nil {
158 t.Fatal(err)
159 }
160 old.Close()
161 if err = s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); !errors.Is(err, ErrChanged) {
162 t.Fatalf("unbound marker skipped unheld history: %v", err)
163 }
164}
165
166// m-8: a marker from 618f3a8a binds device, inode and times beside the
167// digest. Only its digests are compared.
168func TestPlan9Fix2MigrationAcceptsMetadataBoundMarker(t *testing.T) {
169 s, _, in := journalFixture(t)
170 legacy := plan9Fix1Legacy(t, in)
171 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); err != nil {
172 t.Fatal(err)
173 }
174 var parts []string
175 for _, name := range []string{"launches.db", "launches.db-wal", "inventory.db", "inventory.db-wal"} {
176 raw, err := os.ReadFile(filepath.Join(legacy, name))
177 if errors.Is(err, os.ErrNotExist) {
178 parts = append(parts, name+":-")
179 continue
180 }
181 if err != nil {
182 t.Fatal(err)
183 }
184 st, _ := os.Stat(filepath.Join(legacy, name))
185 sys := st.Sys().(*syscall.Stat_t)
186 parts = append(parts, fmt.Sprintf("%s:%d:%d:%d:%d.%d:%d.%d:%s", name, sys.Dev, sys.Ino, st.Size(), sys.Mtim.Sec, sys.Mtim.Nsec, sys.Ctim.Sec+1, sys.Ctim.Nsec, plan9Fix2Digest(raw)))
187 }
188 if _, err := s.Launches.db.Exec("UPDATE journal_migration SET files=?", strings.Join(parts, "/")); err != nil {
189 t.Fatal(err)
190 }
191 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", nil, nil); err != nil {
192 t.Fatalf("618f3a8a marker with unchanged content refused: %v", err)
193 }
194}
195
196// m-10: the imported rows come from exactly the bytes the marker names. A
197// legacy file swapped for the copy and restored afterwards must refuse.
198func TestPlan9Fix2MigrationBindsCopiedBytes(t *testing.T) {
199 s, _, in := journalFixture(t)
200 legacy := plan9Fix1Legacy(t, in)
201 other := plan9Fix1Legacy(t, in)
202 second := in
203 second.Target.Session = 2
204 old, err := OpenLaunchJournal(filepath.Join(other, "launches.db"))
205 if err != nil {
206 t.Fatal(err)
207 }
208 if err = old.Create(t.Context(), second); err != nil {
209 t.Fatal(err)
210 }
211 old.Close()
212 original, err := os.ReadFile(filepath.Join(legacy, "launches.db"))
213 if err != nil {
214 t.Fatal(err)
215 }
216 swapped, err := os.ReadFile(filepath.Join(other, "launches.db"))
217 if err != nil {
218 t.Fatal(err)
219 }
220 restore := legacyCopyHook
221 t.Cleanup(func() { legacyCopyHook = restore })
222 legacyCopyHook = func(name string, copied bool) {
223 if name != "launches.db" {
224 return
225 }
226 body := swapped
227 if copied {
228 body = original
229 }
230 if err := os.WriteFile(filepath.Join(legacy, name), body, 0600); err != nil {
231 t.Error(err)
232 }
233 }
234 if err = s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); !errors.Is(err, ErrChanged) {
235 t.Fatalf("copy of different bytes imported: %v", err)
236 }
237 if _, err = s.Launches.Read(t.Context(), second.Target); !errors.Is(err, sql.ErrNoRows) {
238 t.Fatalf("swapped row imported: %v", err)
239 }
240}
241
242// I2-3: after the marker, legacy files under the agent's directory never stop
243// root's startup. Planted files are ignored and reported; nothing is quiesced.
244func TestPlan9Fix2StartupIgnoresLegacyFilesPlantedAfterMarker(t *testing.T) {
245 s, _, in := journalFixture(t)
246 legacy := t.TempDir()
247 if alert, err := s.CheckLegacyMigration(t.Context(), legacy); err != nil || alert != nil {
248 t.Fatalf("fresh startup: %v %v", alert, err)
249 }
250 // The agent plants a full legacy pair after the marker.
251 planted := plan9Fix1Legacy(t, in)
252 for _, name := range []string{"launches.db", "inventory.db"} {
253 raw, err := os.ReadFile(filepath.Join(planted, name))
254 if err != nil {
255 t.Fatal(err)
256 }
257 if err = os.WriteFile(filepath.Join(legacy, name), raw, 0600); err != nil {
258 t.Fatal(err)
259 }
260 }
261 alert, err := s.CheckLegacyMigration(t.Context(), legacy)
262 if err != nil {
263 t.Fatalf("planted legacy files stopped root: %v", err)
264 }
265 if alert == nil {
266 t.Fatal("planted legacy files not reported")
267 }
268 if got, err := s.Launches.HasHost(t.Context(), in.Target.Host); err != nil || got {
269 t.Fatalf("planted rows imported: %v %v", got, err)
270 }
271 // Garbage and special files are ignored the same way.
272 if err = os.Remove(filepath.Join(legacy, "launches.db")); err != nil {
273 t.Fatal(err)
274 }
275 if err = syscall.Mkfifo(filepath.Join(legacy, "launches.db"), 0600); err != nil {
276 t.Fatal(err)
277 }
278 if alert, err = s.CheckLegacyMigration(t.Context(), legacy); err != nil || alert == nil {
279 t.Fatalf("planted fifo: %v %v", alert, err)
280 }
281}
282
283// I2-3: unmigrated legacy history is not silently dropped: startup refuses
284// until the operator runs the offline migration.
285func TestPlan9Fix2StartupRefusesUnmigratedLegacy(t *testing.T) {
286 s, _, in := journalFixture(t)
287 legacy := plan9Fix1Legacy(t, in)
288 if _, err := s.CheckLegacyMigration(t.Context(), legacy); !errors.Is(err, ErrLegacyJournalMigration) {
289 t.Fatalf("unmigrated legacy journals accepted at startup: %v", err)
290 }
291 var n int
292 if err := s.Launches.db.QueryRow("SELECT count(*) FROM journal_migration").Scan(&n); err != nil || n != 0 {
293 t.Fatalf("marker written over unmigrated history: %d %v", n, err)
294 }
295 if err := s.MigrateLegacy(t.Context(), legacy, "box", "enrollment", plan9Fix1Freeze, plan9Fix1Accept); err != nil {
296 t.Fatal(err)
297 }
298 if alert, err := s.CheckLegacyMigration(t.Context(), legacy); err != nil || alert != nil {
299 t.Fatalf("migrated startup: %v %v", alert, err)
300 }
301}
302
303func plan9Fix2Digest(raw []byte) string {
304 sum := sha256.Sum256(raw)
305 return hex.EncodeToString(sum[:])
306}
307