oficina / home Public

Verify operator SQLite connections against retained file identity

Paul Campbell pushed by paul@keymail.dev a6057b12132ce352b925c1058bf72c51c32ac6da parent 9a6d481
2 files changed, +884 −0
  • internal/operator/open_linux.go +481 −0
  • internal/operator/open_linux_test.go +403 −0

CI passed — run details

diff --git a/internal/operator/open_linux.go b/internal/operator/open_linux.go
new file mode 100644
index 0000000..08546ae
--- /dev/null
+++ b/internal/operator/open_linux.go
@@ -0,0 +1,481 @@
+//go:build linux
+
+package operator
+
+import (
+ "context"
+ "database/sql"
+ "database/sql/driver"
+ "io"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/sys/unix"
+ "modernc.org/sqlite"
+)
+
+type fileIdentity struct{ device, inode uint64 }
+type descriptor struct {
+ identity fileIdentity
+ flags int
+ links uint64
+}
+type boundDatabase struct {
+ db *sql.DB
+ connector *boundConnector
+}
+type boundConnector struct {
+ mu sync.Mutex
+ path string
+ pin *os.File
+ identity fileIdentity
+ deadline time.Time
+ sqlite *sqlite.Driver
+ active bool
+ hook func(string)
+}
+type boundConnection struct {
+ driver.Conn
+ owner *boundConnector
+ mainFD int
+ siblings map[string]fileIdentity
+ closed bool
+}
+
+// newBoundDatabase proves file identity only. The unmounted command adapter must
+// first establish the separately verified launch context and signed authority.
+func newBoundDatabase(path string, device, inode uint64, deadline time.Time) (*boundDatabase, error) {
+ if device == 0 || inode == 0 || !deadline.After(time.Now()) {
+ return nil, ErrRefused
+ }
+ pin, id, e := pinExisting(path)
+ if e != nil {
+ return nil, e
+ }
+ if id != (fileIdentity{device, inode}) {
+ pin.Close()
+ return nil, ErrRefused
+ }
+ c := &boundConnector{path: path, pin: pin, identity: id, deadline: deadline, sqlite: &sqlite.Driver{}}
+ db := sql.OpenDB(c)
+ db.SetMaxOpenConns(1)
+ db.SetMaxIdleConns(1)
+ return &boundDatabase{db, c}, nil
+}
+func (b *boundDatabase) close() error {
+ e := b.db.Close()
+ b.connector.mu.Lock()
+ defer b.connector.mu.Unlock()
+ if b.connector.active {
+ return ErrUnavailable
+ }
+ if x := b.connector.pin.Close(); e == nil {
+ e = x
+ }
+ return e
+}
+func pinExisting(path string) (*os.File, fileIdentity, error) {
+ if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" || len(path) > 1024 || strings.ContainsRune(path, 0) {
+ return nil, fileIdentity{}, ErrRefused
+ }
+ fd, e := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
+ if e != nil {
+ return nil, fileIdentity{}, ErrUnavailable
+ }
+ parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
+ for i, part := range parts {
+ flags := unix.O_RDONLY | unix.O_NOFOLLOW | unix.O_CLOEXEC | unix.O_NONBLOCK
+ if i < len(parts)-1 {
+ flags |= unix.O_DIRECTORY
+ }
+ next, err := unix.Openat(fd, part, flags, 0)
+ unix.Close(fd)
+ if err != nil {
+ return nil, fileIdentity{}, ErrRefused
+ }
+ fd = next
+ }
+ var s unix.Stat_t
+ if unix.Fstat(fd, &s) != nil {
+ unix.Close(fd)
+ return nil, fileIdentity{}, ErrUnavailable
+ }
+ if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Nlink != 1 || s.Dev == 0 || s.Ino == 0 {
+ unix.Close(fd)
+ return nil, fileIdentity{}, ErrRefused
+ }
+ return os.NewFile(uintptr(fd), path), fileIdentity{uint64(s.Dev), s.Ino}, nil
+}
+
+// statExisting does not open the regular file: closing a second descriptor for
+// a SQLite inode would release this process's POSIX locks on that inode.
+func statExisting(path string) (fileIdentity, error) {
+ if !filepath.IsAbs(path) || filepath.Clean(path) != path || path == "/" || len(path) > 1024 || strings.ContainsRune(path, 0) {
+ return fileIdentity{}, ErrRefused
+ }
+ fd, e := unix.Open("/", unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
+ if e != nil {
+ return fileIdentity{}, ErrUnavailable
+ }
+ parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
+ for _, part := range parts[:len(parts)-1] {
+ next, err := unix.Openat(fd, part, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ unix.Close(fd)
+ if err != nil {
+ return fileIdentity{}, ErrRefused
+ }
+ fd = next
+ }
+ defer unix.Close(fd)
+ var s unix.Stat_t
+ if unix.Fstatat(fd, parts[len(parts)-1], &s, unix.AT_SYMLINK_NOFOLLOW) != nil {
+ return fileIdentity{}, ErrRefused
+ }
+ if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Nlink != 1 || s.Dev == 0 || s.Ino == 0 {
+ return fileIdentity{}, ErrRefused
+ }
+ return fileIdentity{uint64(s.Dev), s.Ino}, nil
+}
+func descriptors() (map[int]descriptor, error) {
+ directory, e := os.Open("/proc/self/fd")
+ if e != nil {
+ return nil, ErrUnavailable
+ }
+ defer directory.Close()
+ entries, e := directory.Readdirnames(-1)
+ if e != nil {
+ return nil, ErrUnavailable
+ }
+ out := map[int]descriptor{}
+ for _, entry := range entries {
+ fd, e := strconv.Atoi(entry)
+ if e != nil {
+ return nil, ErrRefused
+ }
+ var s unix.Stat_t
+ if fd == int(directory.Fd()) {
+ continue
+ }
+ if e = unix.Fstat(fd, &s); e != nil {
+ return nil, ErrRefused
+ }
+ if s.Mode&unix.S_IFMT != unix.S_IFREG {
+ continue
+ }
+ flags, e := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0)
+ if e != nil {
+ return nil, ErrUnavailable
+ }
+ out[fd] = descriptor{fileIdentity{uint64(s.Dev), s.Ino}, flags, s.Nlink}
+ }
+ return out, nil
+}
+func writable(flags int) bool { return flags&unix.O_ACCMODE != unix.O_RDONLY }
+func (c *boundConnector) Driver() driver.Driver { return c.sqlite }
+func (c *boundConnector) checkpoint(stage string) {
+ if c.hook != nil {
+ c.hook(stage)
+ }
+}
+func (c *boundConnector) pathMatches() error {
+ if !time.Now().Before(c.deadline) {
+ return ErrRefused
+ }
+ var stat unix.Stat_t
+ if unix.Fstat(int(c.pin.Fd()), &stat) != nil || (fileIdentity{uint64(stat.Dev), stat.Ino}) != c.identity || stat.Nlink != 1 {
+ return ErrRefused
+ }
+ id, e := statExisting(c.path)
+ if e != nil {
+ return e
+ }
+ if id != c.identity {
+ return ErrRefused
+ }
+ return nil
+}
+func (c *boundConnector) Connect(ctx context.Context) (driver.Conn, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.active || ctx.Err() != nil || c.pathMatches() != nil {
+ return nil, ErrRefused
+ }
+ c.checkpoint("before-inventory")
+ baseline, e := descriptors()
+ if e != nil {
+ return nil, e
+ }
+ for _, d := range baseline {
+ if writable(d.flags) {
+ return nil, ErrRefused
+ }
+ }
+ c.checkpoint("before-open")
+ if c.pathMatches() != nil {
+ return nil, ErrRefused
+ }
+ c.checkpoint("before-driver")
+ u := url.URL{Scheme: "file", Path: c.path}
+ q := u.Query()
+ q.Set("mode", "rw")
+ u.RawQuery = q.Encode()
+ native, e := c.sqlite.Open(u.String())
+ if e != nil {
+ return nil, ErrUnavailable
+ }
+ keep := false
+ defer func() {
+ if !keep {
+ native.Close()
+ }
+ }()
+ c.checkpoint("after-open")
+ current, e := descriptors()
+ if e != nil {
+ return nil, e
+ }
+ mainFD := -1
+ for fd, d := range current {
+ if !writable(d.flags) {
+ continue
+ }
+ if _, old := baseline[fd]; old {
+ return nil, ErrRefused
+ }
+ if d.identity == c.identity {
+ if d.flags&unix.O_ACCMODE != unix.O_RDWR || d.links != 1 || mainFD != -1 {
+ return nil, ErrRefused
+ }
+ mainFD = fd
+ }
+ }
+ if mainFD < 0 {
+ return nil, ErrRefused
+ }
+ bound := &boundConnection{Conn: native, owner: c, mainFD: mainFD, siblings: map[string]fileIdentity{}}
+ if e = bound.check(); e != nil {
+ return nil, e
+ }
+ query, ok := native.(driver.QueryerContext)
+ if !ok {
+ return nil, ErrRefused
+ }
+ rows, e := query.QueryContext(ctx, "PRAGMA database_list", nil)
+ if e != nil {
+ return nil, ErrUnavailable
+ }
+ values := make([]driver.Value, 3)
+ if rows.Next(values) != nil || values[1] != "main" || values[2] != c.path {
+ rows.Close()
+ return nil, ErrRefused
+ }
+ if e = rows.Next(values); e != io.EOF {
+ rows.Close()
+ return nil, ErrRefused
+ }
+ if rows.Close() != nil {
+ return nil, ErrUnavailable
+ }
+ c.checkpoint("after-metadata")
+ if e = bound.check(); e != nil {
+ return nil, e
+ }
+ c.active = true
+ keep = true
+ return bound, nil
+}
+func (c *boundConnection) check() error {
+ if c.closed || c.owner.pathMatches() != nil {
+ return ErrRefused
+ }
+ all, e := descriptors()
+ if e != nil {
+ return e
+ }
+ main, ok := all[c.mainFD]
+ if !ok || main.identity != c.owner.identity || main.flags&unix.O_ACCMODE != unix.O_RDWR || main.links != 1 {
+ return ErrRefused
+ }
+ count := 0
+ seenSiblings := map[string]bool{}
+ for fd, d := range all {
+ if !writable(d.flags) {
+ continue
+ }
+ if fd == c.mainFD {
+ count++
+ continue
+ }
+ if d.flags&unix.O_ACCMODE != unix.O_RDWR || d.links != 1 {
+ return ErrRefused
+ }
+ matched := false
+ for _, suffix := range []string{"-wal", "-shm"} {
+ id, e := statExisting(c.owner.path + suffix)
+ if e != nil {
+ continue
+ }
+ if id == d.identity {
+ if seenSiblings[suffix] {
+ return ErrRefused
+ }
+ seenSiblings[suffix] = true
+ if old, exists := c.siblings[suffix]; exists && old != id {
+ return ErrRefused
+ }
+ c.siblings[suffix] = id
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return ErrRefused
+ }
+ }
+ if count != 1 {
+ return ErrRefused
+ }
+ return nil
+}
+func (c *boundConnection) Close() error {
+ c.owner.mu.Lock()
+ defer c.owner.mu.Unlock()
+ if c.closed {
+ return nil
+ }
+ c.closed = true
+ e := c.Conn.Close()
+ c.owner.active = false
+ return e
+}
+func (c *boundConnection) Prepare(string) (driver.Stmt, error) { return nil, ErrRefused }
+func (c *boundConnection) PrepareContext(context.Context, string) (driver.Stmt, error) {
+ return nil, ErrRefused
+}
+func (c *boundConnection) Begin() (driver.Tx, error) {
+ return c.BeginTx(context.Background(), driver.TxOptions{})
+}
+func (c *boundConnection) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
+ if c.check() != nil {
+ return nil, ErrRefused
+ }
+ x, ok := c.Conn.(driver.ConnBeginTx)
+ if !ok {
+ return nil, ErrRefused
+ }
+ tx, e := x.BeginTx(ctx, opts)
+ if e != nil {
+ return nil, e
+ }
+ if c.check() != nil {
+ tx.Rollback()
+ return nil, ErrUnavailable
+ }
+ return &boundTransaction{tx, c}, nil
+}
+func (c *boundConnection) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
+ if query != "ROLLBACK" && c.check() != nil {
+ return nil, ErrRefused
+ }
+ x, ok := c.Conn.(driver.ExecerContext)
+ if !ok {
+ return nil, ErrRefused
+ }
+ result, e := x.ExecContext(ctx, query, args)
+ if e != nil {
+ return nil, e
+ }
+ c.owner.checkpoint("after-exec")
+ if query != "ROLLBACK" && c.check() != nil {
+ return nil, ErrUnavailable
+ }
+ return result, nil
+}
+func (c *boundConnection) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
+ if c.check() != nil {
+ return nil, ErrRefused
+ }
+ x, ok := c.Conn.(driver.QueryerContext)
+ if !ok {
+ return nil, ErrRefused
+ }
+ rows, e := x.QueryContext(ctx, query, args)
+ if e != nil {
+ return nil, e
+ }
+ if c.check() != nil {
+ rows.Close()
+ return nil, ErrUnavailable
+ }
+ return &boundRows{rows, c}, nil
+}
+func (c *boundConnection) Ping(ctx context.Context) error {
+ if c.check() != nil {
+ return ErrRefused
+ }
+ p, ok := c.Conn.(driver.Pinger)
+ if !ok {
+ return ErrRefused
+ }
+ if e := p.Ping(ctx); e != nil {
+ return e
+ }
+ if c.check() != nil {
+ return ErrUnavailable
+ }
+ return nil
+}
+func (c *boundConnection) ResetSession(ctx context.Context) error {
+ if c.check() != nil {
+ return ErrRefused
+ }
+ if r, ok := c.Conn.(driver.SessionResetter); ok {
+ return r.ResetSession(ctx)
+ }
+ return nil
+}
+func (c *boundConnection) IsValid() bool { return c.check() == nil }
+
+type boundRows struct {
+ driver.Rows
+ c *boundConnection
+}
+
+func (r *boundRows) Next(values []driver.Value) error {
+ if r.c.check() != nil {
+ return ErrRefused
+ }
+ e := r.Rows.Next(values)
+ if e != nil {
+ return e
+ }
+ if r.c.check() != nil {
+ return ErrUnavailable
+ }
+ return nil
+}
+
+type boundTransaction struct {
+ driver.Tx
+ c *boundConnection
+}
+
+func (t *boundTransaction) Commit() error {
+ if t.c.check() != nil {
+ return ErrRefused
+ }
+ e := t.Tx.Commit()
+ t.c.owner.checkpoint("after-commit")
+ if e != nil {
+ return e
+ }
+ if t.c.check() != nil {
+ return ErrUnavailable
+ }
+ return nil
+}
diff --git a/internal/operator/open_linux_test.go b/internal/operator/open_linux_test.go
new file mode 100644
index 0000000..5d1cd2f
--- /dev/null
+++ b/internal/operator/open_linux_test.go
@@ -0,0 +1,403 @@
+//go:build linux
+
+package operator
+
+import (
+ "bytes"
+ "context"
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "time"
+
+ sharedseal "amadan.net/oficina/home/handoff/seal"
+ "golang.org/x/sys/unix"
+ "modernc.org/sqlite"
+)
+
+type openProbe struct {
+ Path, Other, Mode string
+ Device, Inode uint64
+}
+
+func seedOpenDB(t *testing.T, path string) {
+ t.Helper()
+ db, e := sql.Open("sqlite", "file:"+path)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if _, e = db.Exec("PRAGMA journal_mode=WAL; CREATE TABLE changes(value INTEGER)"); e != nil {
+ t.Fatal(e)
+ }
+ if e = db.Close(); e != nil {
+ t.Fatal(e)
+ }
+}
+func TestBoundDatabaseSubprocess(t *testing.T) {
+ if os.Getenv("OFICINA_OPEN_PROBE") == "1" {
+ raw, e := io.ReadAll(io.LimitReader(os.Stdin, 16384))
+ if e != nil {
+ t.Fatal(e)
+ }
+ var p openProbe
+ if json.Unmarshal(raw, &p) != nil {
+ t.Fatal("probe input")
+ }
+ if p.Mode == "contender" {
+ db, err := sql.Open("sqlite", "file:"+p.Path+"?mode=rw")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+ _, err = db.Exec("BEGIN IMMEDIATE")
+ var sqliteErr *sqlite.Error
+ if !errors.As(err, &sqliteErr) || sqliteErr.Code() != 5 {
+ t.Fatal("expected SQLITE_BUSY while owner holds transaction", err)
+ }
+ return
+ }
+ saved := p.Path + ".saved"
+ swap := func() {
+ t.Helper()
+ if e := os.Rename(p.Path, saved); e != nil {
+ t.Fatal(e)
+ }
+ if e := os.Rename(p.Other, p.Path); e != nil {
+ t.Fatal(e)
+ }
+ }
+ restore := func() {
+ t.Helper()
+ if e := os.Rename(p.Path, p.Other); e != nil {
+ t.Fatal(e)
+ }
+ if e := os.Rename(saved, p.Path); e != nil {
+ t.Fatal(e)
+ }
+ }
+ if p.Mode == "symlink" {
+ if e = os.Rename(p.Path, saved); e != nil {
+ t.Fatal(e)
+ }
+ if e = os.Symlink(p.Other, p.Path); e != nil {
+ t.Fatal(e)
+ }
+ }
+ if p.Mode == "symlink-ancestor" {
+ link := filepath.Join(filepath.Dir(p.Path), "alias")
+ if e = os.Symlink(filepath.Dir(p.Path), link); e != nil {
+ t.Fatal(e)
+ }
+ p.Path = filepath.Join(link, filepath.Base(p.Path))
+ }
+ if p.Mode == "missing" {
+ if e = os.Remove(p.Path); e != nil {
+ t.Fatal(e)
+ }
+ }
+ if p.Mode == "hardlink" {
+ if e = os.Link(p.Path, saved); e != nil {
+ t.Fatal(e)
+ }
+ }
+ if p.Mode == "inherited-writeonly" || p.Mode == "inherited-readwrite" {
+ flag := os.O_WRONLY
+ if p.Mode == "inherited-readwrite" {
+ flag = os.O_RDWR
+ }
+ f, e := os.OpenFile(p.Other, flag, 0)
+ if e != nil {
+ t.Fatal(e)
+ }
+ defer f.Close()
+ }
+ deadline := time.Now().Add(time.Minute)
+ if p.Mode == "expired" {
+ deadline = time.Now().Add(-time.Second)
+ }
+ bound, e := newBoundDatabase(p.Path, p.Device, p.Inode, deadline)
+ early := map[string]bool{"symlink": true, "symlink-ancestor": true, "missing": true, "hardlink": true, "expired": true}
+ if early[p.Mode] {
+ if e != ErrRefused {
+ if bound != nil {
+ bound.close()
+ }
+ t.Fatal("pin refusal", e)
+ }
+ return
+ }
+ if e != nil {
+ t.Fatal(e)
+ }
+ defer bound.close()
+ if p.Mode == "replace-restore" {
+ bound.connector.hook = func(stage string) {
+ if stage == "before-driver" {
+ swap()
+ }
+ if stage == "after-open" {
+ restore()
+ }
+ }
+ }
+ if p.Mode == "replace-after" {
+ bound.connector.hook = func(stage string) {
+ if stage == "after-open" {
+ swap()
+ }
+ }
+ }
+ var extra *os.File
+ defer func() {
+ if extra != nil {
+ extra.Close()
+ }
+ }()
+ if p.Mode == "symlink-after" || p.Mode == "ambiguous" {
+ bound.connector.hook = func(stage string) {
+ if stage != "after-open" {
+ return
+ }
+ if p.Mode == "ambiguous" {
+ extra, e = os.OpenFile(p.Path, os.O_RDWR, 0)
+ if e != nil {
+ t.Fatal(e)
+ }
+ } else {
+ if e = os.Rename(p.Path, saved); e != nil {
+ t.Fatal(e)
+ }
+ if e = os.Symlink(saved, p.Path); e != nil {
+ t.Fatal(e)
+ }
+ }
+ }
+ }
+ e = bound.db.PingContext(context.Background())
+ refused := p.Mode == "symlink-after" || p.Mode == "ambiguous" || p.Mode == "replace-restore" || p.Mode == "replace-after" || p.Mode == "inherited-writeonly" || p.Mode == "inherited-readwrite"
+ if refused {
+ if e != ErrRefused {
+ t.Fatal("connection refusal", e)
+ }
+ return
+ }
+ if e != nil {
+ t.Fatal("ordinary open", e)
+ }
+ if p.Mode == "locks" || p.Mode == "reconnect" {
+ if p.Mode == "reconnect" {
+ bound.db.SetMaxIdleConns(0)
+ if err := bound.db.Ping(); err != nil {
+ t.Fatal("reconnect", err)
+ }
+ bound.db.SetMaxIdleConns(1)
+ }
+ conn, err := bound.db.Conn(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer conn.Close()
+ if _, err = conn.ExecContext(context.Background(), "BEGIN IMMEDIATE"); err != nil {
+ t.Fatal(err)
+ }
+ defer conn.ExecContext(context.Background(), "ROLLBACK")
+ if _, err = conn.ExecContext(context.Background(), "INSERT INTO changes VALUES(1)"); err != nil {
+ t.Fatal(err)
+ }
+ p.Mode = "contender"
+ raw, _ := json.Marshal(p)
+ cmd := exec.Command(os.Args[0], "-test.run=^TestBoundDatabaseSubprocess$")
+ cmd.Env = append(os.Environ(), "OFICINA_OPEN_PROBE=1")
+ cmd.Stdin = bytes.NewReader(raw)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("lock contender: %v %s", err, output)
+ }
+ return
+ }
+ if p.Mode == "wal-replaced" || p.Mode == "shm-symlink" || p.Mode == "foreign-descriptor" || p.Mode == "unlinked-temp" || p.Mode == "expired-connected" {
+ conn, err := bound.db.Conn(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer conn.Close()
+ var count int
+ if err = conn.QueryRowContext(context.Background(), "SELECT count(*) FROM changes").Scan(&count); err != nil {
+ t.Fatal(err)
+ }
+ if p.Mode == "wal-replaced" || p.Mode == "shm-symlink" {
+ suffix := "-wal"
+ if p.Mode == "shm-symlink" {
+ suffix = "-shm"
+ }
+ if err = os.Rename(p.Path+suffix, p.Path+suffix+".saved"); err != nil {
+ t.Fatal(err)
+ }
+ if p.Mode == "shm-symlink" {
+ err = os.Symlink(p.Path+suffix+".saved", p.Path+suffix)
+ } else {
+ err = os.WriteFile(p.Path+suffix, nil, 0600)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() {
+ os.Remove(p.Path + suffix)
+ os.Rename(p.Path+suffix+".saved", p.Path+suffix)
+ }()
+ } else if p.Mode == "expired-connected" {
+ bound.connector.deadline = time.Now().Add(-time.Second)
+ } else {
+ f, err := os.OpenFile(p.Other, os.O_RDWR, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ if p.Mode == "unlinked-temp" {
+ if err = os.Remove(p.Other); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ if _, err = conn.ExecContext(context.Background(), "INSERT INTO changes VALUES(1)"); err != ErrRefused {
+ t.Fatal("boundary refusal", err)
+ }
+ return
+ }
+ if p.Mode == "post-commit" {
+ bound.connector.hook = func(stage string) {
+ if stage == "after-exec" {
+ swap()
+ }
+ }
+ }
+ _, e = bound.db.ExecContext(context.Background(), "INSERT INTO changes VALUES(1)")
+ if p.Mode == "post-commit" {
+ if e != ErrUnavailable || errors.Is(e, sql.ErrConnDone) {
+ t.Fatal("commit ambiguity", e)
+ }
+ restore()
+ bound.connector.hook = nil
+ return
+ }
+ if e != nil {
+ t.Fatal("ordinary write", e)
+ }
+ return
+ }
+ modes := []string{"symlink-after", "symlink-ancestor", "ambiguous", "wal-replaced", "shm-symlink", "foreign-descriptor", "unlinked-temp", "expired-connected", "reconnect", "locks", "ordinary", "replace-restore", "replace-after", "symlink", "missing", "hardlink", "expired", "inherited-writeonly", "inherited-readwrite", "post-commit"}
+ for _, mode := range modes {
+ t.Run(mode, func(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "home.db")
+ other := filepath.Join(dir, "other.db")
+ seedOpenDB(t, path)
+ seedOpenDB(t, other)
+ var stat unix.Stat_t
+ if e := unix.Stat(path, &stat); e != nil {
+ t.Fatal(e)
+ }
+ raw, e := json.Marshal(openProbe{path, other, mode, uint64(stat.Dev), stat.Ino})
+ if e != nil {
+ t.Fatal(e)
+ }
+ cmd := exec.Command(os.Args[0], "-test.run=^TestBoundDatabaseSubprocess$")
+ cmd.Env = append(os.Environ(), "OFICINA_OPEN_PROBE=1")
+ cmd.Stdin = bytes.NewReader(raw)
+ if output, e := cmd.CombinedOutput(); e != nil {
+ t.Fatalf("probe failed: %v %s", e, output)
+ }
+ if mode == "missing" {
+ if _, e := os.Stat(path); !os.IsNotExist(e) {
+ t.Fatal("missing DB created")
+ }
+ return
+ }
+ readPath := path
+ if mode == "replace-after" || mode == "symlink" || mode == "symlink-after" {
+ readPath = path + ".saved"
+ }
+ db, e := sql.Open("sqlite", "file:"+readPath+"?mode=rw")
+ if e != nil {
+ t.Fatal(e)
+ }
+ defer db.Close()
+ var count int
+ if e = db.QueryRow("SELECT count(*) FROM changes").Scan(&count); e != nil {
+ t.Fatal(e)
+ }
+ expected := 0
+ if mode == "ordinary" || mode == "post-commit" {
+ expected = 1
+ }
+ if count != expected {
+ t.Fatal("wrong mutation count", count, expected)
+ }
+ })
+ }
+}
+
+func TestBoundDatabaseEngineSubprocess(t *testing.T) {
+ if os.Getenv("OFICINA_ENGINE_OPEN_PROBE") == "1" {
+ var p openProbe
+ if json.NewDecoder(io.LimitReader(os.Stdin, 16384)).Decode(&p) != nil {
+ t.Fatal("probe input")
+ }
+ bound, err := newBoundDatabase(p.Path, p.Device, p.Inode, time.Now().Add(time.Minute))
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer bound.close()
+ k, verifier, auth := fixture(t)
+ sealer, err := sharedseal.New(bytes.Repeat([]byte{6}, 32), "oficina/operator")
+ if err != nil {
+ t.Fatal(err)
+ }
+ now := time.Unix(auth.Issued, 0)
+ engine, err := NewEngine(bound.db, verifier, sealer, bytes.Repeat([]byte{7}, 32), func() time.Time { return now })
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw := signed(t, k, auth)
+ out, err := engine.Execute(context.Background(), raw, insertOne)
+ if err != nil || !bytes.Equal(out, privateResult) {
+ t.Fatal("bound engine", err)
+ }
+ now = now.Add(301 * time.Second)
+ out, err = engine.Execute(context.Background(), raw, func(context.Context, *sql.Conn, Request) ([]byte, error) {
+ t.Fatal("replay mutated")
+ return nil, nil
+ })
+ if err != nil || !bytes.Equal(out, privateResult) {
+ t.Fatal("bound recovery", err)
+ }
+ return
+ }
+ f := newEngineFixture(t)
+ if err := f.db.Close(); err != nil {
+ t.Fatal(err)
+ }
+ var stat unix.Stat_t
+ if err := unix.Stat(f.path, &stat); err != nil {
+ t.Fatal(err)
+ }
+ raw, _ := json.Marshal(openProbe{Path: f.path, Device: uint64(stat.Dev), Inode: stat.Ino})
+ cmd := exec.Command(os.Args[0], "-test.run=^TestBoundDatabaseEngineSubprocess$")
+ cmd.Env = append(os.Environ(), "OFICINA_ENGINE_OPEN_PROBE=1")
+ cmd.Stdin = bytes.NewReader(raw)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("engine probe: %v %s", err, out)
+ }
+ db, err := sql.Open("sqlite", "file:"+f.path+"?mode=rw")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+ if countRows(t, db, "test_items") != 1 || countRows(t, db, "directory_operator_results") != 1 {
+ t.Fatal("bound atomic recovery")
+ }
+}