| // Copyright 2026 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "components/sqlite_vfs/shared_locks.h" | |
| #include <atomic> | |
| #include "base/check.h" | |
| #include "base/check_op.h" | |
| #include "base/notreached.h" | |
| #include "third_party/sqlite/sqlite3.h" | |
| namespace sqlite_vfs { | |
| namespace { | |
| // Properties of the primary database lock. | |
| constexpr uint32_t kMaxSharedLocks = 0x08000000; | |
| constexpr uint32_t kSharedMask = 0x0FFFFFFF; | |
| constexpr uint32_t kReservedBit = 0x20000000; | |
| constexpr uint32_t kPendingBit = 0x40000000; | |
| constexpr uint32_t kAbandonedBit = 0x80000000; | |
| // Properties of the WAL-locks. | |
| constexpr uint32_t kWalLockExclusive = 0x80000000; | |
| constexpr uint32_t kWalMaxSharedLocks = 0x7FFFFFFF; | |
| } // namespace | |
| // static | |
| base::UnsafeSharedMemoryRegion SharedLocks::CreateRegion(bool wal_mode) { | |
| size_t size = wal_mode ? sizeof(DatabaseAndWalLocks) : sizeof(DatabaseLock); | |
| return base::UnsafeSharedMemoryRegion::Create(size); | |
| } | |
| // static | |
| std::optional<SharedLocks> SharedLocks::Create( | |
| const base::UnsafeSharedMemoryRegion& region, | |
| bool wal_mode) { | |
| CHECK(region.IsValid()); | |
| size_t required_size = | |
| wal_mode ? sizeof(DatabaseAndWalLocks) : sizeof(DatabaseLock); | |
| CHECK_GE(region.GetSize(), required_size); | |
| auto mapping = region.Map(); | |
| if (!mapping.IsValid()) { | |
| return std::nullopt; | |
| } | |
| return SharedLocks(std::move(mapping), wal_mode); | |
| } | |
| SharedLocks::~SharedLocks() = default; | |
| // The primary database lock is encoded over 32-bits: | |
| // 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 | |
| // 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 | |
| // +---+-+-+-----------------------+-------------------------------+ | |
| // |A|P|R|0| SHARED COUNT | | |
| // +---+-+-+-----------------------+-------------------------------+ | |
| // | |
| // Where | |
| // | |
| // SHARED COUNT: The number of SHARED locks held by readers. | |
| // A: Whether the lock is abandoned. If set no further use is permitted. | |
| // R: The RESERVED lock is held. New shared locks are still permitted. | |
| // P: The PENDING lock is held. No new shared locks are permitted while any | |
| // process holds the PENDING lock. | |
| // | |
| // A process holds the EXCLUSIVE lock when it holds the PENDING lock and the | |
| // SHARED COUNT is zero. | |
| int SharedLocks::Lock(int mode, int& current_mode) { | |
| CHECK(mode == SQLITE_LOCK_SHARED || mode == SQLITE_LOCK_RESERVED || | |
| mode == SQLITE_LOCK_EXCLUSIVE); | |
| CHECK_LT(current_mode, mode); | |
| auto& database_lock = GetDatabaseLock(); | |
| switch (mode) { | |
| case SQLITE_LOCK_SHARED: { | |
| // Try to increment the SHARED lock count as long as the PENDING lock | |
| // remains unheld and there is room remaining to count a new SHARED lock. | |
| uint32_t lock_snapshot = database_lock.load(); | |
| if ((lock_snapshot & kAbandonedBit) != 0) { | |
| return SQLITE_IOERR_LOCK; | |
| } | |
| for (int i = 0; i < 5; ++i) { | |
| if ((lock_snapshot & kPendingBit) != 0 || | |
| (lock_snapshot & kSharedMask) == kMaxSharedLocks) { | |
| break; | |
| } | |
| if (database_lock.compare_exchange_strong(lock_snapshot, | |
| lock_snapshot + 1)) { | |
| // The SHARED lock was successfully acquired. | |
| current_mode = SQLITE_LOCK_SHARED; | |
| return SQLITE_OK; | |
| } | |
| if ((lock_snapshot & kAbandonedBit) != 0) { | |
| return SQLITE_IOERR_LOCK; | |
| } | |
| // Perform up to four retries in case this client is racing against | |
| // other changes to the shared lock. | |
| } | |
| return SQLITE_BUSY; | |
| } | |
| case SQLITE_LOCK_RESERVED: { | |
| // To acquire a RESERVED lock, the current connection must already have | |
| // a shared access to it. | |
| CHECK_EQ(current_mode, SQLITE_LOCK_SHARED); | |
| // Acquire a RESERVED lock to prevent a different writer to declare its | |
| // intention to modify the database. At this point, readers are still | |
| // allowed to get a SHARED lock on the database. | |
| const uint32_t lock_snapshot = database_lock.fetch_or(kReservedBit); | |
| if ((lock_snapshot & kAbandonedBit) != 0) { | |
| return SQLITE_IOERR_LOCK; | |
| } | |
| if ((lock_snapshot & kReservedBit) != 0) { | |
| return SQLITE_BUSY; | |
| } | |
| // The RESERVED lock was successfully acquired. | |
| current_mode = SQLITE_LOCK_RESERVED; | |
| return SQLITE_OK; | |
| } | |
| case SQLITE_LOCK_EXCLUSIVE: { | |
| // Acquiring an EXCLUSIVE lock may happen through multiple calls to | |
| // SandboxedFile::Lock(...) and the PENDING lock may be kept between these | |
| // calls. | |
| // To acquire an EXCLUSIVE lock, the current connection must already have | |
| // at least SHARED lock. Owning RESERVED lock not mandatory. | |
| CHECK_GE(current_mode, SQLITE_LOCK_SHARED); | |
| // Acquire the PENDING lock, if not already acquired. Hold it until the | |
| // EXCLUSIVE lock is obtained. No new SHARED locks will be granted in | |
| // the meantime, but current SHARED locks remain valid. | |
| uint32_t lock_snapshot = 0; | |
| if (current_mode < SQLITE_LOCK_PENDING) { | |
| lock_snapshot = database_lock.fetch_or(kPendingBit); | |
| if ((lock_snapshot & kAbandonedBit) != 0) { | |
| // This instance may have just set `kPendingBit`. There is no need to | |
| // clear it since all other parties will detect that the instance is | |
| // abandoned on their next attempt to acquire any lock. | |
| return SQLITE_IOERR_LOCK; | |
| } | |
| if ((lock_snapshot & kPendingBit) != 0) { | |
| // This connection is not the owner of the PENDING lock. | |
| return SQLITE_BUSY; | |
| } | |
| // The PENDING lock was acquired. Keep it for subsequent calls until all | |
| // SHARED locks are released. | |
| current_mode = SQLITE_LOCK_PENDING; | |
| // Update the copy of the current state of the lock for use below. | |
| lock_snapshot |= kPendingBit; | |
| } else { | |
| // Fetch the current state of the lock for use below. | |
| lock_snapshot = database_lock.load(); | |
| if ((lock_snapshot & kAbandonedBit) != 0) { | |
| return SQLITE_IOERR_LOCK; | |
| } | |
| } | |
| // Do not grant the EXCLUSIVE lock until all other readers have released | |
| // their SHARED locks. This connection still owns and keeps a SHARED lock. | |
| if ((lock_snapshot & kSharedMask) != 1) { | |
| return SQLITE_BUSY; | |
| } | |
| // There is no active SHARED lock except for this connection. The PENDING | |
| // lock is owned by this connection so it is valid to grant the EXCLUSIVE | |
| // lock. | |
| current_mode = SQLITE_LOCK_EXCLUSIVE; | |
| return SQLITE_OK; | |
| } | |
| default: | |
| NOTREACHED(); // Not possible as per CHECK at entry. | |
| } | |
| } | |
| int SharedLocks::Unlock(int mode, int& current_mode) { | |
| CHECK(mode == SQLITE_LOCK_NONE || mode == SQLITE_LOCK_SHARED); | |
| CHECK_GT(current_mode, mode); | |
| auto& database_lock = GetDatabaseLock(); | |
| // Release the RESERVED or RESERVED and PENDING bits, if held. | |
| if (uint32_t clear_mask = | |
| (current_mode >= SQLITE_LOCK_PENDING | |
| ? (kPendingBit | kReservedBit) | |
| : (current_mode == SQLITE_LOCK_RESERVED ? kReservedBit : 0U))) { | |
| database_lock.fetch_and(~clear_mask); | |
| } | |
| // Release the SHARED lock if no longer needed. | |
| if (mode == SQLITE_LOCK_NONE) { | |
| const uint32_t lock_snapshot = database_lock.fetch_sub(1); | |
| CHECK_GE(lock_snapshot & kSharedMask, 1u); | |
| } | |
| // Lock was successfully released. | |
| current_mode = mode; | |
| return SQLITE_OK; | |
| } | |
| bool SharedLocks::IsReserved() { | |
| return (GetDatabaseLock().load() & kReservedBit) != 0; | |
| } | |
| LockState SharedLocks::Abandon() { | |
| uint32_t previous_state = GetDatabaseLock().fetch_or(kAbandonedBit); | |
| if ((previous_state & (kReservedBit | kPendingBit)) != 0) { | |
| return LockState::kWriting; | |
| } | |
| if ((previous_state & kSharedMask) != 0) { | |
| return LockState::kReading; | |
| } | |
| return LockState::kNotHeld; | |
| } | |
| bool SharedLocks::IsAbandoned() const { | |
| return (const_cast<SharedLocks*>(this)->GetDatabaseLock().load( | |
| std::memory_order_relaxed) & | |
| kAbandonedBit) != 0; | |
| } | |
| int SharedLocks::ShmLock(int lock_index, | |
| int num_locks, | |
| LockOperation operation, | |
| LockType type) { | |
| CHECK(wal_mode_); | |
| CHECK_GE(lock_index, 0); | |
| CHECK_GE(num_locks, 0); | |
| CHECK_LE(lock_index + num_locks, kNumWalLocks); | |
| // Releases `count` (out of a maximum of `num_locks`) locks. | |
| auto release_locks = [&](int count) { | |
| for (int j = count - 1; j >= 0; --j) { | |
| auto& wal_lock = GetWalLock(lock_index + j); | |
| if (type == LockType::kExclusive) { // SQLITE_SHM_EXCLUSIVE | |
| const uint32_t previous_value = wal_lock.exchange(0); | |
| CHECK_EQ(previous_value, kWalLockExclusive); | |
| } else { // SQLITE_SHM_SHARED | |
| const uint32_t previous_value = wal_lock.fetch_sub(1); | |
| CHECK_GE(previous_value, 1u); | |
| CHECK_EQ(previous_value & kWalLockExclusive, 0u); | |
| } | |
| } | |
| }; | |
| switch (operation) { | |
| case LockOperation::kAcquire: { // SQLITE_SHM_LOCK | |
| // Stop right away if this database has been abandoned. | |
| if ((GetDatabaseLock().load(std::memory_order_relaxed) & kAbandonedBit) != | |
| 0) { | |
| return SQLITE_IOERR_LOCK; | |
| } | |
| int locks_acquired = 0; | |
| int result = SQLITE_OK; | |
| for (int i = 0; i < num_locks; ++i) { | |
| auto& wal_lock = GetWalLock(lock_index + i); | |
| if (type == LockType::kExclusive) { // SQLITE_SHM_EXCLUSIVE | |
| uint32_t expected = 0; | |
| if (!wal_lock.compare_exchange_strong(expected, kWalLockExclusive)) { | |
| result = SQLITE_BUSY; | |
| break; | |
| } | |
| } else { // SQLITE_SHM_SHARED | |
| uint32_t current = wal_lock.load(); | |
| while (true) { | |
| if (current & kWalLockExclusive) { | |
| // Another connection holds an exclusive lock. | |
| result = SQLITE_BUSY; | |
| break; | |
| } | |
| if (current == kWalMaxSharedLocks) { | |
| // No space to add a new shared lock until another connection | |
| // releases theirs. | |
| result = SQLITE_BUSY; | |
| break; | |
| } | |
| // compare_exchange_weak is safe because spurious failures are | |
| // handled by the retry loop. | |
| if (wal_lock.compare_exchange_weak(current, current + 1)) { | |
| break; | |
| } | |
| } | |
| if (result != SQLITE_OK) { | |
| break; | |
| } | |
| } | |
| locks_acquired = i + 1; | |
| } | |
| // Check once more to be sure that the database wasn't abandoned while | |
| // acquiring the desired lock(s). | |
| if (result == SQLITE_OK && | |
| (GetDatabaseLock().load(std::memory_order_relaxed) & kAbandonedBit) != | |
| 0) { | |
| result = SQLITE_IOERR_LOCK; | |
| } | |
| if (result != SQLITE_OK) { | |
| release_locks(locks_acquired); | |
| return result; | |
| } | |
| return SQLITE_OK; | |
| } | |
| case LockOperation::kRelease: | |
| release_locks(num_locks); | |
| return SQLITE_OK; | |
| } | |
| } | |
| void SharedLocks::ShmBarrier() { | |
| std::atomic_thread_fence(std::memory_order_seq_cst); | |
| } | |
| SharedLocks::SharedLocks(base::WritableSharedMemoryMapping mapping, | |
| bool wal_mode) | |
| : mapping_(std::move(mapping)), wal_mode_(wal_mode) { | |
| static_assert(kNumWalLocks == SQLITE_SHM_NLOCK); | |
| size_t required_size = | |
| wal_mode ? sizeof(DatabaseAndWalLocks) : sizeof(DatabaseLock); | |
| CHECK_GE(mapping_.size(), required_size); | |
| } | |
| SharedLocks::DatabaseLock& SharedLocks::GetDatabaseLock() { | |
| if (wal_mode_) { | |
| return mapping_.GetMemoryAs<DatabaseAndWalLocks>()->primary_lock; | |
| } | |
| return *mapping_.GetMemoryAs<DatabaseLock>(); | |
| } | |
| SharedLocks::WalLock& SharedLocks::GetWalLock(int index) { | |
| CHECK(wal_mode_); | |
| CHECK_GE(index, 0); | |
| CHECK_LT(index, kNumWalLocks); | |
| return mapping_.GetMemoryAs<DatabaseAndWalLocks>()->wal_locks[index]; | |
| } | |
| } // namespace sqlite_vfs |