carlos / platform Public

Pin the accepted database inode through VFS registration

claude agent, operated by paul@keymail.dev f427c79983f5dbfb45813e547e377a5bf41ac9e7 parent f5cc007
6 files changed, +370 −14
  • internal/vfsspike/lock_failure_linux_test.go +3 −0
  • internal/vfsspike/main_pin_linux_test.go +315 −0
  • internal/vfsspike/process_linux_test.go +13 −4
  • internal/vfsspike/review_boundaries_linux_test.go +2 −2
  • internal/vfsspike/vfs_linux.go +35 −6
  • internal/vfsspike/vfs_linux_test.go +2 −2
diff --git a/internal/vfsspike/lock_failure_linux_test.go b/internal/vfsspike/lock_failure_linux_test.go
index 732485beb..1d5466071 100644
--- a/internal/vfsspike/lock_failure_linux_test.go
+++ b/internal/vfsspike/lock_failure_linux_test.go
@@ -109,6 +109,7 @@ func TestPoisonedCloseRetainsOwnershipUntilFinalFile(t *testing.T) {
a := openMainCallback(t, v)
b := openMainCallback(t, v)
afd := getFile(a).fd
+ requireMainCensus(t, v, 3)
path := filepath.Join(dir, "main.db")
start := pending + 2
length := int64(510)
@@ -147,10 +148,12 @@ func TestPoisonedCloseRetainsOwnershipUntilFinalFile(t *testing.T) {
if err := v.Close(); err != unix.EBUSY {
t.Fatalf("unregister with live file: %v", err)
}
+ requireMainCensus(t, v, 3)
if rc := closeFile(v.tls, b); rc != sqlite.SQLITE_OK {
t.Fatal(rc)
}
assertKernelLock(t, path, start, length, unix.F_UNLCK)
+ requireMainCensus(t, v, 1)
if len(v.retired) != 0 || len(v.db.deferred) != 0 || v.shm != nil {
t.Fatal("final cleanup retained ownership")
}
diff --git a/internal/vfsspike/main_pin_linux_test.go b/internal/vfsspike/main_pin_linux_test.go
new file mode 100644
index 000000000..8c85806e5
--- /dev/null
+++ b/internal/vfsspike/main_pin_linux_test.go
@@ -0,0 +1,315 @@
+//go:build linux && (amd64 || arm64)
+
+package vfsspike
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/authority"
+ "golang.org/x/sys/unix"
+)
+
+// acceptedMain observes fixture identity without opening the main inode.
+func acceptedMain(t *testing.T, data *os.File, name string) authority.Facts {
+ t.Helper()
+ var st unix.Stat_t
+ if err := unix.Fstatat(int(data.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
+ t.Fatal(err)
+ }
+ return authority.Facts{Dev: st.Dev, Ino: st.Ino, Mode: st.Mode, Nlink: st.Nlink}
+}
+
+// Stat follows proc's fd links without creating a same-inode descriptor.
+func mainCensus(t *testing.T, id identity) int {
+ t.Helper()
+ entries, err := os.ReadDir("/proc/self/fd")
+ if err != nil {
+ t.Fatal(err)
+ }
+ count := 0
+ for _, entry := range entries {
+ var st unix.Stat_t
+ err := unix.Stat(filepath.Join("/proc/self/fd", entry.Name()), &st)
+ if err == unix.ENOENT {
+ continue
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ if st.Dev == id.dev && st.Ino == id.ino {
+ count++
+ }
+ }
+ return count
+}
+func requireMainCensus(t *testing.T, v *VFS, want int) {
+ t.Helper()
+ if got := mainCensus(t, v.expected); got != want {
+ t.Fatalf("main descriptors = %d, want %d", got, want)
+ }
+}
+
+func TestMainPinZeroHandleLifetime(t *testing.T) {
+ v, dir := setup(t)
+ flags, err := unix.FcntlInt(uintptr(v.pin), unix.F_GETFL, 0)
+ if err != nil || flags&unix.O_ACCMODE != unix.O_RDONLY || flags&unix.O_NONBLOCK == 0 {
+ t.Fatalf("pin flags %x: %v", flags, err)
+ }
+ fdFlags, err := unix.FcntlInt(uintptr(v.pin), unix.F_GETFD, 0)
+ if err != nil || fdFlags&unix.FD_CLOEXEC == 0 {
+ t.Fatalf("pin fd flags %x: %v", fdFlags, err)
+ }
+ db := connect(t, v)
+ execSQL(t, db, "CREATE TABLE data(value); INSERT INTO data VALUES(7)")
+ if err := db.Close(); err != nil {
+ t.Fatal(err)
+ }
+ requireMainCensus(t, v, 1)
+ path := filepath.Join(dir, "main.db")
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, []byte("replacement"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ requireMainCensus(t, v, 1)
+ var st unix.Stat_t
+ if err := unix.Fstat(v.pin, &st); err != nil || !same(&st, v.expected) || st.Nlink != 0 {
+ t.Fatalf("pin identity %+v: %v", st, err)
+ }
+ header := make([]byte, 16)
+ if n, err := unix.Pread(v.pin, header, 0); err != nil || n != 16 || !bytes.Equal(header, []byte("SQLite format 3\x00")) {
+ t.Fatalf("pinned header %q: %v", header, err)
+ }
+ next := connect(t, v)
+ if err := next.Ping(); err == nil {
+ t.Fatal("replacement accepted")
+ }
+ if err := next.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err := v.Close(); err != nil {
+ t.Fatal(err)
+ }
+ requireMainCensus(t, v, 0)
+}
+
+func TestMainPinBusyClose(t *testing.T) {
+ v, _ := setup(t)
+ db := connect(t, v)
+ execSQL(t, db, "CREATE TABLE data(value)")
+ baseline := mainCensus(t, v.expected)
+ if baseline != 2 {
+ t.Fatalf("pin plus SQLite fd = %d", baseline)
+ }
+ if err := v.Close(); err != unix.EBUSY {
+ t.Fatal(err)
+ }
+ requireMainCensus(t, v, baseline)
+ if err := db.Close(); err != nil {
+ t.Fatal(err)
+ }
+ requireMainCensus(t, v, 1)
+ if err := v.Close(); err != nil {
+ t.Fatal(err)
+ }
+ requireMainCensus(t, v, 0)
+}
+
+func TestMainPinSecondRegistrationPreservesLocks(t *testing.T) {
+ v, dir := setup(t)
+ db := connect(t, v)
+ execSQL(t, db, "CREATE TABLE data(value); BEGIN IMMEDIATE")
+ path := filepath.Join(dir, "main.db")
+ assertReserved(t, path)
+ d, err := os.Open(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Close()
+ baseline := mainCensus(t, v.expected)
+ second, err := Register(d, d, "main.db", acceptedMain(t, d, "main.db"))
+ if second != nil {
+ second.Close()
+ t.Fatal("second registration succeeded")
+ }
+ if err != unix.EBUSY {
+ t.Fatal(err)
+ }
+ // Count before the child probe, without relying on GC to close leaked fds.
+ requireMainCensus(t, v, baseline)
+ assertReserved(t, path)
+ execSQL(t, db, "COMMIT")
+}
+
+func pinFixture(t *testing.T) (*os.File, string, authority.Facts) {
+ t.Helper()
+ 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)
+ }
+ t.Cleanup(func() { d.Close() })
+ return d, dir, acceptedMain(t, d, "main.db")
+}
+func requireReservationFree(t *testing.T, d *os.File) {
+ t.Helper()
+ v, err := Register(d, d, "main.db", acceptedMain(t, d, "main.db"))
+ if err != nil {
+ t.Fatal("reservation retained", err)
+ }
+ if err := v.Close(); err != nil {
+ t.Fatal(err)
+ }
+}
+func TestMainPinAcceptedFacts(t *testing.T) {
+ d, dir, accepted := pinFixture(t)
+ if err := os.WriteFile(filepath.Join(dir, "other.db"), nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ other := acceptedMain(t, d, "other.db")
+ v, err := Register(d, d, "main.db", other)
+ if v != nil {
+ v.Close()
+ t.Fatal("wrong accepted identity registered")
+ }
+ if err != unix.ESTALE {
+ t.Fatal(err)
+ }
+ for _, f := range []authority.Facts{accepted, other} {
+ if n := mainCensus(t, identity{f.Dev, f.Ino}); n != 0 {
+ t.Fatal("mismatch opened main", n)
+ }
+ }
+ requireReservationFree(t, d)
+}
+
+func TestMainPinBeforeOpenRace(t *testing.T) {
+ for _, kind := range []string{"replacement", "unlink", "symlink", "hardlink", "directory", "fifo"} {
+ t.Run(kind, func(t *testing.T) {
+ d, dir, accepted := pinFixture(t)
+ path := filepath.Join(dir, "main.db")
+ beforePin = func() {
+ if err := os.Rename(path, filepath.Join(dir, "original.db")); err != nil {
+ t.Fatal(err)
+ }
+ var err error
+ switch kind {
+ case "replacement":
+ err = os.WriteFile(path, nil, 0600)
+ case "symlink":
+ err = os.Symlink("original.db", path)
+ case "hardlink":
+ err = os.Link(filepath.Join(dir, "original.db"), path)
+ case "directory":
+ err = os.Mkdir(path, 0700)
+ case "fifo":
+ err = unix.Mkfifo(path, 0600)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ t.Cleanup(func() { beforePin = nil })
+ v, err := Register(d, d, "main.db", accepted)
+ beforePin = nil
+ if v != nil {
+ v.Close()
+ t.Fatal("raced pin accepted", kind)
+ }
+ if err == nil {
+ t.Fatal("missing refusal")
+ }
+ if got := mainCensus(t, identity{accepted.Dev, accepted.Ino}); got != 0 {
+ t.Fatal("refused pin leaked", got)
+ }
+ if _, err := unix.FcntlInt(0, unix.F_GETFD, 0); err != nil {
+ t.Fatal("stdin closed", err)
+ }
+ if kind == "unlink" {
+ if _, err := os.Lstat(path); !os.IsNotExist(err) {
+ t.Fatal("created missing main", err)
+ }
+ } else {
+ if err := os.Remove(path); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := os.Rename(filepath.Join(dir, "original.db"), path); err != nil {
+ t.Fatal(err)
+ }
+ requireReservationFree(t, d)
+ })
+ }
+}
+func TestMainPinEarlyFailureDoesNotCloseStdin(t *testing.T) {
+ d, dir, accepted := pinFixture(t)
+ closed, err := os.Open(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ closed.Close()
+ if _, err := unix.FcntlInt(0, unix.F_GETFD, 0); err != nil {
+ t.Fatal("fixture stdin", err)
+ }
+ v, err := Register(d, closed, "main.db", accepted)
+ if v != nil {
+ v.Close()
+ t.Fatal("closed meta accepted")
+ }
+ if err == nil {
+ t.Fatal("missing failure")
+ }
+ if _, err := unix.FcntlInt(0, unix.F_GETFD, 0); err != nil {
+ t.Fatal("failure closed stdin", err)
+ }
+ requireReservationFree(t, d)
+}
+func TestMainPinReleaseOrdering(t *testing.T) {
+ v, _ := setup(t)
+ called, held, published := false, false, false
+ count := -1
+ v.beforeRelease = func() {
+ called = true
+ count = mainCensus(t, v.expected)
+ registry.RLock()
+ held = registry.inodes[v.expected]
+ _, published = registry.vfs[v.ptr]
+ registry.RUnlock()
+ }
+ if err := v.Close(); err != nil {
+ t.Fatal(err)
+ }
+ // Assert after Close returns so a failed assertion cannot interrupt disposal.
+ if !called || count != 0 || !held || published {
+ t.Fatalf("release ordering: called=%v descriptors=%d held=%v published=%v", called, count, held, published)
+ }
+}
+func TestMainPinPoisonedCleanupError(t *testing.T) {
+ v, _ := setup(t)
+ // An unregistered mapping makes Munmap refuse, exercising the real cleanup
+ // error path without closing any lock-bearing descriptor outside the VFS.
+ v.poisoned.Store(true)
+ v.shm = &sharedMemory{fd: -1, regions: map[int32][]byte{0: {1}}}
+ if err := v.Close(); err != unix.EIO {
+ t.Fatal("cleanup refusal", err)
+ }
+ requireMainCensus(t, v, 1)
+ registry.RLock()
+ held := registry.inodes[v.expected]
+ registry.RUnlock()
+ if !held {
+ t.Fatal("poison released reservation")
+ }
+ v.shm = nil
+ if err := v.Close(); err != nil {
+ t.Fatal(err)
+ }
+ requireMainCensus(t, v, 0)
+}
diff --git a/internal/vfsspike/process_linux_test.go b/internal/vfsspike/process_linux_test.go
index 07ec3824d..3dc1ac2a9 100644
--- a/internal/vfsspike/process_linux_test.go
+++ b/internal/vfsspike/process_linux_test.go
@@ -17,6 +17,7 @@ import (
"testing"
"time"
+ "github.com/carlosframework/platform/internal/authority"
"golang.org/x/sys/unix"
)
@@ -36,9 +37,9 @@ func TestVFSProcessChild(t *testing.T) {
}
socket := os.NewFile(3, "rights")
defer socket.Close()
- payload, control := make([]byte, 1), make([]byte, unix.CmsgSpace(8))
+ payload, control := make([]byte, 4096), make([]byte, unix.CmsgSpace(8))
n, oobn, flags, _, err := unix.Recvmsg(3, payload, control, unix.MSG_CMSG_CLOEXEC)
- if err != nil || n != 1 || flags&unix.MSG_CTRUNC != 0 {
+ if err != nil || n == 0 || flags&(unix.MSG_CTRUNC|unix.MSG_TRUNC) != 0 {
t.Fatalf("receive: %d %d %v", n, flags, err)
}
messages, err := unix.ParseSocketControlMessage(control[:oobn])
@@ -68,7 +69,11 @@ func TestVFSProcessChild(t *testing.T) {
}
ids = append(ids, st.Dev, st.Ino)
}
- v, err := Register(data, meta, "main.db")
+ var accepted authority.Facts
+ if err := json.Unmarshal(payload[:n], &accepted); err != nil {
+ t.Fatal(err)
+ }
+ v, err := Register(data, meta, "main.db", accepted)
data.Close()
meta.Close()
if err != nil {
@@ -260,7 +265,11 @@ func launchVFSProcess(t *testing.T, data, meta string) *vfsProcess {
t.Fatal(err)
}
defer mf.Close()
- if err := unix.Sendmsg(int(parent.Fd()), []byte{1}, unix.UnixRights(int(df.Fd()), int(mf.Fd())), nil, 0); err != nil {
+ payload, err := json.Marshal(acceptedMain(t, df, "main.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := unix.Sendmsg(int(parent.Fd()), payload, unix.UnixRights(int(df.Fd()), int(mf.Fd())), nil, 0); err != nil {
t.Fatal(err)
}
ready := p.receive()
diff --git a/internal/vfsspike/review_boundaries_linux_test.go b/internal/vfsspike/review_boundaries_linux_test.go
index 2134eaa91..5bb17985c 100644
--- a/internal/vfsspike/review_boundaries_linux_test.go
+++ b/internal/vfsspike/review_boundaries_linux_test.go
@@ -51,7 +51,7 @@ func TestUnsupportedKernelPageSizeRefused(t *testing.T) {
old := pageSize
pageSize = func() int { return 65536 }
defer func() { pageSize = old }()
- v, err := Register(d, d, "main.db")
+ v, err := Register(d, d, "main.db", acceptedMain(t, d, "main.db"))
if v != nil {
v.Close()
}
@@ -59,7 +59,7 @@ func TestUnsupportedKernelPageSizeRefused(t *testing.T) {
t.Fatalf("64KiB page kernel accepted: %v", err)
}
pageSize = old
- v, err = Register(d, d, "main.db")
+ v, err = Register(d, d, "main.db", acceptedMain(t, d, "main.db"))
if err != nil {
t.Fatalf("refusal retained registration: %v", err)
}
diff --git a/internal/vfsspike/vfs_linux.go b/internal/vfsspike/vfs_linux.go
index 69c2f85ee..760694f27 100644
--- a/internal/vfsspike/vfs_linux.go
+++ b/internal/vfsspike/vfs_linux.go
@@ -18,6 +18,7 @@ import (
"time"
"unsafe"
+ "github.com/carlosframework/platform/internal/authority"
"golang.org/x/sys/unix"
"modernc.org/libc"
sqlite "modernc.org/sqlite/lib"
@@ -27,7 +28,7 @@ type identity struct{ dev, ino uint64 }
type VFS struct {
mu sync.Mutex
Name string
- data, meta int
+ data, meta, pin int
main string
expected identity
ptr, cname, methods uintptr
@@ -36,6 +37,7 @@ type VFS struct {
closed bool
rejected []int
beforeOpen func(string)
+ beforeRelease func()
shmAllocate func(int, int64) error
lockHook func(int, int16, int64, int64) error
poisoned atomic.Bool
@@ -67,6 +69,7 @@ var registry = struct {
var barrier atomic.Uint64
var pageSize = unix.Getpagesize
+var beforePin func()
func fp[T any](f T) uintptr { return *(*uintptr)(unsafe.Pointer(&f)) }
@@ -80,8 +83,10 @@ func plain(name string) bool {
}
// Register borrows two already-open directory descriptors and retains its own
-// copies. The main file must already exist. Only one custom VFS may be registered in the process; a second is refused before opening any additional main descriptor.
-func Register(data, meta *os.File, name string) (*VFS, error) {
+// copies. After reserving the process's single registration, it opens and pins
+// the existing regular main file described by accepted. A refused second
+// registration never opens main: closing that fd would drop the live POSIX locks.
+func Register(data, meta *os.File, name string, accepted authority.Facts) (*VFS, error) {
if !plain(name) {
return nil, unix.EINVAL
}
@@ -95,7 +100,10 @@ func Register(data, meta *os.File, name string) (*VFS, error) {
if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Nlink != 1 {
return nil, unix.EINVAL
}
- id := identity{s.Dev, s.Ino}
+ id := identity{accepted.Dev, accepted.Ino}
+ if !same(&s, id) {
+ return nil, unix.ESTALE
+ }
registry.Lock()
if len(registry.inodes) != 0 {
registry.Unlock()
@@ -103,7 +111,7 @@ func Register(data, meta *os.File, name string) (*VFS, error) {
}
registry.inodes[id] = true
registry.Unlock()
- v := &VFS{data: -1, meta: -1, main: name, expected: id, files: make(map[uintptr]*file), tls: libc.NewTLS()}
+ v := &VFS{data: -1, meta: -1, pin: -1, main: name, expected: id, files: make(map[uintptr]*file), tls: libc.NewTLS()}
var err error
defer func() {
if err != nil {
@@ -118,6 +126,17 @@ func Register(data, meta *os.File, name string) (*VFS, error) {
if err != nil {
return nil, err
}
+ if beforePin != nil {
+ beforePin()
+ }
+ v.pin, err = unix.Openat(v.data, name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_NOCTTY|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return nil, err
+ }
+ if err = unix.Fstat(v.pin, &s); err != nil || s.Nlink != 1 || !same(&s, id) {
+ err = unix.ESTALE
+ return nil, err
+ }
nonce := make([]byte, 8)
if _, err = rand.Read(nonce); err != nil {
return nil, err
@@ -180,8 +199,11 @@ func (v *VFS) Close() error {
func (v *VFS) dispose() {
registry.Lock()
delete(registry.vfs, v.ptr)
- delete(registry.inodes, v.expected)
registry.Unlock()
+ if v.pin >= 0 {
+ unix.Close(v.pin)
+ v.pin = -1
+ }
if v.data >= 0 {
unix.Close(v.data)
}
@@ -198,6 +220,13 @@ func (v *VFS) dispose() {
libc.Xfree(v.tls, v.cname)
}
v.tls.Close()
+ // Keep the reservation until no owned descriptor can release another VFS's locks.
+ if v.beforeRelease != nil {
+ v.beforeRelease()
+ }
+ registry.Lock()
+ delete(registry.inodes, v.expected)
+ registry.Unlock()
}
func (v *VFS) allowed(name string) bool {
return name == v.main || name == v.main+"-wal" || name == v.main+"-journal" || name == v.main+"-shm"
diff --git a/internal/vfsspike/vfs_linux_test.go b/internal/vfsspike/vfs_linux_test.go
index 9fa72836e..c4cf81965 100644
--- a/internal/vfsspike/vfs_linux_test.go
+++ b/internal/vfsspike/vfs_linux_test.go
@@ -27,7 +27,7 @@ func setup(t *testing.T) (*VFS, string) {
t.Fatal(err)
}
defer d.Close()
- v, err := Register(d, d, "main.db")
+ v, err := Register(d, d, "main.db", acceptedMain(t, d, "main.db"))
if err != nil {
t.Fatal(err)
}
@@ -290,7 +290,7 @@ func TestSecondRegistrationRefused(t *testing.T) {
t.Fatal(err)
}
defer d.Close()
- if other, err := Register(d, d, "other.db"); err == nil {
+ if other, err := Register(d, d, "other.db", acceptedMain(t, d, "other.db")); err == nil {
other.Close()
t.Fatal("second registration accepted")
}