blob: 703e6a4662517ae9ea2cf10b888a30ace8864f33 [file]
// Copyright 2025 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/sandboxed_file.h"
#include <algorithm>
#include <optional>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/files/platform_file.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "components/sqlite_vfs/file_type.h"
#include "components/sqlite_vfs/metrics_util.h"
#include "third_party/sqlite/sqlite3.h"
namespace sqlite_vfs {
SandboxedFile::SandboxedFile(Client client,
FileType file_type,
base::File file,
AccessRights access_rights,
std::optional<SharedLocks> shared_locks)
: client_(client),
file_type_(file_type),
underlying_file_(std::move(file)),
access_rights_(access_rights),
shared_locks_(std::move(shared_locks)) {
CHECK(!shared_locks_ || file_type_ == FileType::kMainDb);
}
SandboxedFile::~SandboxedFile() = default;
base::File SandboxedFile::TakeUnderlyingFile(FileType file_type) {
CHECK_EQ(file_type, file_type_);
// Lock the file via filesystem APIs if this is the main database file and its
// creator wishes this to be the only connection allowed.
if (file_type == FileType::kMainDb && is_single_connection() &&
!AcquireSingleConnectionlock()) {
return {};
}
return std::move(underlying_file_);
}
void SandboxedFile::OnFileOpened(base::File file) {
CHECK(file.IsValid());
opened_file_ = std::move(file);
}
const base::File& SandboxedFile::GetFile() const {
return underlying_file_.IsValid() ? underlying_file_ : opened_file_;
}
base::File& SandboxedFile::GetFile() {
return const_cast<base::File&>(
const_cast<const SandboxedFile*>(this)->GetFile());
}
int SandboxedFile::Close() {
CHECK(IsValid());
underlying_file_ = std::move(opened_file_);
// Unlock the file via filesystem APIs if this is the main database file and
// its creator wishes this to be the only connection allowed.
if (file_type_ == FileType::kMainDb && is_single_connection()) {
ReleaseSingleConnectionlock();
}
return SQLITE_OK;
}
LockState SandboxedFile::Abandon() {
CHECK_EQ(file_type_, FileType::kMainDb);
CHECK(!is_single_connection());
LockState state = shared_locks_->Abandon();
base::UmaHistogramEnumeration(GetHistogramName(client_, "LockStateOnAbandon"),
state);
return state;
}
int SandboxedFile::Read(void* buffer, int size, sqlite3_int64 offset) {
// Make a safe span from the pair <buffer, size>. The buffer and the
// size are received from sqlite.
CHECK(buffer);
CHECK_GE(size, 0);
CHECK_GE(offset, 0);
const size_t checked_size = base::checked_cast<size_t>(size);
// SAFETY: `buffer` always points to at least `size` valid bytes.
auto data =
UNSAFE_BUFFERS(base::span(static_cast<uint8_t*>(buffer), checked_size));
// Read data from the file.
CHECK(IsValid());
std::optional<size_t> bytes_read = opened_file_.Read(offset, data);
if (!bytes_read.has_value()) {
return SQLITE_IOERR_READ;
}
// The buffer was fully read.
if (bytes_read.value() == checked_size) {
return SQLITE_OK;
}
// Some bytes were read but the buffer was not filled. SQLite requires that
// the unread bytes must be filled with zeros.
auto remaining_bytes = data.subspan(bytes_read.value());
std::fill(remaining_bytes.begin(), remaining_bytes.end(), 0);
return SQLITE_IOERR_SHORT_READ;
}
int SandboxedFile::Write(const void* buffer, int size, sqlite3_int64 offset) {
CHECK(buffer);
CHECK_GE(offset, 0);
CHECK(IsValid());
if (opened_file_.WriteAndCheck(
offset,
// SAFETY: `buffer` always points to at least `size` valid bytes.
UNSAFE_BUFFERS(base::span(static_cast<const uint8_t*>(buffer),
base::checked_cast<size_t>(size))))) {
return SQLITE_OK;
}
// Distinguish disk full from general I/O errors.
return base::File::GetLastFileError() ==
base::File::Error::FILE_ERROR_NO_SPACE
? SQLITE_FULL
: SQLITE_IOERR_WRITE;
}
int SandboxedFile::Truncate(sqlite3_int64 size) {
CHECK(IsValid());
if (!opened_file_.SetLength(size)) {
return SQLITE_IOERR_TRUNCATE;
}
return SQLITE_OK;
}
int SandboxedFile::Sync(int flags) {
CHECK(IsValid());
if (!opened_file_.Flush()) {
return SQLITE_IOERR_FSYNC;
}
return SQLITE_OK;
}
int SandboxedFile::FileSize(sqlite3_int64* result_size) {
CHECK(IsValid());
int64_t length = opened_file_.GetLength();
if (length < 0) {
return SQLITE_IOERR_FSTAT;
}
*result_size = length;
return SQLITE_OK;
}
// This function implements the database locking mechanism as defined by the
// SQLite VFS (Virtual File System) interface. It is responsible for escalating
// locks on the database file to ensure that multiple processes can access the
// database in a controlled and serialized manner, preventing data corruption.
//
// In this shared memory implementation, the lock states are managed directly
// in a shared memory region accessible by all client processes, rather than
// relying on traditional file-system locks (like fcntl on Unix or LockFileEx
// on Windows).
//
// The lock implementation mirrors the state transitions of the standard SQLite
// locking mechanism:
//
// SHARED: Allows multiple readers.
// RESERVED: A process signals its intent to write.
// PENDING: A writer is waiting for readers to finish.
// EXCLUSIVE: A single process has exclusive write access.
//
// The valid transitions are:
//
// UNLOCKED -> SHARED
// SHARED -> RESERVED
// SHARED -> (PENDING) -> EXCLUSIVE
// RESERVED -> (PENDING) -> EXCLUSIVE
// PENDING -> EXCLUSIVE
//
// See original implementation:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/sqlite/src/src/os_win.c;l=3514;drc=4a0b7a332f3aeb27814cfa12dc0ebdbbd994a928
//
// Some issues related to file system locks:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/sqlite/src/src/os_unix.c;l=1077;drc=5d60f47001bf64b48abac68ed59621e528144ea4
//
// The SQLite core uses two distinct strategies to acquire an EXCLUSIVE lock.
// This VFS implementation must correctly handle lock requests from both paths.
//
// 1. Normal transaction path
// The standard database operations (INSERT, UPDATE, BEGIN COMMIT, etc.) on a
// healthy database will escalate the lock sequentially:
// SHARED -> RESERVED -> PENDING -> EXCLUSIVE.
// The intermediate RESERVED lock is mandatory. It signals an intent to write
// while still permitting other connections to hold SHARED locks for reading.
//
// 2. Hot-journal recovery path
// A special case that occurs upon initial connection when a hot-journal is
// detected, indicating a previous crash or power loss. A direct request for
// an EXCLUSIVE lock is required. In this state, the database is known to be
// inconsistent. The RESERVED lock is intentionally skipped because its
// purpose is to allow concurrent readers, which would be disastrous. A direct
// EXCLUSIVE lock acts as an emergency lockdown, preventing ALL other
// connections from reading corrupt data until the recovery process is
// complete.
//
// see:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/sqlite/src/src/pager.c;l=5260;drc=65d0312c96cd23958372fac8940314c782a6b03c
int SandboxedFile::Lock(int mode) {
CHECK_EQ(file_type_, FileType::kMainDb);
// Ensures valid lock states are used (see: sqlite3OsLock(...) assertions).
CHECK(mode == SQLITE_LOCK_SHARED || mode == SQLITE_LOCK_RESERVED ||
mode == SQLITE_LOCK_EXCLUSIVE);
// Do nothing if there is already a lock of this type or more restrictive.
if (sqlite_lock_mode_ >= mode) {
return SQLITE_OK;
}
if (is_single_connection()) {
sqlite_lock_mode_ = mode;
return SQLITE_OK;
}
return shared_locks_->Lock(mode, sqlite_lock_mode_);
}
// This function is the counterpart to Lock and is responsible for reducing the
// lock level on the database file. This typically happens after a transaction
// is committed or rolled back, or when a process holding a write lock is
// ready to allow other readers in.
//
// The valid transitions are:
//
// SHARED -> UNLOCKED
// EXCLUSIVE -> UNLOCKED
// EXCLUSIVE -> SHARED
//
// It is also valid to release any pending state (PENDING or RESERVED) even if
// the state never went to EXCLUSIVE. This can happen when a connection gives up
// on trying to get an EXCLUSIVE lock.
int SandboxedFile::Unlock(int mode) {
CHECK_EQ(file_type_, FileType::kMainDb);
// Ensures valid lock states are used (see: sqlite3OsUnlock(...) assertions).
CHECK(mode == SQLITE_LOCK_NONE || mode == SQLITE_LOCK_SHARED);
// Do nothing if there is already a lock of this type or less restrictive.
if (sqlite_lock_mode_ <= mode) {
return SQLITE_OK;
}
if (is_single_connection()) {
sqlite_lock_mode_ = mode;
return SQLITE_OK;
}
return shared_locks_->Unlock(mode, sqlite_lock_mode_);
}
int SandboxedFile::CheckReservedLock(int* has_reserved_lock) {
CHECK_EQ(file_type_, FileType::kMainDb);
if (is_single_connection()) {
*has_reserved_lock = sqlite_lock_mode_ >= SQLITE_LOCK_RESERVED;
} else {
*has_reserved_lock = shared_locks_->IsReserved() ? 1 : 0;
}
return SQLITE_OK;
}
int SandboxedFile::FileControl(int opcode, void* data) {
return SQLITE_NOTFOUND;
}
int SandboxedFile::SectorSize() {
return 0;
}
int SandboxedFile::DeviceCharacteristics() {
return 0;
}
int SandboxedFile::ShmMap(int page_index,
int page_size,
int extend_file_if_needed,
void volatile** result) {
// Write-ahead logging is only supported in combination with exclusive mode
// (is_single_connection() == true); see https://sqlite.org/wal.html#noshm
NOTREACHED();
}
int SandboxedFile::ShmLock(int offset, int size, int flags) {
// Write-ahead logging is only supported in combination with exclusive mode
// (is_single_connection() == true); see https://sqlite.org/wal.html#noshm
NOTREACHED();
}
void SandboxedFile::ShmBarrier() {
// Write-ahead logging is only supported in combination with exclusive mode
// (is_single_connection() == true); see https://sqlite.org/wal.html#noshm
NOTREACHED();
}
int SandboxedFile::ShmUnmap(int also_delete_file) {
// Write-ahead logging is only supported in combination with exclusive mode
// (is_single_connection() == true); see https://sqlite.org/wal.html#noshm
NOTREACHED();
}
int SandboxedFile::Fetch(sqlite3_int64 offset, int size, void** result) {
// TODO(https://crbug.com/377475540): Implement shared memory.
*result = nullptr;
return SQLITE_IOERR;
}
int SandboxedFile::Unfetch(sqlite3_int64 offset, void* fetch_result) {
// TODO(https://crbug.com/377475540): Implement shared memory.
return SQLITE_IOERR;
}
bool SandboxedFile::AcquireSingleConnectionlock() {
CHECK(underlying_file_.IsValid());
const auto error = underlying_file_.Lock(base::File::LockMode::kExclusive);
base::UmaHistogramExactLinear(GetHistogramName(client_, "LockResult"), -error,
-base::File::FILE_ERROR_MAX);
return error == base::File::FILE_OK;
}
void SandboxedFile::ReleaseSingleConnectionlock() {
CHECK(underlying_file_.IsValid());
underlying_file_.Unlock();
}
} // namespace sqlite_vfs