blob: 9fe7c6b9c1f791f087c88bcbcc27f3c444763ffa [file] [log] [blame]
// Copyright 2023 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/clobber/clobber_lvm.h"
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#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/logging.h>
#include <base/rand_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <chromeos/constants/imageloader.h>
#include <libdlcservice/utils.h>
namespace {
// Size of string for volume group name.
constexpr int kVolumeGroupNameSize = 16;
// Minimal physical volume size (1 default sized extent).
constexpr uint64_t kMinStatefulPartitionSizeMb = 4;
// Percent size of thinpool compared to the physical volume.
constexpr size_t kThinpoolSizePercent = 98;
// thin_metadata_size estimates <2% of the thinpool size can be used safely to
// store metadata for up to 200 logical volumes.
constexpr size_t kThinpoolMetadataSizePercent = 1;
// Create thin logical volumes at 95% of the thinpool's size.
constexpr size_t kLogicalVolumeSizePercent = 95;
} // namespace
ClobberLvm::ClobberLvm(ClobberWipe* clobber_wipe,
std::unique_ptr<brillo::LogicalVolumeManager> lvm)
: clobber_wipe_(clobber_wipe), lvm_(std::move(lvm)) {}
// Use a random 16 character name for the volume group.
std::string ClobberLvm::GenerateRandomVolumeGroupName() {
const char kCharset[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
unsigned char vg_random_value[kVolumeGroupNameSize];
base::RandBytes(vg_random_value);
std::string vg_name(kVolumeGroupNameSize, '0');
for (int i = 0; i < kVolumeGroupNameSize; ++i) {
vg_name[i] = kCharset[vg_random_value[i] % 36];
}
return vg_name;
}
void ClobberLvm::RemoveLogicalVolumeStack(
const base::FilePath stateful_partition_device) {
// For logical volume stateful partition, deactivate the volume group before
// wiping the device.
std::optional<brillo::PhysicalVolume> pv =
lvm_->GetPhysicalVolume(stateful_partition_device);
if (!pv || !pv->IsValid()) {
LOG(WARNING) << "Failed to get physical volume.";
return;
}
std::optional<brillo::VolumeGroup> vg = lvm_->GetVolumeGroup(*pv);
if (!vg || !vg->IsValid()) {
LOG(WARNING) << "Failed to get volume group.";
return;
}
LOG(INFO) << "Deactivating volume group.";
vg->Deactivate();
LOG(INFO) << "Removing volume group.";
vg->Remove();
LOG(INFO) << "Removing physical volume.";
pv->Remove();
}
bool ClobberLvm::ProcessInfo(
const brillo::VolumeGroup& vg,
const PreserveLogicalVolumesWipeInfo& info,
std::unique_ptr<dlcservice::UtilsInterface> utils) {
auto lv = lvm_->GetLogicalVolume(vg, info.lv_name);
if (!lv || !lv->IsValid()) {
LOG(INFO) << "Skipping over logical volume: " << info.lv_name;
return true;
}
// Active logical volumes as not all have udev rule to active by default.
if (!lv->Activate()) {
LOG(ERROR) << "Failed to active logical volume: " << info.lv_name;
return false;
}
// Zero the logical volume.
if (info.zero) {
if (!clobber_wipe_->WipeDevice(lv->GetPath(), /*discard=*/true)) {
LOG(ERROR) << "Failed to wipe logical volume: " << info.lv_name;
return false;
}
}
// Preserve the logical volume.
if (info.preserve) {
LOG(INFO) << "Preserving logical volume: " << info.lv_name;
} else if (!lv->Remove()) {
LOG(ERROR) << "Failed to remove logical volume: " << info.lv_name;
return false;
}
bool remove_lv = false;
// Verify digest of the logical volume.
if (info.digest_info) {
std::vector<uint8_t> actual_digest;
// Logical volumes MUST skip size checking. Stats on it are going to return
// the wrong size or 0.
if (!utils->HashFile(lv->GetPath(), info.digest_info->bytes, &actual_digest,
/*skip_size_check=*/true)) {
LOG(ERROR) << "Failed to check digest of logical volume: "
<< info.lv_name;
// Continue to return `true`, as we DO NOT want all other preservations to
// fail due to a bad digest.
remove_lv = true;
} else if (info.digest_info->digest != actual_digest) {
LOG(ERROR) << "Digests do not match for logical volume: " << info.lv_name;
// Continue to return `true`, as we DO NOT want all other preservations to
// fail due to a bad digest.
remove_lv = true;
}
}
if (remove_lv && !lv->Remove()) {
LOG(ERROR) << "Failed to remove logical volume: " << info.lv_name;
}
return true;
}
bool ClobberLvm::PreserveLogicalVolumesWipe(
const base::FilePath stateful_partition_device,
const PreserveLogicalVolumesWipeInfos& infos) {
auto pv = lvm_->GetPhysicalVolume({stateful_partition_device});
if (!pv || !pv->IsValid()) {
LOG(WARNING) << "Failed to get physical volume.";
return false;
}
auto vg = lvm_->GetVolumeGroup(*pv);
if (!vg || !vg->IsValid()) {
LOG(WARNING) << "Failed to get volume group.";
return false;
}
// Remove all logical volumes we don't need to handle with care.
for (auto& lv : lvm_->ListLogicalVolumes(*vg)) {
const auto& lv_raw_name = lv.GetRawName();
auto it = infos.find({.lv_name = lv_raw_name});
bool found = it != infos.end();
if (found)
continue;
if (!lv.Remove()) {
LOG(ERROR) << "Failed to remove logical volume: " << lv_raw_name;
return false;
}
}
// We must handle logical volume with additional care based on the
// `PreserveLogicalVolumesWipeInfo`.
for (const auto& info : infos) {
if (info.lv_name == kUnencrypted)
continue;
if (!ProcessInfo(*vg, info, std::make_unique<dlcservice::Utils>()))
return false;
}
// Note: Always process unencrypted stateful last.
// This is so when there are crashes, the powerwash file is still accessible
// within unencrypted logical volume to go through and perform the powerwash
// again.
{
auto lv_name = kUnencrypted;
auto info_it = infos.find({.lv_name = lv_name});
if (info_it == infos.end()) {
LOG(ERROR) << "Missing " << lv_name
<< " in preserve logical volumes wipe info.";
return false;
}
if (!ProcessInfo(*vg, *info_it, std::make_unique<dlcservice::Utils>()))
return false;
}
auto old_vg_name = vg->GetName();
auto new_vg_name = GenerateRandomVolumeGroupName();
if (!vg->Rename(new_vg_name)) {
LOG(ERROR) << "Failed to rename volume group from=" << old_vg_name
<< " to=" << new_vg_name;
return false;
}
return true;
}
bool ClobberLvm::CreateUnencryptedStatefulLV(const brillo::VolumeGroup& vg,
const brillo::Thinpool& thinpool,
uint64_t lv_size) {
base::Value::Dict lv_config;
lv_config.Set("name", kUnencrypted);
lv_config.Set("size", base::NumberToString(lv_size));
std::optional<brillo::LogicalVolume> lv =
lvm_->CreateLogicalVolume(vg, thinpool, lv_config);
if (!lv || !lv->IsValid()) {
LOG(ERROR) << "Failed to create " << kUnencrypted << " logical volume.";
return false;
}
if (!lv->Activate()) {
LOG(ERROR) << "Failed to activate thinpool.";
return false;
}
return true;
}
uint64_t ClobberLvm::GetBlkSize(const base::FilePath& device) {
base::ScopedFD fd(HANDLE_EINTR(
open(device.value().c_str(), O_RDONLY | O_NOFOLLOW | O_CLOEXEC)));
if (!fd.is_valid()) {
PLOG(ERROR) << "open " << device.value();
return 0;
}
uint64_t size;
if (ioctl(fd.get(), BLKGETSIZE64, &size)) {
PLOG(ERROR) << "ioctl(BLKGETSIZE): " << device.value();
return 0;
}
return size;
}
std::optional<uint64_t> ClobberLvm::GetPartitionSize(
const base::FilePath& base_device) {
uint64_t partition_size = GetBlkSize(base_device) / (1024 * 1024);
if (partition_size < kMinStatefulPartitionSizeMb) {
LOG(ERROR) << "Invalid partition size (" << partition_size
<< ") for: " << base_device.value();
return std::nullopt;
}
return {partition_size};
}
std::optional<base::FilePath> ClobberLvm::CreateLogicalVolumeStackForPreserved(
const base::FilePath stateful_partition_device) {
std::optional<uint64_t> partition_size =
GetPartitionSize(stateful_partition_device);
if (!partition_size) {
LOG(ERROR) << "Failed to get partition size.";
return std::nullopt;
}
auto pv = lvm_->GetPhysicalVolume({stateful_partition_device});
if (!pv || !pv->IsValid()) {
LOG(WARNING) << "Failed to get physical volume.";
return std::nullopt;
}
auto vg = lvm_->GetVolumeGroup(*pv);
if (!vg || !vg->IsValid()) {
LOG(WARNING) << "Failed to get volume group.";
return std::nullopt;
}
std::optional<brillo::Thinpool> thinpool = lvm_->GetThinpool(*vg, kThinpool);
if (!thinpool || !thinpool->IsValid()) {
LOG(ERROR) << "Failed to get thinpool.";
return std::nullopt;
}
int64_t thinpool_size = partition_size.value() * kThinpoolSizePercent / 100;
uint64_t lv_size = thinpool_size * kLogicalVolumeSizePercent / 100;
if (!CreateUnencryptedStatefulLV(*vg, *thinpool, lv_size)) {
return std::nullopt;
}
return base::FilePath(
base::StringPrintf("/dev/%s/unencrypted", vg->GetName().c_str()));
}
std::optional<base::FilePath> ClobberLvm::CreateLogicalVolumeStack(
base::FilePath base_device) {
std::string vg_name = GenerateRandomVolumeGroupName();
// Get partition size to determine the sizes of the thin pool and the
// logical volume. Use partition size in megabytes: thinpool (and logical
// volume) sizes need to be a multiple of 512.
std::optional<uint64_t> partition_size = GetPartitionSize(base_device);
if (!partition_size) {
LOG(ERROR) << "Failed to get partition size.";
return std::nullopt;
}
std::optional<brillo::PhysicalVolume> pv =
lvm_->CreatePhysicalVolume(base_device);
if (!pv || !pv->IsValid()) {
LOG(ERROR) << "Failed to create physical volume.";
return std::nullopt;
}
std::optional<brillo::VolumeGroup> vg = lvm_->CreateVolumeGroup(*pv, vg_name);
if (!vg || !vg->IsValid()) {
LOG(ERROR) << "Failed to create volume group.";
return std::nullopt;
}
vg->Activate();
base::Value thinpool_config(base::Value::Type::DICT);
int64_t thinpool_size = partition_size.value() * kThinpoolSizePercent / 100;
int64_t thinpool_metadata_size =
thinpool_size * kThinpoolMetadataSizePercent / 100;
auto& dict = thinpool_config.GetDict();
dict.Set("name", kThinpool);
dict.Set("size", base::NumberToString(thinpool_size));
dict.Set("metadata_size", base::NumberToString(thinpool_metadata_size));
std::optional<brillo::Thinpool> thinpool =
lvm_->CreateThinpool(*vg, thinpool_config);
if (!thinpool || !thinpool->IsValid()) {
LOG(ERROR) << "Failed to create thinpool.";
return std::nullopt;
}
uint64_t lv_size = thinpool_size * kLogicalVolumeSizePercent / 100;
if (!CreateUnencryptedStatefulLV(*vg, *thinpool, lv_size)) {
LOG(ERROR) << "Failed to activate thinpool.";
return std::nullopt;
}
return base::FilePath(
base::StringPrintf("/dev/%s/unencrypted", vg_name.c_str()));
}
ClobberLvm::PreserveLogicalVolumesWipeInfos
ClobberLvm::DlcPreserveLogicalVolumesWipeArgs(
const base::FilePath& ps_file_path,
const base::FilePath& dlc_manifest_root_path,
dlcservice::PartitionSlot active_slot,
std::unique_ptr<dlcservice::UtilsInterface> utils) {
if (!base::PathExists(ps_file_path)) {
LOG(WARNING) << "DLC powerwash safe file missing, skipping preservation.";
return {};
}
std::string ps_file_content;
if (!base::ReadFileToString(ps_file_path, &ps_file_content)) {
// Don't end PLOGs with (period).
PLOG(ERROR) << "Failed to read DLC powerwash safe file";
return {};
}
auto dlcs = base::SplitString(ps_file_content, "\n", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
LOG(INFO) << "The powerwash safe DLCs are=" << base::JoinString(dlcs, ",");
ClobberLvm::PreserveLogicalVolumesWipeInfos verified_dlcs;
for (const auto& dlc : dlcs) {
const auto& manifest = utils->GetDlcManifest(dlc, dlc_manifest_root_path);
// Verify against rootfs that these DLCs are in fact, powerwash safe.
if (!manifest || !manifest->powerwash_safe()) {
LOG(WARNING) << "DLC=" << dlc << " is not powerwash safe, but list in "
<< "powerwash safe file, skipping it.";
continue;
}
LOG(INFO) << "DLC=" << dlc << " is set to be preserved.";
// We add the active DLC logical volume.
const auto& dlc_active_lv_name = utils->LogicalVolumeName(dlc, active_slot);
verified_dlcs.emplace(PreserveLogicalVolumesWipeInfo{
.lv_name = dlc_active_lv_name,
.preserve = true,
.zero = false,
.digest_info =
PreserveLogicalVolumesWipeInfo::DigestInfo{
.bytes = manifest->size(),
.digest = manifest->image_sha256(),
},
});
// We also add the inactive DLC logical volume, but simply clear (zero) it.
const auto& dlc_inactive_lv_name = utils->LogicalVolumeName(
dlc, active_slot == dlcservice::PartitionSlot::A
? dlcservice::PartitionSlot::B
: dlcservice::PartitionSlot::A);
verified_dlcs.emplace(PreserveLogicalVolumesWipeInfo{
.lv_name = dlc_inactive_lv_name,
.preserve = true,
.zero = true,
});
}
return verified_dlcs;
}
ClobberLvm::PreserveLogicalVolumesWipeInfos
ClobberLvm::PreserveLogicalVolumesWipeArgs(
const dlcservice::PartitionSlot slot) {
PreserveLogicalVolumesWipeInfos infos;
infos.insert({
{
.lv_name = kThinpool,
.preserve = true,
.zero = false,
},
{
.lv_name = kUnencrypted,
.preserve = false,
.zero = true,
},
});
const auto& dlcs = DlcPreserveLogicalVolumesWipeArgs(
base::FilePath(dlcservice::kDlcPowerwashSafeFile),
base::FilePath(imageloader::kDlcManifestRootpath), slot,
std::make_unique<dlcservice::Utils>());
infos.insert(std::cbegin(dlcs), std::cend(dlcs));
return infos;
}