Verified Commit 2acd716d authored by Nathan Herring's avatar Nathan Herring
Browse files

test: add Linux OFD lock persistence test across os.Close

parent bd9dcb65
Loading
Loading
Loading
Loading

ofd_linux_test.go

0 → 100644
+96 −0
Original line number Diff line number Diff line
// Copyright 2026 The Sqlite Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build linux

package sqlite

import (
	"context"
	"database/sql"
	"path/filepath"
	"syscall"
	"testing"
)

const (
	fOfdSetlk = 37
)

// TestOFDLockSurvivesOSClose verifies that closing an unrelated os.File descriptor
// pointing to the same inode does not strip the active SQLite database lock on Linux.
// Under standard POSIX inode locks (F_SETLK), close() on any descriptor drops
// all locks for that inode across the entire process. With Open File Description
// (OFD) locking (F_OFD_SETLK), locks are attached to the open file description,
// preventing accidental unlock on os.Close().
func TestOFDLockSurvivesOSClose(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "ofd_test.db")

	db, err := sql.Open(driverName, dbPath)
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()

	ctx := context.Background()
	conn, err := db.Conn(ctx)
	if err != nil {
		t.Fatal(err)
	}
	defer conn.Close()

	if _, err := conn.ExecContext(ctx, "CREATE TABLE t(x);"); err != nil {
		t.Fatal(err)
	}

	tx, err := conn.BeginTx(ctx, nil)
	if err != nil {
		t.Fatal(err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx, "INSERT INTO t VALUES(1);"); err != nil {
		t.Fatal(err)
	}

	// Open and close an independent descriptor to the same database file.
	// On standard POSIX locking, this close() unconditionally strips the lock on the inode.
	fd, err := syscall.Open(dbPath, syscall.O_RDONLY, 0)
	if err != nil {
		t.Fatal(err)
	}
	if err := syscall.Close(fd); err != nil {
		t.Fatal(err)
	}

	// Now verify from a raw OS file descriptor that the OFD lock is still held by SQLite.
	// We use syscall.FcntlFlock so that 32-bit platforms (linux/386, linux/arm) route through fcntl64.
	probeFd, err := syscall.Open(dbPath, syscall.O_RDWR, 0)
	if err != nil {
		t.Fatal(err)
	}
	defer syscall.Close(probeFd)

	fl := syscall.Flock_t{
		Type:   syscall.F_WRLCK,
		Whence: 0,
		Start:  0,
		Len:    0,
	}

	err = syscall.FcntlFlock(uintptr(probeFd), fOfdSetlk, &fl)
	if err == syscall.EINVAL {
		t.Skipf("F_OFD_SETLK unsupported on this kernel/filesystem (errno %v)", err)
	}
	if err == nil {
		t.Fatal("kernel allowed conflicting write lock on probeFd; SQLite lock was stripped by close()")
	}
	if err != syscall.EAGAIN && err != syscall.EACCES {
		t.Fatalf("expected EAGAIN or EACCES when probing lock, got: %v", err)
	}

	if err := tx.Commit(); err != nil {
		t.Fatalf("tx.Commit failed: %v", err)
	}
}