From 63eab529dc3353e8d159e097ffc4caa7badb8cb3 Mon Sep 17 00:00:00 2001 From: Ben Johnson Date: Wed, 24 Jan 2024 12:41:05 -0700 Subject: [PATCH] Prevent WAL writes when using EXCLUSIVE locking mode (#426) --- cmd/litefs/mount_test.go | 37 +++++++++++++++++++++++++++++++++++++ db.go | 15 +++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/cmd/litefs/mount_test.go b/cmd/litefs/mount_test.go index 840614b..bf37208 100644 --- a/cmd/litefs/mount_test.go +++ b/cmd/litefs/mount_test.go @@ -2583,6 +2583,43 @@ func TestEventStream(t *testing.T) { }) } +// See: https://github.com/superfly/litefs/issues/425 +func TestPreventExclusiveLockingModeWithWAL(t *testing.T) { + if !testingutil.IsWALMode() { + t.Skip("test only applies to WAL mode, skipping") + } + + // Ensures writes to a new WAL fail if using EXCLUSIVE locking mode. + t.Run("WALHeader", func(t *testing.T) { + cmd0 := runMountCommand(t, newMountCommand(t, t.TempDir(), nil)) + db := testingutil.OpenSQLDB(t, filepath.Join(cmd0.Config.FUSE.Dir, "db")) + + if _, err := db.Exec("PRAGMA locking_mode = EXCLUSIVE"); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`CREATE TABLE t (x)`); err == nil || err.Error() != `disk I/O error` { + t.Fatalf("unexpected error: %v", err) + } + }) + + // Ensures writes to an existing WAL fail if using EXCLUSIVE locking mode. + t.Run("WALFrame", func(t *testing.T) { + cmd0 := runMountCommand(t, newMountCommand(t, t.TempDir(), nil)) + db := testingutil.OpenSQLDB(t, filepath.Join(cmd0.Config.FUSE.Dir, "db")) + + if _, err := db.Exec(`CREATE TABLE t (x)`); err != nil { + t.Fatal(err) + } + + if _, err := db.Exec("PRAGMA locking_mode = EXCLUSIVE"); err != nil { + t.Fatal(err) + } + if _, err := db.Exec(`INSERT INTO t VALUES ('foo')`); err == nil || err.Error() != `disk I/O error` { + t.Fatalf("unexpected error: %v", err) + } + }) +} + // Ensure multiple nodes can run in a cluster for an extended period of time. func TestFunctional_OK(t *testing.T) { if *funTime <= 0 { diff --git a/db.go b/db.go index b6a9494..ab3d591 100644 --- a/db.go +++ b/db.go @@ -1379,6 +1379,11 @@ func (db *DB) writeWALHeader(ctx context.Context, f *os.File, data []byte, offse return fmt.Errorf("WAL header write must be 32 bytes in size, received %d", len(data)) } + // Prevent transactions when the write lock has not been acquired (e.g. EXCLUSIVE lock) + if db.writeLock.State() != RWMutexStateExclusive { + return fmt.Errorf("cannot write to WAL header without WRITE lock, exclusive locking not allowed") + } + // Determine byte order of checksums. switch magic := binary.BigEndian.Uint32(data[0:]); magic { case 0x377f0682: @@ -1427,6 +1432,11 @@ func (db *DB) writeWALFrameHeader(ctx context.Context, f *os.File, data []byte, } }() + // Prevent transactions when the write lock has not been acquired (e.g. EXCLUSIVE lock) + if db.writeLock.State() != RWMutexStateExclusive { + return fmt.Errorf("cannot write to WAL frame header without WRITE lock, exclusive locking not allowed") + } + // Prevent SQLite from writing before the current WAL position. if offset < db.wal.offset { return fmt.Errorf("cannot write wal frame header @%d before current WAL position @%d", offset, db.wal.offset) @@ -1442,6 +1452,11 @@ func (db *DB) writeWALFrameData(ctx context.Context, f *os.File, data []byte, of TraceLog.Printf("[WriteWALFrameData(%s)]: offset=%d size=%d owner=%d %s", db.name, offset, len(data), owner, errorKeyValue(err)) }() + // Prevent transactions when the write lock has not been acquired (e.g. EXCLUSIVE lock) + if db.writeLock.State() != RWMutexStateExclusive { + return fmt.Errorf("cannot write to WAL frame data without WRITE lock, exclusive locking not allowed") + } + // Prevent SQLite from writing before the current WAL position. if offset < db.wal.offset { return fmt.Errorf("cannot write wal frame data @%d before current WAL position @%d", offset, db.wal.offset)