blob: 28f8bbcdde58e44f173927a6196242f60ea8fa73 [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 "chrome/browser/downgrade/downgrade_manager.h"
#include <algorithm>
#include <iterator>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/enterprise_util.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/syslog_logging.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/version.h"
#include "build/build_config.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/downgrade/downgrade_utils.h"
#include "chrome/browser/downgrade/snapshot_manager.h"
#include "chrome/browser/downgrade/user_data_downgrade.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/enterprise/browser/controller/browser_dm_token_storage.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/version_info.h"
#include "components/version_info/version_info_values.h"
#include "content/public/browser/browser_thread.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#if defined(OS_WIN)
#include "chrome/installer/util/install_util.h"
#endif
namespace downgrade {
namespace {
bool g_snapshots_enabled_for_testing = false;
// Moves the contents of a User Data directory at |source| to |target|, with the
// exception of files/directories that should be left behind for a full data
// wipe. Returns no value if the target directory could not be created, or the
// number of items that could not be moved.
absl::optional<int> MoveUserData(const base::FilePath& source,
const base::FilePath& target) {
// Returns true to exclude a file.
auto exclusion_predicate =
base::BindRepeating([](const base::FilePath& name) -> bool {
// TODO(ydago): Share constants instead of hardcoding values here.
static constexpr base::FilePath::StringPieceType kFilesToKeep[] = {
FILE_PATH_LITERAL("browsermetrics"),
FILE_PATH_LITERAL("crashpad"),
FILE_PATH_LITERAL("first run"),
FILE_PATH_LITERAL("last version"),
FILE_PATH_LITERAL("lockfile"),
FILE_PATH_LITERAL("snapshots"),
FILE_PATH_LITERAL("stability"),
};
// Don't try to move the dir into which everything is being moved.
if (name.FinalExtension() == kDowngradeDeleteSuffix)
return true;
return std::find_if(std::begin(kFilesToKeep), std::end(kFilesToKeep),
[&name](const auto& keep) {
return base::EqualsCaseInsensitiveASCII(
name.value(), keep);
}) != std::end(kFilesToKeep);
});
auto result = MoveContents(source, target, std::move(exclusion_predicate));
// Move the Last Version file last so that any crash before this point results
// in a retry on the next launch.
if (!result ||
!MoveWithoutFallback(source.Append(kDowngradeLastVersionFile),
target.Append(kDowngradeLastVersionFile))) {
if (result)
*result += 1;
// Attempt to delete Last Version if all else failed so that Chrome does not
// continually attempt to perform a migration.
base::DeleteFile(source.Append(kDowngradeLastVersionFile));
// Inform system administrators that things have gone awry.
SYSLOG(ERROR) << "Failed to perform User Data migration following a Chrome "
"version downgrade. Chrome will run with User Data from a "
"higher version and may behave unpredictably.";
// At this point, Chrome will relaunch with --user-data-migrated. This
// switch suppresses downgrade processing, so that launch will go through
// normal startup.
}
return result;
}
// Renames |disk_cache_dir| in its containing folder. If that fails, an attempt
// is made to move its contents.
void MoveCache(const base::FilePath& disk_cache_dir) {
// A cache dir at the root of a volume is not supported.
const base::FilePath parent = disk_cache_dir.DirName();
if (parent == disk_cache_dir)
return;
// Move the cache within its parent directory from, for example, CacheDir
// to CacheDir.CHROME_DELETE.
const base::FilePath target =
GetTempDirNameForDelete(parent, disk_cache_dir.BaseName());
// A simple move succeeds in approx 2/3 of attempts.
if (MoveWithoutFallback(disk_cache_dir, target))
return;
// The directory couldn't be moved whole-hog. Attempt a recursive move of its
// contents. This succeeds in nearly all cases.
MoveContents(disk_cache_dir, target, ExclusionPredicate());
}
// Deletes all subdirectories in |dir| named |name|*.CHROME_DELETE.
void DeleteAllRenamedUserDirectories(const base::FilePath& dir,
const base::FilePath& name) {
base::FilePath::StringType pattern = base::StrCat(
{name.value(), FILE_PATH_LITERAL("*"), kDowngradeDeleteSuffix});
base::FileEnumerator enumerator(dir, false, base::FileEnumerator::DIRECTORIES,
pattern);
for (base::FilePath to_delete = enumerator.Next(); !to_delete.empty();
to_delete = enumerator.Next()) {
base::DeletePathRecursively(to_delete);
}
}
// Deletes all moved User Data, Snapshots and Cache directories for the given
// dirs.
void DeleteMovedUserData(const base::FilePath& user_data_dir,
const base::FilePath& disk_cache_dir) {
DeleteAllRenamedUserDirectories(user_data_dir, user_data_dir.BaseName());
DeleteAllRenamedUserDirectories(user_data_dir, base::FilePath(kSnapshotsDir));
// Prior to Chrome M78, User Data was moved to a new name under its parent. In
// that case, User Data at a volume's root was unsupported.
base::FilePath parent = user_data_dir.DirName();
if (parent != user_data_dir)
DeleteAllRenamedUserDirectories(parent, user_data_dir.BaseName());
if (!disk_cache_dir.empty()) {
// Cache dir at a volume's root is unsupported.
parent = disk_cache_dir.DirName();
if (parent != disk_cache_dir)
DeleteAllRenamedUserDirectories(parent, disk_cache_dir.BaseName());
}
}
bool UserDataSnapshotEnabled() {
if (g_snapshots_enabled_for_testing)
return true;
bool is_enterprise_managed =
#if defined(OS_WIN) || defined(OS_MAC)
base::IsMachineExternallyManaged() ||
#endif
policy::BrowserDMTokenStorage::Get()->RetrieveDMToken().is_valid();
return is_enterprise_managed &&
base::FeatureList::IsEnabled(features::kUserDataSnapshot);
}
#if defined(OS_WIN)
bool IsAdministratorDrivenDowngrade(uint16_t current_milestone) {
const auto downgrade_version = InstallUtil::GetDowngradeVersion();
return downgrade_version &&
downgrade_version->components()[0] > current_milestone;
}
#endif
} // namespace
bool DowngradeManager::PrepareUserDataDirectoryForCurrentVersion(
const base::FilePath& user_data_dir) {
DCHECK_EQ(type_, Type::kNone);
DCHECK(!user_data_dir.empty());
// Do not attempt migration if this process is the product of a relaunch from
// a previous in which migration was attempted/performed.
auto& command_line = *base::CommandLine::ForCurrentProcess();
if (command_line.HasSwitch(switches::kUserDataMigrated)) {
// Strip the switch from the command line so that it does not propagate to
// any subsequent relaunches.
command_line.RemoveSwitch(switches::kUserDataMigrated);
return false;
}
absl::optional<base::Version> last_version = GetLastVersion(user_data_dir);
if (!last_version)
return false;
const base::Version& current_version = version_info::GetVersion();
const bool user_data_snapshot_enabled = UserDataSnapshotEnabled();
if (!user_data_snapshot_enabled) {
if (current_version >= *last_version)
return false; // Same version or upgrade.
type_ = GetDowngradeType(user_data_dir, current_version, *last_version);
DCHECK(type_ == Type::kAdministrativeWipe || type_ == Type::kUnsupported);
base::UmaHistogramEnumeration("Downgrade.Type", type_);
return type_ == Type::kAdministrativeWipe;
}
if (current_version == *last_version)
return false; // Nothing to do if the version has not changed.
if (current_version < *last_version) {
type_ = GetDowngradeTypeWithSnapshot(user_data_dir, current_version,
*last_version);
if (type_ != Type::kNone)
base::UmaHistogramEnumeration("Downgrade.Type", type_);
return type_ == Type::kAdministrativeWipe ||
type_ == Type::kSnapshotRestore;
}
auto current_milestone = current_version.components()[0];
int max_number_of_snapshots = g_browser_process->local_state()->GetInteger(
prefs::kUserDataSnapshotRetentionLimit);
absl::optional<uint32_t> purge_milestone;
if (current_milestone == last_version->components()[0]) {
// Mid-milestone snapshots are only taken on canary installs.
if (chrome::GetChannel() != version_info::Channel::CANARY)
return false;
// Keep one snapshot in this milestone unless snapshots are disabled.
max_number_of_snapshots = std::min(max_number_of_snapshots, 1);
purge_milestone = current_milestone;
}
SnapshotManager snapshot_manager(user_data_dir);
snapshot_manager.TakeSnapshot(*last_version);
snapshot_manager.PurgeInvalidAndOldSnapshots(max_number_of_snapshots,
purge_milestone);
return false;
}
void DowngradeManager::UpdateLastVersion(const base::FilePath& user_data_dir) {
DCHECK(!user_data_dir.empty());
DCHECK_NE(type_, Type::kAdministrativeWipe);
const base::StringPiece version(PRODUCT_VERSION);
base::WriteFile(GetLastVersionFile(user_data_dir), version.data(),
version.size());
}
void DowngradeManager::DeleteMovedUserDataSoon(
const base::FilePath& user_data_dir) {
DCHECK(!user_data_dir.empty());
// IWYU note: base/location.h and base/task/task_traits.h are guaranteed to be
// available via base/task/post_task.h.
content::BrowserThread::PostBestEffortTask(
FROM_HERE,
base::ThreadPool::CreateTaskRunner(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
base::BindOnce(&DeleteMovedUserData, user_data_dir, GetDiskCacheDir()));
}
void DowngradeManager::ProcessDowngrade(const base::FilePath& user_data_dir) {
DCHECK(type_ == Type::kAdministrativeWipe || type_ == Type::kSnapshotRestore);
DCHECK(!user_data_dir.empty());
const base::FilePath disk_cache_dir(GetDiskCacheDir());
if (!disk_cache_dir.empty())
MoveCache(disk_cache_dir);
// User Data requires special treatment, as certain files/directories should
// be left behind. Furthermore, User Data is moved to a new directory within
// itself (for example, to User Data/User Data.CHROME_DELETE) to guarantee
// that the movement isn't across volumes.
const auto failure_count = MoveUserData(
user_data_dir,
GetTempDirNameForDelete(user_data_dir, user_data_dir.BaseName()));
enum class UserDataMoveResult {
kCreateTargetFailure = 0,
kSuccess = 1,
kPartialSuccess = 2,
kMaxValue = kPartialSuccess
};
UserDataMoveResult move_result =
!failure_count ? UserDataMoveResult::kCreateTargetFailure
: (*failure_count ? UserDataMoveResult::kPartialSuccess
: UserDataMoveResult::kSuccess);
base::UmaHistogramEnumeration("Downgrade.UserDataDirMove.Result",
move_result);
if (failure_count && *failure_count) {
// Report precise values rather than an exponentially bucketed histogram.
base::UmaHistogramExactLinear("Downgrade.UserDataDirMove.FailureCount",
*failure_count, 50);
}
if (type_ == Type::kSnapshotRestore) {
SnapshotManager snapshot_manager(user_data_dir);
snapshot_manager.RestoreSnapshot(version_info::GetVersion());
}
// Add the migration switch to the command line so that it is propagated to
// the relaunched process. This is used to prevent a relaunch bomb in case of
// pathological failure.
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kUserDataMigrated);
}
// static
void DowngradeManager::EnableSnapshotsForTesting(bool enable) {
g_snapshots_enabled_for_testing = enable;
}
// static
DowngradeManager::Type DowngradeManager::GetDowngradeType(
const base::FilePath& user_data_dir,
const base::Version& current_version,
const base::Version& last_version) {
DCHECK(!user_data_dir.empty());
DCHECK_LT(current_version, last_version);
#if defined(OS_WIN)
// Move User Data aside for a clean launch if it follows an
// administrator-driven downgrade.
if (IsAdministratorDrivenDowngrade(current_version.components()[0]))
return Type::kAdministrativeWipe;
#endif
return Type::kUnsupported;
}
// static
DowngradeManager::Type DowngradeManager::GetDowngradeTypeWithSnapshot(
const base::FilePath& user_data_dir,
const base::Version& current_version,
const base::Version& last_version) {
DCHECK(!user_data_dir.empty());
DCHECK_LT(current_version, last_version);
const uint16_t milestone = current_version.components()[0];
// Move User Data and restore from a snapshot if there is a candidate
// snapshot to restore.
const auto snapshot_to_restore =
GetSnapshotToRestore(current_version, user_data_dir);
#if defined(OS_WIN)
// Move User Data aside for a clean launch if it follows an
// administrator-driven downgrade when no snapshot is found.
if (!snapshot_to_restore && IsAdministratorDrivenDowngrade(milestone))
return Type::kAdministrativeWipe;
#endif
const uint16_t last_milestone = last_version.components()[0];
if (last_milestone > milestone)
return snapshot_to_restore ? Type::kSnapshotRestore : Type::kUnsupported;
return Type::kMinorDowngrade;
}
} // namespace downgrade