carlos / platform Public

Checkpoint descriptor-relative SQLite VFS experiment

claude agent, operated by paul@keymail.dev 9d2fd891a5d0448a8b149ac980d284fd2a1782c8 parent b634c44
3 files changed, +1074 −0
  • internal/vfsspike/locks_linux.go +255 −0
  • internal/vfsspike/vfs_linux.go +473 −0
  • internal/vfsspike/vfs_linux_test.go +346 −0

CI failed — run details

diff --git a/internal/vfsspike/locks_linux.go b/internal/vfsspike/locks_linux.go
new file mode 100644
index 000000000..4aca9b967
--- /dev/null
+++ b/internal/vfsspike/locks_linux.go
@@ -0,0 +1,255 @@
+//go:build linux && (amd64 || arm64)
+
+package vfsspike
+
+import (
+ "errors"
+ "golang.org/x/sys/unix"
+ "modernc.org/libc"
+ sqlite "modernc.org/sqlite/lib"
+ "unsafe"
+)
+
+const pending int64 = 0x40000000
+
+func setLock(fd int, kind int16, start, length int64) error {
+ return unix.FcntlFlock(uintptr(fd), unix.F_SETLK, &unix.Flock_t{Type: kind, Whence: 0, Start: start, Len: length})
+}
+func lockResult(err error) int32 {
+ if err == nil {
+ return sqlite.SQLITE_OK
+ }
+ if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EACCES) {
+ return sqlite.SQLITE_BUSY
+ }
+ return sqlite.SQLITE_IOERR_LOCK
+}
+func lockFile(t *libc.TLS, p uintptr, want int32) int32 {
+ f := getFile(p)
+ f.v.mu.Lock()
+ defer f.v.mu.Unlock()
+ if !f.main {
+ return sqlite.SQLITE_IOERR_LOCK
+ }
+ if want <= f.lock {
+ return sqlite.SQLITE_OK
+ }
+ if f.lock == sqlite.SQLITE_LOCK_NONE {
+ if rc := lockResult(setLock(f.fd, unix.F_RDLCK, pending, 1)); rc != 0 {
+ return rc
+ }
+ err := setLock(f.fd, unix.F_RDLCK, pending+2, 510)
+ unlockErr := setLock(f.fd, unix.F_UNLCK, pending, 1)
+ if err != nil {
+ return lockResult(err)
+ }
+ if unlockErr != nil {
+ return sqlite.SQLITE_IOERR_UNLOCK
+ }
+ f.lock = sqlite.SQLITE_LOCK_SHARED
+ }
+ if want == sqlite.SQLITE_LOCK_SHARED {
+ return sqlite.SQLITE_OK
+ }
+ if want == sqlite.SQLITE_LOCK_RESERVED {
+ if rc := lockResult(setLock(f.fd, unix.F_WRLCK, pending+1, 1)); rc != 0 {
+ return rc
+ }
+ f.lock = want
+ return sqlite.SQLITE_OK
+ }
+ if want != sqlite.SQLITE_LOCK_EXCLUSIVE && want != sqlite.SQLITE_LOCK_PENDING {
+ return sqlite.SQLITE_IOERR_LOCK
+ }
+ if f.lock < sqlite.SQLITE_LOCK_PENDING {
+ if rc := lockResult(setLock(f.fd, unix.F_WRLCK, pending, 1)); rc != 0 {
+ return rc
+ }
+ f.lock = sqlite.SQLITE_LOCK_PENDING
+ }
+ if want == sqlite.SQLITE_LOCK_PENDING {
+ return sqlite.SQLITE_OK
+ }
+ if rc := lockResult(setLock(f.fd, unix.F_WRLCK, pending+2, 510)); rc != 0 {
+ return rc
+ }
+ f.lock = want
+ return sqlite.SQLITE_OK
+}
+func unlockFile(t *libc.TLS, p uintptr, want int32) int32 {
+ f := getFile(p)
+ f.v.mu.Lock()
+ defer f.v.mu.Unlock()
+ if want != sqlite.SQLITE_LOCK_NONE && want != sqlite.SQLITE_LOCK_SHARED {
+ return sqlite.SQLITE_IOERR_UNLOCK
+ }
+ if want >= f.lock {
+ return sqlite.SQLITE_OK
+ }
+ if want == sqlite.SQLITE_LOCK_NONE {
+ if setLock(f.fd, unix.F_UNLCK, pending, 512) != nil {
+ return sqlite.SQLITE_IOERR_UNLOCK
+ }
+ } else {
+ if f.lock == sqlite.SQLITE_LOCK_EXCLUSIVE && setLock(f.fd, unix.F_RDLCK, pending+2, 510) != nil {
+ return sqlite.SQLITE_IOERR_UNLOCK
+ }
+ if setLock(f.fd, unix.F_UNLCK, pending, 2) != nil {
+ return sqlite.SQLITE_IOERR_UNLOCK
+ }
+ }
+ f.lock = want
+ return sqlite.SQLITE_OK
+}
+func reservedFile(t *libc.TLS, p, out uintptr) int32 {
+ f := getFile(p)
+ f.v.mu.Lock()
+ defer f.v.mu.Unlock()
+ *(*int32)(unsafe.Pointer(out)) = 0
+ if f.lock >= sqlite.SQLITE_LOCK_RESERVED {
+ *(*int32)(unsafe.Pointer(out)) = 1
+ return sqlite.SQLITE_OK
+ }
+ l := unix.Flock_t{Type: unix.F_WRLCK, Whence: 0, Start: pending + 1, Len: 1}
+ if unix.FcntlFlock(uintptr(f.fd), unix.F_GETLK, &l) != nil {
+ return sqlite.SQLITE_IOERR_CHECKRESERVEDLOCK
+ }
+ if l.Type != unix.F_UNLCK {
+ *(*int32)(unsafe.Pointer(out)) = 1
+ }
+ return sqlite.SQLITE_OK
+}
+func ensureSHM(f *file) int32 {
+ if f.shm >= 0 {
+ return sqlite.SQLITE_OK
+ }
+ if !f.main || f.v.recheck() != nil {
+ return sqlite.SQLITE_IOERR_SHMOPEN
+ }
+ name := f.name + "-shm"
+ var before unix.Stat_t
+ if err := unix.Fstatat(f.v.data, name, &before, unix.AT_SYMLINK_NOFOLLOW); err == nil && (before.Mode&unix.S_IFMT != unix.S_IFREG || same(&before, f.v.expected)) {
+ return sqlite.SQLITE_IOERR_SHMOPEN
+ }
+ fd, err := unix.Openat(f.v.data, name, unix.O_RDWR|unix.O_CREAT|unix.O_CLOEXEC|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0600)
+ if err != nil {
+ return sqlite.SQLITE_IOERR_SHMOPEN
+ }
+ var s unix.Stat_t
+ if unix.Fstat(fd, &s) != nil || s.Mode&unix.S_IFMT != unix.S_IFREG || s.Nlink != 1 || same(&s, f.v.expected) || f.v.alias(&s) {
+ f.v.reject(fd)
+ return sqlite.SQLITE_IOERR_SHMOPEN
+ }
+ if err = setLock(fd, unix.F_WRLCK, 128, 1); err == nil {
+ if unix.Ftruncate(fd, 0) != nil {
+ f.v.reject(fd)
+ return sqlite.SQLITE_IOERR_SHMSIZE
+ }
+ } else if !errors.Is(err, unix.EAGAIN) && !errors.Is(err, unix.EACCES) {
+ f.v.reject(fd)
+ return sqlite.SQLITE_IOERR_SHMLOCK
+ }
+ if setLock(fd, unix.F_RDLCK, 128, 1) != nil {
+ f.v.reject(fd)
+ return sqlite.SQLITE_BUSY
+ }
+ f.shm = fd
+ f.shmID = identity{s.Dev, s.Ino}
+ return sqlite.SQLITE_OK
+}
+func shmMap(t *libc.TLS, p uintptr, page, size, extend int32, out uintptr) int32 {
+ *(*uintptr)(unsafe.Pointer(out)) = 0
+ f := getFile(p)
+ f.v.mu.Lock()
+ defer f.v.mu.Unlock()
+ if page < 0 || size <= 0 || int64(page)*int64(size)%int64(unix.Getpagesize()) != 0 {
+ return sqlite.SQLITE_IOERR_SHMMAP
+ }
+ if rc := ensureSHM(f); rc != 0 {
+ return rc
+ }
+ if b := f.regions[page]; b != nil {
+ if len(b) != int(size) {
+ return sqlite.SQLITE_IOERR_SHMMAP
+ }
+ *(*uintptr)(unsafe.Pointer(out)) = uintptr(unsafe.Pointer(&b[0]))
+ return sqlite.SQLITE_OK
+ }
+ var s unix.Stat_t
+ if unix.Fstat(f.shm, &s) != nil {
+ 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
+ }
+ }
+ b, err := unix.Mmap(f.shm, int64(page)*int64(size), int(size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
+ if err != nil {
+ return sqlite.SQLITE_IOERR_SHMMAP
+ }
+ f.regions[page] = b
+ *(*uintptr)(unsafe.Pointer(out)) = uintptr(unsafe.Pointer(&b[0]))
+ return sqlite.SQLITE_OK
+}
+func shmLock(t *libc.TLS, p uintptr, offset, n, flags int32) int32 {
+ f := getFile(p)
+ f.v.mu.Lock()
+ defer f.v.mu.Unlock()
+ if offset < 0 || n < 1 || offset+n > 8 {
+ return sqlite.SQLITE_IOERR_SHMLOCK
+ }
+ if rc := ensureSHM(f); rc != 0 {
+ return rc
+ }
+ var kind int16
+ switch flags {
+ case sqlite.SQLITE_SHM_LOCK | sqlite.SQLITE_SHM_SHARED:
+ kind = unix.F_RDLCK
+ case sqlite.SQLITE_SHM_LOCK | sqlite.SQLITE_SHM_EXCLUSIVE:
+ kind = unix.F_WRLCK
+ case sqlite.SQLITE_SHM_UNLOCK | sqlite.SQLITE_SHM_SHARED, sqlite.SQLITE_SHM_UNLOCK | sqlite.SQLITE_SHM_EXCLUSIVE:
+ kind = unix.F_UNLCK
+ default:
+ return sqlite.SQLITE_IOERR_SHMLOCK
+ }
+ if rc := lockResult(setLock(f.shm, kind, 120+int64(offset), int64(n))); rc != 0 {
+ if rc == sqlite.SQLITE_BUSY {
+ return rc
+ }
+ return sqlite.SQLITE_IOERR_SHMLOCK
+ }
+ return sqlite.SQLITE_OK
+}
+func shmBarrier(t *libc.TLS, p uintptr) { barrier.Add(1) }
+func unmapLocked(f *file, remove bool) int32 {
+ rc := int32(sqlite.SQLITE_OK)
+ for page, b := range f.regions {
+ if unix.Munmap(b) != nil {
+ rc = sqlite.SQLITE_IOERR_SHMMAP
+ }
+ delete(f.regions, page)
+ }
+ if f.shm >= 0 {
+ if remove && !f.persistent && f.v.recheck() == nil && setLock(f.shm, unix.F_WRLCK, 128, 1) == nil {
+ if err := unix.Unlinkat(f.v.data, f.name+"-shm", 0); err != nil && !errors.Is(err, unix.ENOENT) {
+ rc = sqlite.SQLITE_IOERR_DELETE
+ }
+ }
+ if unix.Close(f.shm) != nil {
+ rc = sqlite.SQLITE_IOERR_CLOSE
+ }
+ f.shm = -1
+ }
+ return rc
+}
+func shmUnmap(t *libc.TLS, p uintptr, remove int32) int32 {
+ f := getFile(p)
+ f.v.mu.Lock()
+ defer f.v.mu.Unlock()
+ return unmapLocked(f, remove != 0)
+}
diff --git a/internal/vfsspike/vfs_linux.go b/internal/vfsspike/vfs_linux.go
new file mode 100644
index 000000000..896cde8f6
--- /dev/null
+++ b/internal/vfsspike/vfs_linux.go
@@ -0,0 +1,473 @@
+//go:build linux && (amd64 || arm64)
+
+// Package vfsspike is an isolated single-connection experiment. It must not be
+// mixed with another SQLite VFS on the same inode in this process: POSIX locks
+// are process-owned. Multi-connection bookkeeping is a later spike stage.
+package vfsspike
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+ "modernc.org/libc"
+ sqlite "modernc.org/sqlite/lib"
+)
+
+type identity struct{ dev, ino uint64 }
+type VFS struct {
+ mu sync.Mutex
+ Name string
+ data, meta int
+ main string
+ expected identity
+ ptr, cname, methods uintptr
+ tls *libc.TLS
+ files map[string]*file
+ closed bool
+ rejected []int
+ beforeOpen func(string)
+}
+type file struct {
+ v *VFS
+ fd int
+ id identity
+ shmID identity
+ name string
+ main bool
+ temporary bool
+ lock int32
+ shm int
+ regions map[int32][]byte
+ shared, exclusive uint16
+ persistent bool
+}
+
+var registry = struct {
+ sync.RWMutex
+ vfs map[uintptr]*VFS
+ files map[uintptr]*file
+ inodes map[identity]bool
+}{vfs: make(map[uintptr]*VFS), files: make(map[uintptr]*file), inodes: make(map[identity]bool)}
+var barrier atomic.Uint64
+
+func fp[T any](f T) uintptr { return *(*uintptr)(unsafe.Pointer(&f)) }
+func bytesAt(p uintptr, n int) []byte { return unsafe.Slice((*byte)(unsafe.Pointer(p)), n) }
+func getVFS(p uintptr) *VFS { registry.RLock(); defer registry.RUnlock(); return registry.vfs[p] }
+func getFile(p uintptr) *file { registry.RLock(); defer registry.RUnlock(); return registry.files[p] }
+func same(s *unix.Stat_t, id identity) bool {
+ return s.Dev == id.dev && s.Ino == id.ino && s.Mode&unix.S_IFMT == unix.S_IFREG
+}
+func plain(name string) bool {
+ return name != "" && name != "." && name != ".." && !strings.ContainsAny(name, "/\\\x00")
+}
+
+// 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) {
+ if !plain(name) {
+ return nil, unix.EINVAL
+ }
+ var s unix.Stat_t
+ if err := unix.Fstatat(int(data.Fd()), name, &s, unix.AT_SYMLINK_NOFOLLOW); err != nil {
+ return nil, err
+ }
+ if s.Mode&unix.S_IFMT != unix.S_IFREG || s.Nlink != 1 {
+ return nil, unix.EINVAL
+ }
+ id := identity{s.Dev, s.Ino}
+ registry.Lock()
+ if len(registry.inodes) != 0 {
+ registry.Unlock()
+ return nil, unix.EBUSY
+ }
+ registry.inodes[id] = true
+ registry.Unlock()
+ v := &VFS{data: -1, meta: -1, main: name, expected: id, files: make(map[string]*file), tls: libc.NewTLS()}
+ var err error
+ defer func() {
+ if err != nil {
+ v.dispose()
+ }
+ }()
+ v.data, err = unix.FcntlInt(data.Fd(), unix.F_DUPFD_CLOEXEC, 0)
+ if err != nil {
+ return nil, err
+ }
+ v.meta, err = unix.FcntlInt(meta.Fd(), unix.F_DUPFD_CLOEXEC, 0)
+ if err != nil {
+ return nil, err
+ }
+ nonce := make([]byte, 8)
+ if _, err = rand.Read(nonce); err != nil {
+ return nil, err
+ }
+ v.Name = "carlos-spike-" + hex.EncodeToString(nonce)
+ v.cname, err = libc.CString(v.Name)
+ if err != nil {
+ return nil, err
+ }
+ v.ptr = sqlite.Xsqlite3_malloc64(v.tls, uint64(unsafe.Sizeof(sqlite.Tsqlite3_vfs{})))
+ v.methods = sqlite.Xsqlite3_malloc64(v.tls, uint64(unsafe.Sizeof(sqlite.Tsqlite3_io_methods{})))
+ if v.ptr == 0 || v.methods == 0 {
+ err = unix.ENOMEM
+ return nil, err
+ }
+ *(*sqlite.Tsqlite3_io_methods)(unsafe.Pointer(v.methods)) = sqlite.Tsqlite3_io_methods{
+ FiVersion: 2, FxClose: fp(closeFile), FxRead: fp(readFile), FxWrite: fp(writeFile), FxTruncate: fp(truncateFile), FxSync: fp(syncFile), FxFileSize: fp(sizeFile),
+ FxLock: fp(lockFile), FxUnlock: fp(unlockFile), FxCheckReservedLock: fp(reservedFile), FxFileControl: fp(controlFile), FxSectorSize: fp(sectorSize), FxDeviceCharacteristics: fp(characteristics),
+ FxShmMap: fp(shmMap), FxShmLock: fp(shmLock), FxShmBarrier: fp(shmBarrier), FxShmUnmap: fp(shmUnmap),
+ }
+ *(*sqlite.Tsqlite3_vfs)(unsafe.Pointer(v.ptr)) = sqlite.Tsqlite3_vfs{
+ FiVersion: 2, FszOsFile: int32(unsafe.Sizeof(sqlite.Tsqlite3_file{})), FmxPathname: 255, FzName: v.cname,
+ FxOpen: fp(openFile), FxDelete: fp(deleteFile), FxAccess: fp(accessFile), FxFullPathname: fp(fullPath),
+ FxDlOpen: fp(dlOpen), FxDlError: fp(dlError), FxDlSym: fp(dlSym), FxDlClose: fp(dlClose),
+ FxRandomness: fp(randomness), FxSleep: fp(sleep), FxCurrentTime: fp(currentTime), FxCurrentTimeInt64: fp(currentTime64),
+ }
+ registry.Lock()
+ registry.vfs[v.ptr] = v
+ registry.Unlock()
+ if rc := sqlite.Xsqlite3_vfs_register(v.tls, v.ptr, 0); rc != sqlite.SQLITE_OK {
+ err = fmt.Errorf("register: sqlite code %d", rc)
+ return nil, err
+ }
+ return v, nil
+}
+func (v *VFS) Close() error {
+ v.mu.Lock()
+ defer v.mu.Unlock()
+ if v.closed {
+ return nil
+ }
+ if len(v.files) != 0 {
+ return unix.EBUSY
+ }
+ if rc := sqlite.Xsqlite3_vfs_unregister(v.tls, v.ptr); rc != sqlite.SQLITE_OK {
+ return fmt.Errorf("unregister: sqlite code %d", rc)
+ }
+ v.dispose()
+ v.closed = true
+ return nil
+}
+func (v *VFS) dispose() {
+ registry.Lock()
+ delete(registry.vfs, v.ptr)
+ delete(registry.inodes, v.expected)
+ registry.Unlock()
+ if v.data >= 0 {
+ unix.Close(v.data)
+ }
+ if v.meta >= 0 {
+ unix.Close(v.meta)
+ }
+ if v.ptr != 0 {
+ sqlite.Xsqlite3_free(v.tls, v.ptr)
+ }
+ if v.methods != 0 {
+ sqlite.Xsqlite3_free(v.tls, v.methods)
+ }
+ if v.cname != 0 {
+ libc.Xfree(v.tls, v.cname)
+ }
+ v.tls.Close()
+}
+func (v *VFS) allowed(name string) bool {
+ return name == v.main || name == v.main+"-wal" || name == v.main+"-journal" || name == v.main+"-shm"
+}
+func (v *VFS) recheck() error {
+ var s unix.Stat_t
+ if err := unix.Fstatat(v.data, v.main, &s, unix.AT_SYMLINK_NOFOLLOW); err != nil {
+ return err
+ }
+ if !same(&s, v.expected) {
+ return unix.ESTALE
+ }
+ return nil
+}
+func openFile(tls *libc.TLS, p, z, out uintptr, flags int32, outFlags uintptr) int32 {
+ (*sqlite.Tsqlite3_file)(unsafe.Pointer(out)).FpMethods = 0
+ v := getVFS(p)
+ if v == nil {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ v.mu.Lock()
+ defer v.mu.Unlock()
+ if v.closed || v.recheck() != nil {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ name := libc.GoString(z)
+ temporary := z == 0
+ dir := v.data
+ if temporary {
+ b := make([]byte, 12)
+ if _, err := rand.Read(b); err != nil {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ name = "sqlite-spike-" + hex.EncodeToString(b)
+ dir = v.meta
+ } else if !v.allowed(name) {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ if v.files[name] != nil {
+ return sqlite.SQLITE_BUSY
+ }
+ isMain := flags&sqlite.SQLITE_OPEN_MAIN_DB != 0
+ if isMain && name != v.main {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ if !isMain && !temporary && name == v.main {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ oflags := unix.O_CLOEXEC | unix.O_NOFOLLOW | unix.O_NOCTTY | unix.O_NONBLOCK
+ if flags&sqlite.SQLITE_OPEN_READWRITE != 0 {
+ oflags |= unix.O_RDWR
+ } else {
+ oflags |= unix.O_RDONLY
+ }
+ if flags&sqlite.SQLITE_OPEN_CREATE != 0 {
+ oflags |= unix.O_CREAT
+ }
+ if temporary || flags&sqlite.SQLITE_OPEN_EXCLUSIVE != 0 {
+ oflags |= unix.O_EXCL
+ }
+ if v.beforeOpen != nil {
+ v.beforeOpen(name)
+ }
+ fd, err := unix.Openat(dir, name, oflags, 0600)
+ if err != nil {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ var s unix.Stat_t
+ if err = unix.Fstat(fd, &s); err != nil || s.Mode&unix.S_IFMT != unix.S_IFREG || s.Nlink != 1 || v.alias(&s) || (isMain && !same(&s, v.expected)) {
+ v.reject(fd)
+ return sqlite.SQLITE_CANTOPEN
+ }
+ f := &file{v: v, fd: fd, id: identity{s.Dev, s.Ino}, name: name, main: isMain, temporary: temporary, shm: -1, regions: make(map[int32][]byte)}
+ if flags&sqlite.SQLITE_OPEN_DELETEONCLOSE != 0 {
+ if err := unix.Unlinkat(dir, name, 0); err != nil {
+ v.reject(fd)
+ return sqlite.SQLITE_CANTOPEN
+ }
+ }
+ v.files[name] = f
+ registry.Lock()
+ registry.files[out] = f
+ registry.Unlock()
+ (*sqlite.Tsqlite3_file)(unsafe.Pointer(out)).FpMethods = v.methods
+ if outFlags != 0 {
+ *(*int32)(unsafe.Pointer(outFlags)) = flags
+ }
+ return sqlite.SQLITE_OK
+}
+func closeFile(tls *libc.TLS, p uintptr) int32 {
+ f := getFile(p)
+ if f == nil {
+ return sqlite.SQLITE_IOERR_CLOSE
+ }
+ v := f.v
+ v.mu.Lock()
+ defer v.mu.Unlock()
+ rc := int32(sqlite.SQLITE_OK)
+ if f.shm >= 0 {
+ rc = unmapLocked(f, false)
+ }
+ err := unix.Close(f.fd)
+ delete(v.files, f.name)
+ v.drainRejected()
+ registry.Lock()
+ delete(registry.files, p)
+ registry.Unlock()
+ (*sqlite.Tsqlite3_file)(unsafe.Pointer(p)).FpMethods = 0
+ if err != nil {
+ return sqlite.SQLITE_IOERR_CLOSE
+ }
+ return rc
+}
+func readFile(tls *libc.TLS, p, b uintptr, n int32, off int64) int32 {
+ f := getFile(p)
+ buf := bytesAt(b, int(n))
+ got, err := unix.Pread(f.fd, buf, off)
+ if err != nil && err != io.EOF {
+ return sqlite.SQLITE_IOERR_READ
+ }
+ if got < len(buf) {
+ clear(buf[got:])
+ return sqlite.SQLITE_IOERR_SHORT_READ
+ }
+ return sqlite.SQLITE_OK
+}
+func writeFile(tls *libc.TLS, p, b uintptr, n int32, off int64) int32 {
+ f := getFile(p)
+ buf := bytesAt(b, int(n))
+ for len(buf) > 0 {
+ got, err := unix.Pwrite(f.fd, buf, off)
+ if err != nil || got == 0 {
+ return sqlite.SQLITE_IOERR_WRITE
+ }
+ buf = buf[got:]
+ off += int64(got)
+ }
+ return sqlite.SQLITE_OK
+}
+func truncateFile(tls *libc.TLS, p uintptr, n int64) int32 {
+ if unix.Ftruncate(getFile(p).fd, n) != nil {
+ return sqlite.SQLITE_IOERR_TRUNCATE
+ }
+ return sqlite.SQLITE_OK
+}
+func syncFile(tls *libc.TLS, p uintptr, flags int32) int32 {
+ f := getFile(p)
+ if unix.Fsync(f.fd) != nil {
+ return sqlite.SQLITE_IOERR_FSYNC
+ }
+ if !f.temporary && unix.Fsync(f.v.data) != nil {
+ return sqlite.SQLITE_IOERR_DIR_FSYNC
+ }
+ return sqlite.SQLITE_OK
+}
+func sizeFile(tls *libc.TLS, p, out uintptr) int32 {
+ var s unix.Stat_t
+ if unix.Fstat(getFile(p).fd, &s) != nil {
+ return sqlite.SQLITE_IOERR_FSTAT
+ }
+ *(*int64)(unsafe.Pointer(out)) = s.Size
+ return sqlite.SQLITE_OK
+}
+func fullPath(tls *libc.TLS, p, z uintptr, n int32, out uintptr) int32 {
+ v := getVFS(p)
+ name := libc.GoString(z)
+ if !v.allowed(name) || len(name) >= int(n) {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ buf := bytesAt(out, int(n))
+ copy(buf, name)
+ buf[len(name)] = 0
+ return sqlite.SQLITE_OK
+}
+func accessFile(tls *libc.TLS, p, z uintptr, flags int32, out uintptr) int32 {
+ *(*int32)(unsafe.Pointer(out)) = 0
+ v := getVFS(p)
+ name := libc.GoString(z)
+ if !v.allowed(name) {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ var s unix.Stat_t
+ err := unix.Fstatat(v.data, name, &s, unix.AT_SYMLINK_NOFOLLOW)
+ if errors.Is(err, unix.ENOENT) {
+ return sqlite.SQLITE_OK
+ }
+ if err != nil {
+ return sqlite.SQLITE_IOERR_ACCESS
+ }
+ if s.Mode&unix.S_IFMT != unix.S_IFREG {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ if name == v.main && !same(&s, v.expected) {
+ return sqlite.SQLITE_CANTOPEN
+ }
+ *(*int32)(unsafe.Pointer(out)) = 1
+ return sqlite.SQLITE_OK
+}
+func deleteFile(tls *libc.TLS, p, z uintptr, syncDir int32) int32 {
+ v := getVFS(p)
+ name := libc.GoString(z)
+ if !v.allowed(name) || name == v.main || v.recheck() != nil {
+ return sqlite.SQLITE_IOERR_DELETE
+ }
+ if err := unix.Unlinkat(v.data, name, 0); err != nil && !errors.Is(err, unix.ENOENT) {
+ return sqlite.SQLITE_IOERR_DELETE
+ }
+ if syncDir != 0 && unix.Fsync(v.data) != nil {
+ return sqlite.SQLITE_IOERR_DIR_FSYNC
+ }
+ return sqlite.SQLITE_OK
+}
+func dlOpen(*libc.TLS, uintptr, uintptr) uintptr { return 0 }
+func dlError(tls *libc.TLS, p uintptr, n int32, out uintptr) {
+ if n > 0 {
+ *(*byte)(unsafe.Pointer(out)) = 0
+ }
+}
+func dlSym(*libc.TLS, uintptr, uintptr, uintptr) uintptr { return 0 }
+func dlClose(*libc.TLS, uintptr, uintptr) {}
+func randomness(tls *libc.TLS, p uintptr, n int32, out uintptr) int32 {
+ b := bytesAt(out, int(n))
+ if _, err := rand.Read(b); err != nil {
+ return 0
+ }
+ return n
+}
+func sleep(tls *libc.TLS, p uintptr, n int32) int32 {
+ time.Sleep(time.Duration(n) * time.Microsecond)
+ return n
+}
+func currentTime64(tls *libc.TLS, p, out uintptr) int32 {
+ *(*int64)(unsafe.Pointer(out)) = time.Now().UnixMilli() + 210866760000000
+ return sqlite.SQLITE_OK
+}
+func currentTime(tls *libc.TLS, p, out uintptr) int32 {
+ *(*float64)(unsafe.Pointer(out)) = float64(time.Now().UnixMilli())/86400000 + 2440587.5
+ return sqlite.SQLITE_OK
+}
+func sectorSize(*libc.TLS, uintptr) int32 { return 4096 }
+func characteristics(*libc.TLS, uintptr) int32 { return 0 }
+func controlFile(tls *libc.TLS, p uintptr, op int32, arg uintptr) int32 {
+ f := getFile(p)
+ switch op {
+ case sqlite.SQLITE_FCNTL_HAS_MOVED:
+ var moved int32
+ if f.v.recheck() != nil {
+ moved = 1
+ }
+ *(*int32)(unsafe.Pointer(arg)) = moved
+ return sqlite.SQLITE_OK
+ case sqlite.SQLITE_FCNTL_LOCKSTATE:
+ *(*int32)(unsafe.Pointer(arg)) = f.lock
+ return sqlite.SQLITE_OK
+ case sqlite.SQLITE_FCNTL_PERSIST_WAL:
+ v := *(*int32)(unsafe.Pointer(arg))
+ if v >= 0 {
+ f.persistent = v != 0
+ }
+ if f.persistent {
+ *(*int32)(unsafe.Pointer(arg)) = 1
+ } else {
+ *(*int32)(unsafe.Pointer(arg)) = 0
+ }
+ return sqlite.SQLITE_OK
+ case sqlite.SQLITE_FCNTL_POWERSAFE_OVERWRITE:
+ *(*int32)(unsafe.Pointer(arg)) = 0
+ return sqlite.SQLITE_OK
+ default:
+ return sqlite.SQLITE_NOTFOUND
+ }
+}
+
+func (v *VFS) alias(s *unix.Stat_t) bool {
+ id := identity{s.Dev, s.Ino}
+ for _, f := range v.files {
+ if f.id == id || (f.shm >= 0 && f.shmID == id) {
+ return true
+ }
+ }
+ return false
+}
+func (v *VFS) reject(fd int) { v.rejected = append(v.rejected, fd); v.drainRejected() }
+func (v *VFS) drainRejected() {
+ if len(v.files) != 0 {
+ return
+ }
+ for _, fd := range v.rejected {
+ unix.Close(fd)
+ }
+ v.rejected = nil
+}
diff --git a/internal/vfsspike/vfs_linux_test.go b/internal/vfsspike/vfs_linux_test.go
new file mode 100644
index 000000000..8b73c432c
--- /dev/null
+++ b/internal/vfsspike/vfs_linux_test.go
@@ -0,0 +1,346 @@
+//go:build linux && (amd64 || arm64)
+
+package vfsspike
+
+import (
+ "database/sql"
+ "golang.org/x/sys/unix"
+ "modernc.org/libc"
+ _ "modernc.org/sqlite"
+ sqlite "modernc.org/sqlite/lib"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "unsafe"
+)
+
+func setup(t *testing.T) (*VFS, string) {
+ 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)
+ }
+ defer d.Close()
+ v, err := Register(d, d, "main.db")
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if err := v.Close(); err != nil {
+ t.Error(err)
+ }
+ })
+ return v, dir
+}
+func connect(t *testing.T, v *VFS) *sql.DB {
+ t.Helper()
+ db, err := sql.Open("sqlite", "file:main.db?vfs="+v.Name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ db.SetMaxOpenConns(1)
+ t.Cleanup(func() { db.Close() })
+ return db
+}
+func execSQL(t *testing.T, db *sql.DB, q string) {
+ t.Helper()
+ if _, err := db.Exec(q); err != nil {
+ t.Fatalf("%s: %v", q, err)
+ }
+}
+func TestWALCheckpointReopen(t *testing.T) {
+ v, _ := setup(t)
+ db := connect(t, v)
+ var mode string
+ if err := db.QueryRow("PRAGMA journal_mode=WAL").Scan(&mode); err != nil || mode != "wal" {
+ t.Fatalf("mode %q: %v", mode, err)
+ }
+ execSQL(t, db, "CREATE TABLE data (value TEXT)")
+ execSQL(t, db, "INSERT INTO data VALUES ('retained')")
+ execSQL(t, db, "PRAGMA wal_checkpoint(TRUNCATE)")
+ if err := db.Close(); err != nil {
+ t.Fatal(err)
+ }
+ db = connect(t, v)
+ var value string
+ if err := db.QueryRow("SELECT value FROM data").Scan(&value); err != nil || value != "retained" {
+ t.Fatalf("value %q: %v", value, err)
+ }
+}
+func TestRefusesChangedMainAndUnsafeSidecars(t *testing.T) {
+ for _, kind := range []string{"replacement", "main-symlink", "main-fifo", "wal-symlink", "wal-fifo"} {
+ t.Run(kind, func(t *testing.T) {
+ v, dir := setup(t)
+ name := filepath.Join(dir, "main.db")
+ switch kind {
+ case "replacement":
+ if err := os.Rename(name, name+".old"); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(name, nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ case "main-symlink", "main-fifo":
+ if err := os.Remove(name); err != nil {
+ t.Fatal(err)
+ }
+ if kind == "main-symlink" {
+ if err := os.Symlink("target", name); err != nil {
+ t.Fatal(err)
+ }
+ } else if err := unix.Mkfifo(name, 0600); err != nil {
+ t.Fatal(err)
+ }
+ case "wal-symlink":
+ if err := os.Symlink("target", name+"-wal"); err != nil {
+ t.Fatal(err)
+ }
+ case "wal-fifo":
+ if err := unix.Mkfifo(name+"-wal", 0600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ db := connect(t, v)
+ if _, err := db.Exec("PRAGMA journal_mode=WAL; CREATE TABLE data(value)"); err == nil {
+ t.Fatal("unsafe file accepted")
+ }
+ })
+ }
+}
+func TestSecondConnectionRefused(t *testing.T) {
+ v, _ := setup(t)
+ first := connect(t, v)
+ execSQL(t, first, "CREATE TABLE data(value)")
+ second := connect(t, v)
+ if err := second.Ping(); err == nil {
+ t.Fatal("second connection accepted")
+ }
+ execSQL(t, first, "INSERT INTO data VALUES (1)")
+}
+func TestDirectoryRenameRetainsAuthority(t *testing.T) {
+ v, dir := setup(t)
+ moved := dir + "-moved"
+ if err := os.Rename(dir, moved); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { os.RemoveAll(moved) })
+ if err := os.Mkdir(dir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ db := connect(t, v)
+ execSQL(t, db, "PRAGMA journal_mode=WAL; CREATE TABLE data(value); INSERT INTO data VALUES (1)")
+ if _, err := os.Stat(filepath.Join(dir, "main.db")); !os.IsNotExist(err) {
+ t.Fatalf("replacement directory touched: %v", err)
+ }
+}
+
+func TestLockProbeChild(t *testing.T) {
+ path := os.Getenv("VFSSPIKE_LOCK_PROBE")
+ if path == "" {
+ t.Skip("child only")
+ }
+ fd, err := unix.Open(path, unix.O_RDWR|unix.O_NOFOLLOW, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer unix.Close(fd)
+ l := unix.Flock_t{Type: unix.F_WRLCK, Whence: 0, Start: pending + 1, Len: 1}
+ if err := unix.FcntlFlock(uintptr(fd), unix.F_GETLK, &l); err != nil {
+ t.Fatal(err)
+ }
+ if l.Type != unix.F_WRLCK {
+ t.Fatal("reserved lock disappeared")
+ }
+}
+func assertReserved(t *testing.T, path string) {
+ t.Helper()
+ exe, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.Command(exe, "-test.run=^TestLockProbeChild$")
+ if runner := os.Getenv("VFSSPIKE_TEST_RUNNER"); runner != "" {
+ cmd = exec.Command(runner, exe, "-test.run=^TestLockProbeChild$")
+ }
+ cmd.Env = append(os.Environ(), "VFSSPIKE_LOCK_PROBE="+path)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("probe: %v %s", err, output)
+ }
+}
+func TestRejectedAliasPreservesLocks(t *testing.T) {
+ for _, midOpen := range []bool{false, true} {
+ name := "existing"
+ if midOpen {
+ name = "mid-open"
+ }
+ t.Run(name, func(t *testing.T) {
+ v, dir := setup(t)
+ db := connect(t, v)
+ execSQL(t, db, "CREATE TABLE data(value)")
+ execSQL(t, db, "BEGIN IMMEDIATE")
+ path := filepath.Join(dir, "main.db")
+ assertReserved(t, path)
+ link := func() {
+ if err := os.Link(path, path+"-wal"); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if midOpen {
+ v.beforeOpen = func(name string) {
+ if name == "main.db-wal" {
+ link()
+ }
+ }
+ } else {
+ link()
+ }
+ z, err := libc.CString("main.db-wal")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer libc.Xfree(v.tls, z)
+ out := sqlite.Xsqlite3_malloc64(v.tls, uint64(unsafe.Sizeof(sqlite.Tsqlite3_file{})))
+ defer sqlite.Xsqlite3_free(v.tls, out)
+ rc := openFile(v.tls, v.ptr, z, out, sqlite.SQLITE_OPEN_READWRITE|sqlite.SQLITE_OPEN_CREATE|sqlite.SQLITE_OPEN_WAL, 0)
+ if rc == sqlite.SQLITE_OK {
+ t.Fatal("alias accepted")
+ }
+ if len(v.rejected) != 1 {
+ t.Fatalf("rejected descriptors %d", len(v.rejected))
+ }
+ assertReserved(t, path)
+ v.beforeOpen = nil
+ if err := os.Remove(path + "-wal"); err != nil {
+ t.Fatal(err)
+ }
+ execSQL(t, db, "ROLLBACK")
+ if err := db.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if len(v.rejected) != 0 {
+ t.Fatal("rejected descriptors retained after final close")
+ }
+ })
+ }
+}
+
+func TestRefusesUnsafeSHM(t *testing.T) {
+ for _, kind := range []string{"symlink", "fifo"} {
+ t.Run(kind, func(t *testing.T) {
+ v, dir := setup(t)
+ db := connect(t, v)
+ execSQL(t, db, "PRAGMA journal_mode=WAL; CREATE TABLE data(value)")
+ if err := db.Close(); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(dir, "main.db-shm")
+ if kind == "symlink" {
+ if err := os.Symlink("target", path); err != nil {
+ t.Fatal(err)
+ }
+ } else if err := unix.Mkfifo(path, 0600); err != nil {
+ t.Fatal(err)
+ }
+ db = connect(t, v)
+ if _, err := db.Exec("INSERT INTO data VALUES (1)"); err == nil {
+ t.Fatal("unsafe SHM accepted")
+ }
+ })
+ }
+}
+func TestSecondRegistrationRefused(t *testing.T) {
+ _, dir := setup(t)
+ if err := os.WriteFile(filepath.Join(dir, "other.db"), nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ d, err := os.Open(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer d.Close()
+ if other, err := Register(d, d, "other.db"); err == nil {
+ other.Close()
+ t.Fatal("second registration accepted")
+ }
+}
+
+func TestRefusedOpenClearsMethods(t *testing.T) {
+ v, _ := setup(t)
+ for _, tc := range []struct {
+ name string
+ ptr uintptr
+ }{{"main.db", 0}, {"outside.db", v.ptr}} {
+ t.Run(tc.name, func(t *testing.T) {
+ z, err := libc.CString(tc.name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer libc.Xfree(v.tls, z)
+ out := sqlite.Xsqlite3_malloc64(v.tls, uint64(unsafe.Sizeof(sqlite.Tsqlite3_file{})))
+ if out == 0 {
+ t.Fatal("allocation")
+ }
+ defer sqlite.Xsqlite3_free(v.tls, out)
+ output := (*sqlite.Tsqlite3_file)(unsafe.Pointer(out))
+ output.FpMethods = v.methods
+ if rc := openFile(v.tls, tc.ptr, z, out, sqlite.SQLITE_OPEN_MAIN_DB|sqlite.SQLITE_OPEN_READWRITE, 0); rc == sqlite.SQLITE_OK {
+ t.Fatal("open accepted")
+ }
+ if output.FpMethods != 0 {
+ t.Fatal("refused open retained methods")
+ }
+ })
+ }
+}
+
+func TestNamedFileSyncRequiresDirectorySync(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ kind int32
+ }{{"main.db", sqlite.SQLITE_OPEN_MAIN_DB}, {"main.db-journal", sqlite.SQLITE_OPEN_MAIN_JOURNAL}, {"main.db-wal", sqlite.SQLITE_OPEN_WAL}} {
+ t.Run(tc.name, func(t *testing.T) {
+ v, _ := setup(t)
+ z, err := libc.CString(tc.name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer libc.Xfree(v.tls, z)
+ out := sqlite.Xsqlite3_malloc64(v.tls, uint64(unsafe.Sizeof(sqlite.Tsqlite3_file{})))
+ if out == 0 {
+ t.Fatal("allocation")
+ }
+ defer sqlite.Xsqlite3_free(v.tls, out)
+ if rc := openFile(v.tls, v.ptr, z, out, tc.kind|sqlite.SQLITE_OPEN_READWRITE|sqlite.SQLITE_OPEN_CREATE, 0); rc != sqlite.SQLITE_OK {
+ t.Fatalf("open code %d", rc)
+ }
+ defer func() {
+ if rc := closeFile(v.tls, out); rc != sqlite.SQLITE_OK {
+ t.Errorf("close code %d", rc)
+ }
+ }()
+ if _, err := unix.Pwrite(getFile(out).fd, []byte("sync test"), 0); err != nil {
+ t.Fatal(err)
+ }
+ if rc := syncFile(v.tls, out, sqlite.SQLITE_SYNC_FULL); rc != sqlite.SQLITE_OK {
+ t.Fatalf("sync code %d", rc)
+ }
+ faultDir, err := unix.Openat(v.data, ".", unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer unix.Close(faultDir)
+ saved := v.data
+ v.data = faultDir
+ rc := syncFile(v.tls, out, sqlite.SQLITE_SYNC_FULL)
+ v.data = saved
+ if rc != sqlite.SQLITE_IOERR_DIR_FSYNC {
+ t.Fatalf("directory sync failure: got %d want %d", rc, sqlite.SQLITE_IOERR_DIR_FSYNC)
+ }
+ })
+ }
+}