//go:build unix package host import ( "bytes" "errors" "go/ast" "go/parser" "go/token" "os" "os/user" "path/filepath" "strings" "testing" ) func TestRootRegistryRefusesPlantedFilesBeforeSQLite(t *testing.T) { u, err := user.Current() if err != nil { t.Fatal(err) } if u.Uid == "0" { t.Skip("this test simulates root repair with the current non-root service identity; real root credentials have separate coverage") } for _, name := range []string{"symlink", "dangling-symlink", "hardlink-wal", "hardlink-shm", "symlink-parent"} { t.Run(name, func(t *testing.T) { dir := t.TempDir() victim := filepath.Join(dir, "victim") data := filepath.Join(dir, "data") if err := os.Mkdir(data, 0700); err != nil { t.Fatal(err) } path := filepath.Join(data, "registry.db") if name != "dangling-symlink" { if err := os.WriteFile(victim, nil, 0640); err != nil { t.Fatal(err) } } switch name { case "symlink", "dangling-symlink": err = os.Symlink(victim, path) case "hardlink-wal": err = os.Link(victim, path+"-wal") case "hardlink-shm": err = os.Link(victim, path+"-shm") case "symlink-parent": alias := filepath.Join(dir, "alias") err = os.Symlink(data, alias) path = filepath.Join(alias, "registry.db") } if err != nil { t.Fatal(err) } before, beforeErr := os.Stat(victim) logged := captureLog(t) reg, openErr := openRegistryConverging(0, u.Username, path) if reg != nil { reg.Close() t.Fatal("unsafe registry reached SQLite") } if openErr == nil { t.Fatal("unsafe registry accepted") } if !strings.Contains(strings.ToLower(logged()), "alert") { t.Errorf("refused file had no alert: %s", logged()) } after, afterErr := os.Stat(victim) if errors.Is(beforeErr, os.ErrNotExist) { if !errors.Is(afterErr, os.ErrNotExist) { t.Fatalf("dangling link target created: %v", afterErr) } } else { if afterErr != nil { t.Fatal(afterErr) } beforeUID, beforeGID, _ := fileOwner(before) afterUID, afterGID, _ := fileOwner(after) if beforeUID != afterUID || beforeGID != afterGID || before.Mode() != after.Mode() || before.ModTime() != after.ModTime() { t.Fatal("victim ownership, mode or modification time changed") } contents, err := os.ReadFile(victim) if err != nil || !bytes.Equal(contents, []byte{}) { t.Fatalf("victim changed: %q, %v", contents, err) } } for _, suffix := range []string{"-wal", "-shm", "-journal"} { if _, err := os.Lstat(victim + suffix); !errors.Is(err, os.ErrNotExist) { t.Fatalf("SQLite created victim sidecar %s: %v", suffix, err) } } }) } } func TestPrivilegedRegistryEntryPointsUseServiceBoundary(t *testing.T) { for _, tc := range []struct{ file, function, opener string }{ {"agent.go", "runAdoptWithEnsureSystemUnits", "withClaimRootRelease"}, {"agent.go", "runAdoptUnderRootRelease", "openServiceRegistry"}, {"systemapply.go", "openSystemRegistry", "openServiceRegistry"}, {"claimstop_run.go", "RunInstanceControl", "openServiceRegistry"}, {"claimstop_run.go", "instanceRegistryHostApp", "openServiceRegistryReadOnly"}, {"rootgeneration_rows.go", "productionGenerationRegistryInventory", "openServiceRegistryReadOnly"}, {"claimjournal_migration.go", "RunSystemJournalMigrate", "openServiceRegistry"}, } { t.Run(tc.function, func(t *testing.T) { f, err := parser.ParseFile(token.NewFileSet(), tc.file, nil, 0) if err != nil { t.Fatal(err) } found := false for _, decl := range f.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok || fn.Name.Name != tc.function { continue } ast.Inspect(fn.Body, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } if id, ok := call.Fun.(*ast.Ident); ok && id.Name == tc.opener { found = true } if sel, ok := call.Fun.(*ast.SelectorExpr); ok && (sel.Sel.Name == "Open" || sel.Sel.Name == "OpenReadOnly") { if id, ok := sel.X.(*ast.Ident); ok && id.Name == "registry" { t.Error("privileged caller directly opens SQLite") } } return true }) } if !found { t.Fatal("privileged caller does not cross the service registry boundary") } }) } } func TestStagedExecutableRegistryLookupCannotOpenSQLite(t *testing.T) { entries, err := os.ReadDir("../instancectl") if err != nil { t.Fatal(err) } for _, entry := range entries { if !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") { continue } f, err := parser.ParseFile(token.NewFileSet(), filepath.Join("../instancectl", entry.Name()), nil, parser.ImportsOnly) if err != nil { t.Fatal(err) } for _, imp := range f.Imports { if imp.Path.Value == `"database/sql"` || strings.Contains(imp.Path.Value, "sqlite") { t.Errorf("instancectl/%s imports SQLite instead of receiving the service registry view", entry.Name()) } } } }