blob: 73e52e66463b469669dede74421f57796b1b630e [file] [log] [blame]
// Copyright 2018 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 "init/clobber_state.h"
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <base/callback_helpers.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/strings/stringprintf.h>
#include <base/logging.h>
#include <base/strings/string_split.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <brillo/process.h>
#include <rootdev/rootdev.h>
#include <vboot/cgpt_params.h>
#include <vboot/vboot_host.h>
#include <chromeos/secure_erase_file/secure_erase_file.h>
#include "init/crossystem.h"
namespace {
constexpr char kStatefulPath[] = "/mnt/stateful_partition";
constexpr char kPowerWashCountPath[] = "unencrypted/preserve/powerwash_count";
constexpr char kClobberLogPath[] = "/tmp/clobber-state.log";
constexpr char kBioWashPath[] = "/usr/bin/bio_wash";
constexpr char kPreservedFilesTarPath[] = "/tmp/preserve.tar";
constexpr char kUbiRootDisk[] = "/dev/mtd0";
constexpr char kUbiDevicePrefix[] = "/dev/ubi";
constexpr char kUbiDeviceStatefulFormat[] = "/dev/ubi%d_0";
// |strip_partition| attempts to remove the partition number from the result.
base::FilePath GetRootDevice(bool strip_partition) {
char buf[PATH_MAX];
int ret = rootdev(buf, PATH_MAX, /*use_slave=*/true, strip_partition);
if (ret == 0) {
return base::FilePath(buf);
} else {
return base::FilePath();
}
}
void CgptFindShowFunctionNoOp(struct CgptFindParams*,
const char*,
int,
GptEntry*) {}
bool ReadFileToInt(const base::FilePath& path, int* value) {
std::string str;
if (!base::ReadFileToString(path, &str)) {
return false;
}
base::TrimWhitespaceASCII(str, base::TRIM_ALL, &str);
return base::StringToInt(str, value);
}
// Calculate the maximum number of bad blocks per 1024 blocks for UBI.
int CalculateUBIMaxBadBlocksPer1024(int partition_number) {
// The max bad blocks per 1024 is based on total device size,
// not the partition size.
int mtd_size = 0;
ReadFileToInt(base::FilePath("/sys/class/mtd/mtd0/size"), &mtd_size);
int erase_size;
ReadFileToInt(base::FilePath("/sys/class/mtd/mtd0/erasesize"), &erase_size);
int block_count = mtd_size / erase_size;
int reserved_error_blocks = 0;
base::FilePath reserved_for_bad(base::StringPrintf(
"/sys/class/ubi/ubi%d/reserved_for_bad", partition_number));
ReadFileToInt(reserved_for_bad, &reserved_error_blocks);
return reserved_error_blocks * 1024 / block_count;
}
bool GetBlockCount(const base::FilePath& device_path,
int64_t* block_count_out) {
if (!block_count_out)
return false;
brillo::ProcessImpl dumpe2fs;
dumpe2fs.AddArg("/sbin/dumpe2fs");
dumpe2fs.AddArg("-h");
dumpe2fs.AddArg(device_path.value());
base::FilePath temp_file;
base::CreateTemporaryFile(&temp_file);
dumpe2fs.RedirectOutput(temp_file.value());
if (dumpe2fs.Run() == 0) {
std::string output;
base::ReadFileToString(temp_file, &output);
size_t label = output.find("Block count");
size_t value_start = output.find_first_of("0123456789", label);
size_t value_end = output.find_first_not_of("0123456789", value_start);
if (value_start != std::string::npos && value_end != std::string::npos) {
int64_t block_count;
if (base::StringToInt64(
output.substr(value_start, value_end - value_start),
&block_count)) {
*block_count_out = block_count;
return true;
}
}
}
// Fallback if using dumpe2fs failed.
base::FilePath fp("/sys/class/block");
fp = fp.Append(device_path.BaseName());
fp = fp.Append("size");
std::string block_count_str;
if (base::ReadFileToString(fp, &block_count_str)) {
base::TrimWhitespaceASCII(block_count_str, base::TRIM_ALL,
&block_count_str);
int64_t block_count;
if (base::StringToInt64(block_count_str, &block_count)) {
*block_count_out = block_count;
return true;
}
}
return false;
}
bool MakeTTYRaw(const base::File& tty) {
struct termios terminal_properties;
if (tcgetattr(tty.GetPlatformFile(), &terminal_properties) != 0) {
PLOG(WARNING) << "Getting properties of output TTY failed";
return false;
}
cfmakeraw(&terminal_properties);
if (tcsetattr(tty.GetPlatformFile(), TCSANOW, &terminal_properties) != 0) {
PLOG(WARNING) << "Setting properties of output TTY failed";
return false;
}
return true;
}
void AppendFileToLog(const base::FilePath& file) {
std::string file_contents;
if (!base::ReadFileToString(file, &file_contents)) {
// Even if reading file failed, some of its contents may have been read
// successfully, so we still attempt to append them to our log file.
PLOG(ERROR) << "Reading from temporary file failed: " << file.value();
}
if (!base::AppendToFile(base::FilePath(kClobberLogPath),
file_contents.c_str(), file_contents.size())) {
PLOG(ERROR) << "Appending " << file.value()
<< " to clobber-state log failed";
}
}
} // namespace
// static
ClobberState::Arguments ClobberState::ParseArgv(int argc,
char const* const argv[]) {
Arguments args;
if (argc <= 1)
return args;
// Due to historical usage, the command line parsing is a bit weird.
// We split the first argument into multiple keywords.
std::vector<std::string> split_args = base::SplitString(
argv[1], " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (int i = 2; i < argc; ++i)
split_args.push_back(argv[i]);
for (const std::string& arg : split_args) {
if (arg == "factory") {
args.factory_wipe = true;
} else if (arg == "fast") {
args.fast_wipe = true;
} else if (arg == "keepimg") {
args.keepimg = true;
} else if (arg == "safe") {
args.safe_wipe = true;
} else if (arg == "rollback") {
args.rollback_wipe = true;
}
}
return args;
}
// static
bool ClobberState::IncrementFileCounter(const base::FilePath& path) {
int value;
if (!ReadFileToInt(path, &value)) {
return base::WriteFile(path, "1\n", 2) == 2;
}
std::string new_value = std::to_string(value + 1);
new_value.append("\n");
return new_value.size() ==
base::WriteFile(path, new_value.c_str(), new_value.size());
}
// static
int ClobberState::PreserveFiles(
const base::FilePath& preserved_files_root,
const std::vector<base::FilePath>& preserved_files,
const base::FilePath& tar_file_path) {
// Remove any stale tar files from previous clobber-state runs.
base::DeleteFile(tar_file_path, /*recursive=*/false);
// We want to preserve permissions and recreate the directory structure
// for all of the files in |preserved_files|. In order to do so we run tar
// --no-recursion and specify the names of each of the parent directories.
// For example for home/.shadow/install_attributes.pb
// we pass to tar home, home/.shadow, home/.shadow/install_attributes.pb.
std::vector<std::string> paths_to_tar;
for (const base::FilePath& path : preserved_files) {
// All paths should be relative to |preserved_files_root|.
if (path.IsAbsolute()) {
LOG(WARNING) << "Non-relative path " << path.value()
<< " passed to PreserveFiles, ignoring.";
continue;
}
if (!base::PathExists(preserved_files_root.Append(path)))
continue;
base::FilePath current = path;
while (current != base::FilePath(base::FilePath::kCurrentDirectory)) {
// List of paths is built in an order that is reversed from what we want
// (parent directories first), but will then be passed to tar in reverse
// order.
//
// e.g. for home/.shadow/install_attributes.pb, |paths_to_tar| will have
// home/.shadow/install_attributes.pb, then home/.shadow, then home.
paths_to_tar.push_back(current.value());
current = current.DirName();
}
}
// We can't create an empty tar file.
if (paths_to_tar.size() == 0) {
LOG(INFO)
<< "PreserveFiles found no files to preserve, no tar file created.";
return 0;
}
brillo::ProcessImpl tar;
tar.AddArg("/bin/tar");
tar.AddArg("-c");
tar.AddStringOption("-f", tar_file_path.value());
tar.AddStringOption("-C", preserved_files_root.value());
tar.AddArg("--no-recursion");
tar.AddArg("--");
// Add paths in reverse order because we built up the list of paths backwards.
for (auto it = paths_to_tar.rbegin(); it != paths_to_tar.rend(); ++it) {
tar.AddArg(*it);
}
return tar.Run();
}
// static
bool ClobberState::GetDevicePathComponents(const base::FilePath& device,
std::string* base_device_out,
int* partition_out) {
if (!partition_out || !base_device_out)
return false;
const std::string& path = device.value();
// MTD devices sometimes have a trailing "_0" after the partition which
// we should ignore.
std::string mtd_suffix = "_0";
size_t suffix_index = path.length();
if (base::EndsWith(path, mtd_suffix, base::CompareCase::SENSITIVE)) {
suffix_index = path.length() - mtd_suffix.length();
}
size_t last_non_numeric =
path.find_last_not_of("0123456789", suffix_index - 1);
// If there are no non-numeric characters, this is a malformed device.
if (last_non_numeric == std::string::npos) {
return false;
}
std::string partition_number_string =
path.substr(last_non_numeric + 1, suffix_index - (last_non_numeric + 1));
int partition_number;
if (!base::StringToInt(partition_number_string, &partition_number)) {
return false;
}
*partition_out = partition_number;
*base_device_out = path.substr(0, last_non_numeric + 1);
return true;
}
bool ClobberState::IsRotational(const base::FilePath& device_path) {
if (!dev_.IsParent(device_path)) {
LOG(ERROR) << "Non-device given as argument to IsRotational: "
<< device_path.value();
return false;
}
// Since there doesn't seem to be a good way to get from a partition name
// to the base device name beyond simple heuristics, just find the device
// with the same major number but with minor 0.
struct stat st;
if (Stat(device_path, &st) != 0) {
return false;
}
unsigned int major_device_number = major(st.st_rdev);
base::FileEnumerator enumerator(dev_, /*recursive=*/true,
base::FileEnumerator::FileType::FILES);
for (base::FilePath base_device_path = enumerator.Next();
!base_device_path.empty(); base_device_path = enumerator.Next()) {
if (Stat(base_device_path, &st) == 0 && S_ISBLK(st.st_mode) &&
major(st.st_rdev) == major_device_number && minor(st.st_rdev) == 0) {
// |base_device_path| must be the base device for |device_path|.
base::FilePath rotational_file = sys_.Append("block")
.Append(base_device_path.BaseName())
.Append("queue/rotational");
int value;
if (ReadFileToInt(rotational_file, &value)) {
return value == 1;
}
}
}
return false;
}
// static
bool ClobberState::GetDevicesToWipe(
const base::FilePath& root_disk,
const base::FilePath& root_device,
const ClobberState::PartitionNumbers& partitions,
ClobberState::DeviceWipeInfo* wipe_info_out) {
if (!wipe_info_out) {
LOG(ERROR) << "wipe_info_out must be non-null";
return false;
}
if (partitions.root_a < 0 || partitions.root_b < 0 ||
partitions.kernel_a < 0 || partitions.kernel_b < 0 ||
partitions.stateful < 0) {
LOG(ERROR) << "Invalid partition numbers for GetDevicesToWipe";
return false;
}
if (root_disk.empty()) {
LOG(ERROR) << "Invalid root disk for GetDevicesToWipe";
return false;
}
if (root_device.empty()) {
LOG(ERROR) << "Invalid root device for GetDevicesToWipe";
return false;
}
std::string base_device;
int active_root_partition;
if (!GetDevicePathComponents(root_device, &base_device,
&active_root_partition)) {
LOG(ERROR) << "Extracting partition number and base device from "
"root_device failed: "
<< root_device.value();
return false;
}
ClobberState::DeviceWipeInfo wipe_info;
if (active_root_partition == partitions.root_a) {
wipe_info.inactive_root_device =
base::FilePath(base_device + std::to_string(partitions.root_b));
wipe_info.inactive_kernel_device =
base::FilePath(base_device + std::to_string(partitions.kernel_b));
wipe_info.active_kernel_partition = partitions.kernel_a;
} else if (active_root_partition == partitions.root_b) {
wipe_info.inactive_root_device =
base::FilePath(base_device + std::to_string(partitions.root_a));
wipe_info.inactive_kernel_device =
base::FilePath(base_device + std::to_string(partitions.kernel_a));
wipe_info.active_kernel_partition = partitions.kernel_b;
} else {
LOG(ERROR) << "Active root device partition number ("
<< active_root_partition
<< ") does not match either root partition number: "
<< partitions.root_a << ", " << partitions.root_b;
return false;
}
base::FilePath kernel_device;
if (root_disk == base::FilePath(kUbiRootDisk)) {
/*
* WARNING: This code has not been sufficiently tested and almost certainly
* does not work. If you are adding support for MTD flash, you would be
* well served to review it and add test coverage.
*/
// Special casing for NAND devices.
wipe_info.is_mtd_flash = true;
wipe_info.stateful_device = base::FilePath(
base::StringPrintf(kUbiDeviceStatefulFormat, partitions.stateful));
// On NAND, kernel is stored on /dev/mtdX.
if (active_root_partition == partitions.root_a) {
kernel_device =
base::FilePath("/dev/mtd" + std::to_string(partitions.kernel_a));
} else if (active_root_partition == partitions.root_b) {
kernel_device =
base::FilePath("/dev/mtd" + std::to_string(partitions.kernel_b));
}
/*
* End of untested MTD code.
*/
} else {
wipe_info.stateful_device =
base::FilePath(base_device + std::to_string(partitions.stateful));
if (active_root_partition == partitions.root_a) {
kernel_device =
base::FilePath(base_device + std::to_string(partitions.kernel_a));
} else if (active_root_partition == partitions.root_b) {
kernel_device =
base::FilePath(base_device + std::to_string(partitions.kernel_b));
}
}
*wipe_info_out = wipe_info;
return true;
}
// static
bool ClobberState::WipeMTDDevice(
const base::FilePath& device_path,
const ClobberState::PartitionNumbers& partitions) {
/*
* WARNING: This code has not been sufficiently tested and almost certainly
* does not work. If you are adding support for MTD flash, you would be
* well served to review it and add test coverage.
*/
if (!base::StartsWith(device_path.value(), kUbiDevicePrefix,
base::CompareCase::SENSITIVE)) {
LOG(ERROR) << "Cannot wipe device " << device_path.value();
return false;
}
std::string base_device;
int partition_number;
if (!GetDevicePathComponents(device_path, &base_device, &partition_number)) {
LOG(ERROR) << "Getting partition number from device failed: "
<< device_path.value();
return false;
}
std::string partition_name;
if (partition_number == partitions.stateful) {
partition_name = "STATE";
} else if (partition_number == partitions.root_a) {
partition_name = "ROOT-A";
} else if (partition_number == partitions.root_b) {
partition_name = "ROOT-B";
} else {
partition_name = base::StringPrintf("UNKNOWN_%d", partition_number);
LOG(ERROR) << "Do not know how to name UBI partition for "
<< device_path.value();
}
base::FilePath temp_file;
base::CreateTemporaryFile(&temp_file);
std::string physical_device =
base::StringPrintf("/dev/ubi%d", partition_number);
struct stat st;
stat(physical_device.c_str(), &st);
if (!S_ISCHR(st.st_mode)) {
// Try to attach the volume to obtain info about it.
brillo::ProcessImpl ubiattach;
ubiattach.AddArg("/bin/ubiattach");
ubiattach.AddIntOption("-m", partition_number);
ubiattach.AddIntOption("-d", partition_number);
ubiattach.RedirectOutput(temp_file.value());
ubiattach.Run();
AppendFileToLog(temp_file);
}
int max_bad_blocks_per_1024 =
CalculateUBIMaxBadBlocksPer1024(partition_number);
int volume_size;
base::FilePath data_bytes(base::StringPrintf(
"/sys/class/ubi/ubi%d_0/data_bytes", partition_number));
ReadFileToInt(data_bytes, &volume_size);
brillo::ProcessImpl ubidetach;
ubidetach.AddArg("/bin/ubidetach");
ubidetach.AddIntOption("-d", partition_number);
ubidetach.RedirectOutput(temp_file.value());
int detach_ret = ubidetach.Run();
AppendFileToLog(temp_file);
if (detach_ret) {
LOG(ERROR) << "Detaching MTD volume failed with code " << detach_ret;
}
brillo::ProcessImpl ubiformat;
ubiformat.AddArg("/bin/ubiformat");
ubiformat.AddArg("-y");
ubiformat.AddIntOption("-e", 0);
ubiformat.AddArg(base::StringPrintf("/dev/mtd%d", partition_number));
ubiformat.RedirectOutput(temp_file.value());
int format_ret = ubiformat.Run();
AppendFileToLog(temp_file);
if (format_ret) {
LOG(ERROR) << "Formatting MTD volume failed with code " << format_ret;
}
// We need to attach so that we could set max beb/1024 and create a volume.
// After a volume is created, we don't need to specify max beb/1024 anymore.
brillo::ProcessImpl ubiattach;
ubiattach.AddArg("/bin/ubiattach");
ubiattach.AddIntOption("-d", partition_number);
ubiattach.AddIntOption("-m", partition_number);
ubiattach.AddIntOption("--max-beb-per1024", max_bad_blocks_per_1024);
ubiattach.RedirectOutput(temp_file.value());
int attach_ret = ubiattach.Run();
AppendFileToLog(temp_file);
if (attach_ret) {
LOG(ERROR) << "Reattaching MTD volume failed with code " << attach_ret;
}
brillo::ProcessImpl ubimkvol;
ubimkvol.AddArg("/bin/ubimkvol");
ubimkvol.AddIntOption("-s", volume_size);
ubimkvol.AddStringOption("-N", partition_name);
ubimkvol.AddArg(physical_device);
ubimkvol.RedirectOutput(temp_file.value());
int mkvol_ret = ubimkvol.Run();
AppendFileToLog(temp_file);
if (mkvol_ret) {
LOG(ERROR) << "Making MTD volume failed with code " << mkvol_ret;
}
return detach_ret == 0 && format_ret == 0 && attach_ret == 0 &&
mkvol_ret == 0;
/*
* End of untested MTD code.
*/
}
// static
bool ClobberState::WipeBlockDevice(const base::FilePath& device_path,
const base::File& progress_tty,
bool fast) {
const int write_block_size = 4 * 1024 * 1024;
int64_t to_write = 0;
if (fast) {
to_write = write_block_size;
} else {
// Wipe the filesystem size if we can determine it. Full partition wipe
// takes a long time on 16G SSD or rotating media.
struct stat st;
if (stat(device_path.value().c_str(), &st) == -1) {
PLOG(ERROR) << "Unable to stat " << device_path.value();
return false;
}
int64_t block_size = st.st_blksize;
int64_t block_count;
if (!GetBlockCount(device_path, &block_count)) {
LOG(ERROR) << "Unable to get block count for " << device_path.value();
return false;
}
to_write = block_count * block_size;
LOG(INFO) << "Filesystem block size: " << block_size;
LOG(INFO) << "Filesystem block count: " << block_count;
}
LOG(INFO) << "Wiping block device " << device_path.value()
<< (fast ? " (fast) " : "");
LOG(INFO) << "Number of bytes to write: " << to_write;
base::File device(open(device_path.value().c_str(), O_WRONLY | O_SYNC));
if (!device.IsValid()) {
PLOG(ERROR) << "Unable to open " << device_path.value();
return false;
}
// Don't display progress in fast mode since it runs so quickly.
bool display_progress =
!fast && base::PathExists(base::FilePath("/usr/bin/pv"));
brillo::ProcessImpl pv;
if (display_progress) {
pv.AddArg("/usr/bin/pv");
pv.AddArg("--timer");
pv.AddArg("--progress");
pv.AddArg("--eta");
pv.AddStringOption("--size", base::Int64ToString(to_write));
// Track bytes written to the file descriptor for the device to wipe.
pv.AddStringOption(
"--watchfd",
base::StringPrintf("%d:%d", getpid(), device.GetPlatformFile()));
pv.BindFd(progress_tty.GetPlatformFile(), STDERR_FILENO);
if (!pv.Start()) {
LOG(WARNING) << "Starting pv failed";
display_progress = false;
}
}
const std::vector<char> buffer(write_block_size, '\0');
int64_t total_written = 0;
while (total_written < to_write) {
int write_size = std::min(static_cast<int64_t>(write_block_size),
to_write - total_written);
int64_t bytes_written = device.WriteAtCurrentPos(buffer.data(), write_size);
if (bytes_written < 0) {
PLOG(ERROR) << "Failed to write to " << device_path.value();
LOG(ERROR) << "Wrote " << total_written << " bytes before failing";
return false;
}
total_written += bytes_written;
}
LOG(INFO) << "Successfully wrote " << total_written << " bytes to "
<< device_path.value();
// Close the device so that pv will stop.
device.Close();
if (display_progress) {
int ret = pv.Wait();
if (ret != 0) {
LOG(WARNING) << "pv failed with code " << ret;
}
}
return true;
}
// static
void ClobberState::RemoveVPDKeys() {
std::vector<std::string> keys_to_remove{"recovery_count",
"first_active_omaha_ping_sent"};
base::FilePath temp_file;
base::CreateTemporaryFile(&temp_file);
for (const std::string& key : keys_to_remove) {
brillo::ProcessImpl vpd;
vpd.AddArg("/usr/sbin/vpd");
vpd.AddStringOption("-i", "RW_VPD");
vpd.AddStringOption("-d", key);
// Do not report failures as the key might not even exist in the VPD.
vpd.RedirectOutput(temp_file.value());
vpd.Run();
AppendFileToLog(temp_file);
}
}
// static
int ClobberState::GetPartitionNumber(const base::FilePath& drive_name,
const std::string& partition_label) {
// TODO(C++20): Switch to aggregate initialization once we require C++20.
CgptFindParams params = {};
params.set_label = 1;
params.label = partition_label.c_str();
params.drive_name = drive_name.value().c_str();
params.show_fn = &CgptFindShowFunctionNoOp;
CgptFind(&params);
if (params.hits != 1) {
LOG(ERROR) << "Could not find partition number for partition "
<< partition_label;
return -1;
}
return params.match_partnum;
}
// static
bool ClobberState::ReadPartitionMetadata(const base::FilePath& disk,
int partition_number,
bool* successful_out,
int* priority_out) {
if (!successful_out || !priority_out)
return false;
// TODO(C++20): Switch to aggregate initialization once we require C++20.
CgptAddParams params = {};
params.drive_name = disk.value().c_str();
params.partition = partition_number;
if (CgptGetPartitionDetails(&params) == CGPT_OK) {
*successful_out = params.successful;
*priority_out = params.priority;
return true;
} else {
return false;
}
}
// static
void ClobberState::EnsureKernelIsBootable(const base::FilePath root_disk,
int kernel_partition) {
bool successful = false;
int priority = 0;
if (!ReadPartitionMetadata(root_disk, kernel_partition, &successful,
&priority)) {
LOG(ERROR) << "Failed to read partition metadata from partition "
<< kernel_partition << " on disk " << root_disk.value();
// If we couldn't read, we'll err on the side of caution and try to set the
// successful bit and priority anyways.
}
if (!successful) {
// TODO(C++20): Switch to aggregate initialization once we require C++20.
CgptAddParams params = {};
params.partition = kernel_partition;
params.set_successful = 1;
params.drive_name = root_disk.value().c_str();
params.successful = 1;
if (CgptAdd(&params) != CGPT_OK) {
LOG(ERROR) << "Failed to set sucessful for active kernel partition: "
<< kernel_partition;
}
}
if (priority < 1) {
// TODO(C++20): Switch to aggregate initialization once we require C++20.
CgptPrioritizeParams params = {};
params.set_partition = kernel_partition;
params.drive_name = root_disk.value().c_str();
// When reordering kernel priorities to set the active kernel to highest,
// use 3 as the highest value. Since there are only 3 kernel partitions,
// this ensures that all priorities are unique.
params.max_priority = 3;
if (CgptPrioritize(&params) != CGPT_OK) {
LOG(ERROR) << "Failed to prioritize active kernel partition: "
<< kernel_partition;
}
}
sync();
}
ClobberState::ClobberState(const Arguments& args,
std::unique_ptr<CrosSystem> cros_system)
: args_(args),
cros_system_(std::move(cros_system)),
stateful_(kStatefulPath),
dev_("/dev"),
sys_("/sys") {
base::FilePath terminal_path;
if (base::PathExists(base::FilePath("/sbin/frecon"))) {
terminal_path = base::FilePath("/run/frecon/vt0");
} else {
terminal_path = base::FilePath("/dev/tty1");
}
terminal_ =
base::File(terminal_path, base::File::FLAG_OPEN | base::File::FLAG_WRITE);
if (!terminal_.IsValid()) {
PLOG(WARNING) << "Could not open terminal " << terminal_path.value()
<< " falling back to /dev/null";
terminal_ = base::File(base::FilePath("/dev/null"),
base::File::FLAG_OPEN | base::File::FLAG_WRITE);
}
MakeTTYRaw(terminal_);
}
std::vector<base::FilePath> ClobberState::GetPreservedFilesList() {
std::vector<std::string> stateful_paths;
// Preserve these files in safe mode. (Please request a privacy review before
// adding files.)
//
// - unencrypted/preserve/update_engine/prefs/rollback-happened: Contains a
// boolean value indicating whether a rollback has happened since the last
// update check where device policy was available. Needed to avoid forced
// updates after rollbacks (device policy is not yet loaded at this time).
if (args_.safe_wipe) {
stateful_paths.push_back(kPowerWashCountPath);
stateful_paths.push_back(
"unencrypted/preserve/tpm_firmware_update_request");
stateful_paths.push_back(
"unencrypted/preserve/update_engine/prefs/rollback-happened");
stateful_paths.push_back(
"unencrypted/preserve/update_engine/prefs/rollback-version");
// Preserve pre-installed demo mode resources for offline Demo Mode.
std::string demo_mode_resources_dir =
"unencrypted/cros-components/offline-demo-mode-resources/";
stateful_paths.push_back(demo_mode_resources_dir + "image.squash");
stateful_paths.push_back(demo_mode_resources_dir + "imageloader.json");
stateful_paths.push_back(demo_mode_resources_dir + "imageloader.sig.1");
stateful_paths.push_back(demo_mode_resources_dir + "imageloader.sig.2");
stateful_paths.push_back(demo_mode_resources_dir + "manifest.fingerprint");
stateful_paths.push_back(demo_mode_resources_dir + "manifest.json");
stateful_paths.push_back(demo_mode_resources_dir + "table");
// For rollback wipes, we also preserve rollback data. This is an encrypted
// proto which contains install attributes, device policy and owner.key
// (used to keep the enrollment), also other device-level configurations
// e.g. shill configuration to restore network connection after rollback.
// We also preserve the attestation DB (needed because we don't do TPM clear
// in this case).
if (args_.rollback_wipe) {
stateful_paths.push_back("unencrypted/preserve/attestation.epb");
stateful_paths.push_back("unencrypted/preserve/rollback_data");
}
}
// Test images in the lab enable certain extra behaviors if the
// .labmachine flag file is present. Those behaviors include some
// important recovery behaviors (cf. the recover_duts upstart job).
// We need those behaviors to survive across power wash, otherwise,
// the current boot could wind up as a black hole.
int debug_build;
if (cros_system_->GetInt(CrosSystem::kDebugBuild, &debug_build) &&
debug_build == 1) {
stateful_paths.push_back(".labmachine");
}
std::vector<base::FilePath> preserved_files;
for (const std::string& path : stateful_paths) {
preserved_files.push_back(base::FilePath(path));
}
if (args_.factory_wipe) {
base::FileEnumerator enumerator(
stateful_.Append("unencrypted/import_extensions/extensions"), false,
base::FileEnumerator::FileType::FILES, "*.crx");
for (base::FilePath name = enumerator.Next(); !name.empty();
name = enumerator.Next()) {
preserved_files.push_back(
base::FilePath("unencrypted/import_extensions/extensions")
.Append(name.BaseName()));
}
}
return preserved_files;
}
int ClobberState::CreateStatefulFileSystem() {
base::FilePath temp_file;
base::CreateTemporaryFile(&temp_file);
brillo::ProcessImpl mkfs;
if (wipe_info_.is_mtd_flash) {
mkfs.AddArg("/sbin/mkfs.ubifs");
mkfs.AddArg("-y");
mkfs.AddStringOption("-x", "none");
mkfs.AddIntOption("-R", 0);
mkfs.AddArg(wipe_info_.stateful_device.value());
} else {
mkfs.AddArg("/sbin/mkfs.ext4");
mkfs.AddArg(wipe_info_.stateful_device.value());
// TODO(wad) tune2fs.
}
mkfs.RedirectOutput(temp_file.value());
LOG(INFO) << "Creating stateful file system";
int ret = mkfs.Run();
AppendFileToLog(temp_file);
return ret;
}
int ClobberState::Run() {
DCHECK(cros_system_);
// Defer callback to relocate log file back to stateful partition so that it
// will be preserved after a reboot.
base::ScopedClosureRunner relocate_clobber_state_log(base::BindRepeating(
[](base::FilePath stateful_path) {
base::Move(base::FilePath(kClobberLogPath),
stateful_path.Append("unencrypted/clobber-state.log"));
},
stateful_));
LOG(INFO) << "Beginning clobber-state run";
LOG(INFO) << "Factory wipe: " << args_.factory_wipe;
LOG(INFO) << "Fast wipe: " << args_.fast_wipe;
LOG(INFO) << "Keepimg: " << args_.keepimg;
LOG(INFO) << "Safe wipe: " << args_.safe_wipe;
LOG(INFO) << "Rollback wipe: " << args_.rollback_wipe;
// Most effective means of destroying user data is run at the start: Throwing
// away the key to encrypted stateful by requesting the TPM to be cleared at
// next boot. We shouldn't do this for rollback wipes.
if (!args_.rollback_wipe) {
if (!cros_system_->SetInt(CrosSystem::kClearTpmOwnerRequest, 1)) {
LOG(ERROR) << "Requesting TPM wipe via crossystem failed";
}
}
// In cases where biometric sensors are available, reset the internal entropy
// used by those sensors for encryption, to render related data/templates etc.
// undecipherable.
if (!ClearBiometricSensorEntropy()) {
LOG(ERROR) << "Clearing biometric sensor internal entropy failed";
}
if (args_.safe_wipe) {
IncrementFileCounter(stateful_.Append(kPowerWashCountPath));
}
std::vector<base::FilePath> preserved_files = GetPreservedFilesList();
for (const base::FilePath& fp : preserved_files) {
LOG(INFO) << "Preserving file: " << fp.value();
}
base::FilePath preserved_tar_file(kPreservedFilesTarPath);
int ret = PreserveFiles(stateful_, preserved_files, preserved_tar_file);
if (ret) {
LOG(ERROR) << "Preserving files failed with code " << ret;
}
// As we move factory wiping from release image to factory test image,
// clobber-state will be invoked directly under a tmpfs. GetRootDevice cannot
// report correct output under such a situation. Therefore, the output is
// preserved then assigned to environment variables ROOT_DEV/ROOT_DISK for
// clobber-state. For other cases, the environment variables will be empty and
// it falls back to using GetRootDevice.
const char* root_disk_cstr = getenv("ROOT_DISK");
if (root_disk_cstr != nullptr) {
root_disk_ = base::FilePath(root_disk_cstr);
} else {
root_disk_ = GetRootDevice(/*strip_partition=*/true);
}
// Special casing for NAND devices
if (base::StartsWith(root_disk_.value(), kUbiDevicePrefix,
base::CompareCase::SENSITIVE)) {
root_disk_ = base::FilePath(kUbiRootDisk);
}
base::FilePath root_device;
const char* root_device_cstr = getenv("ROOT_DEV");
if (root_device_cstr != nullptr) {
root_device = base::FilePath(root_device_cstr);
} else {
root_device = GetRootDevice(/*strip_partition=*/false);
}
LOG(INFO) << "Root disk: " << root_disk_.value();
LOG(INFO) << "Root device: " << root_device.value();
partitions_.stateful = GetPartitionNumber(root_disk_, "STATE");
partitions_.root_a = GetPartitionNumber(root_disk_, "ROOT-A");
partitions_.root_b = GetPartitionNumber(root_disk_, "ROOT-B");
partitions_.kernel_a = GetPartitionNumber(root_disk_, "KERN-A");
partitions_.kernel_b = GetPartitionNumber(root_disk_, "KERN-B");
if (!GetDevicesToWipe(root_disk_, root_device, partitions_, &wipe_info_)) {
LOG(ERROR) << "Getting devices to wipe failed, aborting run";
return 1;
}
// Determine if stateful partition's device is backed by a rotational disk.
bool is_rotational = false;
if (!wipe_info_.is_mtd_flash) {
is_rotational = IsRotational(wipe_info_.stateful_device);
}
LOG(INFO) << "Stateful device: " << wipe_info_.stateful_device.value();
LOG(INFO) << "Inactive root device: "
<< wipe_info_.inactive_root_device.value();
LOG(INFO) << "Inactive kernel device: "
<< wipe_info_.inactive_kernel_device.value();
base::FilePath temp_file;
base::CreateTemporaryFile(&temp_file);
brillo::ProcessImpl log_preserve;
log_preserve.AddArg("/sbin/clobber-log");
log_preserve.AddArg("--preserve");
log_preserve.AddArg("clobber-state");
if (args_.factory_wipe)
log_preserve.AddArg("factory");
if (args_.fast_wipe)
log_preserve.AddArg("fast");
if (args_.keepimg)
log_preserve.AddArg("keepimg");
if (args_.safe_wipe)
log_preserve.AddArg("safe");
if (args_.rollback_wipe)
log_preserve.AddArg("rollback");
log_preserve.RedirectOutput(temp_file.value());
log_preserve.Run();
AppendFileToLog(temp_file);
AttemptSwitchToFastWipe(is_rotational);
// Make sure the stateful partition has been unmounted.
LOG(INFO) << "Unmounting stateful partition";
ret = umount(stateful_.value().c_str());
if (ret)
PLOG(ERROR) << "Unable to unmount " << stateful_.value();
// Destroy user data: wipe the stateful partition.
if (!WipeDevice(wipe_info_.stateful_device)) {
LOG(ERROR) << "Unable to wipe device "
<< wipe_info_.stateful_device.value();
}
ret = CreateStatefulFileSystem();
if (ret)
LOG(ERROR) << "Unable to create stateful file system. Error code: " << ret;
// Mount the fresh image for last minute additions.
std::string file_system_type = wipe_info_.is_mtd_flash ? "ubifs" : "ext4";
if (mount(wipe_info_.stateful_device.value().c_str(),
stateful_.value().c_str(), file_system_type.c_str(), 0,
nullptr) != 0) {
PLOG(ERROR) << "Unable to mount stateful partition at "
<< stateful_.value();
}
if (base::PathExists(preserved_tar_file)) {
brillo::ProcessImpl tar;
tar.AddArg("/bin/tar");
tar.AddStringOption("-C", stateful_.value());
tar.AddArg("-x");
tar.AddStringOption("-f", preserved_tar_file.value());
tar.RedirectOutput(temp_file.value());
ret = tar.Run();
AppendFileToLog(temp_file);
if (ret != 0) {
LOG(WARNING) << "Restoring preserved files failed with code " << ret;
}
base::WriteFile(stateful_.Append("unencrypted/.powerwash_completed"), "",
0);
}
brillo::ProcessImpl log_restore;
log_restore.AddArg("/sbin/clobber-log");
log_restore.AddArg("--restore");
log_restore.AddArg("clobber-state");
log_restore.RedirectOutput(temp_file.value());
ret = log_restore.Run();
AppendFileToLog(temp_file);
if (ret != 0) {
LOG(WARNING) << "Restoring clobber.log failed with code " << ret;
}
// Destroy less sensitive data.
if (!args_.safe_wipe) {
RemoveVPDKeys();
}
if (!args_.keepimg) {
EnsureKernelIsBootable(root_disk_, wipe_info_.active_kernel_partition);
WipeDevice(wipe_info_.inactive_root_device);
WipeDevice(wipe_info_.inactive_kernel_device);
}
// Check if we're in developer mode, and if so, create developer mode marker
// file so that we don't run clobber-state again after reboot.
if (!MarkDeveloperMode()) {
LOG(ERROR) << "Creating developer mode marker file failed.";
}
// Schedule flush of filesystem caches to disk.
sync();
LOG(INFO) << "clobber-state has completed";
relocate_clobber_state_log.RunAndReset();
// Factory wipe should stop here.
if (args_.factory_wipe)
return 0;
// If everything worked, reboot.
// This return won't actually be reached unless reboot fails.
return Reboot();
}
bool ClobberState::MarkDeveloperMode() {
std::string firmware_name;
int dev_mode_flag;
if (cros_system_->GetInt(CrosSystem::kDevSwitchBoot, &dev_mode_flag) &&
dev_mode_flag == 1 &&
cros_system_->GetString(CrosSystem::kMainFirmwareActive,
&firmware_name) &&
firmware_name != "recovery") {
return base::WriteFile(stateful_.Append(".developer_mode"), "", 0) == 0;
}
return true;
}
void ClobberState::AttemptSwitchToFastWipe(bool is_rotational) {
// On a non-fast wipe, rotational drives take too long. Override to run them
// through "fast" mode, with a forced delay. Sensitive contents should already
// be encrypted.
if (!args_.fast_wipe && is_rotational) {
LOG(INFO) << "Stateful device is on rotational disk, shredding files";
ShredRotationalStatefulFiles();
ForceDelay();
args_.fast_wipe = true;
LOG(INFO) << "Switching to fast wipe";
}
// For drives that support secure erasure, wipe the keysets,
// and then run the drives through "fast" mode, with a forced delay.
//
// Note: currently only eMMC-based SSDs are supported.
if (!args_.fast_wipe) {
LOG(INFO) << "Attempting to wipe encryption keysets";
if (WipeKeysets()) {
LOG(INFO) << "Wiping encryption keysets succeeded";
ForceDelay();
args_.fast_wipe = true;
LOG(INFO) << "Switching to fast wipe";
} else {
LOG(INFO) << "Wiping encryption keysets failed";
}
}
}
void ClobberState::ShredRotationalStatefulFiles() {
// Directly remove things that are already encrypted (which are also the
// large things), or are static from images.
base::DeleteFile(stateful_.Append("encrypted.block"), /*recursive=*/false);
base::DeleteFile(stateful_.Append("var_overlay"), /*recursive=*/true);
base::DeleteFile(stateful_.Append("dev_image"), /*recursive=*/true);
base::FileEnumerator shadow_files(
stateful_.Append("home/.shadow"),
/*recursive=*/true, base::FileEnumerator::FileType::DIRECTORIES);
for (base::FilePath path = shadow_files.Next(); !path.empty();
path = shadow_files.Next()) {
if (path.BaseName() == base::FilePath("vault")) {
base::DeleteFile(path, /*recursive=*/true);
}
}
base::FilePath temp_file;
base::CreateTemporaryFile(&temp_file);
// Shred everything else. We care about contents not filenames, so do not
// use "-u" since metadata updates via fdatasync dominate the shred time.
// Note that if the count-down is interrupted, the reset file continues
// to exist, which correctly continues to indicate a needed wipe.
brillo::ProcessImpl shred;
shred.AddArg("/usr/bin/shred");
shred.AddArg("--force");
shred.AddArg("--zero");
base::FileEnumerator stateful_files(stateful_, /*recursive=*/true,
base::FileEnumerator::FileType::FILES);
for (base::FilePath path = stateful_files.Next(); !path.empty();
path = stateful_files.Next()) {
shred.AddArg(path.value());
}
shred.RedirectOutput(temp_file.value());
shred.Run();
AppendFileToLog(temp_file);
sync();
}
bool ClobberState::WipeKeysets() {
std::vector<std::string> key_files{
"encrypted.key", "encrypted.needs-finalization",
"home/.shadow/cryptohome.key", "home/.shadow/salt",
"home/.shadow/salt.sum"};
for (const std::string& str : key_files) {
base::FilePath path = stateful_.Append(str);
if (base::PathExists(path)) {
if (!SecureErase(path)) {
LOG(ERROR) << "Securely erasing file failed: " << path.value();
return false;
}
}
}
// Delete files named 'master' in directories contained in '.shadow'.
base::FileEnumerator directories(stateful_.Append("home/.shadow"),
/*recursive=*/false,
base::FileEnumerator::FileType::DIRECTORIES);
for (base::FilePath dir = directories.Next(); !dir.empty();
dir = directories.Next()) {
base::FileEnumerator files(dir, /*recursive=*/false,
base::FileEnumerator::FileType::FILES);
for (base::FilePath file = files.Next(); !file.empty();
file = files.Next()) {
if (file.RemoveExtension().BaseName() == base::FilePath("master")) {
if (!SecureErase(file)) {
LOG(ERROR) << "Securely erasing file failed: " << file.value();
return false;
}
}
}
}
return DropCaches();
}
void ClobberState::ForceDelay() {
for (int delay = 300; delay >= 0; delay--) {
std::string count =
base::StringPrintf("%2d:%02d\r", delay / 60, delay % 60);
terminal_.WriteAtCurrentPos(count.c_str(), count.size());
sleep(1);
}
}
// Wrapper around secure_erase_file::SecureErase(const base::FilePath&).
bool ClobberState::SecureErase(const base::FilePath& path) {
return secure_erase_file::SecureErase(path);
}
// Wrapper around secure_erase_file::DropCaches(). Must be called after
// a call to SecureEraseFile. Files are only securely deleted if DropCaches
// returns true.
bool ClobberState::DropCaches() {
return secure_erase_file::DropCaches();
}
bool ClobberState::WipeDevice(const base::FilePath& device_path) {
if (wipe_info_.is_mtd_flash) {
return WipeMTDDevice(device_path, partitions_);
} else {
return WipeBlockDevice(device_path, terminal_, args_.fast_wipe);
}
}
void ClobberState::SetArgsForTest(const ClobberState::Arguments& args) {
args_ = args;
}
ClobberState::Arguments ClobberState::GetArgsForTest() {
return args_;
}
void ClobberState::SetStatefulForTest(const base::FilePath& stateful_path) {
stateful_ = stateful_path;
}
void ClobberState::SetDevForTest(const base::FilePath& dev_path) {
dev_ = dev_path;
}
void ClobberState::SetSysForTest(const base::FilePath& sys_path) {
sys_ = sys_path;
}
int ClobberState::Stat(const base::FilePath& path, struct stat* st) {
return stat(path.value().c_str(), st);
}
bool ClobberState::ClearBiometricSensorEntropy() {
if (base::PathExists(base::FilePath(kBioWashPath))) {
brillo::ProcessImpl bio_wash;
bio_wash.AddArg(kBioWashPath);
return bio_wash.Run() == 0;
}
// Return true here so that we don't report spurious failures on platforms
// without the bio_wash executable.
return true;
}
int ClobberState::Reboot() {
brillo::ProcessImpl proc;
proc.AddArg("/sbin/shutdown");
proc.AddArg("-r");
proc.AddArg("now");
int ret = proc.Run();
if (ret == 0) {
// Wait for reboot to finish (it's an async call).
sleep(60 * 60 * 24);
}
// If we've reached here, reboot (probably) failed.
LOG(ERROR) << "Requesting reboot failed with failure code " << ret;
return ret;
}