blob: 553bf3dc75105e10d11bc45c319e51cd587fb69b [file] [log] [blame]
// Copyright 2020 The Fuchsia 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 "src/storage/lib/paver/gpt.h"
#include <dirent.h>
#include <fidl/fuchsia.device/cpp/wire.h>
#include <lib/fdio/directory.h>
#include <lib/fit/defer.h>
#include <string_view>
#include <fbl/algorithm.h>
#include <fbl/unique_fd.h>
#include <gpt/c/gpt.h>
#include "src/storage/lib/block_client/cpp/remote_block_device.h"
#include "src/storage/lib/paver/pave-logging.h"
#include "src/storage/lib/paver/utils.h"
namespace paver {
namespace {
using uuid::Uuid;
namespace block = fuchsia_hardware_block;
namespace device = fuchsia_device;
constexpr size_t ReservedHeaderBlocks(size_t blk_size) {
constexpr size_t kReservedEntryBlocks{static_cast<size_t>(16) * 1024};
return (kReservedEntryBlocks + 2 * blk_size) / blk_size;
}
zx::result<> RebindGptDriver(fidl::UnownedClientEnd<fuchsia_io::Directory> svc_root,
block_client::BlockDevice& device) {
zx::result pauser = BlockWatcherPauser::Create(svc_root);
if (pauser.is_error()) {
return pauser.take_error();
}
return device.Rebind("gpt.cm");
}
} // namespace
zx::result<Uuid> GptPartitionType(Partition type, PartitionScheme s) {
if (s == PartitionScheme::kLegacy) {
switch (type) {
case Partition::kBootloaderA:
return zx::ok(Uuid(GUID_EFI_VALUE));
case Partition::kZirconA:
return zx::ok(Uuid(GUID_ZIRCON_A_VALUE));
case Partition::kZirconB:
return zx::ok(Uuid(GUID_ZIRCON_B_VALUE));
case Partition::kZirconR:
return zx::ok(Uuid(GUID_ZIRCON_R_VALUE));
case Partition::kVbMetaA:
return zx::ok(Uuid(GUID_VBMETA_A_VALUE));
case Partition::kVbMetaB:
return zx::ok(Uuid(GUID_VBMETA_B_VALUE));
case Partition::kVbMetaR:
return zx::ok(Uuid(GUID_VBMETA_R_VALUE));
case Partition::kAbrMeta:
return zx::ok(Uuid(GUID_ABR_META_VALUE));
case Partition::kFuchsiaVolumeManager:
return zx::ok(Uuid(GUID_FVM_VALUE));
default:
ERROR("Partition type is invalid\n");
return zx::error(ZX_ERR_INVALID_ARGS);
}
} else {
switch (type) {
case Partition::kBootloaderA:
return zx::ok(Uuid(GUID_EFI_VALUE));
case Partition::kZirconA:
case Partition::kZirconB:
case Partition::kZirconR:
return zx::ok(Uuid(GPT_ZIRCON_ABR_TYPE_GUID));
case Partition::kVbMetaA:
case Partition::kVbMetaB:
case Partition::kVbMetaR:
return zx::ok(Uuid(GPT_VBMETA_ABR_TYPE_GUID));
case Partition::kAbrMeta:
return zx::ok(Uuid(GPT_DURABLE_BOOT_TYPE_GUID));
case Partition::kFuchsiaVolumeManager:
return zx::ok(Uuid(GPT_FVM_TYPE_GUID));
default:
ERROR("Partition type is invalid\n");
return zx::error(ZX_ERR_INVALID_ARGS);
}
}
}
bool FilterByName(const gpt_partition_t& part, std::string_view name) {
char cstring_name[GPT_NAME_LEN / 2 + 1];
::utf16_to_cstring(cstring_name, reinterpret_cast<const uint16_t*>(part.name),
sizeof(cstring_name));
if (name.length() != strnlen(cstring_name, sizeof(cstring_name))) {
return false;
}
// We use a case-insensitive comparison to be compatible with the previous naming scheme.
// On a ChromeOS device, all of the kernel partitions share a common GUID type, so we
// distinguish Zircon kernel partitions based on name.
return strncasecmp(cstring_name, name.data(), name.length()) == 0;
}
bool FilterByTypeAndName(const gpt_partition_t& part, const Uuid& type, std::string_view name) {
return type == Uuid(part.type) && FilterByName(part, name);
}
zx::result<std::vector<GptDevicePartitioner::GptClients>> GptDevicePartitioner::FindGptDevices(
const fbl::unique_fd& devfs_root) {
fbl::unique_fd block_fd;
if (zx_status_t status =
fdio_open_fd_at(devfs_root.get(), "class/block", 0, block_fd.reset_and_get_address());
status != ZX_OK) {
ERROR("Cannot inspect block devices: %s\n", zx_status_get_string(status));
return zx::error(status);
}
DIR* d = fdopendir(block_fd.duplicate().release());
if (d == nullptr) {
ERROR("Cannot inspect block devices: %s\n", strerror(errno));
return zx::error(ZX_ERR_INTERNAL);
}
const auto closer = fit::defer([d]() { closedir(d); });
fdio_cpp::FdioCaller block_caller(std::move(block_fd));
struct dirent* de;
std::vector<GptClients> found_devices;
while ((de = readdir(d)) != nullptr) {
if (std::string_view{de->d_name} == ".") {
continue;
}
zx::result block_endpoints = fidl::CreateEndpoints<fuchsia_hardware_block::Block>();
if (block_endpoints.is_error()) {
return zx::error(ZX_ERR_INTERNAL);
}
if (zx_status_t status =
fdio_service_connect_at(block_caller.borrow_channel(), de->d_name,
block_endpoints->server.TakeChannel().release());
status != ZX_OK) {
ERROR("Cannot connect %s: %s\n", de->d_name, zx_status_get_string(status));
continue;
}
{
const fidl::WireResult result = fidl::WireCall(block_endpoints->client)->GetInfo();
if (!result.ok()) {
ERROR("Cannot get block info from %s: %s\n", de->d_name,
result.FormatDescription().c_str());
continue;
}
const fit::result response = result.value();
if (response.is_error()) {
ERROR("Cannot get block info from %s: %s\n", de->d_name,
zx_status_get_string(response.error_value()));
continue;
}
if (response.value()->info.flags & fuchsia_hardware_block::wire::Flag::kRemovable) {
continue;
}
}
zx::result controller_endpoints = fidl::CreateEndpoints<fuchsia_device::Controller>();
if (controller_endpoints.is_error()) {
return zx::error(ZX_ERR_INTERNAL);
}
std::string controller_path = std::string(de->d_name) + "/device_controller";
if (zx_status_t status =
fdio_service_connect_at(block_caller.borrow_channel(), controller_path.c_str(),
controller_endpoints->server.TakeChannel().release());
status != ZX_OK) {
ERROR("Cannot connect %s: %s\n", de->d_name, zx_status_get_string(status));
continue;
}
const fidl::WireResult result =
fidl::WireCall(controller_endpoints->client)->GetTopologicalPath();
if (!result.ok()) {
ERROR("Cannot get topological path from %s: %s\n", de->d_name,
result.FormatDescription().c_str());
continue;
}
const fit::result response = result.value();
if (response.is_error()) {
ERROR("Cannot get topological path from %s: %s\n", de->d_name,
zx_status_get_string(response.error_value()));
continue;
}
std::string path_str(response.value()->path.get());
// The GPT which will be a non-removable block device that isn't a partition or fvm created
// partition itself.
if (path_str.find("part-") == std::string::npos &&
path_str.find("/fvm/") == std::string::npos) {
found_devices.emplace_back(GptClients{
.topological_path = path_str,
.block = std::move(block_endpoints->client),
.controller = std::move(controller_endpoints->client),
});
}
}
if (found_devices.empty()) {
ERROR("No candidate GPT found\n");
return zx::error(ZX_ERR_NOT_FOUND);
}
return zx::ok(std::move(found_devices));
}
zx::result<std::unique_ptr<GptDevicePartitioner>> GptDevicePartitioner::InitializeProvidedGptDevice(
fbl::unique_fd devfs_root, fidl::UnownedClientEnd<fuchsia_io::Directory> svc_root,
fidl::UnownedClientEnd<fuchsia_device::Controller> gpt_device) {
auto pauser = BlockWatcherPauser::Create(svc_root);
if (pauser.is_error()) {
ERROR("Failed to pause the block watcher\n");
return pauser.take_error();
}
// Connect to the volume protocol.
zx::result volume_endpoints = fidl::CreateEndpoints<fuchsia_hardware_block_volume::Volume>();
if (volume_endpoints.is_error()) {
ERROR("Warning: failed to create block endpoints: %s\n", volume_endpoints.status_string())
return volume_endpoints.take_error();
}
auto& [device, volume_server] = volume_endpoints.value();
if (fidl::OneWayError response =
fidl::WireCall(gpt_device)->ConnectToDeviceFidl(volume_server.TakeChannel());
!response.ok()) {
ERROR("Warning: failed to connect to GPT block protocol: %s\n",
response.FormatDescription().c_str());
return zx::error(response.status());
}
// Connect to the controller protocol.
zx::result controller_endpoints = fidl::CreateEndpoints<fuchsia_device::Controller>();
if (controller_endpoints.is_error()) {
ERROR("Warning: failed to create controller endpoints: %s\n",
controller_endpoints.status_string())
return controller_endpoints.take_error();
}
auto& [controller, controller_server] = controller_endpoints.value();
if (fidl::OneWayError response =
fidl::WireCall(gpt_device)->ConnectToController(std::move(controller_server));
!response.ok()) {
ERROR("Warning: failed to connect to GPT controller protocol: %s\n",
response.FormatDescription().c_str());
return zx::error(response.status());
}
const fidl::WireResult result = fidl::WireCall(device)->GetInfo();
if (!result.ok()) {
ERROR("Warning: Could not acquire GPT block info: %s\n", result.FormatDescription().c_str());
return zx::error(result.status());
}
fit::result response = result.value();
if (response.is_error()) {
ERROR("Warning: Could not acquire GPT block info: %s\n",
zx_status_get_string(response.error_value()));
return response.take_error();
}
const fuchsia_hardware_block::wire::BlockInfo& info = response.value()->info;
zx::result remote_device =
block_client::RemoteBlockDevice::Create(std::move(device), std::move(controller));
if (!remote_device.is_ok()) {
return remote_device.take_error();
}
zx::result gpt_result =
GptDevice::Create(std::move(remote_device.value()), info.block_size, info.block_count);
if (gpt_result.is_error()) {
ERROR("Failed to get GPT info: %s\n", gpt_result.status_string());
return zx::error(ZX_ERR_BAD_STATE);
}
std::unique_ptr<GptDevice>& gpt = gpt_result.value();
if (!gpt->Valid()) {
ERROR("Located GPT is invalid; Attempting to initialize\n");
if (gpt->RemoveAllPartitions() != ZX_OK) {
ERROR("Failed to create empty GPT\n");
return zx::error(ZX_ERR_BAD_STATE);
}
if (gpt->Sync() != ZX_OK) {
ERROR("Failed to sync empty GPT\n");
return zx::error(ZX_ERR_BAD_STATE);
}
if (zx::result status = RebindGptDriver(svc_root, gpt->device()); status.is_error()) {
ERROR("Failed to re-read GPT\n");
return status.take_error();
}
printf("Rebound GPT driver successfully\n");
}
return zx::ok(new GptDevicePartitioner(std::move(devfs_root), svc_root, std::move(gpt), info));
}
zx::result<GptDevicePartitioner::InitializeGptResult> GptDevicePartitioner::InitializeGpt(
fbl::unique_fd devfs_root, fidl::UnownedClientEnd<fuchsia_io::Directory> svc_root,
fidl::ClientEnd<fuchsia_device::Controller> block_controller) {
if (block_controller) {
zx::result status =
InitializeProvidedGptDevice(std::move(devfs_root), svc_root, std::move(block_controller));
if (status.is_error()) {
return status.take_error();
}
return zx::ok(InitializeGptResult{std::move(status.value()), false});
}
zx::result gpt_devices = FindGptDevices(devfs_root);
if (gpt_devices.is_error()) {
ERROR("Failed to find GPT: %s\n", gpt_devices.status_string());
return gpt_devices.take_error();
}
std::vector<fidl::ClientEnd<fuchsia_device::Controller>> non_removable_gpt_devices;
std::unique_ptr<GptDevicePartitioner> gpt_partitioner;
for (auto& gpt_device : gpt_devices.value()) {
const fidl::WireResult result = fidl::WireCall(gpt_device.block)->GetInfo();
if (!result.ok()) {
ERROR("Warning: Could not acquire GPT block info: %s\n", result.FormatDescription().c_str());
return zx::error(result.status());
}
fit::result response = result.value();
if (response.is_error()) {
ERROR("Warning: Could not acquire GPT block info: %s\n",
zx_status_get_string(response.error_value()));
return response.take_error();
}
const fuchsia_hardware_block::wire::BlockInfo& info = response.value()->info;
if (info.flags & block::wire::Flag::kRemovable) {
continue;
}
auto [controller, controller_server] = fidl::Endpoints<fuchsia_device::Controller>::Create();
if (fidl::OneWayStatus status = fidl::WireCall(gpt_device.controller)
->ConnectToController(std::move(controller_server));
!status.ok()) {
ERROR("Failed to connect to new controller %s\n", status.FormatDescription().c_str());
continue;
}
zx::result remote_device = block_client::RemoteBlockDevice::Create(
fidl::ClientEnd<fuchsia_hardware_block_volume::Volume>(gpt_device.block.TakeChannel()),
std::move(gpt_device.controller));
if (!remote_device.is_ok()) {
return remote_device.take_error();
}
zx::result gpt_result =
GptDevice::Create(std::move(remote_device.value()), info.block_size, info.block_count);
if (gpt_result.is_error()) {
ERROR("Failed to get GPT info: %s\n", gpt_result.status_string());
return zx::error(ZX_ERR_BAD_STATE);
}
std::unique_ptr<GptDevice>& gpt = gpt_result.value();
if (!gpt->Valid()) {
continue;
}
non_removable_gpt_devices.push_back(std::move(controller));
auto partitioner = WrapUnique(
new GptDevicePartitioner(devfs_root.duplicate(), svc_root, std::move(gpt), info));
if (partitioner->FindPartition(IsFvmPartition).is_error()) {
continue;
}
if (gpt_partitioner) {
ERROR("Found multiple block devices with valid GPTs. Unsuppported.\n");
return zx::error(ZX_ERR_NOT_SUPPORTED);
}
gpt_partitioner = std::move(partitioner);
}
if (gpt_partitioner) {
return zx::ok(InitializeGptResult{std::move(gpt_partitioner), false});
}
if (non_removable_gpt_devices.size() == 1) {
// If we only find a single non-removable gpt device, we initialize it's partition table.
auto status = InitializeProvidedGptDevice(std::move(devfs_root), svc_root,
std::move(non_removable_gpt_devices[0]));
if (status.is_error()) {
return status.take_error();
}
return zx::ok(InitializeGptResult{std::move(status.value()), true});
}
ERROR(
"Unable to find a valid GPT on this device with the expected partitions. "
"Please run *one* of the following command(s):\n");
for (const auto& device : gpt_devices.value()) {
ERROR("fx init-partition-tables %s\n", device.topological_path.c_str());
}
return zx::error(ZX_ERR_NOT_FOUND);
}
struct PartitionPosition {
size_t start; // Block, inclusive
size_t length; // In Blocks
};
zx::result<GptDevicePartitioner::FindFirstFitResult> GptDevicePartitioner::FindFirstFit(
size_t bytes_requested) const {
LOG("Looking for space\n");
// Gather GPT-related information.
size_t blocks_requested = (bytes_requested + block_info_.block_size - 1) / block_info_.block_size;
// Sort all partitions by starting block.
// For simplicity, include the 'start' and 'end' reserved spots as
// partitions.
size_t partition_count = 0;
PartitionPosition partitions[gpt::kPartitionCount + 2];
const size_t reserved_blocks = ReservedHeaderBlocks(block_info_.block_size);
partitions[partition_count].start = 0;
partitions[partition_count++].length = reserved_blocks;
partitions[partition_count].start = block_info_.block_count - reserved_blocks;
partitions[partition_count++].length = reserved_blocks;
for (uint32_t i = 0; i < gpt::kPartitionCount; i++) {
zx::result<const gpt_partition_t*> p = gpt_->GetPartition(i);
if (p.is_error()) {
continue;
}
partitions[partition_count].start = (*p)->first;
partitions[partition_count].length = (*p)->last - (*p)->first + 1;
LOG("Partition seen with start %zu, end %zu (length %zu)\n", (*p)->first, (*p)->last,
partitions[partition_count].length);
partition_count++;
}
LOG("Sorting\n");
qsort(partitions, partition_count, sizeof(PartitionPosition), [](const void* p1, const void* p2) {
ssize_t s1 = static_cast<ssize_t>(static_cast<const PartitionPosition*>(p1)->start);
ssize_t s2 = static_cast<ssize_t>(static_cast<const PartitionPosition*>(p2)->start);
return s1 == s2 ? 0 : (s1 > s2 ? +1 : -1);
});
// Look for space between the partitions. Since the reserved spots of the
// GPT were included in |partitions|, all available space will be located
// "between" partitions.
for (size_t i = 0; i < partition_count - 1; i++) {
const size_t next = partitions[i].start + partitions[i].length;
LOG("Partition[%zu] From Block [%zu, %zu) ... (next partition starts at block %zu)\n", i,
partitions[i].start, next, partitions[i + 1].start);
if (next > partitions[i + 1].start) {
ERROR("Corrupted GPT\n");
return zx::error(ZX_ERR_IO);
}
const size_t free_blocks = partitions[i + 1].start - next;
LOG(" There are %zu free blocks (%zu requested)\n", free_blocks, blocks_requested);
if (free_blocks >= blocks_requested) {
return zx::ok(FindFirstFitResult{next, free_blocks});
}
}
ERROR("No GPT space found\n");
return zx::error(ZX_ERR_NO_RESOURCES);
}
zx::result<Uuid> GptDevicePartitioner::CreateGptPartition(const char* name, const Uuid& type,
uint64_t offset, uint64_t blocks) const {
Uuid guid = Uuid::Generate();
if (zx_status_t status =
gpt_->AddPartition(name, type.bytes(), guid.bytes(), offset, blocks, 0).status_value();
status != ZX_OK) {
ERROR("Failed to add partition\n");
return zx::error(status);
}
if (zx_status_t status = gpt_->Sync(); status != ZX_OK) {
ERROR("Failed to sync GPT\n");
return zx::error(status);
}
if (auto status = zx::make_result(gpt_->ClearPartition(offset, 1)); status.is_error()) {
ERROR("Failed to clear first block of new partition\n");
return status.take_error();
}
if (zx::result status = RebindGptDriver(svc_root_, gpt_->device()); status.is_error()) {
ERROR("Failed to rebind GPT\n");
return status.take_error();
}
return zx::ok(guid);
}
zx::result<std::unique_ptr<PartitionClient>> GptDevicePartitioner::AddPartition(
const char* name, const Uuid& type, size_t minimum_size_bytes,
size_t optional_reserve_bytes) const {
auto status = FindFirstFit(minimum_size_bytes);
if (status.is_error()) {
ERROR("Couldn't find fit\n");
return status.take_error();
}
const size_t start = status->start;
size_t length = status->length;
LOG("Found space in GPT - OK %zu @ %zu\n", length, start);
if (optional_reserve_bytes) {
// If we can fulfill the requested size, and we still have space for the
// optional reserve section, then we should shorten the amount of blocks
// we're asking for.
//
// This isn't necessary, but it allows growing the GPT later, if necessary.
const size_t optional_reserve_blocks = optional_reserve_bytes / block_info_.block_size;
if (length - optional_reserve_bytes > (minimum_size_bytes / block_info_.block_size)) {
LOG("Space for reserve - OK\n");
length -= optional_reserve_blocks;
}
} else {
length = fbl::round_up(minimum_size_bytes, block_info_.block_size) / block_info_.block_size;
}
LOG("Final space in GPT - OK %zu @ %zu\n", length, start);
auto status_or_guid = CreateGptPartition(name, type, start, length);
if (status_or_guid.is_error()) {
return status_or_guid.take_error();
}
LOG("Added partition, waiting for bind\n");
auto status_or_part = OpenBlockPartition(devfs_root_, status_or_guid.value(), type, ZX_SEC(15));
if (status_or_part.is_error()) {
ERROR("Added partition, waiting for bind - NOT FOUND\n");
return status_or_part.take_error();
}
LOG("Added partition, waiting for bind - OK\n");
return zx::ok(new BlockPartitionClient(std::move(status_or_part.value())));
}
zx::result<GptDevicePartitioner::FindPartitionResult> GptDevicePartitioner::FindPartition(
FilterCallback filter) const {
for (uint32_t i = 0; i < gpt::kPartitionCount; i++) {
zx::result<gpt_partition_t*> p = gpt_->GetPartition(i);
if (p.is_error()) {
continue;
}
if (filter(**p)) {
LOG("Found partition in GPT, partition %u\n", i);
auto status = OpenBlockPartition(devfs_root_, Uuid((*p)->guid), Uuid((*p)->type), ZX_SEC(5));
if (status.is_error()) {
ERROR("Couldn't open partition: %s\n", status.status_string());
return status.take_error();
}
auto part = std::make_unique<BlockPartitionClient>(std::move(status.value()));
return zx::ok(FindPartitionResult{std::move(part), *p});
}
}
return zx::error(ZX_ERR_NOT_FOUND);
}
zx::result<> GptDevicePartitioner::WipePartitions(FilterCallback filter) const {
bool modify = false;
for (uint32_t i = 0; i < gpt::kPartitionCount; i++) {
zx::result<const gpt_partition_t*> p = gpt_->GetPartition(i);
if (p.is_error() || !filter(**p)) {
continue;
}
modify = true;
// Ignore the return status; wiping is a best-effort approach anyway.
static_cast<void>(WipeBlockPartition(devfs_root_, Uuid((*p)->guid), Uuid((*p)->type)));
if (gpt_->RemovePartition((*p)->guid) != ZX_OK) {
ERROR("Warning: Could not remove partition\n");
} else {
// If we successfully clear the partition, then all subsequent
// partitions get shifted down. If we just deleted partition 'i',
// we now need to look at partition 'i' again, since it's now
// occupied by what was in 'i+1'.
i--;
}
}
if (modify) {
gpt_->Sync();
LOG("Immediate reboot strongly recommended\n");
}
static_cast<void>(RebindGptDriver(svc_root_, gpt_->device()));
return zx::ok();
}
zx::result<> GptDevicePartitioner::WipeFvm() const {
return WipeBlockPartition(devfs_root_, std::nullopt, Uuid(GUID_FVM_VALUE));
}
zx::result<> GptDevicePartitioner::WipePartitionTables() const {
return WipePartitions([](const gpt_partition_t&) { return true; });
}
} // namespace paver