| // Copyright 2020 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_io/native_io_host.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/files/file.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/task/post_task.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/task_runner.h" |
| #include "build/build_config.h" |
| #include "content/browser/native_io/native_io_file_host.h" |
| #include "content/browser/native_io/native_io_manager.h" |
| #include "mojo/public/cpp/bindings/message.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "third_party/blink/public/common/native_io/native_io_utils.h" |
| #include "third_party/blink/public/mojom/native_io/native_io.mojom.h" |
| |
| using blink::mojom::NativeIOError; |
| using blink::mojom::NativeIOErrorPtr; |
| using blink::mojom::NativeIOErrorType; |
| |
| namespace content { |
| |
| namespace { |
| |
| bool IsValidNativeIONameCharacter(char name_char) { |
| return base::IsAsciiLower(name_char) || base::IsAsciiDigit(name_char) || |
| name_char == '_'; |
| } |
| |
| // Maximum allowed filename length, inclusive. |
| const int kMaximumFilenameLength = 100; |
| |
| bool IsValidNativeIOName(const std::string& name) { |
| if (name.empty()) |
| return false; |
| |
| if (name.length() > kMaximumFilenameLength) |
| return false; |
| |
| return std::all_of(name.begin(), name.end(), &IsValidNativeIONameCharacter); |
| } |
| |
| base::FilePath GetNativeIOFilePath(const base::FilePath& root_path, |
| const std::string& name) { |
| DCHECK(IsValidNativeIOName(name)); |
| DCHECK(!root_path.empty()); |
| |
| // This simple implementation assumes that the name doesn't have any special |
| // meaning to the host operating system. |
| base::FilePath file_path = root_path.AppendASCII(name); |
| DCHECK(root_path.IsParent(file_path)); |
| return file_path; |
| } |
| |
| // Creates a task runner suitable for running file I/O tasks. |
| scoped_refptr<base::TaskRunner> CreateFileTaskRunner() { |
| // We use a SequencedTaskRunner so that there is a global ordering to an |
| // origin's directory operations. |
| return base::ThreadPool::CreateSequencedTaskRunner({ |
| // Needed for file I/O. |
| base::MayBlock(), |
| |
| // Reasonable compromise, given that a few database operations are |
| // blocking, while most operations are not. We should be able to do better |
| // when we get scheduling APIs on the Web Platform. |
| base::TaskPriority::USER_VISIBLE, |
| |
| // BLOCK_SHUTDOWN is definitely not appropriate. We might be able to move |
| // to CONTINUE_ON_SHUTDOWN after very careful analysis. |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, |
| }); |
| } |
| |
| // Performs the file I/O work in OpenFile(). |
| std::pair<base::File, int64_t> DoOpenFile(const base::FilePath& root_path, |
| const std::string& name) { |
| DCHECK(IsValidNativeIOName(name)); |
| DCHECK(!root_path.empty()); |
| |
| // Lazily create the origin's directory. |
| base::File::Error error; |
| if (!base::CreateDirectoryAndGetError(root_path, &error)) |
| return {base::File(), /*file_length=*/0}; |
| |
| // SHARE_DELETE allows the browser to delete files even if a compromised |
| // renderer refuses to close its file handles. |
| int open_flags = base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ | |
| base::File::FLAG_WRITE | base::File::FLAG_SHARE_DELETE; |
| base::File file(GetNativeIOFilePath(root_path, name), open_flags); |
| |
| int64_t file_length = file.IsValid() ? file.GetLength() : 0; |
| |
| return {std::move(file), file_length}; |
| } |
| |
| // Performs the file I/O work in DeleteFile(). |
| std::pair<blink::mojom::NativeIOErrorPtr, int64_t> DoDeleteFile( |
| const base::FilePath& root_path, |
| const std::string& name) { |
| DCHECK(IsValidNativeIOName(name)); |
| DCHECK(!root_path.empty()); |
| |
| // If the origin's directory wasn't created yet, there's nothing to delete. |
| if (!base::PathExists(root_path)) |
| return {NativeIOError::New(NativeIOErrorType::kSuccess, ""), |
| /*deleted_file_length=*/0}; |
| |
| int64_t deleted_file_length; |
| base::FilePath file_path = GetNativeIOFilePath(root_path, name); |
| // If the file wasn't created yet, there's nothing to delete. |
| if (!base::PathExists(file_path)) |
| return {NativeIOError::New(NativeIOErrorType::kSuccess, ""), |
| /*deleted_file_length=*/0}; |
| if (!base::GetFileSize(file_path, &deleted_file_length)) |
| return {NativeIOManager::FileErrorToNativeIOError( |
| base::File::GetLastFileError()), |
| /*deleted_file_length=*/0}; |
| |
| if (!base::DeleteFile(file_path)) |
| return {NativeIOManager::FileErrorToNativeIOError( |
| base::File::GetLastFileError()), |
| /*deleted_file_length=*/0}; |
| |
| return {NativeIOError::New(NativeIOErrorType::kSuccess, ""), |
| deleted_file_length}; |
| } |
| |
| using GetAllFileNamesResult = |
| std::pair<base::File::Error, std::vector<std::string>>; |
| |
| // Performs the file I/O work in GetAllFileNames(). |
| GetAllFileNamesResult DoGetAllFileNames(const base::FilePath& root_path) { |
| DCHECK(!root_path.empty()); |
| |
| std::vector<std::string> result; |
| |
| // If the origin's directory wasn't created yet, there's no file to report. |
| if (!base::PathExists(root_path)) |
| return {base::File::FILE_OK, std::move(result)}; |
| |
| base::FileEnumerator file_enumerator( |
| root_path, /*recursive=*/false, base::FileEnumerator::FILES, |
| /*pattern=*/base::FilePath::StringType(), |
| base::FileEnumerator::FolderSearchPolicy::ALL, |
| base::FileEnumerator::ErrorPolicy::STOP_ENUMERATION); |
| |
| // TODO(pwnall): The result vector can grow to an unbounded size. Add a limit |
| // parameter with a reasonable upper bound. |
| |
| for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty(); |
| file_path = file_enumerator.Next()) { |
| // If the file name has a non-ASCII character, |file_name| will be the empty |
| // string. This will correctly be flagged as corruption by the check below. |
| std::string file_name = file_path.BaseName().MaybeAsASCII(); |
| |
| // Chrome's NativeIO implementation only creates files that have valid |
| // NativeIO names. Any other file names imply directory corruption. |
| if (!IsValidNativeIOName(file_name)) { |
| // TODO(pwnall): Figure out the corruption handling strategy. We could |
| // silently ignore the corrupted file, delete it, or stop |
| // and report an error. |
| continue; |
| } |
| result.push_back(std::move(file_name)); |
| } |
| |
| // Don't return a partial list of files if an error occurred. The partial list |
| // isn't meaningful, and may be useful information for a compromised renderer. |
| // |
| // TODO(pwnall): Reconsider this if we end up making NativeIO unusually |
| // friendly to corruption recovery. |
| base::File::Error enumeration_error = file_enumerator.GetError(); |
| if (enumeration_error != base::File::FILE_OK) |
| result.clear(); |
| |
| return {enumeration_error, std::move(result)}; |
| } |
| |
| // Reports the result of the file I/O work in GetAllFileNames(). |
| void DidGetAllFileNames( |
| blink::mojom::NativeIOHost::GetAllFileNamesCallback callback, |
| GetAllFileNamesResult result) { |
| std::move(callback).Run(result.first == base::File::FILE_OK, |
| std::move(result.second)); |
| } |
| |
| // Performs the file I/O work in RenameFile(). |
| NativeIOErrorPtr DoRenameFile(const base::FilePath& root_path, |
| const std::string& old_name, |
| const std::string& new_name) { |
| DCHECK(!root_path.empty()); |
| DCHECK(IsValidNativeIOName(old_name)); |
| DCHECK(IsValidNativeIOName(new_name)); |
| |
| base::File::Error error = base::File::FILE_OK; |
| // If the origin's directory wasn't created yet, there's nothing to rename. |
| // This error cannot be used to determine the existence of files outside of |
| // the origin's directory, as |old_name| is a valid NativeIO name. |
| if (!base::PathExists(root_path) || |
| !base::PathExists(GetNativeIOFilePath(root_path, old_name))) { |
| return NativeIOError::New(NativeIOErrorType::kNotFound, |
| "Source file does not exist"); |
| } |
| |
| // Do not overwrite an existing file. This error cannot be used to determine |
| // the existence of files outside of the origin's directory, as `new_name` is |
| // a valid NativeIO name. |
| if (base::PathExists(GetNativeIOFilePath(root_path, new_name))) |
| return NativeIOError::New(NativeIOErrorType::kNoModificationAllowed, |
| "Target file exists"); |
| |
| base::ReplaceFile(GetNativeIOFilePath(root_path, old_name), |
| GetNativeIOFilePath(root_path, new_name), &error); |
| return NativeIOManager::FileErrorToNativeIOError(error); |
| } |
| |
| // Performs the file I/O work in DeleteAllData(). |
| base::File::Error DoDeleteAllData(const base::FilePath& origin_dir) { |
| DCHECK(!origin_dir.empty()); |
| CHECK(!origin_dir.ReferencesParent()) |
| << "Removing a parent directory is disallowed."; |
| bool delete_success = base::DeletePathRecursively(origin_dir); |
| if (!delete_success) { |
| return base::File::GetLastFileError(); |
| } |
| return base::File::FILE_OK; |
| } |
| |
| } // namespace |
| |
| NativeIOHost::NativeIOHost(const url::Origin& origin, |
| base::FilePath root_path, |
| #if defined(OS_MAC) |
| bool allow_set_length_ipc, |
| #endif // defined(OS_MAC) |
| NativeIOManager* manager) |
| : origin_(origin), |
| root_path_(std::move(root_path)), |
| #if defined(OS_MAC) |
| allow_set_length_ipc_(allow_set_length_ipc), |
| #endif // defined(OS_MAC) |
| manager_(manager), |
| file_task_runner_(CreateFileTaskRunner()) { |
| DCHECK(manager != nullptr); |
| |
| // base::Unretained is safe here because this NativeIOHost owns |receivers_|. |
| // So, the unretained NativeIOHost is guaranteed to outlive |receivers_| and |
| // the closure that it uses. |
| receivers_.set_disconnect_handler(base::BindRepeating( |
| &NativeIOHost::OnReceiverDisconnect, base::Unretained(this))); |
| } |
| |
| NativeIOHost::~NativeIOHost() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| void NativeIOHost::BindReceiver( |
| mojo::PendingReceiver<blink::mojom::NativeIOHost> receiver) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| receivers_.Add(this, std::move(receiver)); |
| } |
| |
| void NativeIOHost::OpenFile( |
| const std::string& name, |
| mojo::PendingReceiver<blink::mojom::NativeIOFileHost> file_host_receiver, |
| OpenFileCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (is_incognito_mode()) { |
| std::move(callback).Run( |
| base::File(), /*file_length=*/0, |
| NativeIOError::New(NativeIOErrorType::kInvalidState, |
| "StorageFoundation unavailable for this host")); |
| return; |
| } |
| |
| if (delete_all_data_in_progress()) { |
| std::move(callback).Run( |
| base::File(), /*file_length=*/0, |
| NativeIOError::New(NativeIOErrorType::kInvalidState, |
| "Data removal pending on origin")); |
| return; |
| } |
| |
| if (!IsValidNativeIOName(name)) { |
| mojo::ReportBadMessage("Invalid file name"); |
| std::move(callback).Run( |
| base::File(), /*file_length=*/0, |
| NativeIOError::New(NativeIOErrorType::kUnknown, "Invalid file name")); |
| return; |
| } |
| |
| if (open_file_hosts_.find(name) != open_file_hosts_.end()) { |
| std::move(callback).Run( |
| base::File(), /*file_length=*/0, |
| NativeIOError::New(NativeIOErrorType::kNoModificationAllowed, |
| "File is open")); |
| return; |
| } |
| |
| auto insert_result = io_pending_files_.insert(name); |
| bool insert_success = insert_result.second; |
| if (!insert_success) { |
| std::move(callback).Run( |
| base::File(), /*file_length=*/0, |
| NativeIOError::New(NativeIOErrorType::kNoModificationAllowed, |
| "Operation pending on file")); |
| return; |
| } |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoOpenFile, root_path_, name), |
| base::BindOnce(&NativeIOHost::DidOpenFile, weak_factory_.GetWeakPtr(), |
| name, std::move(file_host_receiver), std::move(callback))); |
| } |
| |
| void NativeIOHost::DeleteFile(const std::string& name, |
| DeleteFileCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (is_incognito_mode()) { |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kInvalidState, |
| "StorageFoundation unavailable for this host"), |
| /*granted_capacity_delta=*/0); |
| return; |
| } |
| |
| if (delete_all_data_in_progress()) { |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kInvalidState, |
| "Data removal pending on origin"), |
| /*granted_capacity_delta=*/0); |
| return; |
| } |
| |
| if (!IsValidNativeIOName(name)) { |
| mojo::ReportBadMessage("Invalid file name"); |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kUnknown, "Invalid file name"), |
| /*granted_capacity_delta=*/0); |
| return; |
| } |
| |
| if (open_file_hosts_.find(name) != open_file_hosts_.end()) { |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kNoModificationAllowed, |
| "File is open"), |
| /*granted_capacity_delta=*/0); |
| return; |
| } |
| |
| auto insert_result = io_pending_files_.insert(name); |
| bool insert_success = insert_result.second; |
| if (!insert_success) { |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kNoModificationAllowed, |
| "Operation pending on file"), |
| /*granted_capacity_delta=*/0); |
| return; |
| } |
| |
| manager_->quota_manager_proxy()->NotifyStorageAccessed( |
| origin_, blink::mojom::StorageType::kTemporary, base::Time::Now()); |
| |
| // The deletion task runs on the file_task_runner and is skipped on shutdown, |
| // as is ok for origin data deletion. |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoDeleteFile, root_path_, name), |
| base::BindOnce(&NativeIOHost::DidDeleteFile, weak_factory_.GetWeakPtr(), |
| name, std::move(callback))); |
| } |
| |
| void NativeIOHost::GetAllFileNames(GetAllFileNamesCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (is_incognito_mode()) { |
| std::move(callback).Run(false, {}); |
| return; |
| } |
| |
| if (delete_all_data_in_progress()) { |
| std::move(callback).Run(false, {}); |
| return; |
| } |
| |
| manager_->quota_manager_proxy()->NotifyStorageAccessed( |
| origin_, blink::mojom::StorageType::kTemporary, base::Time::Now()); |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoGetAllFileNames, root_path_), |
| base::BindOnce(&DidGetAllFileNames, std::move(callback))); |
| } |
| |
| void NativeIOHost::RenameFile(const std::string& old_name, |
| const std::string& new_name, |
| RenameFileCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (is_incognito_mode()) { |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kInvalidState, |
| "StorageFoundation unavailable for this host")); |
| return; |
| } |
| |
| if (delete_all_data_in_progress()) { |
| std::move(callback).Run(NativeIOError::New( |
| NativeIOErrorType::kInvalidState, "Data removal pending on origin")); |
| return; |
| } |
| |
| if (!IsValidNativeIOName(old_name) || !IsValidNativeIOName(new_name)) { |
| mojo::ReportBadMessage("Invalid file name"); |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kUnknown, "Invalid file name")); |
| return; |
| } |
| |
| if (open_file_hosts_.find(old_name) != open_file_hosts_.end() || |
| open_file_hosts_.find(new_name) != open_file_hosts_.end()) { |
| std::move(callback).Run(NativeIOError::New( |
| NativeIOErrorType::kNoModificationAllowed, "Source file is open")); |
| return; |
| } |
| |
| if (open_file_hosts_.find(old_name) != open_file_hosts_.end()) { |
| std::move(callback).Run(NativeIOError::New( |
| NativeIOErrorType::kNoModificationAllowed, "Target file is open")); |
| return; |
| } |
| |
| auto old_iterator_and_success = io_pending_files_.insert(old_name); |
| if (!old_iterator_and_success.second) { |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kNoModificationAllowed, |
| "Operation pending on source file")); |
| return; |
| } |
| auto new_iterator_and_success = io_pending_files_.insert(new_name); |
| if (!new_iterator_and_success.second) { |
| io_pending_files_.erase(old_iterator_and_success.first); |
| std::move(callback).Run( |
| NativeIOError::New(NativeIOErrorType::kNoModificationAllowed, |
| "Operation pending on target file")); |
| return; |
| } |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoRenameFile, root_path_, old_name, new_name), |
| base::BindOnce(&NativeIOHost::DidRenameFile, weak_factory_.GetWeakPtr(), |
| old_name, new_name, std::move(callback))); |
| } |
| |
| void NativeIOHost::RequestCapacityChange( |
| int64_t capacity_delta, |
| RequestCapacityChangeCallback callback) { |
| if (is_incognito_mode()) { |
| std::move(callback).Run(0); |
| return; |
| } |
| |
| // TODO(rstz): Implement quota limits. |
| constexpr int64_t kMaximumAllocation = int64_t{8} * 1024 * 1024 * 1024; |
| if (capacity_delta > kMaximumAllocation) { |
| std::move(callback).Run(0); |
| return; |
| } |
| std::move(callback).Run(capacity_delta); |
| } |
| |
| void NativeIOHost::OnFileClose(NativeIOFileHost* file_host) { |
| DCHECK(open_file_hosts_.count(file_host->file_name()) > 0); |
| DCHECK_EQ(open_file_hosts_[file_host->file_name()].get(), file_host); |
| |
| open_file_hosts_.erase(file_host->file_name()); |
| } |
| |
| void NativeIOHost::DeleteAllData(DeleteAllDataCallback callback) { |
| delete_all_data_callbacks_.push_back(std::move(callback)); |
| if (delete_all_data_callbacks_.size() > 1) { |
| return; |
| } |
| |
| // Clearing open file hosts informs the renderer that the file handles should |
| // not be used any longer. |
| open_file_hosts_.clear(); |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoDeleteAllData, root_path_), |
| base::BindOnce(&NativeIOHost::DidDeleteAllData, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void NativeIOHost::OnReceiverDisconnect() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| manager_->OnHostReceiverDisconnect(this); |
| } |
| |
| void NativeIOHost::DidOpenFile( |
| const std::string& name, |
| mojo::PendingReceiver<blink::mojom::NativeIOFileHost> file_host_receiver, |
| OpenFileCallback callback, |
| std::pair<base::File, int64_t> result) { |
| DCHECK(io_pending_files_.count(name)); |
| DCHECK(!open_file_hosts_.count(name)); |
| base::File file = std::move(result.first); |
| int64_t length = result.second; |
| io_pending_files_.erase(name); |
| |
| base::File::Error open_error = file.error_details(); |
| |
| if (!file.IsValid()) { |
| // Make sure an error is reported whenever the file is not valid. |
| open_error = open_error != base::File::FILE_OK |
| ? open_error |
| : base::File::FILE_ERROR_FAILED; |
| std::move(callback).Run( |
| std::move(file), length, |
| NativeIOManager::FileErrorToNativeIOError(open_error)); |
| return; |
| } |
| |
| // DoOpenFile may create a file if none exists, which justifies |
| // NotifyStorageModified. |
| manager_->quota_manager_proxy()->NotifyStorageModified( |
| storage::QuotaClientType::kNativeIO, origin_, |
| blink::mojom::StorageType::kTemporary, 0, base::Time::Now()); |
| |
| open_file_hosts_.insert({ |
| name, std::make_unique<NativeIOFileHost>(this, name, |
| #if defined(OS_MAC) |
| allow_set_length_ipc_, |
| #endif // defined(OS_MAC) |
| std::move(file_host_receiver)) |
| }); |
| |
| std::move(callback).Run( |
| std::move(file), length, |
| NativeIOManager::FileErrorToNativeIOError(open_error)); |
| return; |
| } |
| |
| void NativeIOHost::DidDeleteFile( |
| const std::string& name, |
| DeleteFileCallback callback, |
| std::pair<blink::mojom::NativeIOErrorPtr, int64_t> delete_result) { |
| DCHECK(io_pending_files_.count(name)); |
| DCHECK(!open_file_hosts_.count(name)); |
| io_pending_files_.erase(name); |
| |
| manager_->quota_manager_proxy()->NotifyStorageModified( |
| storage::QuotaClientType::kNativeIO, origin_, |
| blink::mojom::StorageType::kTemporary, 0, base::Time::Now()); |
| |
| std::move(callback).Run(std::move(delete_result.first), delete_result.second); |
| return; |
| } |
| |
| void NativeIOHost::DidRenameFile(const std::string& old_name, |
| const std::string& new_name, |
| RenameFileCallback callback, |
| NativeIOErrorPtr rename_error) { |
| DCHECK(io_pending_files_.count(old_name)); |
| DCHECK(!open_file_hosts_.count(old_name)); |
| DCHECK(io_pending_files_.count(new_name)); |
| DCHECK(!open_file_hosts_.count(new_name)); |
| io_pending_files_.erase(old_name); |
| io_pending_files_.erase(new_name); |
| |
| manager_->quota_manager_proxy()->NotifyStorageModified( |
| storage::QuotaClientType::kNativeIO, origin_, |
| blink::mojom::StorageType::kTemporary, 0, base::Time::Now()); |
| |
| std::move(callback).Run(std::move(rename_error)); |
| return; |
| } |
| |
| void NativeIOHost::DidDeleteAllData(base::File::Error error) { |
| // Moving callbacks to a local variable to avoid race conditions if the vector |
| // is accessed concurrently. |
| std::vector<DeleteAllDataCallback> callbacks = |
| std::move(delete_all_data_callbacks_); |
| delete_all_data_callbacks_.clear(); |
| for (DeleteAllDataCallback& callback : callbacks) { |
| std::move(callback).Run(error, this); |
| } |
| } |
| |
| } // namespace content |