blob: ab98d9d3f496e7c297e4b74c767153f229005919 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "init/startup/stateful_mount.h"
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/json/json_reader.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/values.h>
#include <brillo/blkdev_utils/lvm.h>
#include <brillo/blkdev_utils/storage_utils.h>
#include <brillo/files/file_util.h>
#include <brillo/process/process.h>
#include <brillo/key_value_store.h>
#include <brillo/secure_blob.h>
#include <libstorage/platform/platform.h>
#include <metrics/bootstat.h>
#include <rootdev/rootdev.h>
#include "init/startup/constants.h"
#include "init/startup/flags.h"
#include "init/startup/mount_helper.h"
#include "init/startup/security_manager.h"
#include "init/startup/startup_dep_impl.h"
#include "init/utils.h"
namespace {
constexpr char kExt4Features[] = "sys/fs/ext4/features";
constexpr char kReservedBlocksGID[] = "20119";
constexpr char kQuotaOpt[] = "quota";
constexpr char kDumpe2fsStatefulLog[] =
"run/chromeos_startup/dumpe2fs_stateful.log";
constexpr char kDirtyExpireCentisecs[] = "proc/sys/vm/dirty_expire_centisecs";
constexpr char kUpdateAvailable[] = ".update_available";
constexpr char kLabMachine[] = ".labmachine";
constexpr char kVar[] = "var";
constexpr char kNew[] = "_new";
constexpr char kOverlay[] = "_overlay";
constexpr char kVarLogAsan[] = "var/log/asan";
constexpr char kStatefulDevImage[] = "dev_image";
constexpr char kUsrLocal[] = "usr/local";
constexpr char kDisableStatefulSecurityHardening[] =
"usr/share/cros/startup/disable_stateful_security_hardening";
constexpr char kTmpPortage[] = "var/tmp/portage";
constexpr char kProcMounts[] = "proc/mounts";
constexpr char kMountOptionsLog[] = "var/log/mount_options.log";
constexpr char kPartitionsVars[] = "usr/sbin/partition_vars.json";
const std::vector<const char*> kMountDirs = {"db/pkg", "lib/portage",
"cache/dlc-images"};
// TODO(asavery): update the check for removable devices to be
// more advanced, b/209476959
bool RemovableRootdev(const base::FilePath& path, int* ret) {
base::FilePath removable("/sys/block");
removable = removable.Append(path.BaseName());
removable = removable.Append("removable");
return utils::ReadFileToInt(removable, ret);
}
uint64_t GetDirtyExpireCentisecs(libstorage::Platform* platform,
const base::FilePath& root) {
std::string dirty_expire;
uint64_t dirty_expire_centisecs = 0;
base::FilePath centisecs_path = root.Append(kDirtyExpireCentisecs);
if (!platform->ReadFileToString(centisecs_path, &dirty_expire)) {
PLOG(WARNING) << "Failed to read " << centisecs_path.value();
return 0;
}
base::TrimWhitespaceASCII(dirty_expire, base::TRIM_ALL, &dirty_expire);
if (!base::StringToUint64(dirty_expire, &dirty_expire_centisecs)) {
PLOG(WARNING) << "Failed to parse contents of " << centisecs_path.value();
}
return dirty_expire_centisecs;
}
// Get the partition number for the given key,
// e.g. "PARTITION_NUM_STATE". Fails with a `CHECK()` if any error occurs.
int GetPartitionNumFromImageVars(const base::Value::Dict& image_dict,
std::string_view key) {
const std::string* value = image_dict.FindString(key);
CHECK_NE(value, nullptr);
int num = 0;
CHECK(base::StringToInt(*value, &num));
return num;
}
} // namespace
namespace startup {
StatefulMount::StatefulMount(const Flags& flags,
const base::FilePath& root,
const base::FilePath& stateful,
libstorage::Platform* platform,
StartupDep* startup_dep,
MountHelper* mount_helper)
: flags_(flags),
root_(root),
stateful_(stateful),
platform_(platform),
startup_dep_(startup_dep),
mount_helper_(mount_helper) {}
std::optional<base::Value> StatefulMount::GetImageVars(base::FilePath json_file,
std::string key) {
std::string json_string;
if (!platform_->ReadFileToString(json_file, &json_string)) {
PLOG(ERROR) << "Unable to read json file: " << json_file;
return std::nullopt;
}
std::optional<base::Value> part_vars = base::JSONReader::Read(
json_string, base::JSON_PARSE_RFC, 10 /* max_depth */);
if (!part_vars) {
PLOG(ERROR) << "Failed to parse image variables.";
return std::nullopt;
}
if (!part_vars->is_dict()) {
LOG(ERROR) << "Failed to read json file as a dictionary";
return std::nullopt;
}
base::Value::Dict* image_vars = part_vars->GetDict().FindDict(key);
if (image_vars == nullptr) {
LOG(ERROR) << "Failed to get image variables from " << json_file;
return std::nullopt;
}
return base::Value(std::move(*image_vars));
}
void StatefulMount::AppendQuotaFeaturesAndOptions(
std::vector<std::string>* sb_options,
std::vector<std::string>* sb_features) {
// Enable/disable quota feature.
// Add Android's AID_RESERVED_DISK to resgid.
sb_features->push_back("-g");
sb_features->push_back(kReservedBlocksGID);
// Quota is enabled in the kernel, make sure that quota is enabled in
// the filesystem
sb_options->push_back(kQuotaOpt);
sb_features->push_back("-Qusrquota,grpquota");
if (flags_.prjquota) {
sb_features->push_back("-Qprjquota");
} else {
sb_features->push_back("-Q^prjquota");
}
}
void StatefulMount::EnableExt4Features() {
std::vector<std::string> sb_features = GenerateExt4Features();
if (!sb_features.empty()) {
brillo::ProcessImpl tune2fs;
tune2fs.AddArg("/sbin/tune2fs");
for (const std::string& arg : sb_features) {
tune2fs.AddArg(arg);
}
tune2fs.AddArg(state_dev_.value());
tune2fs.RedirectOutputToMemory(true);
int status = tune2fs.Run();
if (status == 0) {
return;
} else if (status < 0) {
PLOG(ERROR) << "Failed to run tune2fs";
} else {
LOG(WARNING) << "tune2fs returned non zero exit code: " << status;
}
}
}
std::vector<std::string> StatefulMount::GenerateExt4Features() {
std::vector<std::string> sb_features;
std::vector<std::string> sb_options;
base::FilePath encryption = root_.Append(kExt4Features).Append("encryption");
if (flags_.direncryption && platform_->FileExists(encryption)) {
sb_options.push_back("encrypt");
}
base::FilePath verity_file = root_.Append(kExt4Features).Append("verity");
if (flags_.fsverity && platform_->FileExists(verity_file)) {
sb_options.push_back("verity");
}
AppendQuotaFeaturesAndOptions(&sb_options, &sb_features);
if (!sb_features.empty() || !sb_options.empty()) {
// Ensure to replay the journal first so it doesn't overwrite the flag.
startup_dep_->ReplayExt4Journal(state_dev_);
if (!sb_options.empty()) {
std::string opts = base::JoinString(sb_options, ",");
sb_features.push_back("-O");
sb_features.push_back(opts);
}
}
return sb_features;
}
void StatefulMount::ClobberStateful(
const std::vector<std::string>& clobber_args,
const std::string& clobber_message) {
startup_dep_->BootAlert("self_repair");
startup_dep_->ClobberLogRepair(state_dev_, clobber_message);
startup_dep_->AddClobberCrashReport(
{"--mount_failure", "--mount_device=stateful"});
startup_dep_->Clobber(clobber_args);
}
bool StatefulMount::AttemptStatefulMigration() {
std::unique_ptr<brillo::Process> thinpool_migrator =
platform_->CreateProcessInstance();
thinpool_migrator->AddArg("/usr/sbin/thinpool_migrator");
thinpool_migrator->AddArg(
base::StringPrintf("--device=%s", state_dev_.value().c_str()));
if (thinpool_migrator->Run() != 0) {
LOG(ERROR) << "Failed to run thinpool migrator";
return false;
}
return true;
}
void StatefulMount::MountStateful() {
base::FilePath root_dev;
// Prepare to mount stateful partition.
root_device_ = utils::GetRootDevice(true);
int removable = 0;
if (root_device_.empty()) {
PLOG(INFO) << "rootdev could not find root device.";
} else if (!RemovableRootdev(root_device_, &removable)) {
PLOG(WARNING)
<< "Unable to read if rootdev is removable; assuming it's not";
}
std::string load_vars;
if (removable == 1) {
load_vars = "load_partition_vars";
} else {
load_vars = "load_base_vars";
}
base::FilePath json_file = root_.Append(kPartitionsVars);
std::optional<base::Value> image_vars = GetImageVars(json_file, load_vars);
if (!image_vars) {
return;
}
if (!image_vars->is_dict()) {
PLOG(ERROR) << "Failed to parse dictionary from " << json_file;
return;
}
const base::Value::Dict& image_vars_dict = image_vars->GetDict();
bool status;
int32_t stateful_mount_flags;
std::string stateful_mount_opts;
// Check if we are booted on physical media. rootdev will fail if we are in
// an initramfs or tmpfs rootfs (ex, factory installer images. Note recovery
// image also uses initramfs but it never reaches here). When using
// initrd+tftpboot (some old netboot factory installer), ROOTDEV_TYPE will be
// /dev/ram.
if (!root_device_.empty() && root_device_ != base::FilePath("/dev/ram")) {
// Find our stateful partition mount point.
stateful_mount_flags = kCommonMountFlags | MS_NOATIME;
const int part_num_state =
GetPartitionNumFromImageVars(image_vars_dict, "PARTITION_NUM_STATE");
const std::string* fs_form_state =
image_vars_dict.FindString("FS_FORMAT_STATE");
state_dev_ = brillo::AppendPartition(root_device_, part_num_state);
if (fs_form_state->compare("ext4") == 0) {
int dirty_expire_centisecs = GetDirtyExpireCentisecs(platform_, root_);
int commit_interval = dirty_expire_centisecs / 100;
if (commit_interval != 0) {
stateful_mount_opts = "commit=" + std::to_string(commit_interval);
stateful_mount_opts.append(",discard");
} else {
LOG(INFO) << "Using default value for commit interval";
stateful_mount_opts = "discard";
}
}
bool should_mount_lvm = false;
if (USE_LVM_STATEFUL_PARTITION && flags_.lvm_stateful) {
brillo::LogicalVolumeManager* lvm = platform_->GetLogicalVolumeManager();
// Attempt to get a valid volume group name.
bootstat_.LogEvent("pre-lvm-activation");
std::optional<brillo::PhysicalVolume> pv =
lvm->GetPhysicalVolume(state_dev_);
if (!pv && flags_.lvm_migration) {
// Attempt to migrate to thinpool if migration is enabled: if the
// migration fails, then we expect the stateful partition to be back to
// its original state.
if (!AttemptStatefulMigration()) {
LOG(ERROR) << "Failed to migrate stateful partition to a thinpool";
} else {
// Reset the physical volume on success from thinpool migration.
pv = lvm->GetPhysicalVolume(state_dev_);
}
}
if (pv && pv->IsValid()) {
volume_group_ = lvm->GetVolumeGroup(*pv);
if (volume_group_ && volume_group_->IsValid()) {
// First attempt to activate the thinpool. If the activation of the
// thinpool fails, run thin_check to check all mappings.
std::optional<brillo::Thinpool> thinpool =
lvm->GetThinpool(*volume_group_, "thinpool");
if (!thinpool) {
LOG(ERROR) << "Thinpool does not exist";
ClobberStateful({"fast", "keepimg"}, "Invalid thinpool");
}
if (!thinpool->Activate()) {
LOG(WARNING) << "Failed to activate thinpool, attempting repair";
if (!thinpool->Activate(/*check=*/true)) {
LOG(ERROR) << "Failed to repair and activate thinpool";
ClobberStateful({"fast", "keepimg"}, "Corrupt thinpool");
}
}
// Attempt to now activate the unencrypted stateful logical volume.
std::optional<brillo::LogicalVolume> unencrypted_lv =
lvm->GetLogicalVolume(*volume_group_, "unencrypted");
if (!unencrypted_lv || !unencrypted_lv->Activate()) {
LOG(ERROR) << "Failed to activate unencrypted stateful logical "
<< "volume.";
ClobberStateful({"fast", "keepimg", "preserve_lvs"},
"Invalid unencrypted logical volume");
}
should_mount_lvm = true;
}
}
bootstat_.LogEvent("lvm-activation-complete");
}
if (should_mount_lvm) {
state_dev_ = root_.Append("dev")
.Append(volume_group_->GetName())
.Append("unencrypted");
}
EnableExt4Features();
// Mount stateful partition from state_dev.
if (!platform_->Mount(state_dev_, stateful_, *fs_form_state,
stateful_mount_flags, stateful_mount_opts)) {
// Try to rebuild the stateful partition by clobber-state. (Not using fast
// mode out of security consideration: the device might have gotten into
// this state through power loss during dev mode transition).
platform_->ReportFilesystemDetails(state_dev_,
root_.Append(kDumpe2fsStatefulLog));
ClobberStateful({"keepimg", "preserve_lvs"},
"Self-repair corrupted stateful partition");
}
// Mount the OEM partition.
// mount_or_fail isn't used since this partition only has a filesystem
// on some boards.
int32_t oem_flags = MS_RDONLY | kCommonMountFlags;
const int part_num_oem =
GetPartitionNumFromImageVars(image_vars_dict, "PARTITION_NUM_OEM");
const std::string* fs_form_oem =
image_vars_dict.FindString("FS_FORMAT_OEM");
const base::FilePath oem_dev =
brillo::AppendPartition(root_device_, part_num_oem);
status = platform_->Mount(oem_dev, base::FilePath("/usr/share/oem"),
*fs_form_oem, oem_flags, "");
if (!status) {
PLOG(WARNING) << "mount of /usr/share/oem failed with code " << status;
}
}
}
base::FilePath StatefulMount::GetStateDev() {
return state_dev_;
}
// Updates stateful partition if pending
// update is available.
// Returns true if there is no need to update or successful update.
bool StatefulMount::DevUpdateStatefulPartition(const std::string& args) {
base::FilePath stateful_update_file = stateful_.Append(kUpdateAvailable);
std::string stateful_update_args = args;
if (stateful_update_args.empty()) {
if (!platform_->ReadFileToString(stateful_update_file,
&stateful_update_args)) {
PLOG(WARNING) << "Failed to read from " << stateful_update_file.value();
return true;
}
// The file often ends with a new line.
base::TrimString(stateful_update_args, "\n", &stateful_update_args);
}
// To remain compatible with the prior update_stateful tarballs, expect
// the "var_new" unpack location, but move it into the new "var_overlay"
// target location.
std::string var(kVar);
std::string dev_image(kStatefulDevImage);
base::FilePath var_new = stateful_.Append(var + kNew);
base::FilePath developer_new = stateful_.Append(dev_image + kNew);
base::FilePath developer_target = stateful_.Append(dev_image);
base::FilePath var_target = stateful_.Append(var + kOverlay);
std::vector<base::FilePath> paths_to_rm;
// Only replace the developer and var_overlay directories if new replacements
// are available.
if (platform_->DirectoryExists(developer_new) &&
platform_->DirectoryExists(var_new)) {
std::string update = "Updating from " + developer_new.value() + " && " +
var_new.value() + ".";
startup_dep_->ClobberLog(update);
for (const std::string& path : {var, dev_image}) {
base::FilePath path_new = stateful_.Append(path + kNew);
base::FilePath path_target;
if (path == "var") {
path_target = stateful_.Append(path + kOverlay);
} else {
path_target = stateful_.Append(path);
}
if (!platform_->DeletePathRecursively(path_target)) {
PLOG(WARNING) << "Failed to delete " << path_target.value();
}
if (!platform_->CreateDirectory(path_target)) {
PLOG(WARNING) << "Failed to create " << path_target.value();
}
if (!platform_->SetPermissions(path_target, 0755)) {
PLOG(WARNING) << "chmod failed for " << path_target.value();
}
std::unique_ptr<libstorage::FileEnumerator> enumerator(
platform_->GetFileEnumerator(
path_new, false /* recursive */,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS));
for (base::FilePath fd = enumerator->Next(); !fd.empty();
fd = enumerator->Next()) {
// Filesystem crossing if new var comes in a logical volume.
if (!platform_->Rename(fd, path_target.Append(fd.BaseName()), true)) {
PLOG(WARNING) << "Failed to copy " << fd.value() << " to "
<< path_target.value();
}
}
paths_to_rm.push_back(path_new);
}
startup_dep_->RemoveInBackground(paths_to_rm);
} else {
std::string update = "Stateful update did not find " +
developer_new.value() + " & " + var_new.value() +
".'\n'Keeping old development tools.";
startup_dep_->ClobberLog(update);
}
// Check for clobber.
if (stateful_update_args.compare("clobber") == 0) {
// Because we want to preserve the testing tools under the /usr/local for
// test image, we only delete the cryptohome related LVs here.
// volume_group_ may be null if it was not found in MountStateful.
if (USE_LVM_STATEFUL_PARTITION && volume_group_ &&
volume_group_->IsValid()) {
volume_group_->Activate();
brillo::LogicalVolumeManager* lvm = platform_->GetLogicalVolumeManager();
std::vector<brillo::LogicalVolume> lvs =
lvm->ListLogicalVolumes(volume_group_.value(), "cryptohome*");
for (auto& lv : lvs) {
if (!lv.Remove()) {
LOG(WARNING) << "Failed to remove logical volume: " << lv.GetName();
}
}
}
base::FilePath preserve_dir = stateful_.Append("unencrypted/preserve");
// Find everything in stateful and delete it, except for protected paths,
// and non-empty directories. The non-empty directories contain protected
// content or they would already be empty from depth first traversal.
std::vector<base::FilePath> preserved_paths = {
stateful_.Append(kLabMachine), developer_target, var_target,
preserve_dir};
std::unique_ptr<libstorage::FileEnumerator> enumerator(
platform_->GetFileEnumerator(stateful_, true,
base::FileEnumerator::FILES |
base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS));
for (auto path = enumerator->Next(); !path.empty();
path = enumerator->Next()) {
bool preserve = false;
for (auto& preserved_path : preserved_paths) {
if (path == preserved_path || preserved_path.IsParent(path) ||
path.IsParent(preserved_path)) {
preserve = true;
break;
}
}
if (!preserve) {
if (platform_->DirectoryExists(path)) {
platform_->DeletePathRecursively(path);
} else {
platform_->DeleteFile(path);
}
}
}
// Let's really be done before coming back.
sync();
}
std::vector<base::FilePath> rm_paths{stateful_update_file};
startup_dep_->RemoveInBackground(rm_paths);
return true;
}
// Gather logs.
void StatefulMount::DevGatherLogs(const base::FilePath& base_dir) {
// For dev/test images, if .gatherme presents, copy files listed in .gatherme
// to /mnt/stateful_partition/unencrypted/prior_logs.
base::FilePath lab_preserve_logs = stateful_.Append(".gatherme");
base::FilePath prior_log_dir = stateful_.Append("unencrypted/prior_logs");
std::string log_path;
if (!platform_->FileExists(lab_preserve_logs)) {
return;
}
std::string files;
platform_->ReadFileToString(lab_preserve_logs, &files);
std::vector<std::string> split_files = base::SplitString(
files, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (std::string log_path : split_files) {
if (log_path.find("#") != std::string::npos) {
continue;
}
base::FilePath log(log_path);
if (platform_->DirectoryExists(log)) {
if (!platform_->Copy(log, prior_log_dir)) {
PLOG(WARNING) << "Failed to copy directory " << log_path;
}
} else {
if (!platform_->Copy(log, prior_log_dir.Append(log.BaseName()))) {
PLOG(WARNING) << "Failed to copy file " << log_path;
}
}
}
if (!platform_->DeleteFile(lab_preserve_logs)) {
PLOG(WARNING) << "Failed to delete file: " << lab_preserve_logs;
}
}
void StatefulMount::DevMountPackages() {
// Set up the logging dir that ASAN compiled programs will write to. We want
// any privileged account to be able to write here so unittests need not worry
// about setting things up ahead of time. See crbug.com/453579 for details.
base::FilePath asan_dir = root_.Append(kVarLogAsan);
if (!platform_->CreateDirectory(asan_dir)) {
PLOG(WARNING) << "Unable to create /var/log/asan directory, error code.";
}
if (!platform_->SetPermissions(asan_dir, 01777)) {
PLOG(WARNING) << "Set permissions failed for /var/log/asan";
}
// Capture a snapshot of "normal" mount state here, for auditability,
// before we start applying devmode-specific changes.
std::string mount_contents;
base::FilePath proc_mounts = root_.Append(kProcMounts);
if (!platform_->ReadFileToString(proc_mounts, &mount_contents)) {
PLOG(ERROR) << "Reading from " << proc_mounts.value() << " failed.";
}
base::FilePath mount_options = root_.Append(kMountOptionsLog);
if (!platform_->WriteStringToFile(mount_options, mount_contents)) {
PLOG(ERROR) << "Writing " << proc_mounts.value()
<< "to mount_options.log failed.";
}
// Create dev_image directory in base images in developer mode.
base::FilePath stateful_dev_image = stateful_.Append(kStatefulDevImage);
if (!platform_->DirectoryExists(stateful_dev_image)) {
if (!platform_->CreateDirectory(stateful_dev_image)) {
PLOG(ERROR) << "Failed to create " << stateful_dev_image.value();
}
if (!platform_->SetPermissions(stateful_dev_image, 0755)) {
PLOG(ERROR) << "Failed to set permissions for "
<< stateful_dev_image.value();
}
}
// Checks and updates stateful partition.
DevUpdateStatefulPartition("");
// Mount and then remount to enable exec/suid.
base::FilePath usrlocal = root_.Append(kUsrLocal);
mount_helper_->BindMountOrFail(stateful_dev_image, usrlocal);
if (!platform_->Mount(base::FilePath(), usrlocal, "", MS_REMOUNT, "")) {
PLOG(WARNING) << "Failed to remount " << usrlocal.value();
}
base::FilePath disable_state_sec_hard =
root_.Append(kDisableStatefulSecurityHardening);
if (!platform_->FileExists(disable_state_sec_hard)) {
// Add exceptions to allow symlink traversal and opening of FIFOs in the
// dev_image subtree.
for (const auto& path : {root_.Append(kTmpPortage), stateful_dev_image}) {
if (!platform_->DirectoryExists(path)) {
if (!platform_->CreateDirectory(path)) {
PLOG(ERROR) << "Failed to create " << path.value();
}
if (!platform_->SetPermissions(path, 0755)) {
PLOG(ERROR) << "Failed to set permissions for " << path.value();
}
}
AllowSymlink(platform_, root_, path.value());
AllowFifo(platform_, root_, path.value());
}
}
// Set up /var elements needed for deploying packages.
base::FilePath base = stateful_.Append("var_overlay");
if (platform_->DirectoryExists(base)) {
for (const auto dir : kMountDirs) {
base::FilePath full = base.Append(dir);
if (!platform_->DirectoryExists(full)) {
continue;
}
base::FilePath dest = root_.Append(kVar).Append(dir);
if (!platform_->DirectoryExists(dest)) {
LOG(WARNING) << "Path does not exists, can not mount: " << dest.value();
continue;
}
mount_helper_->BindMountOrFail(full, dest);
}
}
}
} // namespace startup