//go:build linux package host import ( "context" "database/sql" "encoding/json" "errors" "net" "os" "path/filepath" "strconv" "strings" "time" "github.com/benbjohnson/litestream" "github.com/carlosframework/platform/internal/dbreads" ) // dbreadTestMarkers holds marker files the root-runner tests drop to change // one thing about the worker. The unit can read /run outside /run/carlos. const dbreadTestMarkers = "/run/carlos-dbread-test" func dbreadMarker(name string) bool { _, err := os.Stat(filepath.Join(dbreadTestMarkers, name)) return err == nil } // dbreadsTestWorker is this test binary acting as /opt/carlos/carlos system // db-worker inside the installed carlos-dbread@ unit. Without a marker it is // the real worker (dbreads.RunWorkerWith with RunWorker's options). func dbreadsTestWorker() { opt := dbreads.WorkerOptions{Scratch: "/var/tmp"} switch { case dbreadMarker("exit-1"): os.Exit(1) case dbreadMarker("witness"): dbreadsWitness() return } if dbreadMarker("version-2") { opt.Self = func() dbreads.HelloReply { h := dbreads.SelfView(); h.V = 2; return h } } if dbreadMarker("hang-restore") { // A restore that ignores its context, as litestream's compactor // does (spec ยง8.3): only the 5 s exit ends it. opt.Restore = func(context.Context, litestream.ReplicaClient, string, uint64) error { time.Sleep(time.Hour) return nil } } dbreads.RunWorkerWith(opt) } // dbreadsWitness is T3 from inside the unit: everything a member's SQL could // reach for if SQLite let it, tried directly. It reports each attempt's // error ("" for success) as one JSON object on the connection. func dbreadsWitness() { conn, err := dbreads.StdinConn(os.Stdin, dbreads.CarlosUID) if err != nil { os.Exit(2) } defer conn.Close() report := map[string]string{} try := func(name string, f func() error) { if err := f(); err != nil { report[name] = err.Error() } else { report[name] = "" } } open := func(p string) func() error { return func() error { f, err := os.Open(p) if err == nil { f.Close() } return err } } dial := func(addr string) func() error { return func() error { c, err := net.DialTimeout("tcp", addr, 2*time.Second) if err == nil { c.Close() } return err } } write := func(p string) func() error { return func() error { return os.WriteFile(p, []byte("x"), 0o600) } } try("open /data/registry.db", open("/data/registry.db")) try("open /etc/carlos/host.env", open("/etc/carlos/host.env")) try("open another instance", open("/data/instances/other.example.com/other.db")) try("connect the metadata service", dial("169.254.169.254:80")) try("connect a TCP address", dial("1.1.1.1:443")) // Network-namespace evidence that needs no socket (plan review finding // 17): with RestrictAddressFamilies=AF_UNIX the TCP dials above fail // even without PrivateNetwork, so they cannot witness it. This can: it // "succeeds" (and so fails the test) when any interface but loopback is // visible, which is the case the moment PrivateNetwork is removed. try("see an interface other than loopback", func() error { raw, err := os.ReadFile("/proc/self/net/dev") if err != nil { return nil // unobservable counts as reachable: the test fails } ifs := strings.Join(dbreadsParseNetDev(string(raw)), ",") if ifs == "lo" { return errors.New("only lo") } return nil }) try("write /opt/carlos", write("/opt/carlos/witness")) try("write /var/lib", write("/var/lib/witness")) try("write /etc", write("/etc/witness")) try("write /var/tmp", write("/var/tmp/witness")) // the positive control // SQLite with the attach limit removed: the mutation. Only the sandbox // stands between these statements and the box. The attached targets are // real SQLite databases the runner wrote on the host, so a statement // fails only because the file cannot be reached, never because it is // not a database. sqlTry := func(name, q string) { try(name, func() error { db, err := sql.Open("sqlite", "file:/var/tmp/witness.db") if err != nil { return err } defer db.Close() _, err = db.Exec(q) return err }) } sqlTry("attach /data/registry.db", "ATTACH DATABASE '/data/registry.db' AS r; SELECT count(*) FROM r.sqlite_master") sqlTry("attach /etc/carlos/witness.db", "ATTACH DATABASE '/etc/carlos/witness.db' AS e; SELECT count(*) FROM e.sqlite_master") sqlTry("vacuum into /opt/carlos", "VACUUM INTO '/opt/carlos/copy.db'") sqlTry("load_extension", "SELECT load_extension('/lib/x86_64-linux-gnu/libc.so.6')") json.NewEncoder(conn).Encode(report) } // dbreadsParseNetDev is dbreads.parseNetDev, restated: that one is // unexported in another package. func dbreadsParseNetDev(s string) []string { var out []string for i, line := range strings.Split(s, "\n") { if i < 2 { continue } if name, _, ok := strings.Cut(line, ":"); ok && strings.TrimSpace(name) != "" { out = append(out, strings.TrimSpace(name)) } } return out } // dbreadsTenantWitness is this test binary as a tenant inside the installed // tenant unit (TestRootDBReadTenantCannotReachTheWorker): it tries every way // to the db-read worker a tenant running as carlos might have, and writes // what happened ("" for success) to its working directory. func dbreadsTenantWitness() { report := map[string]string{"uid": strconv.Itoa(os.Getuid())} for _, p := range []string{dbReadSocketPath, "/run/carlos-dbread/worker.sock"} { c, err := net.DialTimeout("unix", p, 2*time.Second) if err == nil { c.Close() report["connect "+p] = "" } else { report["connect "+p] = err.Error() } } b, _ := json.Marshal(report) if err := os.WriteFile("dbread-witness.json", b, 0o600); err != nil { os.Exit(1) } }