carlos / platform Public

Refuse unsafe VFS opens and verify custom-first crash recovery

claude agent, operated by paul@keymail.dev 0e9de1b26f0f482a2dd2468eff7f9dfe8bc54e41 parent 36aa460
4 files changed, +205 −13
  • internal/vfsspike/locks_linux.go +12 −7
  • internal/vfsspike/process_linux_test.go +21 −4
  • internal/vfsspike/review_boundaries_linux_test.go +164 −0
  • internal/vfsspike/vfs_linux.go +8 −2

CI passed — run details

diff --git a/internal/vfsspike/locks_linux.go b/internal/vfsspike/locks_linux.go
index 761aebd1c..0ee3b9cde 100644
--- a/internal/vfsspike/locks_linux.go
+++ b/internal/vfsspike/locks_linux.go
@@ -97,13 +97,18 @@ func shmMap(t *libc.TLS, p uintptr, page, size, extend int32, out uintptr) int32
return sqlite.SQLITE_IOERR_SHMSIZE
}
end := (int64(page) + 1) * int64(size)
- if s.Size < end {
- if extend == 0 {
- return sqlite.SQLITE_OK
- }
- if unix.Ftruncate(f.shm, end) != nil {
- return sqlite.SQLITE_IOERR_SHMSIZE
- }
+ if s.Size < end && extend == 0 {
+ return sqlite.SQLITE_OK
+ }
+ // File length is not proof of backing: a failed allocation may have
+ // extended the file while leaving holes. Reserve every unmapped extent
+ // before mmap, preserving existing bytes. Unsupported filesystems refuse.
+ allocate := f.v.shmAllocate
+ if allocate == nil {
+ allocate = func(fd int, end int64) error { return unix.Fallocate(fd, 0, 0, end) }
+ }
+ if allocate(f.shm, end) != nil {
+ return sqlite.SQLITE_IOERR_SHMSIZE
}
b, err := unix.Mmap(f.shm, int64(page)*int64(size), int(size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
diff --git a/internal/vfsspike/process_linux_test.go b/internal/vfsspike/process_linux_test.go
index 7de94a420..07ec3824d 100644
--- a/internal/vfsspike/process_linux_test.go
+++ b/internal/vfsspike/process_linux_test.go
@@ -27,6 +27,7 @@ type processResponse struct {
Rows [][]int64
Inodes []uint64
TempFiles []string
+ Integrity string
}
func TestVFSProcessChild(t *testing.T) {
@@ -118,6 +119,8 @@ func TestVFSProcessChild(t *testing.T) {
}
v.mu.Unlock()
}
+ case "integrity":
+ err = db.QueryRow("PRAGMA integrity_check").Scan(&response.Integrity)
case "query":
var rows *sql.Rows
rows, err = db.Query(req.SQL)
@@ -519,16 +522,30 @@ func TestVFSProcessCrashRecovery(t *testing.T) {
t.Fatalf("uncommitted transaction did not spill: before=%d after=%d", before.Size(), after.Size())
}
t.Logf("uncommitted %s spill grew %s from %d to %d bytes before SIGKILL", mode, filepath.Base(spillPath), before.Size(), after.Size())
+ // No ordinary connection may recover or retain a SHM dead-man lock
+ // before the new custom process gets the first post-crash read.
+ if err := db.Close(); err != nil {
+ t.Fatal(err)
+ }
p.stop(true)
+ next := launchVFSProcess(t, data, meta)
+ next.ok("open", "")
+ next.count(1)
+ if got := next.ok("integrity", "").Integrity; got != "ok" {
+ t.Fatalf("custom recovery integrity %q", got)
+ }
+ if mode == "DELETE" {
+ if _, err := os.Stat(filepath.Join(data, "main.db-journal")); !os.IsNotExist(err) {
+ t.Fatalf("custom recovery retained hot journal: %v", err)
+ }
+ }
+ db = processOrdinary(t, filepath.Join(data, "main.db"))
ordinaryCount(t, db, 1)
var check string
if err := db.QueryRow("PRAGMA integrity_check").Scan(&check); err != nil || check != "ok" {
- t.Fatalf("integrity %q: %v", check, err)
+ t.Fatalf("ordinary confirmation integrity %q: %v", check, err)
}
execSQL(t, db, "INSERT INTO data VALUES(2,NULL)")
- ordinaryCount(t, db, 2)
- next := launchVFSProcess(t, data, meta)
- next.ok("open", "")
next.count(2)
next.stop(false)
})
diff --git a/internal/vfsspike/review_boundaries_linux_test.go b/internal/vfsspike/review_boundaries_linux_test.go
new file mode 100644
index 000000000..2134eaa91
--- /dev/null
+++ b/internal/vfsspike/review_boundaries_linux_test.go
@@ -0,0 +1,164 @@
+//go:build linux && (amd64 || arm64)
+
+package vfsspike
+
+import (
+ "bytes"
+ "database/sql"
+ "golang.org/x/sys/unix"
+ "modernc.org/libc"
+ sqlite "modernc.org/sqlite/lib"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestReadOnlyWALMainRefusedWithoutPoison(t *testing.T) {
+ v, _ := setup(t)
+ a := connect(t, v)
+ execSQL(t, a, "PRAGMA journal_mode=WAL; CREATE TABLE data(value); INSERT INTO data VALUES(1)")
+ b, err := sql.Open("sqlite", "file:main.db?vfs="+v.Name+"&mode=ro")
+ if err != nil {
+ t.Fatal(err)
+ }
+ b.SetMaxOpenConns(1)
+ var count int
+ readErr := b.QueryRow("SELECT count(*) FROM data").Scan(&count)
+ closeErr := b.Close()
+ _, writeErr := a.Exec("INSERT INTO data VALUES(2)")
+ t.Logf("read-only read=%v close=%v poisoned=%v surviving writer=%v", readErr, closeErr, v.poisoned.Load(), writeErr)
+ if readErr == nil {
+ t.Error("read-only main was accepted")
+ }
+ if v.poisoned.Load() || writeErr != nil {
+ t.Fatal("read-only attempt damaged existing writer")
+ }
+ if scalar(t, a, "SELECT count(*) FROM data") != 2 {
+ t.Fatal("surviving writer lost row")
+ }
+}
+
+func TestUnsupportedKernelPageSizeRefused(t *testing.T) {
+ dir := t.TempDir()
+ if err := os.WriteFile(filepath.Join(dir, "main.db"), nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ d, err := os.Open(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Close()
+ old := pageSize
+ pageSize = func() int { return 65536 }
+ defer func() { pageSize = old }()
+ v, err := Register(d, d, "main.db")
+ if v != nil {
+ v.Close()
+ }
+ if err != unix.ENOTSUP {
+ t.Fatalf("64KiB page kernel accepted: %v", err)
+ }
+ pageSize = old
+ v, err = Register(d, d, "main.db")
+ if err != nil {
+ t.Fatalf("refusal retained registration: %v", err)
+ }
+ if err := v.Close(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestMainUnlinkBeforeOpenDoesNotCreate(t *testing.T) {
+ v, dir := setup(t)
+ v.beforeOpen = func(name string) {
+ if name == "main.db" {
+ if err := os.Remove(filepath.Join(dir, name)); err != nil {
+ t.Fatal(err)
+ }
+ }
+ }
+ name, err := libc.CString("main.db")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer libc.Xfree(v.tls, name)
+ out := sqliteBuffer(t, v.tls, 32)
+ rc := openFile(v.tls, v.ptr, name, out, sqlite.SQLITE_OPEN_MAIN_DB|sqlite.SQLITE_OPEN_READWRITE|sqlite.SQLITE_OPEN_CREATE, 0)
+ if rc != sqlite.SQLITE_CANTOPEN {
+ if rc == sqlite.SQLITE_OK {
+ closeFile(v.tls, out)
+ }
+ t.Fatalf("unlinked main opened: %d", rc)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "main.db")); !os.IsNotExist(err) {
+ t.Fatalf("stray main created: %v", err)
+ }
+}
+
+func TestSHMMappingAllocatesBacking(t *testing.T) {
+ v, _ := setup(t)
+ p := openMainCallback(t, v)
+ out := sqliteBuffer(t, v.tls, 8)
+ if rc := shmMap(v.tls, p, 0, 32768, 0, out); rc != 0 || foreignLoad[uintptr](v.tls, out) != 0 {
+ t.Fatalf("nonextending absent region: %d", rc)
+ }
+ if rc := shmMap(v.tls, p, 0, 32768, 1, out); rc != 0 {
+ t.Fatal(rc)
+ }
+ var st unix.Stat_t
+ if err := unix.Fstat(v.shm.fd, &st); err != nil {
+ t.Fatal(err)
+ }
+ if st.Size != 32768 || st.Blocks*512 < 32768 {
+ t.Fatalf("SHM has sparse backing: size=%d allocated=%d", st.Size, st.Blocks*512)
+ }
+}
+
+func TestSHMAllocationFailureRetryPreservesBytes(t *testing.T) {
+ for _, failure := range []error{unix.ENOSPC, unix.EOPNOTSUPP} {
+ t.Run(failure.Error(), func(t *testing.T) {
+ v, _ := setup(t)
+ p := openMainCallback(t, v)
+ out := sqliteBuffer(t, v.tls, 8)
+ if rc := shmMap(v.tls, p, 0, 32768, 1, out); rc != 0 {
+ t.Fatal(rc)
+ }
+ sentinel := bytes.Repeat([]byte{0xa5}, 32768)
+ if n, err := unix.Pwrite(v.shm.fd, sentinel, 0); err != nil || n != len(sentinel) {
+ t.Fatalf("sentinel: %d %v", n, err)
+ }
+ calls := 0
+ v.shmAllocate = func(fd int, end int64) error {
+ calls++
+ if calls == 1 {
+ if err := unix.Ftruncate(fd, end); err != nil {
+ t.Fatal(err)
+ }
+ return failure
+ }
+ return unix.Fallocate(fd, 0, 0, end)
+ }
+ foreignStore(v.tls, out, uintptr(123))
+ if rc := shmMap(v.tls, p, 1, 32768, 1, out); rc != sqlite.SQLITE_IOERR_SHMSIZE || foreignLoad[uintptr](v.tls, out) != 0 || len(v.shm.regions) != 1 {
+ t.Fatalf("allocation failure mapped: rc=%d regions=%d", rc, len(v.shm.regions))
+ }
+ // The failed allocator grew the logical size. Even extend=0 must now
+ // prove backing before returning a mapping for that existing extent.
+ if rc := shmMap(v.tls, p, 1, 32768, 0, out); rc != 0 || foreignLoad[uintptr](v.tls, out) == 0 || calls != 2 {
+ t.Fatalf("retry skipped backing: rc=%d calls=%d", rc, calls)
+ }
+ got := make([]byte, len(sentinel))
+ if n, err := unix.Pread(v.shm.fd, got, 0); err != nil || n != len(got) || !bytes.Equal(got, sentinel) {
+ t.Fatal("allocation changed existing SHM bytes")
+ }
+ var st unix.Stat_t
+ if err := unix.Fstat(v.shm.fd, &st); err != nil || st.Blocks*512 < 65536 {
+ t.Fatalf("retry left sparse backing: %+v %v", st, err)
+ }
+ v.shmAllocate = func(int, int64) error { t.Error("cached map allocated again"); return unix.ENOSPC }
+ if rc := shmMap(v.tls, p, 1, 32768, 0, out); rc != 0 {
+ t.Fatal(rc)
+ }
+ })
+ }
+}
diff --git a/internal/vfsspike/vfs_linux.go b/internal/vfsspike/vfs_linux.go
index d1ee8a7e7..69c2f85ee 100644
--- a/internal/vfsspike/vfs_linux.go
+++ b/internal/vfsspike/vfs_linux.go
@@ -36,6 +36,7 @@ type VFS struct {
closed bool
rejected []int
beforeOpen func(string)
+ shmAllocate func(int, int64) error
lockHook func(int, int16, int64, int64) error
poisoned atomic.Bool
retired []*file
@@ -65,6 +66,8 @@ var registry = struct {
}{vfs: make(map[uintptr]*VFS), files: make(map[uintptr]*file), inodes: make(map[identity]bool)}
var barrier atomic.Uint64
+var pageSize = unix.Getpagesize
+
func fp[T any](f T) uintptr { return *(*uintptr)(unsafe.Pointer(&f)) }
func getVFS(p uintptr) *VFS { registry.RLock(); defer registry.RUnlock(); return registry.vfs[p] }
@@ -82,6 +85,9 @@ func Register(data, meta *os.File, name string) (*VFS, error) {
if !plain(name) {
return nil, unix.EINVAL
}
+ if pageSize() > 32768 {
+ return nil, unix.ENOTSUP
+ }
var s unix.Stat_t
if err := unix.Fstatat(int(data.Fd()), name, &s, unix.AT_SYMLINK_NOFOLLOW); err != nil {
return nil, err
@@ -231,7 +237,7 @@ func openFile(tls *libc.TLS, p, z, out uintptr, flags int32, outFlags uintptr) i
return sqlite.SQLITE_CANTOPEN
}
isMain := flags&sqlite.SQLITE_OPEN_MAIN_DB != 0
- if isMain && name != v.main {
+ if isMain && (name != v.main || flags&sqlite.SQLITE_OPEN_READWRITE == 0 || flags&sqlite.SQLITE_OPEN_READONLY != 0) {
return sqlite.SQLITE_CANTOPEN
}
if !isMain && !temporary && name == v.main {
@@ -243,7 +249,7 @@ func openFile(tls *libc.TLS, p, z, out uintptr, flags int32, outFlags uintptr) i
} else {
oflags |= unix.O_RDONLY
}
- if flags&sqlite.SQLITE_OPEN_CREATE != 0 {
+ if !isMain && flags&sqlite.SQLITE_OPEN_CREATE != 0 {
oflags |= unix.O_CREAT
}
if temporary || flags&sqlite.SQLITE_OPEN_EXCLUSIVE != 0 {