// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ash/fileapi/diversion_file_manager.h"
#include <algorithm>
#include <limits>
#include <optional>
#include <utility>
#include "base/containers/circular_deque.h"
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/numerics/clamped_math.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "build/buildflag.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "storage/common/file_system/file_system_util.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include <fcntl.h>
#include <unistd.h>
#include "base/posix/eintr_wrapper.h"
#else
#error "DiversionFileManager code only builds on ChromeOS"
#endif
namespace ash {
namespace {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// In theory, we could get this at runtime via Profile::GetPath and
// ProfileManager::GetActiveUserProfile, but calling those needs to happen on
// the UI thread while the code in this C++ primarily happens on the IO thread.
// It's simpler, assuming we're on ChromeOS, to just hard-code the home dir.
constexpr char kChronosHomeDir[] = "/home/chronos/user/";
#else
#error "DiversionFileManager code only builds on ChromeOS"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
using GetFileInfoCallback =
base::OnceCallback<void(base::File::Error result,
const base::File::Info& file_info)>;
using StatusCallback = base::OnceCallback<void(base::File::Error result)>;
// A temporary file (as a file descriptor) and its size in bytes, or the first
// error (if any) encountered in accessing it.
//
// The underlying file (in the kernel sense) is an O_TMPFILE file (and so does
// not need any further Chromium code for garbage collecting used files),
// created under kChronosHomeDir (not /tmp) so that it is disk-backed instead
// of memory-backed, as disk is more plentiful than RAM.
//
// O_TMPFILE and the hard-coded kChronosHomeDir may be Linux-only and
// ChromeOS-only concepts but this C++ file only builds when IS_CHROMEOS_ASH.
struct Tmpfile {
base::ScopedFD scoped_fd;
int64_t file_size = 0;
net::Error net_error = net::OK;
Tmpfile() = default;
explicit Tmpfile(net::Error e) : net_error(e) {}
};
// An asynchronous operation involving a transform function that runs on a
// base::BlockingType::MAY_BLOCK thread - it can perform blocking I/O. A
// Tmpfile (which holds a base::ScopedFD, an ownership type) is passed to that
// thread and is returned to the original (content::BrowserThread::IO) thread
// via the Callback.
//
// The transform field may be a null OnceCallback, meaning the identity (no-op)
// transform. The Callback field must not be a null OnceCallback.
struct Op {
// The int means whatever the Transform and Callback agree that it means. It
// often means the number of bytes read or written, or it is ignored. The
// Tmpfile itself already holds a net::Error.
using Transform = base::OnceCallback<std::pair<Tmpfile, int>(Tmpfile)>;
using Callback = base::OnceCallback<void(const Tmpfile&, int)>;
Transform transform;
Callback callback;
};
} // namespace
// A DiversionFileManager is essentially a map from FileSystemURL to
// scoped_refptr<Entry>. An Entry serializes the Ops enqueued to it such that
// at most one Op is in-flight at a time. It also holds the Tmpfile (which owns
// the underlying file descriptor, via a ScopedFD) when that Tmpfile is not
// loaned out to any in-flight Op.
//
// An Entry also starts an idle_timeout timer (1) at construction and (2) after
// each time the last (so far) reader/writer Worker is destroyed. If no new
// Workers were created (and Finish was not called) between a timer starting
// and expiring, the Entry times out and its implicit_callback is run.
class DiversionFileManager::Entry
: public base::RefCounted<DiversionFileManager::Entry> {
public:
Entry(const storage::FileSystemURL& url,
scoped_refptr<DiversionFileManager> manager,
base::TimeDelta idle_timeout,
Callback implicit_callback);
void OnWorkerConstructed();
void OnWorkerDestroyed();
void Enqueue(Op op);
void Finish(Callback explicit_callback);
private:
friend class base::RefCounted<DiversionFileManager::Entry>;
~Entry();
void Run(Op op);
void OnRunComplete(Op::Callback callback, std::pair<Tmpfile, int> tmpfile);
bool ShouldRunCallback();
void RunCallback();
bool ShouldPostIdleTimer();
void PostIdleTimer();
static void OnIdleTimer(
scoped_refptr<Entry> refptr_this,
uint64_t num_workers_constructed_as_at_post_idle_timer);
const storage::FileSystemURL url_;
scoped_refptr<DiversionFileManager> manager_;
base::TimeDelta idle_timeout_;
Tmpfile tmpfile_;
uint64_t num_workers_constructed_ = 0;
uint64_t num_workers_destroyed_ = 0;
std::optional<StoppedReason> stopped_reason_ = std::nullopt;
bool is_running_an_op_ = false;
base::circular_deque<Op> pending_ops_;
Callback implicit_callback_;
Callback explicit_callback_;
};
DiversionFileManager::Entry::Entry(const storage::FileSystemURL& url,
scoped_refptr<DiversionFileManager> manager,
base::TimeDelta idle_timeout,
Callback implicit_callback)
: url_(url),
manager_(manager),
idle_timeout_(idle_timeout),
implicit_callback_(std::move(implicit_callback)) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
// transform creates the O_TMPFILE file.
static constexpr auto transform =
[](std::string tmpfile_dir, Tmpfile tmpfile) -> std::pair<Tmpfile, int> {
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
base::ScopedFD sfd(open(tmpfile_dir.c_str(),
O_CLOEXEC | O_EXCL | O_TMPFILE | O_RDWR, 0600));
if (sfd.is_valid()) {
tmpfile.scoped_fd = std::move(sfd);
} else {
tmpfile.net_error = net::ERR_FILE_NO_SPACE;
}
return std::make_pair(std::move(tmpfile), 0);
};
Run({base::BindOnce(transform, manager->TmpfileDirAsString()),
Op::Callback()});
PostIdleTimer();
}
DiversionFileManager::Entry::~Entry() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
}
void DiversionFileManager::Entry::OnWorkerConstructed() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
num_workers_constructed_++;
}
void DiversionFileManager::Entry::OnWorkerDestroyed() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
num_workers_destroyed_++;
if (ShouldRunCallback()) {
RunCallback();
}
if (ShouldPostIdleTimer()) {
PostIdleTimer();
}
}
void DiversionFileManager::Entry::Enqueue(Op op) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (is_running_an_op_) {
pending_ops_.push_back(std::move(op));
} else {
Run(std::move(op));
}
}
void DiversionFileManager::Entry::Run(Op op) {
CHECK(!is_running_an_op_);
is_running_an_op_ = true;
if (op.transform) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(std::move(op.transform), std::move(tmpfile_)),
base::BindOnce(&Entry::OnRunComplete, scoped_refptr<Entry>(this),
std::move(op.callback)));
} else {
OnRunComplete(std::move(op.callback),
std::make_pair(std::move(tmpfile_), 0));
}
}
void DiversionFileManager::Entry::OnRunComplete(
Op::Callback op_callback,
std::pair<Tmpfile, int> result) {
CHECK(is_running_an_op_);
is_running_an_op_ = false;
tmpfile_ = std::move(result.first);
if (op_callback) {
std::move(op_callback).Run(tmpfile_, result.second);
}
if (!pending_ops_.empty()) {
Op op = std::move(pending_ops_.front());
pending_ops_.pop_front();
Run(std::move(op));
} else if (ShouldRunCallback()) {
RunCallback();
}
}
void DiversionFileManager::Entry::Finish(Callback explicit_callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
CHECK(!stopped_reason_.has_value());
stopped_reason_ = StoppedReason::kExplicitFinish;
explicit_callback_ = std::move(explicit_callback);
if (ShouldRunCallback()) {
RunCallback();
}
}
bool DiversionFileManager::Entry::ShouldRunCallback() {
return !is_running_an_op_ && pending_ops_.empty() &&
stopped_reason_.has_value() &&
(num_workers_constructed_ == num_workers_destroyed_);
}
// Precondition: ShouldRunCallback().
void DiversionFileManager::Entry::RunCallback() {
Callback callback;
switch (stopped_reason_.value()) {
case StoppedReason::kExplicitFinish:
callback = std::move(explicit_callback_);
break;
case StoppedReason::kImplicitIdle:
callback = std::move(implicit_callback_);
break;
}
if (callback) {
std::move(callback).Run(stopped_reason_.value(), url_,
std::move(tmpfile_.scoped_fd), tmpfile_.file_size,
storage::NetErrorToFileError(tmpfile_.net_error));
}
}
bool DiversionFileManager::Entry::ShouldPostIdleTimer() {
return !stopped_reason_.has_value() &&
(num_workers_constructed_ == num_workers_destroyed_);
}
// Precondition: ShouldPostIdleTimer().
void DiversionFileManager::Entry::PostIdleTimer() {
content::GetIOThreadTaskRunner({})->PostDelayedTask(
FROM_HERE,
base::BindOnce(&DiversionFileManager::Entry::OnIdleTimer,
scoped_refptr<Entry>(this), num_workers_constructed_),
idle_timeout_);
}
// static
void DiversionFileManager::Entry::OnIdleTimer(
scoped_refptr<Entry> refptr_this,
uint64_t num_workers_constructed_as_at_post_idle_timer) {
if (num_workers_constructed_as_at_post_idle_timer !=
refptr_this->num_workers_constructed_) {
return;
} else if (refptr_this->stopped_reason_.has_value()) {
return;
}
refptr_this->stopped_reason_ = StoppedReason::kImplicitIdle;
DiversionFileManager::Map& map = refptr_this->manager_->entries_;
if (auto iter = map.find(refptr_this->url_); iter != map.end()) {
CHECK_EQ(refptr_this, iter->second);
map.erase(iter);
}
if (refptr_this->ShouldRunCallback()) {
refptr_this->RunCallback();
}
}
// ----
// A reader or writer for an Entry's temporary-file. Each worker has an
// independent file-offset.
class DiversionFileManager::Worker : public storage::FileStreamReader,
public storage::FileStreamWriter {
public:
enum class Role {
kReader,
kWriter,
};
Worker(Role role, scoped_refptr<Entry> entry, int64_t offset);
~Worker() override;
// storage::FileStreamReader overrides.
int Read(net::IOBuffer* buf,
int buf_len,
net::CompletionOnceCallback callback) override;
int64_t GetLength(net::Int64CompletionOnceCallback callback) override;
// storage::FileStreamWriter overrides.
int Write(net::IOBuffer* buf,
int buf_len,
net::CompletionOnceCallback callback) override;
int Cancel(net::CompletionOnceCallback callback) override;
int Flush(storage::FlushMode flush_mode,
net::CompletionOnceCallback callback) override;
private:
void ReadOrWrite(net::IOBuffer* buf,
int buf_len,
net::CompletionOnceCallback callback);
static void OnReadOrWrite(base::WeakPtr<Worker> weak_ptr,
net::CompletionOnceCallback callback,
const Tmpfile& tmpfile,
int byte_count);
Role role_;
scoped_refptr<Entry> entry_;
int64_t offset_;
base::WeakPtrFactory<Worker> weak_ptr_factory_{this};
};
DiversionFileManager::Worker::Worker(Role role,
scoped_refptr<Entry> entry,
int64_t offset)
: role_(role), entry_(std::move(entry)), offset_(offset) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
entry_->OnWorkerConstructed();
}
DiversionFileManager::Worker::~Worker() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
entry_->OnWorkerDestroyed();
}
int DiversionFileManager::Worker::Read(net::IOBuffer* buf,
int buf_len,
net::CompletionOnceCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
CHECK_EQ(role_, Role::kReader);
ReadOrWrite(buf, buf_len, std::move(callback));
return net::ERR_IO_PENDING;
}
int64_t DiversionFileManager::Worker::GetLength(
net::Int64CompletionOnceCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
CHECK_EQ(role_, Role::kReader);
static constexpr auto reply = [](net::Int64CompletionOnceCallback callback,
const Tmpfile& tmpfile, int ignored) {
std::move(callback).Run((tmpfile.net_error < 0)
? static_cast<int64_t>(tmpfile.net_error)
: tmpfile.file_size);
};
entry_->Enqueue(
{Op::Transform(), base::BindOnce(reply, std::move(callback))});
return net::ERR_IO_PENDING;
}
int DiversionFileManager::Worker::Write(net::IOBuffer* buf,
int buf_len,
net::CompletionOnceCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
CHECK_EQ(role_, Role::kWriter);
ReadOrWrite(buf, buf_len, std::move(callback));
return net::ERR_IO_PENDING;
}
int DiversionFileManager::Worker::Cancel(net::CompletionOnceCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
CHECK_EQ(role_, Role::kWriter);
// Unimplemented.
return net::OK;
}
int DiversionFileManager::Worker::Flush(storage::FlushMode flush_mode,
net::CompletionOnceCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
CHECK_EQ(role_, Role::kWriter);
// Flush is a no-op. An O_TMPFILE file isn't persistent anyway if the process
// crashes. Within this process (and its open file descriptor), any
// previously written bytes should already be readable.
return net::OK;
}
void DiversionFileManager::Worker::ReadOrWrite(
net::IOBuffer* buf,
int buf_len,
net::CompletionOnceCallback callback) {
static constexpr auto transform =
[](Role role, char* data_ptr, int data_len, int64_t offset,
Tmpfile tmpfile) -> std::pair<Tmpfile, int> {
if (tmpfile.net_error != net::OK) {
return std::make_pair(std::move(tmpfile), 0);
} else if (!tmpfile.scoped_fd.is_valid()) {
return std::make_pair(Tmpfile(net::ERR_INVALID_HANDLE), 0);
}
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
const int64_t original_offset = offset;
const int fd = tmpfile.scoped_fd.get();
while (data_len > 0) {
if (offset > std::numeric_limits<off_t>::max()) {
return std::make_pair(Tmpfile(net::ERR_FILE_TOO_BIG), 0);
}
size_t arg2 = static_cast<size_t>(data_len);
off_t arg3 = static_cast<off_t>(offset);
ssize_t n = (role == Role::kReader)
? HANDLE_EINTR(pread(fd, data_ptr, arg2, arg3))
: HANDLE_EINTR(pwrite(fd, data_ptr, arg2, arg3));
if (n == 0) {
break;
} else if (n < 0) {
return std::make_pair(Tmpfile((errno == ENOSPC) ? net::ERR_FILE_NO_SPACE
: net::ERR_FAILED),
0);
}
data_ptr += n;
data_len -= static_cast<int>(n);
offset = base::ClampAdd(offset, static_cast<int64_t>(n));
}
tmpfile.file_size = std::max(tmpfile.file_size, offset);
return std::make_pair(std::move(tmpfile),
static_cast<int>(offset - original_offset));
};
entry_->Enqueue(
{base::BindOnce(transform, role_, buf->data(), buf_len, offset_),
base::BindOnce(&DiversionFileManager::Worker::OnReadOrWrite,
weak_ptr_factory_.GetWeakPtr(), std::move(callback))});
}
// static
void DiversionFileManager::Worker::OnReadOrWrite(
base::WeakPtr<Worker> weak_ptr,
net::CompletionOnceCallback callback,
const Tmpfile& tmpfile,
int byte_count) {
Worker* weak_this = weak_ptr.get();
if (weak_this && (byte_count > 0)) {
weak_this->offset_ =
base::ClampAdd(weak_this->offset_, static_cast<int64_t>(byte_count));
}
std::move(callback).Run((tmpfile.net_error < 0) ? tmpfile.net_error
: byte_count);
}
// ----
DiversionFileManager::DiversionFileManager() = default;
DiversionFileManager::~DiversionFileManager() = default;
bool DiversionFileManager::IsDiverting(const storage::FileSystemURL& url) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
return entries_.find(url) != entries_.end();
}
DiversionFileManager::StartDivertingResult DiversionFileManager::StartDiverting(
const storage::FileSystemURL& url,
base::TimeDelta idle_timeout,
Callback implicit_callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
const auto [iter, success] = entries_.insert(
std::make_pair(url, base::MakeRefCounted<Entry>(
url, scoped_refptr<DiversionFileManager>(this),
idle_timeout, std::move(implicit_callback))));
return success
? DiversionFileManager::StartDivertingResult::kOK
: DiversionFileManager::StartDivertingResult::kWasAlreadyDiverted;
}
DiversionFileManager::FinishDivertingResult
DiversionFileManager::FinishDiverting(const storage::FileSystemURL& url,
Callback explicit_callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (auto iter = entries_.find(url); iter != entries_.end()) {
scoped_refptr<Entry> entry = std::move(iter->second);
entries_.erase(iter);
entry->Finish(std::move(explicit_callback));
return DiversionFileManager::FinishDivertingResult::kOK;
}
return DiversionFileManager::FinishDivertingResult::kWasNotDiverting;
}
std::unique_ptr<storage::FileStreamReader>
DiversionFileManager::CreateDivertedFileStreamReader(
const storage::FileSystemURL& url,
int64_t offset) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (auto iter = entries_.find(url); iter != entries_.end()) {
return std::make_unique<Worker>(Worker::Role::kReader, iter->second,
offset);
}
return nullptr;
}
std::unique_ptr<storage::FileStreamWriter>
DiversionFileManager::CreateDivertedFileStreamWriter(
const storage::FileSystemURL& url,
int64_t offset) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (auto iter = entries_.find(url); iter != entries_.end()) {
return std::make_unique<Worker>(Worker::Role::kWriter, iter->second,
offset);
}
return nullptr;
}
void DiversionFileManager::GetDivertedFileInfo(
const storage::FileSystemURL& url,
storage::FileSystemOperation::GetMetadataFieldSet fields,
GetFileInfoCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
static constexpr auto reply = [](GetFileInfoCallback callback,
const Tmpfile& tmpfile, int ignored) {
base::File::Info info;
if (tmpfile.net_error != net::OK) {
std::move(callback).Run(storage::NetErrorToFileError(tmpfile.net_error),
info);
return;
}
info.size = tmpfile.file_size;
info.is_directory = false;
info.is_symbolic_link = false;
std::move(callback).Run(base::File::FILE_OK, info);
};
if (auto iter = entries_.find(url); iter != entries_.end()) {
iter->second->Enqueue(
{Op::Transform(), base::BindOnce(reply, std::move(callback))});
return;
}
base::File::Info info;
std::move(callback).Run(base::File::FILE_ERROR_INVALID_OPERATION, info);
}
void DiversionFileManager::TruncateDivertedFile(
const storage::FileSystemURL& url,
int64_t length,
StatusCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
static constexpr auto transform =
[](int64_t length, Tmpfile tmpfile) -> std::pair<Tmpfile, int> {
if (tmpfile.net_error != net::OK) {
return std::make_pair(std::move(tmpfile), 0);
} else if (!tmpfile.scoped_fd.is_valid()) {
return std::make_pair(Tmpfile(net::ERR_INVALID_HANDLE), 0);
} else if ((length < 0) || (static_cast<uint64_t>(length) >
std::numeric_limits<off_t>::max())) {
return std::make_pair(Tmpfile(net::ERR_INVALID_ARGUMENT), 0);
}
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
if (HANDLE_EINTR(ftruncate(tmpfile.scoped_fd.get(),
static_cast<off_t>(length))) < 0) {
tmpfile.net_error = net::ERR_FAILED;
}
return std::make_pair(std::move(tmpfile), 0);
};
static constexpr auto reply = [](StatusCallback callback,
const Tmpfile& tmpfile, int ignored) {
std::move(callback).Run(storage::NetErrorToFileError(tmpfile.net_error));
};
if (auto iter = entries_.find(url); iter != entries_.end()) {
iter->second->Enqueue({base::BindOnce(transform, length),
base::BindOnce(reply, std::move(callback))});
return;
}
std::move(callback).Run(base::File::FILE_ERROR_INVALID_OPERATION);
}
void DiversionFileManager::OverrideTmpfileDirForTesting(
const base::FilePath& tmpfile_dir) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
tmpfile_dir_ = tmpfile_dir;
}
std::string DiversionFileManager::TmpfileDirAsString() const {
return tmpfile_dir_.empty() ? std::string(kChronosHomeDir)
: tmpfile_dir_.AsUTF8Unsafe();
}
} // namespace ash