| // Copyright 2018 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 "chrome/chrome_cleaner/zip_archiver/sandboxed_zip_archiver.h" |
| |
| #include <utility> |
| |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/system/sys_info.h" |
| #include "base/win/scoped_handle.h" |
| #include "chrome/chrome_cleaner/constants/quarantine_constants.h" |
| #include "chrome/chrome_cleaner/os/disk_util.h" |
| #include "mojo/public/cpp/system/platform_handle.h" |
| |
| namespace chrome_cleaner { |
| |
| namespace { |
| |
| using mojom::ZipArchiverResultCode; |
| |
| // According to the zip structure and tests, zipping one file with STORE |
| // compression level should not increase the file size more than 1KB. |
| constexpr int64_t kZipAdditionalSize = 1024; |
| |
| constexpr wchar_t kDefaultFileStreamSuffix[] = L"::$DATA"; |
| constexpr uint32_t kMinimizedReadAccess = |
| SYNCHRONIZE | FILE_READ_DATA | FILE_READ_ATTRIBUTES; |
| constexpr uint32_t kMinimizedWriteAccess = |
| SYNCHRONIZE | FILE_WRITE_DATA | FILE_READ_ATTRIBUTES; |
| |
| // NTFS file stream can be specified by appending ":" to the filename. We remove |
| // the default file stream "::$DATA" so it won't break the filename in the |
| // following uses. For other file streams, we don't archive and ignore them. |
| bool GetSanitizedFileName(const base::FilePath& path, |
| base::string16* output_sanitized_filename) { |
| DCHECK(output_sanitized_filename); |
| |
| base::string16 sanitized_filename = path.BaseName().AsUTF16Unsafe(); |
| if (base::EndsWith(sanitized_filename, kDefaultFileStreamSuffix, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| // Remove the default file stream suffix. |
| sanitized_filename.erase( |
| sanitized_filename.end() - wcslen(kDefaultFileStreamSuffix), |
| sanitized_filename.end()); |
| } |
| // If there is any ":" in |sanitized_filename|, it either points to a |
| // non-default file stream or is abnormal. Don't archive in this case. |
| if (sanitized_filename.find(L":") != base::string16::npos) |
| return false; |
| |
| *output_sanitized_filename = sanitized_filename; |
| return true; |
| } |
| |
| void RunArchiver(mojom::ZipArchiverPtr* zip_archiver_ptr, |
| mojo::ScopedHandle mojo_src_handle, |
| mojo::ScopedHandle mojo_zip_handle, |
| const std::string& filename, |
| const std::string& password, |
| mojom::ZipArchiver::ArchiveCallback callback) { |
| DCHECK(zip_archiver_ptr); |
| |
| (*zip_archiver_ptr) |
| ->Archive(std::move(mojo_src_handle), std::move(mojo_zip_handle), |
| filename, password, std::move(callback)); |
| } |
| |
| void OnArchiveDone(const base::FilePath& zip_file_path, |
| SandboxedZipArchiver::ArchiveResultCallback result_callback, |
| ZipArchiverResultCode result_code) { |
| if (result_code != ZipArchiverResultCode::kSuccess) { |
| // The zip file handle has been closed by mojo. Delete the incomplete zip |
| // file directly. |
| if (!base::DeleteFile(zip_file_path, /*recursive=*/false)) |
| LOG(ERROR) << "Failed to delete the incomplete zip file."; |
| } |
| // Call |result_callback| for SandboxedZipArchiver::Archive. |
| std::move(result_callback).Run(result_code); |
| } |
| |
| } // namespace |
| |
| namespace internal { |
| |
| // Zip file name format: "|filename|_|file_hash|.zip" |
| base::string16 ConstructZipArchiveFileName(const base::string16& filename, |
| const std::string& file_hash) { |
| const std::string normalized_file_hash = base::ToUpperASCII(file_hash); |
| return base::StrCat( |
| {filename, L"_", base::UTF8ToUTF16(normalized_file_hash), L".zip"}); |
| } |
| |
| } // namespace internal |
| |
| SandboxedZipArchiver::SandboxedZipArchiver( |
| scoped_refptr<MojoTaskRunner> mojo_task_runner, |
| UniqueZipArchiverPtr zip_archiver_ptr, |
| const base::FilePath& dst_archive_folder, |
| const std::string& zip_password) |
| : mojo_task_runner_(mojo_task_runner), |
| zip_archiver_ptr_(std::move(zip_archiver_ptr)), |
| dst_archive_folder_(dst_archive_folder), |
| zip_password_(zip_password) { |
| // Make sure the |zip_archiver_ptr| is bound with the |mojo_task_runner|. |
| DCHECK(zip_archiver_ptr_.get_deleter().task_runner_ == mojo_task_runner); |
| } |
| |
| SandboxedZipArchiver::~SandboxedZipArchiver() = default; |
| |
| // |SandboxedZipArchiver::Archive| archives the source file into a |
| // password-protected zip file stored in the |dst_archive_folder|. The format of |
| // zip file name is "|basename of the source file|_|hexdigest of the source file |
| // hash|.zip". |
| void SandboxedZipArchiver::Archive(const base::FilePath& src_file_path, |
| ArchiveResultCallback result_callback) { |
| // Open the source file with minimized rights for reading. |
| // Allowing all sharing accesses increases the chances of being able to open |
| // and archive the file. Because |base::IsLink| doesn't work on Windows, use |
| // |FILE_FLAG_OPEN_REPARSE_POINT| to open a symbolic link then check. To |
| // eliminate TOCTTOU, use |FILE_FLAG_BACKUP_SEMANTICS| to open a directory |
| // then check. |
| base::File src_file(::CreateFile( |
| src_file_path.AsUTF16Unsafe().c_str(), kMinimizedReadAccess, |
| FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, |
| OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, |
| nullptr)); |
| if (!src_file.IsValid()) { |
| LOG(ERROR) << "Unable to open the source file."; |
| std::move(result_callback) |
| .Run(ZipArchiverResultCode::kErrorCannotOpenSourceFile); |
| return; |
| } |
| |
| BY_HANDLE_FILE_INFORMATION src_file_info; |
| if (!::GetFileInformationByHandle(src_file.GetPlatformFile(), |
| &src_file_info)) { |
| LOG(ERROR) << "Unable to get the source file information."; |
| std::move(result_callback).Run(ZipArchiverResultCode::kErrorIO); |
| return; |
| } |
| |
| // Don't archive symbolic links. |
| if (src_file_info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { |
| std::move(result_callback).Run(ZipArchiverResultCode::kIgnoredSourceFile); |
| return; |
| } |
| |
| // Don't archive directories. And |ZipArchiver| shouldn't get called with a |
| // directory path. |
| if (src_file_info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { |
| LOG(ERROR) << "Tried to archive a directory."; |
| std::move(result_callback).Run(ZipArchiverResultCode::kIgnoredSourceFile); |
| return; |
| } |
| |
| base::string16 sanitized_src_filename; |
| if (!GetSanitizedFileName(src_file_path, &sanitized_src_filename)) { |
| std::move(result_callback).Run(ZipArchiverResultCode::kIgnoredSourceFile); |
| return; |
| } |
| |
| const ZipArchiverResultCode result_code = CheckFileSize(&src_file); |
| if (result_code != ZipArchiverResultCode::kSuccess) { |
| std::move(result_callback).Run(result_code); |
| return; |
| } |
| |
| std::string src_file_hash; |
| if (!ComputeSHA256DigestOfPath(src_file_path, &src_file_hash)) { |
| LOG(ERROR) << "Unable to hash the source file."; |
| std::move(result_callback).Run(ZipArchiverResultCode::kErrorIO); |
| return; |
| } |
| |
| const base::string16 zip_filename = internal::ConstructZipArchiveFileName( |
| sanitized_src_filename, src_file_hash); |
| const base::FilePath zip_file_path = dst_archive_folder_.Append(zip_filename); |
| |
| // Fail if the zip file exists. |
| if (base::PathExists(zip_file_path)) { |
| std::move(result_callback).Run(ZipArchiverResultCode::kZipFileExists); |
| return; |
| } |
| |
| // Create and open the zip file with minimized rights for writing. |
| base::File zip_file(::CreateFile(zip_file_path.AsUTF16Unsafe().c_str(), |
| kMinimizedWriteAccess, 0, nullptr, |
| CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)); |
| if (!zip_file.IsValid()) { |
| LOG(ERROR) << "Unable to create the zip file."; |
| std::move(result_callback) |
| .Run(ZipArchiverResultCode::kErrorCannotCreateZipFile); |
| return; |
| } |
| |
| const std::string filename_in_zip = base::UTF16ToUTF8(sanitized_src_filename); |
| // Do archive. |
| // Unretained pointer of |zip_archiver_ptr_| is safe because its deleter |
| // is run on the same task runner. If |zip_archiver_ptr_| is destructed later, |
| // the deleter will be scheduled after this task. |
| auto done_callback = |
| base::BindOnce(OnArchiveDone, zip_file_path, std::move(result_callback)); |
| mojo_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(RunArchiver, base::Unretained(zip_archiver_ptr_.get()), |
| mojo::WrapPlatformFile(src_file.TakePlatformFile()), |
| mojo::WrapPlatformFile(zip_file.TakePlatformFile()), |
| filename_in_zip, zip_password_, std::move(done_callback))); |
| } |
| |
| ZipArchiverResultCode SandboxedZipArchiver::CheckFileSize(base::File* file) { |
| const int64_t file_size = file->GetLength(); |
| if (file_size == -1) { |
| LOG(ERROR) << "Unable to get the file size."; |
| return ZipArchiverResultCode::kErrorIO; |
| } |
| if (file_size > kQuarantineSourceSizeLimit) { |
| LOG(ERROR) << "Source file is too big."; |
| return ZipArchiverResultCode::kErrorSourceFileTooBig; |
| } |
| |
| const int64_t dst_disk_space = |
| base::SysInfo::AmountOfFreeDiskSpace(dst_archive_folder_); |
| if (dst_disk_space == -1) { |
| LOG(ERROR) << "Unable to get the free disk space."; |
| return ZipArchiverResultCode::kErrorIO; |
| } |
| if (file_size + kZipAdditionalSize > dst_disk_space) { |
| LOG(ERROR) << "Not enough disk space."; |
| return ZipArchiverResultCode::kErrorNotEnoughDiskSpace; |
| } |
| |
| return ZipArchiverResultCode::kSuccess; |
| } |
| |
| ResultCode SpawnZipArchiverSandbox( |
| const base::FilePath& dst_archive_folder, |
| const std::string& zip_password, |
| scoped_refptr<MojoTaskRunner> mojo_task_runner, |
| const SandboxConnectionErrorCallback& connection_error_callback, |
| std::unique_ptr<SandboxedZipArchiver>* sandboxed_zip_archiver) { |
| DCHECK(sandboxed_zip_archiver); |
| |
| auto error_handler = |
| base::BindOnce(connection_error_callback, SandboxType::kZipArchiver); |
| ZipArchiverSandboxSetupHooks setup_hooks(mojo_task_runner, |
| std::move(error_handler)); |
| ResultCode result_code = |
| SpawnSandbox(&setup_hooks, SandboxType::kZipArchiver); |
| if (result_code == RESULT_CODE_SUCCESS) { |
| *sandboxed_zip_archiver = std::make_unique<SandboxedZipArchiver>( |
| mojo_task_runner, setup_hooks.TakeZipArchiverPtr(), dst_archive_folder, |
| zip_password); |
| } |
| |
| return result_code; |
| } |
| |
| } // namespace chrome_cleaner |