blob: 69469a45be157c317ccb72c4ebf003922e6c9ef6 [file] [log] [blame]
// Copyright 2017 The Chromium OS 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 "cryptohome/dircrypto_data_migrator/migration_helper.h"
#include <algorithm>
#include <deque>
#include <memory>
#include <string>
#include <vector>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/capability.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <base/bind.h>
#include <base/files/file.h>
#include <base/files/file_path.h>
#include <base/message_loop/message_pump_type.h>
#include <base/timer/elapsed_timer.h>
#include <base/strings/string_number_conversions.h>
#include <base/system/sys_info.h>
#include <base/threading/thread.h>
#include <chromeos/dbus/service_constants.h>
#include "cryptohome/cryptohome_metrics.h"
#include "cryptohome/storage/mount.h"
extern "C" {
#include <linux/fs.h>
}
namespace cryptohome {
namespace dircrypto_data_migrator {
namespace {
constexpr char kMtimeXattrName[] = "trusted.CrosDirCryptoMigrationMtime";
constexpr char kAtimeXattrName[] = "trusted.CrosDirCryptoMigrationAtime";
// Expected maximum erasure block size on devices (4MB).
constexpr uint64_t kErasureBlockSize = 4 << 20;
// Free space required for migration overhead (FS metadata, duplicated
// in-progress directories, etc). Must be smaller than kMinFreeSpace.
constexpr uint64_t kFreeSpaceBuffer = kErasureBlockSize;
// The maximum size of job list.
constexpr size_t kDefaultMaxJobListSize = 100000;
// List of paths in the root part of the user home to be migrated when minimal
// migration is performed. If the last component of a path is *, it means that
// all children should be migrated too.
const char* const kMinimalMigrationRootPathsAllowlist[] = {
// Keep the user policy - network/proxy settings could be stored here and
// chrome will need network access to re-setup the wiped profile. Also, we
// want to make absolutely sure that the user session does not end up in an
// unmanaged state (without policy).
"session_manager/policy",
};
// List of paths in the user part of the user home to be migrated when minimal
// migration is performed. If the path refers to a directory, all children will
// be migrated too.
const char* const kMinimalMigrationUserPathsAllowlist[] = {
// Migrate the log directory, because it only gets created on fresh user
// home creation by copying the skeleton structure. If it's missing, chrome
// user sessoin won't log.
"log",
// Migrate the user's certificate database, in case the user has client
// certificates necesary to access networks.
".pki",
// Migrate Cookies, as authentiation tokens might be stored in cookies.
"Cookies",
"Cookies-journal",
// Migrate state realted to HTTPS, especially channel binding state (Origin
// Bound Certs), and transport security (HSTS).
"Origin Bound Certs",
"Origin Bound Certs-journal",
"TransportSecurity",
// Web Data contains the Token Service Table which authentication tokens for
// chrome services (sign-in OAuth2 token).
"Web Data",
"Web Data-journal",
};
// Sends the UMA stat for the start/end status of migration respectively in the
// constructor/destructor. By default the "generic error" end status is set, so
// to report other status, call an appropriate method to overwrite it.
class MigrationStartAndEndStatusReporter {
public:
MigrationStartAndEndStatusReporter(MigrationType migration_type,
bool resumed,
const AtomicFlag& is_cancelled)
: migration_type_(migration_type),
resumed_(resumed),
is_cancelled_(is_cancelled),
end_status_(resumed ? kResumedMigrationFailedGeneric
: kNewMigrationFailedGeneric) {
ReportDircryptoMigrationStartStatus(
migration_type, resumed_ ? kMigrationResumed : kMigrationStarted);
}
MigrationStartAndEndStatusReporter(
const MigrationStartAndEndStatusReporter&) = delete;
MigrationStartAndEndStatusReporter& operator=(
const MigrationStartAndEndStatusReporter&) = delete;
~MigrationStartAndEndStatusReporter() {
if (is_cancelled_.IsSet()) {
end_status_ =
resumed_ ? kResumedMigrationCancelled : kNewMigrationCancelled;
}
ReportDircryptoMigrationEndStatus(migration_type_, end_status_);
}
void SetSuccess() {
end_status_ = resumed_ ? kResumedMigrationFinished : kNewMigrationFinished;
}
void SetLowDiskSpaceFailure() {
end_status_ = resumed_ ? kResumedMigrationFailedLowDiskSpace
: kNewMigrationFailedLowDiskSpace;
}
void SetFileErrorFailure(DircryptoMigrationFailedOperationType operation,
base::File::Error error) {
// Some notable special cases are given distinct enum values.
if (operation == kMigrationFailedAtOpenSourceFile &&
error == base::File::FILE_ERROR_IO) {
end_status_ = resumed_ ? kResumedMigrationFailedFileErrorOpenEIO
: kNewMigrationFailedFileErrorOpenEIO;
} else {
end_status_ = resumed_ ? kResumedMigrationFailedFileError
: kNewMigrationFailedFileError;
}
}
private:
const MigrationType migration_type_;
const bool resumed_;
const AtomicFlag& is_cancelled_;
DircryptoMigrationEndStatus end_status_;
};
struct PathTypeMapping {
const char* path;
DircryptoMigrationFailedPathType type;
};
const PathTypeMapping kPathTypeMappings[] = {
{"root/android-data", kMigrationFailedUnderAndroidOther},
{"user/Downloads", kMigrationFailedUnderDownloads},
{"user/Cache", kMigrationFailedUnderCache},
{"user/GCache", kMigrationFailedUnderGcache},
};
} // namespace
constexpr char kMigrationStartedFileName[] = "crypto-migration.started";
// A file to store a list of files skipped during migration. This lives in
// root/ of the destination directory so that it is encrypted.
constexpr char kSkippedFileListFileName[] =
"root/crypto-migration.files-skipped";
// TODO(dspaid): Determine performance impact so we can potentially increase
// frequency.
constexpr base::TimeDelta kStatusSignalInterval =
base::TimeDelta::FromSeconds(1);
// {Source,Referrer}URL xattrs are from chrome downloads and are not used on
// ChromeOS. They may be very large though, potentially preventing the
// migration of other attributes.
constexpr char kSourceURLXattrName[] = "user.xdg.origin.url";
constexpr char kReferrerURLXattrName[] = "user.xdg.referrer.url";
// Job represents a job to migrate a file or a symlink.
struct MigrationHelper::Job {
Job() = default;
~Job() = default;
base::FilePath child;
FileEnumerator::FileInfo info;
};
// WorkerPool manages jobs and job threads.
// All public methods must be called on the main thread unless otherwise
// specified.
class MigrationHelper::WorkerPool {
public:
explicit WorkerPool(MigrationHelper* migration_helper)
: migration_helper_(migration_helper),
job_thread_wakeup_condition_(&jobs_lock_),
main_thread_wakeup_condition_(&jobs_lock_) {}
WorkerPool(const WorkerPool&) = delete;
WorkerPool& operator=(const WorkerPool&) = delete;
~WorkerPool() { Join(); }
// Starts job threads.
bool Start(size_t num_job_threads, size_t max_job_list_size) {
job_threads_.resize(num_job_threads);
job_thread_results_.resize(num_job_threads, false);
max_job_list_size_ = max_job_list_size;
for (size_t i = 0; i < job_threads_.size(); ++i) {
job_threads_[i] = std::make_unique<base::Thread>(
"MigrationHelper worker #" + base::NumberToString(i));
base::Thread::Options options;
options.message_pump_type = base::MessagePumpType::IO;
if (!job_threads_[i]->StartWithOptions(options)) {
LOG(ERROR) << "Failed to start a job thread.";
return false;
}
job_threads_[i]->task_runner()->PostTask(
FROM_HERE,
base::Bind(&WorkerPool::ProcessJobs, base::Unretained(this),
&job_thread_results_[i]));
}
return true;
}
// Adds a job to the job list.
bool PushJob(const Job& job) {
base::AutoLock lock(jobs_lock_);
while (jobs_.size() >= max_job_list_size_ && !should_abort_) {
main_thread_wakeup_condition_.Wait();
}
if (should_abort_) {
return false;
}
jobs_.push_back(job);
// Let a job thread process the new job.
job_thread_wakeup_condition_.Signal();
return true;
}
// Waits for job threads to process all pushed jobs and returns true if there
// was no error.
bool Join() {
{
// Wake up all waiting job threads.
base::AutoLock lock(jobs_lock_);
no_more_new_jobs_ = true;
job_thread_wakeup_condition_.Broadcast();
}
job_threads_.clear(); // Join threads.
base::AutoLock lock(jobs_lock_); // For should_abort_.
return std::count(job_thread_results_.begin(), job_thread_results_.end(),
false) == 0 &&
!should_abort_;
}
// Aborts job processing.
// Can be called on any thread.
void Abort() {
base::AutoLock lock(jobs_lock_);
no_more_new_jobs_ = true;
should_abort_ = true;
main_thread_wakeup_condition_.Signal();
job_thread_wakeup_condition_.Broadcast();
}
private:
// Processes jobs fed by the main thread.
// Must be called on a job thread.
void ProcessJobs(bool* result) {
// Continue running on a job thread while the main thread feeds jobs.
while (true) {
Job job;
if (!PopJob(&job)) { // No more new jobs.
*result = true;
return;
}
if (!migration_helper_->ProcessJob(job)) {
LOG(ERROR) << "Failed to migrate \"" << job.child.value() << "\"";
Abort();
*result = false;
return;
}
}
}
// Pops a job from the job list. Returns false when the thread should stop.
// Must be called on a job thread.
bool PopJob(Job* job) {
base::AutoLock lock(jobs_lock_);
while (jobs_.empty()) {
if (no_more_new_jobs_)
return false;
job_thread_wakeup_condition_.Wait();
}
if (should_abort_) {
return false;
}
*job = jobs_.front();
jobs_.pop_front();
// Let the main thread feed new jobs.
main_thread_wakeup_condition_.Signal();
return true;
}
MigrationHelper* migration_helper_;
std::vector<std::unique_ptr<base::Thread>> job_threads_; // The job threads.
// deque instead of vector to avoid vector<bool> specialization.
std::deque<bool> job_thread_results_;
size_t max_job_list_size_ = 0;
std::deque<Job> jobs_; // The FIFO job list.
bool no_more_new_jobs_ = false;
bool should_abort_ = false;
// Lock for jobs_, no_more_new_jobs_, and should_abort_.
base::Lock jobs_lock_;
// Condition variables associated with jobs_lock_.
base::ConditionVariable job_thread_wakeup_condition_;
base::ConditionVariable main_thread_wakeup_condition_;
};
MigrationHelper::MigrationHelper(Platform* platform,
const base::FilePath& from,
const base::FilePath& to,
const base::FilePath& status_files_dir,
uint64_t max_chunk_size,
MigrationType migration_type)
: platform_(platform),
from_base_path_(from),
to_base_path_(to),
status_files_dir_(status_files_dir),
max_chunk_size_(max_chunk_size),
migration_type_(migration_type),
effective_chunk_size_(0),
total_byte_count_(0),
total_directory_byte_count_(0),
n_files_(0),
n_dirs_(0),
n_symlinks_(0),
migrated_byte_count_(0),
namespaced_mtime_xattr_name_(kMtimeXattrName),
namespaced_atime_xattr_name_(kAtimeXattrName),
failed_operation_type_(kMigrationFailedAtOtherOperation),
failed_path_type_(kMigrationFailedUnderOther),
failed_error_type_(base::File::FILE_OK),
num_job_threads_(0),
max_job_list_size_(kDefaultMaxJobListSize),
worker_pool_(new WorkerPool(this)) {
if (migration_type_ == MigrationType::MINIMAL) {
for (const char* path : kMinimalMigrationRootPathsAllowlist) {
minimal_migration_paths_.emplace_back(
base::FilePath(kRootHomeSuffix).Append(path));
}
for (const char* path : kMinimalMigrationUserPathsAllowlist) {
minimal_migration_paths_.emplace_back(
base::FilePath(kUserHomeSuffix).Append(path));
}
}
}
MigrationHelper::~MigrationHelper() {}
bool MigrationHelper::Migrate(const ProgressCallback& progress_callback) {
base::ElapsedTimer timer;
skipped_file_list_path_ = to_base_path_.Append(kSkippedFileListFileName);
const bool resumed = IsMigrationStarted();
MigrationStartAndEndStatusReporter status_reporter(migration_type_, resumed,
is_cancelled_);
if (progress_callback.is_null()) {
LOG(ERROR) << "Invalid progress callback";
return false;
}
progress_callback_ = progress_callback;
ReportStatus(user_data_auth::DIRCRYPTO_MIGRATION_INITIALIZING);
if (!from_base_path_.IsAbsolute() || !to_base_path_.IsAbsolute()) {
LOG(ERROR) << "Migrate must be given absolute paths";
return false;
}
if (!platform_->DirectoryExists(from_base_path_)) {
LOG(ERROR) << "Directory does not exist: " << from_base_path_.value();
return false;
}
if (!platform_->TouchFileDurable(
status_files_dir_.Append(kMigrationStartedFileName))) {
LOG(ERROR) << "Failed to create migration-started file";
return false;
}
initial_free_space_bytes_ = platform_->AmountOfFreeDiskSpace(to_base_path_);
if (initial_free_space_bytes_ < 0) {
LOG(ERROR) << "Failed to determine free disk space";
return false;
}
const uint64_t kRequiredFreeSpaceForMainThread =
kFreeSpaceBuffer + total_directory_byte_count_;
// Calculate required space used by the number of job threads (or a minimum of
// 1 thread of the number is dynamic)
const uint64_t kRequiredFreeSpace =
kRequiredFreeSpaceForMainThread +
(num_job_threads_ == 0 ? 1 : num_job_threads_) * kErasureBlockSize;
if (static_cast<uint64_t>(initial_free_space_bytes_) < kRequiredFreeSpace) {
LOG(ERROR) << "Not enough space to begin the migration";
status_reporter.SetLowDiskSpaceFailure();
return false;
}
const uint64_t kFreeSpaceForJobThreads =
initial_free_space_bytes_ - kRequiredFreeSpaceForMainThread;
if (num_job_threads_ == 0) {
// Limit the number of job threads based on the available free space.
num_job_threads_ =
std::min(static_cast<uint64_t>(base::SysInfo::NumberOfProcessors() * 2),
kFreeSpaceForJobThreads / kErasureBlockSize);
}
effective_chunk_size_ =
std::min(max_chunk_size_, kFreeSpaceForJobThreads / num_job_threads_);
if (effective_chunk_size_ > kErasureBlockSize)
effective_chunk_size_ =
effective_chunk_size_ - (effective_chunk_size_ % kErasureBlockSize);
if (migration_type_ == MigrationType::FULL) {
// Only calculate data size if not doing a minimal migration, as we're
// skipping most data in minimal migration.
if (!CalculateDataToMigrate(from_base_path_)) {
LOG(ERROR) << "Failed to calculate number of bytes to migrate";
return false;
}
if (!resumed) {
ReportDircryptoMigrationTotalByteCountInMb(total_byte_count_ / 1024 /
1024);
ReportDircryptoMigrationTotalFileCount(n_files_ + n_dirs_ + n_symlinks_);
}
}
ReportStatus(user_data_auth::DIRCRYPTO_MIGRATION_IN_PROGRESS);
base::stat_wrapper_t from_stat;
if (!platform_->Stat(from_base_path_, &from_stat)) {
PLOG(ERROR) << "Failed to stat from directory";
RecordFileErrorWithCurrentErrno(kMigrationFailedAtStat, base::FilePath());
status_reporter.SetFileErrorFailure(failed_operation_type_,
failed_error_type_);
return false;
}
const auto migration_timer_id = migration_type_ == MigrationType::MINIMAL
? kDircryptoMinimalMigrationTimer
: kDircryptoMigrationTimer;
ReportTimerStart(migration_timer_id);
LOG(INFO) << "Preparation took " << timer.Elapsed().InMilliseconds()
<< " ms.";
// MigrateDir() recursively traverses the directory tree on the main thread,
// while the job threads migrate files and symlinks.
bool success =
worker_pool_->Start(num_job_threads_, max_job_list_size_) &&
MigrateDir(base::FilePath(base::FilePath::kCurrentDirectory),
FileEnumerator::FileInfo(from_base_path_, from_stat));
// No matter if successful or not, always join the job threads.
if (!worker_pool_->Join())
success = false;
if (!success) {
LOG(ERROR) << "Migration Failed, aborting.";
status_reporter.SetFileErrorFailure(failed_operation_type_,
failed_error_type_);
return false;
}
if (!resumed)
ReportTimerStop(migration_timer_id);
// One more progress update to say that we've hit 100%
ReportStatus(user_data_auth::DIRCRYPTO_MIGRATION_IN_PROGRESS);
status_reporter.SetSuccess();
const int elapsed_ms = timer.Elapsed().InMilliseconds();
const int speed_kb_per_s = elapsed_ms ? (total_byte_count_ / elapsed_ms) : 0;
if (migration_type_ == MigrationType::MINIMAL) {
LOG(INFO) << "Minimal migration took " << elapsed_ms << " ms.";
} else {
LOG(INFO) << "Migrated " << total_byte_count_ << " bytes in " << elapsed_ms
<< " ms at " << speed_kb_per_s << " KB/s.";
}
return true;
}
bool MigrationHelper::IsMigrationStarted() const {
return platform_->FileExists(
status_files_dir_.Append(kMigrationStartedFileName));
}
void MigrationHelper::Cancel() {
worker_pool_->Abort();
is_cancelled_.Set();
}
bool MigrationHelper::CalculateDataToMigrate(const base::FilePath& from) {
total_byte_count_ = 0;
total_directory_byte_count_ = 0;
migrated_byte_count_ = 0;
std::unique_ptr<FileEnumerator> enumerator(platform_->GetFileEnumerator(
from, true /* recursive */,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS));
for (base::FilePath entry = enumerator->Next(); !entry.empty();
entry = enumerator->Next()) {
if (is_cancelled_.IsSet()) {
return false;
}
const FileEnumerator::FileInfo& info = enumerator->GetInfo();
total_byte_count_ += info.GetSize();
if (S_ISREG(info.stat().st_mode))
++n_files_;
if (S_ISDIR(info.stat().st_mode)) {
total_directory_byte_count_ += info.GetSize();
++n_dirs_;
}
if (S_ISLNK(info.stat().st_mode))
++n_symlinks_;
}
LOG(INFO) << "Number of files: " << n_files_;
LOG(INFO) << "Number of directories: " << n_dirs_;
LOG(INFO) << "Number of symlinks: " << n_symlinks_;
return true;
}
void MigrationHelper::IncrementMigratedBytes(uint64_t bytes) {
base::AutoLock lock(migrated_byte_count_lock_);
migrated_byte_count_ += bytes;
if (next_report_ < base::TimeTicks::Now())
ReportStatus(user_data_auth::DIRCRYPTO_MIGRATION_IN_PROGRESS);
}
void MigrationHelper::ReportStatus(
user_data_auth::DircryptoMigrationStatus status) {
// Don't report for minimal migration, because we haven't calculated totals.
if (migration_type_ == MigrationType::MINIMAL) {
return;
}
user_data_auth::DircryptoMigrationProgress progress;
progress.set_status(status);
progress.set_current_bytes(migrated_byte_count_);
progress.set_total_bytes(total_byte_count_);
progress_callback_.Run(progress);
next_report_ = base::TimeTicks::Now() + kStatusSignalInterval;
}
bool MigrationHelper::ShouldMigrateFile(const base::FilePath& child) {
if (migration_type_ == MigrationType::FULL) {
// crbug.com/728892: This directory can be falling into a weird state that
// confuses the migrator. Never try migration. Just delete it. This is fine
// because Cryptohomed anyway creates a pass-through directory at this path
// and Chrome never uses contents of the directory left by old sessions.
if (child == base::FilePath(kUserHomeSuffix)
.Append(kGCacheDir)
.Append(kGCacheVersion1Dir)
.Append(kGCacheTmpDir)) {
return false;
}
return true;
} else {
// Minimal migration - process the allowlist. Because the allowlist is
// supposed to be small, we won't recurse into many subdirectories, so we
// assume that iterating all allowlist elements for each file is fine.
for (const auto& migration_path : minimal_migration_paths_) {
// If the current path is one of the allowlisted paths, or its
// parent, migrate it.
if (child == migration_path || child.IsParent(migration_path))
return true;
// Recursively migrate contents of directories specified for migration.
if (migration_path.IsParent(child))
return true;
}
return false;
}
}
bool MigrationHelper::MigrateDir(const base::FilePath& child,
const FileEnumerator::FileInfo& info) {
if (is_cancelled_.IsSet()) {
return false;
}
const base::FilePath from_dir = from_base_path_.Append(child);
const base::FilePath to_dir = to_base_path_.Append(child);
if (!platform_->CreateDirectory(to_dir)) {
LOG(ERROR) << "Failed to create directory " << to_dir.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtMkdir, child);
return false;
}
if (!platform_->SyncDirectory(to_dir.DirName())) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSync, child);
return false;
}
if (!CopyAttributes(child, info))
return false;
// Dummy child count increment to protect this directory while reading.
IncrementChildCount(child);
std::unique_ptr<FileEnumerator> enumerator(platform_->GetFileEnumerator(
from_dir, false /* is_recursive */,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS));
for (base::FilePath entry = enumerator->Next(); !entry.empty();
entry = enumerator->Next()) {
const FileEnumerator::FileInfo& entry_info = enumerator->GetInfo();
const base::FilePath& new_child = child.Append(entry.BaseName());
mode_t mode = entry_info.stat().st_mode;
if (!ShouldMigrateFile(new_child)) {
// Delete paths which should be skipped
if (!platform_->DeletePathRecursively(entry)) {
PLOG(ERROR) << "Failed to delete " << entry.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtDelete, entry);
return false;
}
continue;
}
IncrementChildCount(child);
if (S_ISDIR(mode)) {
// Directory.
if (!MigrateDir(new_child, entry_info))
return false;
IncrementMigratedBytes(entry_info.GetSize());
} else {
Job job;
job.child = new_child;
job.info = entry_info;
if (!worker_pool_->PushJob(job))
return false;
}
}
enumerator.reset();
// Decrement the placeholder child count.
return DecrementChildCountAndDeleteIfNecessary(child);
}
bool MigrationHelper::MigrateLink(const base::FilePath& child,
const FileEnumerator::FileInfo& info) {
const base::FilePath source = from_base_path_.Append(child);
const base::FilePath new_path = to_base_path_.Append(child);
base::FilePath target;
if (!platform_->ReadLink(source, &target)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtReadLink, child);
return false;
}
if (from_base_path_.IsParent(target)) {
base::FilePath new_target = to_base_path_;
from_base_path_.AppendRelativePath(target, &new_target);
target = new_target;
}
// In the case that the link was already created by a previous migration
// it should be removed to prevent errors recreating it below.
if (!platform_->DeleteFile(new_path)) {
PLOG(ERROR) << "Failed to delete existing symlink " << new_path.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtDelete, child);
return false;
}
if (!platform_->CreateSymbolicLink(new_path, target)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtCreateLink, child);
return false;
}
if (!CopyAttributes(child, info))
return false;
// We don't need to modify the source file, so we can safely set times here
// directly instead of storing them in xattrs first.
if (!platform_->SetFileTimes(new_path, info.stat().st_atim,
info.stat().st_mtim, false /* follow_links */)) {
PLOG(ERROR) << "Failed to set mtime for " << new_path.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSetAttribute, child);
return false;
}
// We can't explicitly f(data)sync symlinks, so we have to do a full FS sync.
platform_->Sync();
return true;
}
bool MigrationHelper::MigrateFile(const base::FilePath& child,
const FileEnumerator::FileInfo& info) {
const base::FilePath& from_child = from_base_path_.Append(child);
const base::FilePath& to_child = to_base_path_.Append(child);
base::File from_file;
platform_->InitializeFile(
&from_file, from_child,
base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE);
if (!from_file.IsValid()) {
if (from_file.error_details() == base::File::FILE_ERROR_IO) {
// b/37444422 causes IO errors when opening this file in some cases. User
// had a unreadable file, skipping this file means user will no longer
// have a file but not worse off.
LOG(WARNING) << "Found file that cannot be opened with EIO, skipping "
<< from_child.value();
RecordFileError(kMigrationFailedAtOpenSourceFileNonFatal, child,
from_file.error_details());
RecordSkippedFile(child);
return true;
}
PLOG(ERROR) << "Failed to open file " << from_child.value();
RecordFileError(kMigrationFailedAtOpenSourceFile, child,
from_file.error_details());
return false;
}
base::File to_file;
platform_->InitializeFile(
&to_file, to_child,
base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_WRITE);
if (!to_file.IsValid()) {
PLOG(ERROR) << "Failed to open file " << to_child.value();
RecordFileError(kMigrationFailedAtOpenDestinationFile, child,
to_file.error_details());
return false;
}
if (!platform_->SyncDirectory(to_child.DirName())) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSync, child);
return false;
}
int64_t from_length = from_file.GetLength();
int64_t to_length = to_file.GetLength();
if (from_length < 0) {
LOG(ERROR) << "Failed to get length of " << from_child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtStat, child);
return false;
}
if (to_length < 0) {
LOG(ERROR) << "Failed to get length of " << to_child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtStat, child);
return false;
}
if (to_length < from_length) {
// SetLength will call truncate, which on filesystems supporting sparse
// files should not cause any actual disk space usage. Instead only the
// file's metadata is updated to reflect the new size. Actual block
// allocation will occur when attempting to write into space in the file
// which is not yet allocated.
if (!to_file.SetLength(from_length)) {
PLOG(ERROR) << "Failed to set file length of " << to_child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtTruncate, child);
return false;
}
}
if (!CopyAttributes(child, info))
return false;
while (from_length > 0) {
if (is_cancelled_.IsSet()) {
return false;
}
size_t to_read = from_length % effective_chunk_size_;
if (to_read == 0) {
to_read = effective_chunk_size_;
}
off_t offset = from_length - to_read;
if (to_file.Seek(base::File::FROM_BEGIN, offset) != offset) {
LOG(ERROR) << "Failed to seek in " << to_child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSeek, child);
return false;
}
// Sendfile is used here instead of a read to memory then write since it is
// more efficient for transferring data from one file to another. In
// particular the data is passed directly from the read call to the write
// in the kernel, never making a trip back out to user space.
if (!platform_->SendFile(to_file.GetPlatformFile(),
from_file.GetPlatformFile(), offset, to_read)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSendfile, child);
return false;
}
// For the last chunk, SyncFile will be called later so no need to flush
// here. The same goes for SetLength as from_file will be deleted soon.
if (offset > 0) {
if (!to_file.Flush()) {
PLOG(ERROR) << "Failed to flush " << to_child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSync, child);
return false;
}
if (!from_file.SetLength(offset)) {
PLOG(ERROR) << "Failed to truncate file " << from_child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtTruncate, child);
return false;
}
}
from_length = offset;
IncrementMigratedBytes(to_read);
}
from_file.Close();
to_file.Close();
if (!FixTimes(child))
return false;
if (!platform_->SyncFile(to_child)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSync, child);
return false;
}
if (!RemoveTimeXattrs(child))
return false;
return true;
}
bool MigrationHelper::CopyAttributes(const base::FilePath& child,
const FileEnumerator::FileInfo& info) {
const base::FilePath from = from_base_path_.Append(child);
const base::FilePath to = to_base_path_.Append(child);
uid_t user_id = info.stat().st_uid;
gid_t group_id = info.stat().st_gid;
if (!platform_->SetOwnership(to, user_id, group_id,
false /* follow_links */)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSetAttribute, child);
return false;
}
if (!CopyExtendedAttributes(child))
return false;
mode_t mode = info.stat().st_mode;
// We don't need to modify the source file, so no special timestamp handling
// needed. Permissions and flags are also not supported on symlinks in linux.
if (S_ISLNK(mode))
return true;
if (!platform_->SetPermissions(to, mode)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSetAttribute, child);
return false;
}
const auto& mtime = info.stat().st_mtim;
const auto& atime = info.stat().st_atim;
if (!SetExtendedAttributeIfNotPresent(child, namespaced_mtime_xattr_name_,
reinterpret_cast<const char*>(&mtime),
sizeof(mtime))) {
return false;
}
if (!SetExtendedAttributeIfNotPresent(child, namespaced_atime_xattr_name_,
reinterpret_cast<const char*>(&atime),
sizeof(atime))) {
return false;
}
int flags;
if (!platform_->GetExtFileAttributes(from, &flags)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtGetAttribute, child);
return false;
}
if (!platform_->SetExtFileAttributes(to, flags)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSetAttribute, child);
return false;
}
return true;
}
bool MigrationHelper::FixTimes(const base::FilePath& child) {
const base::FilePath file = to_base_path_.Append(child);
struct timespec mtime;
if (!platform_->GetExtendedFileAttribute(file, namespaced_mtime_xattr_name_,
reinterpret_cast<char*>(&mtime),
sizeof(mtime))) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtGetAttribute, child);
return false;
}
struct timespec atime;
if (!platform_->GetExtendedFileAttribute(file, namespaced_atime_xattr_name_,
reinterpret_cast<char*>(&atime),
sizeof(atime))) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtGetAttribute, child);
return false;
}
if (!platform_->SetFileTimes(file, atime, mtime, true /* follow_links */)) {
PLOG(ERROR) << "Failed to set mtime on " << file.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSetAttribute, child);
return false;
}
return true;
}
bool MigrationHelper::RemoveTimeXattrs(const base::FilePath& child) {
const base::FilePath file = to_base_path_.Append(child);
if (!platform_->RemoveExtendedFileAttribute(file,
namespaced_mtime_xattr_name_)) {
PLOG(ERROR) << "Failed to remove mtime extended attribute from "
<< file.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtRemoveAttribute, child);
return false;
}
if (!platform_->RemoveExtendedFileAttribute(file,
namespaced_atime_xattr_name_)) {
PLOG(ERROR) << "Failed to remove atime extended attribute from "
<< file.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtRemoveAttribute, child);
return false;
}
return true;
}
bool MigrationHelper::CopyExtendedAttributes(const base::FilePath& child) {
const base::FilePath from = from_base_path_.Append(child);
const base::FilePath to = to_base_path_.Append(child);
std::vector<std::string> xattr_names;
if (!platform_->ListExtendedFileAttributes(from, &xattr_names)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtGetAttribute, child);
return false;
}
for (const std::string& name : xattr_names) {
if (name == namespaced_mtime_xattr_name_ ||
name == namespaced_atime_xattr_name_ || name == kSourceURLXattrName ||
name == kReferrerURLXattrName) {
continue;
}
std::string value;
if (!platform_->GetExtendedFileAttributeAsString(from, name, &value)) {
RecordFileErrorWithCurrentErrno(kMigrationFailedAtGetAttribute, child);
return false;
}
if (!platform_->SetExtendedFileAttribute(to, name, value.data(),
value.length())) {
bool nospace_error = errno == ENOSPC;
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSetAttribute, child);
if (nospace_error) {
ReportTotalXattrSize(to, name.length() + 1 + value.length());
}
return false;
}
}
return true;
}
bool MigrationHelper::SetExtendedAttributeIfNotPresent(
const base::FilePath& child,
const std::string& xattr,
const char* value,
ssize_t size) {
base::FilePath file = to_base_path_.Append(child);
// If the attribute already exists we assume it was set during a previous
// migration attempt and use the existing one instead of writing a new one.
if (platform_->HasExtendedFileAttribute(file, xattr)) {
return true;
}
if (errno != ENODATA) {
PLOG(ERROR) << "Failed to get extended attribute " << xattr << " for "
<< file.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtGetAttribute, child);
return false;
}
if (!platform_->SetExtendedFileAttribute(file, xattr, value, size)) {
bool nospace_error = errno == ENOSPC;
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSetAttribute, child);
if (nospace_error) {
ReportTotalXattrSize(file, xattr.length() + 1 + size);
}
return false;
}
return true;
}
void MigrationHelper::RecordFileError(
DircryptoMigrationFailedOperationType operation,
const base::FilePath& child,
base::File::Error error) {
DircryptoMigrationFailedPathType path = kMigrationFailedUnderOther;
for (const auto& path_type_mapping : kPathTypeMappings) {
if (base::FilePath(path_type_mapping.path).IsParent(child)) {
path = path_type_mapping.type;
break;
}
}
// Android cache files are either under
// root/android-data/data/data/<package name>/cache
// root/android-data/data/media/0/Android/data/<package name>/cache
if (path == kMigrationFailedUnderAndroidOther) {
std::vector<std::string> components;
child.GetComponents(&components);
if ((components.size() >= 7u && components[2] == "data" &&
components[3] == "data" && components[5] == "cache") ||
(components.size() >= 10u && components[2] == "data" &&
components[3] == "media" && components[4] == "0" &&
components[5] == "Android" && components[6] == "data" &&
components[8] == "cache")) {
path = kMigrationFailedUnderAndroidCache;
}
}
// Report UMA stats here for each single error.
ReportDircryptoMigrationFailedOperationType(operation);
ReportDircryptoMigrationFailedPathType(path);
ReportDircryptoMigrationFailedErrorCode(error);
if (error == base::File::FILE_ERROR_NO_SPACE) {
ReportDircryptoMigrationFailedNoSpace(
initial_free_space_bytes_ / (1024 * 1024),
platform_->AmountOfFreeDiskSpace(to_base_path_) / (1024 * 1024));
}
{ // Record the data for the final end-status report.
base::AutoLock lock(failure_info_lock_);
failed_operation_type_ = operation;
failed_path_type_ = path;
failed_error_type_ = error;
}
}
void MigrationHelper::RecordFileErrorWithCurrentErrno(
DircryptoMigrationFailedOperationType operation,
const base::FilePath& child) {
RecordFileError(operation, child, base::File::OSErrorToFileError(errno));
}
void MigrationHelper::RecordSkippedFile(const base::FilePath& rel_path) {
base::File skipped_file_list;
platform_->InitializeFile(
&skipped_file_list, skipped_file_list_path_,
base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_APPEND);
if (!skipped_file_list.IsValid()) {
PLOG(ERROR) << "Could not open list of skipped files at"
<< skipped_file_list_path_.value() << ", " << rel_path.value()
<< " not added";
return;
}
if (!platform_->LockFile(skipped_file_list.GetPlatformFile())) {
PLOG(ERROR) << "Failed to lock " << skipped_file_list_path_.value();
return;
}
std::string data = rel_path.value() + "\n";
int write_size = data.size();
// O_APPEND was used to open, so write is always done at the end of the file
// even without seek.
if (write_size !=
skipped_file_list.WriteAtCurrentPos(data.data(), write_size)) {
PLOG(ERROR) << "Failed to write " << rel_path.value()
<< " to the list of skipped files";
return;
}
if (!skipped_file_list.Flush()) {
PLOG(ERROR) << "Failed to flush " << rel_path.value()
<< " to the list of skipped files";
}
if (skipped_file_list.created()) {
// Sync the parent directory to persist the file.
if (!platform_->SyncDirectory(skipped_file_list_path_.DirName()))
PLOG(ERROR) << "Failed to sync parent directory when creating list of "
"skipped files "
<< skipped_file_list_path_.value();
}
}
bool MigrationHelper::ProcessJob(const Job& job) {
if (S_ISLNK(job.info.stat().st_mode)) {
// Symlink
if (!MigrateLink(job.child, job.info))
return false;
IncrementMigratedBytes(job.info.GetSize());
} else if (S_ISREG(job.info.stat().st_mode)) {
// File
if (!MigrateFile(job.child, job.info))
return false;
} else {
LOG(ERROR) << "Unknown file type: " << job.child.value();
}
if (!platform_->DeleteFile(from_base_path_.Append(job.child))) {
LOG(ERROR) << "Failed to delete file " << job.child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtDelete, job.child);
return false;
}
// The file/symlink was removed.
// Decrement the child count of the parent directory.
return DecrementChildCountAndDeleteIfNecessary(job.child.DirName());
}
void MigrationHelper::IncrementChildCount(const base::FilePath& child) {
base::AutoLock lock(child_counts_lock_);
++child_counts_[child];
}
bool MigrationHelper::DecrementChildCountAndDeleteIfNecessary(
const base::FilePath& child) {
{
base::AutoLock lock(child_counts_lock_);
auto it = child_counts_.find(child);
--(it->second);
if (it->second > 0) // This directory is not empty yet.
return true;
child_counts_.erase(it);
}
// The last child was removed. Finish migrating this directory.
const base::FilePath from_dir = from_base_path_.Append(child);
const base::FilePath to_dir = to_base_path_.Append(child);
if (!FixTimes(child)) {
LOG(ERROR) << "Failed to fix times " << child.value();
return false;
}
if (!platform_->SyncDirectory(to_dir)) {
LOG(ERROR) << "Failed to sync " << child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtSync, child);
return false;
}
if (!RemoveTimeXattrs(child))
return false;
// Don't delete the top directory.
if (child.value() == base::FilePath::kCurrentDirectory)
return true;
if (!platform_->DeleteFile(from_dir)) {
PLOG(ERROR) << "Failed to delete " << child.value();
RecordFileErrorWithCurrentErrno(kMigrationFailedAtDelete, child);
return false;
}
// Decrement the parent directory's child count.
return DecrementChildCountAndDeleteIfNecessary(child.DirName());
}
void MigrationHelper::ReportTotalXattrSize(const base::FilePath& path,
int failed_xattr_size) {
std::vector<std::string> xattr_names;
if (!platform_->ListExtendedFileAttributes(path, &xattr_names)) {
LOG(ERROR) << "Error listing extended attributes for " << path.value();
return;
}
int xattr_size = failed_xattr_size;
for (const std::string& name : xattr_names) {
xattr_size += name.length() + 1; // Add one byte for null termination.
std::string value;
if (!platform_->GetExtendedFileAttributeAsString(path, name, &value)) {
LOG(ERROR) << "Error getting value for extended attribute " << name
<< " on " << path.value();
return;
}
xattr_size += value.length();
}
ReportDircryptoMigrationFailedNoSpaceXattrSizeInBytes(xattr_size);
}
} // namespace dircrypto_data_migrator
} // namespace cryptohome