blob: 76a064bdabafef1f3e40c49f45cbc7d9c0d04074 [file] [log] [blame]
// 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/os/file_remover.h"
#include <stdint.h>
#include <unordered_set>
#include <utility>
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/synchronization/waitable_event.h"
#include "chrome/chrome_cleaner/interfaces/zip_archiver.mojom.h"
#include "chrome/chrome_cleaner/logging/proto/removal_status.pb.h"
#include "chrome/chrome_cleaner/os/disk_util.h"
#include "chrome/chrome_cleaner/os/file_path_sanitization.h"
#include "chrome/chrome_cleaner/os/file_removal_status_updater.h"
#include "chrome/chrome_cleaner/os/pre_fetched_paths.h"
#include "chrome/chrome_cleaner/os/registry_util.h"
#include "chrome/chrome_cleaner/os/scoped_disable_wow64_redirection.h"
#include "chrome/chrome_cleaner/os/whitelisted_directory.h"
namespace chrome_cleaner {
namespace {
bool RegisterFileForPostRebootRemoval(const base::FilePath path) {
// Don't allow directories to be deleted post-reboot. The directory will only
// be deleted if it is empty, and we can't ensure it will be so don't worry
// about it.
if (base::DirectoryExists(path))
return false;
// MoveFileEx with MOVEFILE_DELAY_UNTIL_REBOOT flag and null destination
// registers |file_path| to be deleted on the next system restarts.
constexpr DWORD flags = MOVEFILE_DELAY_UNTIL_REBOOT;
return ::MoveFileEx(path.value().c_str(), nullptr, flags) != 0;
}
void DeleteEmptyDirectories(base::FilePath directory) {
while (
base::DirectoryExists(directory) && base::IsDirectoryEmpty(directory) &&
!WhitelistedDirectory::GetInstance()->IsWhitelistedDirectory(directory)) {
// Empty directories deleted in this cleanup are not logged in the matched
// folders list for the corresponding UwS, because they are not necessarily
// matched by any rule by the scanner.
LOG(INFO) << "Deleting empty directory " << SanitizePath(directory);
if (!base::DeleteFile(directory, /*recursive=*/false))
break;
directory = directory.DirName();
}
}
// Sanity checks file names to ensure they don't contain ".." or specify a
// drive root.
bool IsSafeNameForDeletion(const base::FilePath& path) {
// Empty path isn't allowed
if (path.empty())
return false;
const base::string16& path_str = path.value();
// Disallow anything with "\..\".
if (path_str.find(L"\\..\\") != base::string16::npos)
return false;
// Ensure the path does not specify a drive root: require a character other
// than \/:. after the last :
size_t last_colon_pos = path_str.rfind(L':');
if (last_colon_pos == base::string16::npos)
return true;
for (size_t index = last_colon_pos + 1; index < path_str.size(); ++index) {
wchar_t character = path_str[index];
if (character != L'\\' && character != L'/' && character != L'.')
return true;
}
return false;
}
void OnArchiveDone(FileRemover::QuarantineResultCallback archival_done_callback,
mojom::ZipArchiverResultCode result_code) {
switch (result_code) {
// If the archive exists, |path| has already been quarantined.
case mojom::ZipArchiverResultCode::kSuccess:
case mojom::ZipArchiverResultCode::kZipFileExists:
std::move(archival_done_callback).Run(QUARANTINE_STATUS_QUARANTINED);
return;
case mojom::ZipArchiverResultCode::kIgnoredSourceFile:
std::move(archival_done_callback).Run(QUARANTINE_STATUS_SKIPPED);
return;
default:
LOG(ERROR) << "ZipArchiver returned an error code: " << result_code;
break;
}
std::move(archival_done_callback).Run(QUARANTINE_STATUS_ERROR);
}
} // namespace
FileRemover::FileRemover(scoped_refptr<DigestVerifier> digest_verifier,
std::unique_ptr<ZipArchiver> archiver,
const LayeredServiceProviderAPI& lsp,
base::RepeatingClosure reboot_needed_callback)
: digest_verifier_(digest_verifier),
archiver_(std::move(archiver)),
reboot_needed_callback_(reboot_needed_callback) {
LSPPathToGUIDs providers;
GetLayeredServiceProviders(lsp, &providers);
for (const auto& provider : providers)
deletion_forbidden_paths_.Insert(provider.first);
deletion_forbidden_paths_.Insert(
PreFetchedPaths::GetInstance()->GetExecutablePath());
}
FileRemover::~FileRemover() = default;
void FileRemover::RemoveNow(const base::FilePath& path,
DoneCallback callback) const {
FileRemovalStatusUpdater* removal_status_updater =
FileRemovalStatusUpdater::GetInstance();
switch (CanRemove(path)) {
case DeletionValidationStatus::FORBIDDEN:
removal_status_updater->UpdateRemovalStatus(
path, REMOVAL_STATUS_BLACKLISTED_FOR_REMOVAL);
std::move(callback).Run(false);
return;
case DeletionValidationStatus::ALLOWED:
// No-op. Proceed to removal.
break;
}
chrome_cleaner::ScopedDisableWow64Redirection disable_wow64_redirection;
if (!base::PathExists(path)) {
removal_status_updater->UpdateRemovalStatus(path, REMOVAL_STATUS_NOT_FOUND);
std::move(callback).Run(true);
return;
}
TryToQuarantine(
path, base::BindOnce(&FileRemover::RemoveFile, base::Unretained(this),
path, base::Passed(&callback)));
}
void FileRemover::RegisterPostRebootRemoval(const base::FilePath& file_path,
DoneCallback callback) const {
FileRemovalStatusUpdater* removal_status_updater =
FileRemovalStatusUpdater::GetInstance();
switch (CanRemove(file_path)) {
case DeletionValidationStatus::FORBIDDEN:
removal_status_updater->UpdateRemovalStatus(
file_path, REMOVAL_STATUS_BLACKLISTED_FOR_REMOVAL);
std::move(callback).Run(false);
return;
case DeletionValidationStatus::ALLOWED:
// No-op. Proceed to removal.
break;
}
chrome_cleaner::ScopedDisableWow64Redirection disable_wow64_redirection;
if (!base::PathExists(file_path)) {
removal_status_updater->UpdateRemovalStatus(file_path,
REMOVAL_STATUS_NOT_FOUND);
std::move(callback).Run(true);
return;
}
TryToQuarantine(file_path, base::BindOnce(&FileRemover::ScheduleRemoval,
base::Unretained(this), file_path,
base::Passed(&callback)));
}
FileRemoverAPI::DeletionValidationStatus FileRemover::CanRemove(
const base::FilePath& file) const {
// Allow removing of all files if |digest_verifier_| is unavailable.
// Otherwise, allow removing only files unknown to |digest_verifier_|.
if (digest_verifier_ && digest_verifier_->IsKnownFile(file)) {
LOG(ERROR) << "Cannot remove known file " << SanitizePath(file);
return DeletionValidationStatus::FORBIDDEN;
}
if (WhitelistedDirectory::GetInstance()->IsWhitelistedDirectory(file)) {
// We are logging the path in both sanitized and non-sanitized form since
// this should never happen unless we are breaking something, in which
// case we will need precise information.
LOG(ERROR) << "Cannot remove a CSIDL path: " << file.value()
<< "', sanitized: '" << SanitizePath(file);
return DeletionValidationStatus::FORBIDDEN;
}
if (!IsSafeNameForDeletion(file) || !file.IsAbsolute() ||
deletion_forbidden_paths_.Contains(file)) {
return DeletionValidationStatus::FORBIDDEN;
}
chrome_cleaner::ScopedDisableWow64Redirection disable_wow64_redirection;
if (base::DirectoryExists(file))
return DeletionValidationStatus::FORBIDDEN;
return DeletionValidationStatus::ALLOWED;
}
void FileRemover::TryToQuarantine(const base::FilePath& path,
QuarantineResultCallback callback) const {
// Archiver may not be provided in tests.
if (archiver_ == nullptr) {
std::move(callback).Run(QUARANTINE_STATUS_DISABLED);
return;
}
archiver_->Archive(path, base::BindOnce(&OnArchiveDone, std::move(callback)));
}
void FileRemover::RemoveFile(const base::FilePath& path,
DoneCallback removal_done_callback,
QuarantineStatus quarantine_status) const {
FileRemovalStatusUpdater* removal_status_updater =
FileRemovalStatusUpdater::GetInstance();
removal_status_updater->UpdateQuarantineStatus(path, quarantine_status);
if (quarantine_status == QUARANTINE_STATUS_ERROR) {
removal_status_updater->UpdateRemovalStatus(
path, REMOVAL_STATUS_ERROR_IN_ARCHIVER);
std::move(removal_done_callback).Run(false);
return;
}
if (!base::DeleteFile(path, /*recursive=*/false)) {
// If the attempt to delete the file fails, propagate the failure as
// normal so that the engine knows about it and can try a backup action,
// but also register the file for post-reboot removal in case the engine
// doesn't have any effective backup action.
//
// A potential downside to this implementation is that if the file is
// later successfully deleted, we might ask users to reboot when no
// reboot is needed. See b/66944160 for more details.
if (RegisterFileForPostRebootRemoval(path)) {
reboot_needed_callback_.Run();
removal_status_updater->UpdateRemovalStatus(
path, REMOVAL_STATUS_SCHEDULED_FOR_REMOVAL_FALLBACK);
} else {
removal_status_updater->UpdateRemovalStatus(
path, REMOVAL_STATUS_FAILED_TO_REMOVE);
}
std::move(removal_done_callback).Run(false);
return;
}
removal_status_updater->UpdateRemovalStatus(path, REMOVAL_STATUS_REMOVED);
DeleteEmptyDirectories(path.DirName());
std::move(removal_done_callback).Run(true);
}
void FileRemover::ScheduleRemoval(
const base::FilePath& file_path,
FileRemover::DoneCallback removal_done_callback,
QuarantineStatus quarantine_status) const {
FileRemovalStatusUpdater* removal_status_updater =
FileRemovalStatusUpdater::GetInstance();
removal_status_updater->UpdateQuarantineStatus(file_path, quarantine_status);
if (quarantine_status == QUARANTINE_STATUS_ERROR) {
removal_status_updater->UpdateRemovalStatus(
file_path, REMOVAL_STATUS_ERROR_IN_ARCHIVER);
std::move(removal_done_callback).Run(false);
return;
}
if (!RegisterFileForPostRebootRemoval(file_path)) {
PLOG(ERROR) << "Failed to schedule delete file: "
<< chrome_cleaner::SanitizePath(file_path);
removal_status_updater->UpdateRemovalStatus(
file_path, REMOVAL_STATUS_FAILED_TO_SCHEDULE_FOR_REMOVAL);
std::move(removal_done_callback).Run(false);
return;
}
reboot_needed_callback_.Run();
removal_status_updater->UpdateRemovalStatus(
file_path, REMOVAL_STATUS_SCHEDULED_FOR_REMOVAL);
std::move(removal_done_callback).Run(true);
}
} // namespace chrome_cleaner