// 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 #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 "chrome/chrome_cleaner/os/file_path_sanitization.h" #include "mojo/public/cpp/bindings/remote.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 size_t kDefaultMaxComponentLength = 255; 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, std::wstring* output_sanitized_filename) { DCHECK(output_sanitized_filename); std::wstring sanitized_filename = path.BaseName().value(); 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":") != std::wstring::npos) return false; *output_sanitized_filename = sanitized_filename; return true; } void RunArchiver(mojo::Remote* zip_archiver, mojo::PlatformHandle mojo_src_handle, mojo::PlatformHandle mojo_zip_handle, const std::string& filename, const std::string& password, mojom::ZipArchiver::ArchiveCallback callback) { DCHECK(zip_archiver); (*zip_archiver) ->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)) 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" std::wstring ConstructZipArchiveFileName(const std::wstring& filename, const std::string& file_hash, size_t max_filename_length) { const std::wstring normalized_hash = base::UTF8ToWide(base::ToUpperASCII(file_hash)); // Length of the ".zip" suffix and the "_" infix. constexpr size_t kAffixSize = 5; // If the constructed filename is too long for the destination volume, use a // prefix of the filename. std::wstring normalized_filename; if (filename.size() + normalized_hash.size() + kAffixSize > max_filename_length) { size_t trimmed_length = max_filename_length - normalized_hash.size() - kAffixSize; normalized_filename = filename.substr(0, trimmed_length); } else { normalized_filename = filename; } std::wstring result = base::StrCat({normalized_filename, L"_", normalized_hash, L".zip"}); DCHECK(result.size() <= max_filename_length); DCHECK(result.size() == normalized_filename.size() + normalized_hash.size() + kAffixSize); return result; } } // namespace internal SandboxedZipArchiver::SandboxedZipArchiver( scoped_refptr mojo_task_runner, RemoteZipArchiverPtr zip_archiver, const base::FilePath& dst_archive_folder, const std::string& zip_password) : mojo_task_runner_(mojo_task_runner), zip_archiver_(std::move(zip_archiver)), dst_archive_folder_(dst_archive_folder), zip_password_(zip_password) { // Make sure the |zip_archiver| is bound with the |mojo_task_runner|. DCHECK(zip_archiver_.get_deleter().task_runner_ == mojo_task_runner); int max_component_length = base::GetMaximumPathComponentLength(dst_archive_folder_); dst_max_component_length_ = max_component_length > 0 ? max_component_length : kDefaultMaxComponentLength; } 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.value().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; } std::wstring 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 std::wstring zip_filename = internal::ConstructZipArchiveFileName( sanitized_src_filename, src_file_hash, dst_max_component_length_); base::FilePath zip_file_path = dst_archive_folder_.Append(zip_filename); // If the full path is longer than MAX_PATH, prepending "\\?\" allows to // extend the path limit (see CreateFile documentation). if (zip_file_path.value().size() >= MAX_PATH) { std::wstring long_file_path = std::wstring(L"\\\\?\\") + zip_file_path.value(); zip_file_path = base::FilePath(long_file_path); } // 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.value().c_str(), kMinimizedWriteAccess, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)); if (!zip_file.IsValid()) { PLOG(ERROR) << "Unable to create the zip file at path " << SanitizePath(zip_file_path); std::move(result_callback) .Run(ZipArchiverResultCode::kErrorCannotCreateZipFile); return; } const std::string filename_in_zip = base::WideToUTF8(sanitized_src_filename); // Do archive. // Unretained pointer of |zip_archiver_| is safe because its deleter is run on // the same task runner. If |zip_archiver_| 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_.get()), mojo::PlatformHandle( base::ScopedPlatformFile(src_file.TakePlatformFile())), mojo::PlatformHandle( base::ScopedPlatformFile(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 mojo_task_runner, const SandboxConnectionErrorCallback& connection_error_callback, std::unique_ptr* 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( mojo_task_runner, setup_hooks.TakeZipArchiverRemote(), dst_archive_folder, zip_password); } return result_code; } } // namespace chrome_cleaner