blob: fc97c20705786877a4272c4a128b3ea9e977a706 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/native_file_system/native_file_system_file_writer_impl.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/task/post_task.h"
#include "components/services/quarantine/quarantine.h"
#include "content/browser/native_file_system/native_file_system_error.h"
#include "content/browser/native_file_system/native_file_system_manager_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "crypto/secure_hash.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "storage/browser/fileapi/file_system_operation_runner.h"
#include "third_party/blink/public/mojom/blob/blob.mojom.h"
#include "third_party/blink/public/mojom/native_file_system/native_file_system_error.mojom.h"
using blink::mojom::NativeFileSystemStatus;
using storage::BlobDataHandle;
using storage::FileSystemOperation;
using storage::FileSystemOperationRunner;
namespace {
quarantine::mojom::QuarantineFileResult AnnotateFileSync(
const std::string& client_id,
const base::FilePath& path,
const GURL& referrer_url) {
// TODO(https://crbug/990997): Integrate with async Quarantine Service mojo
// API when it's ready.
quarantine::mojom::QuarantineFileResult result = quarantine::QuarantineFile(
path, /*source_url=*/GURL(), referrer_url, client_id);
return result;
}
// For after write checks we need the hash and size of the file. That data is
// calculated on a worker thread, and this struct is used to pass it back.
struct HashResult {
base::File::Error status;
// SHA256 hash of the file contents, an empty string if some error occurred.
std::string hash;
// Can be -1 to indicate an error calculating the hash and/or size.
int64_t file_size = -1;
};
HashResult ReadAndComputeSHA256ChecksumAndSize(const base::FilePath& path) {
base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
if (!file.IsValid())
return {file.error_details(), std::string(), -1};
std::unique_ptr<crypto::SecureHash> hash =
crypto::SecureHash::Create(crypto::SecureHash::SHA256);
std::vector<char> buffer(8 * 1024);
int bytes_read = file.ReadAtCurrentPos(buffer.data(), buffer.size());
while (bytes_read > 0) {
hash->Update(buffer.data(), bytes_read);
bytes_read = file.ReadAtCurrentPos(buffer.data(), buffer.size());
}
// If bytes_read is -ve, it means there were issues reading from disk.
if (bytes_read < 0)
return {file.error_details(), std::string(), -1};
std::string hash_str(hash->GetHashLength(), 0);
hash->Finish(base::data(hash_str), hash_str.size());
return {file.error_details(), hash_str, file.GetLength()};
}
} // namespace
namespace content {
struct NativeFileSystemFileWriterImpl::WriteState {
WriteCallback callback;
uint64_t bytes_written = 0;
};
NativeFileSystemFileWriterImpl::NativeFileSystemFileWriterImpl(
NativeFileSystemManagerImpl* manager,
const BindingContext& context,
const storage::FileSystemURL& url,
const storage::FileSystemURL& swap_url,
const SharedHandleState& handle_state,
bool has_transient_user_activation)
: NativeFileSystemHandleBase(manager,
context,
url,
handle_state,
/*is_directory=*/false),
swap_url_(swap_url),
has_transient_user_activation_(has_transient_user_activation) {
DCHECK_EQ(swap_url.type(), url.type());
}
NativeFileSystemFileWriterImpl::~NativeFileSystemFileWriterImpl() {
if (can_purge()) {
DoFileSystemOperation(FROM_HERE, &FileSystemOperationRunner::RemoveFile,
base::BindOnce(
[](const storage::FileSystemURL& swap_url,
base::File::Error result) {
if (result != base::File::FILE_OK) {
DLOG(ERROR)
<< "Error Deleting Swap File, status: "
<< base::File::ErrorToString(result)
<< " path: " << swap_url.path();
}
},
swap_url()),
swap_url());
}
}
void NativeFileSystemFileWriterImpl::Write(
uint64_t offset,
mojo::PendingRemote<blink::mojom::Blob> data,
WriteCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
RunWithWritePermission(
base::BindOnce(&NativeFileSystemFileWriterImpl::WriteImpl,
weak_factory_.GetWeakPtr(), offset, std::move(data)),
base::BindOnce([](WriteCallback callback) {
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kPermissionDenied),
/*bytes_written=*/0);
}),
std::move(callback));
}
void NativeFileSystemFileWriterImpl::WriteStream(
uint64_t offset,
mojo::ScopedDataPipeConsumerHandle stream,
WriteStreamCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
RunWithWritePermission(
base::BindOnce(&NativeFileSystemFileWriterImpl::WriteStreamImpl,
weak_factory_.GetWeakPtr(), offset, std::move(stream)),
base::BindOnce([](WriteStreamCallback callback) {
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kPermissionDenied),
/*bytes_written=*/0);
}),
std::move(callback));
}
void NativeFileSystemFileWriterImpl::Truncate(uint64_t length,
TruncateCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
RunWithWritePermission(
base::BindOnce(&NativeFileSystemFileWriterImpl::TruncateImpl,
weak_factory_.GetWeakPtr(), length),
base::BindOnce([](TruncateCallback callback) {
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kPermissionDenied));
}),
std::move(callback));
}
void NativeFileSystemFileWriterImpl::Close(CloseCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
RunWithWritePermission(
base::BindOnce(&NativeFileSystemFileWriterImpl::CloseImpl,
weak_factory_.GetWeakPtr()),
base::BindOnce([](CloseCallback callback) {
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kPermissionDenied));
}),
std::move(callback));
}
void NativeFileSystemFileWriterImpl::WriteImpl(
uint64_t offset,
mojo::PendingRemote<blink::mojom::Blob> data,
WriteCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK_EQ(GetWritePermissionStatus(),
blink::mojom::PermissionStatus::GRANTED);
if (is_closed()) {
std::move(callback).Run(
native_file_system_error::FromStatus(
NativeFileSystemStatus::kInvalidState,
"An attempt was made to write to a closed writer."),
/*bytes_written=*/0);
return;
}
blob_context()->GetBlobDataFromBlobRemote(
std::move(data),
base::BindOnce(&NativeFileSystemFileWriterImpl::DoWriteBlob,
weak_factory_.GetWeakPtr(), std::move(callback), offset));
}
void NativeFileSystemFileWriterImpl::DoWriteBlob(
WriteCallback callback,
uint64_t position,
std::unique_ptr<BlobDataHandle> blob) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!blob) {
std::move(callback).Run(
native_file_system_error::FromStatus(
NativeFileSystemStatus::kInvalidArgument, "Blob does not exist"),
/*bytes_written=*/0);
return;
}
DoFileSystemOperation(
FROM_HERE, &FileSystemOperationRunner::Write,
base::BindRepeating(&NativeFileSystemFileWriterImpl::DidWrite,
weak_factory_.GetWeakPtr(),
base::Owned(new WriteState{std::move(callback)})),
swap_url(), std::move(blob), position);
}
void NativeFileSystemFileWriterImpl::WriteStreamImpl(
uint64_t offset,
mojo::ScopedDataPipeConsumerHandle stream,
WriteStreamCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK_EQ(GetWritePermissionStatus(),
blink::mojom::PermissionStatus::GRANTED);
if (is_closed()) {
std::move(callback).Run(
native_file_system_error::FromStatus(
NativeFileSystemStatus::kInvalidState,
"An attempt was made to write to a closed writer."),
/*bytes_written=*/0);
return;
}
DoFileSystemOperation(
FROM_HERE, &FileSystemOperationRunner::WriteStream,
base::BindRepeating(&NativeFileSystemFileWriterImpl::DidWrite,
weak_factory_.GetWeakPtr(),
base::Owned(new WriteState{std::move(callback)})),
swap_url(), std::move(stream), offset);
}
void NativeFileSystemFileWriterImpl::DidWrite(WriteState* state,
base::File::Error result,
int64_t bytes,
bool complete) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(state);
state->bytes_written += bytes;
if (complete) {
std::move(state->callback)
.Run(native_file_system_error::FromFileError(result),
state->bytes_written);
}
}
void NativeFileSystemFileWriterImpl::TruncateImpl(uint64_t length,
TruncateCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK_EQ(GetWritePermissionStatus(),
blink::mojom::PermissionStatus::GRANTED);
if (is_closed()) {
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kInvalidState,
"An attempt was made to write to a closed writer."));
return;
}
DoFileSystemOperation(
FROM_HERE, &FileSystemOperationRunner::Truncate,
base::BindOnce(
[](TruncateCallback callback, base::File::Error result) {
std::move(callback).Run(
native_file_system_error::FromFileError(result));
},
std::move(callback)),
swap_url(), length);
}
void NativeFileSystemFileWriterImpl::CloseImpl(CloseCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK_EQ(GetWritePermissionStatus(),
blink::mojom::PermissionStatus::GRANTED);
if (is_closed()) {
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kInvalidState,
"An attempt was made to close an already closed writer."));
return;
}
// Should the writer be destructed at this point, we want to allow the
// close operation to run its course, so we should not purge the swap file.
// If the after write check fails, the callback for that will clean up the
// swap file even if the writer was destroyed at that point.
state_ = State::kClosePending;
if (!RequireAfterWriteCheck() || !manager()->permission_context()) {
DidPassAfterWriteCheck(std::move(callback));
return;
}
ComputeHashForSwapFile(base::BindOnce(
&NativeFileSystemFileWriterImpl::DoAfterWriteCheck,
weak_factory_.GetWeakPtr(), swap_url().path(), std::move(callback)));
}
// static
void NativeFileSystemFileWriterImpl::DoAfterWriteCheck(
base::WeakPtr<NativeFileSystemFileWriterImpl> file_writer,
const base::FilePath& swap_path,
NativeFileSystemFileWriterImpl::CloseCallback callback,
base::File::Error hash_result,
const std::string& hash,
int64_t size) {
if (!file_writer || hash_result != base::File::FILE_OK) {
// If writer was deleted, or calculating the hash failed try deleting the
// swap file and invoke the callback.
base::PostTask(FROM_HERE, {base::ThreadPool(), base::MayBlock()},
base::BindOnce(base::IgnoreResult(&base::DeleteFile),
swap_path, /*recursive=*/false));
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kOperationAborted,
"Failed to perform Safe Browsing check."));
return;
}
auto item = std::make_unique<NativeFileSystemWriteItem>();
item->target_file_path = file_writer->url().path();
item->full_path = file_writer->swap_url().path();
item->sha256_hash = hash;
item->size = size;
item->frame_url = file_writer->context().url;
item->has_user_gesture = file_writer->has_transient_user_activation_;
file_writer->manager()->permission_context()->PerformAfterWriteChecks(
std::move(item), file_writer->context().process_id,
file_writer->context().frame_id,
base::BindOnce(&NativeFileSystemFileWriterImpl::DidAfterWriteCheck,
file_writer, swap_path, std::move(callback)));
}
// static
void NativeFileSystemFileWriterImpl::DidAfterWriteCheck(
base::WeakPtr<NativeFileSystemFileWriterImpl> file_writer,
const base::FilePath& swap_path,
NativeFileSystemFileWriterImpl::CloseCallback callback,
NativeFileSystemPermissionContext::AfterWriteCheckResult result) {
if (file_writer &&
result ==
NativeFileSystemPermissionContext::AfterWriteCheckResult::kAllow) {
file_writer->DidPassAfterWriteCheck(std::move(callback));
return;
}
// Writer is gone, or safe browsing check failed. In this case we should
// try deleting the swap file and call the callback to report that close
// failed.
base::PostTask(FROM_HERE, {base::ThreadPool(), base::MayBlock()},
base::BindOnce(base::IgnoreResult(&base::DeleteFile),
swap_path, /*recursive=*/false));
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kOperationAborted,
"Write operation blocked by Safe Browsing."));
return;
}
void NativeFileSystemFileWriterImpl::DidPassAfterWriteCheck(
CloseCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// If the move operation succeeds, the path pointing to the swap file
// will not exist anymore.
// In case of error, the swap file URL will point to a valid filesystem
// location. The file at this URL will be deleted when the mojo pipe closes.
DoFileSystemOperation(
FROM_HERE, &FileSystemOperationRunner::Move,
base::BindOnce(&NativeFileSystemFileWriterImpl::DidSwapFileBeforeClose,
weak_factory_.GetWeakPtr(), std::move(callback)),
swap_url(), url(),
storage::FileSystemOperation::OPTION_PRESERVE_LAST_MODIFIED);
}
void NativeFileSystemFileWriterImpl::DidSwapFileBeforeClose(
CloseCallback callback,
base::File::Error result) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (result != base::File::FILE_OK) {
state_ = State::kCloseError;
DLOG(ERROR) << "Swap file move operation failed source: "
<< swap_url().path() << " dest: " << url().path()
<< " error: " << base::File::ErrorToString(result);
std::move(callback).Run(native_file_system_error::FromFileError(result));
return;
}
if (CanSkipQuarantineCheck()) {
state_ = State::kClosed;
std::move(callback).Run(native_file_system_error::Ok());
return;
}
GURL referrer_url = manager()->is_off_the_record() ? GURL() : context().url;
base::PostTaskAndReplyWithResult(
FROM_HERE, {base::ThreadPool(), base::MayBlock()},
base::BindOnce(&AnnotateFileSync,
GetContentClient()
->browser()
->GetApplicationClientGUIDForQuarantineCheck(),
url().path(), referrer_url),
base::BindOnce(&NativeFileSystemFileWriterImpl::DidAnnotateFile,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void NativeFileSystemFileWriterImpl::DidAnnotateFile(
CloseCallback callback,
quarantine::mojom::QuarantineFileResult result) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
state_ = State::kClosed;
if (result != quarantine::mojom::QuarantineFileResult::OK &&
result != quarantine::mojom::QuarantineFileResult::ANNOTATION_FAILED) {
// If malware was detected, or the file referrer was blocked by policy, the
// file will be deleted at this point by AttachmentServices on Windows.
// There is nothing to do except to return the error message to the
// application.
std::move(callback).Run(native_file_system_error::FromStatus(
NativeFileSystemStatus::kOperationAborted,
"Write operation aborted due to security policy."));
return;
}
std::move(callback).Run(native_file_system_error::Ok());
}
void NativeFileSystemFileWriterImpl::ComputeHashForSwapFile(
HashCallback callback) {
DCHECK_EQ(swap_url().type(), storage::kFileSystemTypeNativeLocal);
base::PostTaskAndReplyWithResult(
FROM_HERE, {base::ThreadPool(), base::MayBlock()},
base::BindOnce(&ReadAndComputeSHA256ChecksumAndSize, swap_url().path()),
base::BindOnce(
[](HashCallback callback, HashResult result) {
std::move(callback).Run(result.status, result.hash,
result.file_size);
},
std::move(callback)));
}
base::WeakPtr<NativeFileSystemHandleBase>
NativeFileSystemFileWriterImpl::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
} // namespace content