Checkpoint descriptor-relative SQLite VFS experiment
3 files changed,
+1074
−0
internal/vfsspike/locks_linux.go+255 −0internal/vfsspike/vfs_linux.go+473 −0internal/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) |
| + } |
| + }) |
| + } |
| +} |