blob: 91435bc7dd87980763817f31f287c33352ffccc6 [file]
// Copyright 2022 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/virtualization/bin/termina_guest_manager/block_devices.h"
#include <dirent.h>
#include <fcntl.h>
#include <fidl/fuchsia.io/cpp/wire.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/namespace.h>
#include <lib/fit/defer.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/trace/event.h>
#include <zircon/hw/gpt.h>
#include <filesystem>
#include <fbl/unique_fd.h>
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/storage/block_client/cpp/remote_block_device.h"
namespace {
namespace fio = fuchsia_io;
constexpr size_t kNumRetries = 5;
constexpr auto kRetryDelay = zx::msec(100);
constexpr const char kBlockPath[] = "/dev/class/block";
constexpr auto kGuidSize = fuchsia::hardware::block::partition::GUID_LENGTH;
constexpr std::array<uint8_t, kGuidSize> kFvmGuid = GUID_FVM_VALUE;
constexpr std::array<uint8_t, kGuidSize> kGptFvmGuid = GPT_FVM_TYPE_GUID;
using VolumeHandle = fidl::InterfaceHandle<fuchsia::hardware::block::volume::Volume>;
using ManagerHandle = fidl::InterfaceHandle<fuchsia::hardware::block::volume::VolumeManager>;
// Information about a disk image.
struct DiskImage {
const char* path; // Path to the file containing the image
bool read_only;
bool create_file;
};
#if defined(USE_VOLATILE_BLOCK)
constexpr bool kForceVolatileWrites = true;
#else
constexpr bool kForceVolatileWrites = false;
#endif
constexpr DiskImage kBlockFileStatefulImage = DiskImage{
// NOTE: This assumes the /data directory is using Fxfs
.path = "/data/fxfs_virtualization_guest_image",
.read_only = false,
.create_file = true,
};
constexpr DiskImage kFileStatefulImage = DiskImage{
.path = "/data/fxfs_virtualization_guest_image",
.read_only = false,
.create_file = true,
};
constexpr DiskImage kExtrasImage = DiskImage{
.path = "/pkg/data/termina_extras.img",
.read_only = true,
.create_file = false,
};
// Finds the guest FVM partition, and the FVM GPT partition.
zx::result<std::tuple<VolumeHandle, ManagerHandle>> FindPartitions(DIR* dir) {
VolumeHandle volume;
ManagerHandle manager;
fdio_cpp::UnownedFdioCaller caller(dirfd(dir));
for (dirent* entry; (entry = readdir(dir)) != nullptr;) {
fuchsia::hardware::block::partition::PartitionSyncPtr partition;
zx_status_t status = fdio_service_connect_at(caller.borrow_channel(), entry->d_name,
partition.NewRequest().TakeChannel().release());
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to connect to '" << entry->d_name
<< "': " << zx_status_get_string(status);
return zx::error(status);
}
zx_status_t guid_status;
std::unique_ptr<fuchsia::hardware::block::partition::Guid> guid;
status = partition->GetTypeGuid(&guid_status, &guid);
if (status != ZX_OK || guid_status != ZX_OK || !guid) {
continue;
}
if (std::equal(kGuestPartitionGuid.begin(), kGuestPartitionGuid.end(), guid->value.begin())) {
// If we find the guest FVM partition, then we can break out of the loop.
// We only need to find the FVM GPT partition if there is no guest FVM
// partition, in order to create the guest FVM partition.
volume.set_channel(partition.Unbind().TakeChannel());
break;
}
if (std::equal(kFvmGuid.begin(), kFvmGuid.end(), guid->value.begin()) ||
std::equal(kGptFvmGuid.begin(), kGptFvmGuid.end(), guid->value.begin())) {
fuchsia::device::ControllerSyncPtr controller;
controller.Bind(partition.Unbind().TakeChannel());
fuchsia::device::Controller_GetTopologicalPath_Result topo_result;
status = controller->GetTopologicalPath(&topo_result);
if (status != ZX_OK || topo_result.is_err()) {
FX_LOGS(ERROR) << "Failed to get topological path for '" << entry->d_name << "'";
return zx::error(ZX_ERR_IO);
}
auto fvm_path = topo_result.response().path + "/fvm";
status = fdio_service_connect(fvm_path.data(), manager.NewRequest().TakeChannel().release());
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to connect to '" << fvm_path
<< "': " << zx_status_get_string(status);
return zx::error(status);
}
}
}
return zx::ok(std::make_tuple(std::move(volume), std::move(manager)));
}
// Waits for the guest partition to be allocated.
//
// TODO(fxbug.dev/90469): Use a directory watcher instead of scanning for
// new partitions.
zx::result<VolumeHandle> WaitForPartition(DIR* dir) {
for (size_t retry = 0; retry != kNumRetries; retry++) {
auto partitions = FindPartitions(dir);
if (partitions.is_error()) {
return partitions.take_error();
}
auto& [volume, manager] = *partitions;
if (volume) {
return zx::ok(std::move(volume));
}
zx::nanosleep(zx::deadline_after(kRetryDelay));
}
FX_LOGS(ERROR) << "Failed to create guest partition";
return zx::error(ZX_ERR_IO);
}
// Locates the FVM partition for a guest block device. If a partition does not
// exist, allocate one.
zx::result<VolumeHandle> FindOrAllocatePartition(std::string_view path, size_t partition_size,
size_t min_size) {
auto dir = opendir(path.data());
if (dir == nullptr) {
FX_LOGS(ERROR) << "Failed to open directory '" << path << "'";
return zx::error(ZX_ERR_IO);
}
auto defer = fit::defer([dir] { closedir(dir); });
auto partitions = FindPartitions(dir);
if (partitions.is_error()) {
return partitions.take_error();
}
auto& [volume, manager] = *partitions;
if (!volume) {
if (!manager) {
FX_LOGS(ERROR) << "Failed to find FVM";
return zx::error(ZX_ERR_NOT_FOUND);
}
auto sync = manager.BindSync();
zx_status_t info_status = ZX_OK;
// Get the partition slice size.
std::unique_ptr<fuchsia::hardware::block::volume::VolumeManagerInfo> info;
zx_status_t status = sync->GetInfo(&info_status, &info);
if (status != ZX_OK || info_status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to get volume info: " << zx_status_get_string(status) << " and "
<< zx_status_get_string(info_status);
return zx::error(ZX_ERR_IO);
}
// Round up to the next full slice.
size_t slices = (partition_size + info->slice_size - 1) / info->slice_size;
// Avoid allocating more than 90% of the disk space. This is a somewhat arbitrary limit prevent
// fully consuming all remaining disk space for the VM.
size_t available_slices = info->slice_count - info->assigned_slice_count;
size_t usable_slices = available_slices * 9 / 10;
size_t usable_bytes = usable_slices * info->slice_size;
if (usable_bytes < min_size) {
FX_LOGS(ERROR) << "Only " << usable_bytes << "b usable (limited to 90%% of available space);"
<< " unable to allocate disk";
return zx::error(ZX_ERR_NO_SPACE);
}
if (slices > usable_slices) {
size_t requested_bytes = slices * info->slice_size;
FX_LOGS(WARNING) << "Requested disk of " << requested_bytes << "b but only " << usable_bytes
<< "b usable (90%% of avilable space); clamping";
FX_LOGS(WARNING) << "\tslice_size " << info->slice_size;
FX_LOGS(WARNING) << "\tslice_count " << info->slice_count;
FX_LOGS(WARNING) << "\tassigned_slice_count " << info->assigned_slice_count;
FX_LOGS(WARNING) << "\tmaxiumum_slice_count " << info->maximum_slice_count;
FX_LOGS(WARNING) << "\tmax_virtual_slice " << info->max_virtual_slice;
slices = usable_slices;
}
zx_status_t part_status = ZX_OK;
status = sync->AllocatePartition(slices, {.value = kGuestPartitionGuid}, {},
kGuestPartitionName, 0, &part_status);
if (status != ZX_OK || part_status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to allocate partition: " << zx_status_get_string(status) << " and "
<< zx_status_get_string(part_status);
return zx::error(ZX_ERR_IO);
}
return WaitForPartition(dir);
}
return zx::ok(std::move(volume));
}
// Opens the given disk image.
zx::result<fuchsia::io::FileHandle> GetPartition(const DiskImage& image) {
TRACE_DURATION("termina_guest_manager", "GetPartition");
fuchsia::io::OpenFlags flags = fuchsia::io::OpenFlags::RIGHT_READABLE;
if (!image.read_only) {
flags |= fuchsia::io::OpenFlags::RIGHT_WRITABLE;
}
if (image.create_file) {
flags |= fuchsia::io::OpenFlags::CREATE;
}
fuchsia::io::FileHandle file;
zx_status_t status = fdio_open(image.path, static_cast<uint32_t>(flags),
file.NewRequest().TakeChannel().release());
if (status) {
return zx::error(status);
}
return zx::ok(std::move(file));
}
// Opens the given disk image.
zx::result<fidl::InterfaceHandle<fuchsia::hardware::block::Block>> GetFxfsPartition(
const DiskImage& image, const size_t image_size_bytes) {
TRACE_DURATION("linux_runner", "GetFxfsPartition");
// First, use regular file operations to make a huge file at image.path
// NOTE: image.path is assumed to be a path on an Fxfs filesystem
fbl::unique_fd fd(open(image.path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR));
if (!fd) {
FX_LOGS(ERROR) << "open(image.path) failed with errno: " << strerror(errno);
return zx::error(ZX_ERR_IO);
}
// Make sure the file is the requested size (image_size_bytes).
// NOTE: This is usually a huge size (e.g. 40 gigabytes).
off_t existingFilesize = lseek(fd.get(), 0, SEEK_END);
if (existingFilesize == static_cast<off_t>(-1) ||
static_cast<size_t>(existingFilesize) < image_size_bytes) {
if (ftruncate(fd.get(), image_size_bytes) == -1) {
FX_LOGS(ERROR) << "ftruncate(image.path) failed with errno: " << strerror(errno);
return zx::error(ZX_ERR_IO);
}
}
if (close(fd.release()) == -1) {
FX_LOGS(ERROR) << "close(image.path) failed with errno: " << strerror(errno);
return zx::error(ZX_ERR_IO);
}
/// Now we can try to reopen the file, but in block device mode
/// First we have to open the parent directory...
auto dir_endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
if (dir_endpoints.status_value() != ZX_OK) {
FX_PLOGS(ERROR, dir_endpoints.status_value())
<< "CreateEndpoints() for Fxfs parent directory failed";
return zx::error(dir_endpoints.status_value());
}
auto [dir_client, dir_server] = *std::move(dir_endpoints);
std::filesystem::path image_path(image.path);
uint32_t dir_flags = static_cast<uint32_t>(
fio::OpenFlags::kRightReadable | fio::OpenFlags::kRightWritable | fio::OpenFlags::kDirectory);
zx_status_t dir_open_status =
fdio_open(image_path.parent_path().c_str(), dir_flags, dir_server.TakeChannel().release());
if (dir_open_status != ZX_OK) {
FX_PLOGS(ERROR, dir_open_status) << "fdio_open(Fxfs image.path.parent) failed";
return zx::error(dir_open_status);
}
// We want to open the "file" at image.path, but as a block device (i.e. fuchsia.hardware.block).
fio::OpenFlags flags = fio::OpenFlags::kRightReadable;
if (!image.read_only) {
flags |= fio::OpenFlags::kRightWritable;
}
auto device_endpoints = fidl::CreateEndpoints<fuchsia_io::Node>();
if (device_endpoints.status_value() != ZX_OK) {
FX_PLOGS(ERROR, device_endpoints.status_value())
<< "CreateEndpoints() for Fxfs block device file failed";
return zx::error(device_endpoints.status_value());
}
auto [device_client, device_server] = *std::move(device_endpoints);
flags |= fio::OpenFlags::kBlockDevice;
// TODO(fxbug.dev/103241): Consider using io2 for the Open() call.
auto device_open_result =
fidl::WireCall(dir_client)
->Open(flags, {}, fidl::StringView::FromExternal(image_path.filename().c_str()),
std::move(device_server));
if (!device_open_result.ok()) {
FX_PLOGS(ERROR, device_open_result.status())
<< "WireCall->Open(image.path) as Fxfs block device failed";
return zx::error(device_open_result.status());
}
return zx::ok(fuchsia::hardware::block::BlockHandle(device_client.TakeChannel()));
}
} // namespace
fit::result<std::string, std::vector<fuchsia::virtualization::BlockSpec>> GetBlockDevices(
const termina_config::Config& structured_config, size_t min_size) {
TRACE_DURATION("termina_guest_manager", "Guest::GetBlockDevices");
std::vector<fuchsia::virtualization::BlockSpec> devices;
const uint64_t stateful_image_size_bytes = structured_config.stateful_partition_size();
// Get/create the stateful partition.
fuchsia::virtualization::BlockSpec stateful_spec;
stateful_spec.id = "stateful";
FX_LOGS(INFO) << "Adding stateful partition type: "
<< structured_config.stateful_partition_type();
if (structured_config.stateful_partition_type() == "block-file") {
// Use a file opened with OpenFlags.BLOCK_DEVICE.
auto handle = GetFxfsPartition(kBlockFileStatefulImage, stateful_image_size_bytes);
if (handle.is_error()) {
return fit::error(
fxl::StringPrintf("Failed to open or create stateful Fxfs file / block device: %s",
zx_status_get_string(handle.error_value())));
}
stateful_spec.mode = fuchsia::virtualization::BlockMode::READ_WRITE;
stateful_spec.format.set_block(std::move(handle.value()));
} else if (structured_config.stateful_partition_type() == "fvm") {
// FVM
auto handle = FindOrAllocatePartition(kBlockPath, stateful_image_size_bytes, min_size);
if (handle.is_error()) {
return fit::error(fxl::StringPrintf("Failed to find or allocate a partition: %s",
zx_status_get_string(handle.error_value())));
}
stateful_spec.mode = fuchsia::virtualization::BlockMode::READ_WRITE;
stateful_spec.format.set_block(
fidl::InterfaceHandle<fuchsia::hardware::block::Block>(handle.value().TakeChannel()));
} else if (structured_config.stateful_partition_type() == "file") {
// Simple files.
auto handle = GetPartition(kFileStatefulImage);
if (handle.is_error()) {
return fit::error(fxl::StringPrintf("Failed to open or create stateful file: %s",
zx_status_get_string(handle.error_value())));
}
auto ptr = handle->BindSync();
fuchsia::io::File_Resize_Result resize_result;
zx_status_t status = ptr->Resize(stateful_image_size_bytes, &resize_result);
if (status != ZX_OK || resize_result.is_err()) {
return fit::error(fxl::StringPrintf("Failed resize stateful file: %s/%s",
zx_status_get_string(status),
zx_status_get_string(resize_result.err())));
}
stateful_spec.mode = fuchsia::virtualization::BlockMode::READ_WRITE;
stateful_spec.format.set_file(ptr.Unbind());
}
if (kForceVolatileWrites) {
stateful_spec.mode = fuchsia::virtualization::BlockMode::VOLATILE_WRITE;
}
devices.push_back(std::move(stateful_spec));
// Add the extras partition if it exists.
auto extras = GetPartition(kExtrasImage);
if (extras.is_ok()) {
devices.push_back({
.id = "extras",
.mode = fuchsia::virtualization::BlockMode::VOLATILE_WRITE,
.format = fuchsia::virtualization::BlockFormat::WithFile(std::move(extras.value())),
});
}
return fit::success(std::move(devices));
}
void DropDevNamespace() {
// Drop access to /dev, in order to prevent any further access.
fdio_ns_t* ns;
zx_status_t status = fdio_ns_get_installed(&ns);
FX_CHECK(status == ZX_OK) << "Failed to get installed namespace";
if (fdio_ns_is_bound(ns, "/dev")) {
status = fdio_ns_unbind(ns, "/dev");
FX_CHECK(status == ZX_OK) << "Failed to unbind '/dev' from the installed namespace";
}
}
zx::result<> WipeStatefulPartition(size_t bytes_to_zero, uint8_t value,
VolumeAction volume_action) {
auto dir = opendir(kBlockPath);
if (dir == nullptr) {
FX_LOGS(ERROR) << "Failed to open directory '" << kBlockPath << "'";
return zx::error(ZX_ERR_IO);
}
auto defer = fit::defer([dir] { closedir(dir); });
auto partitions = FindPartitions(dir);
if (partitions.is_error()) {
FX_LOGS(ERROR) << "Failed to find partition";
return zx::error(ZX_ERR_NOT_FOUND);
}
auto& [volume, manager] = *partitions;
if (!volume) {
FX_LOGS(ERROR) << "Failed to find volume";
return zx::error(ZX_ERR_NOT_FOUND);
}
// For devices that support TRIM, there is a more efficient path we could take. Since we expect
// to move the stateful partition to fxfs before too long we keep this logic simple and don't
// attempt to optimize for devices that support TRIM.
constexpr size_t kWipeBufferSize = 65536; // 64 KiB write buffer
uint8_t bytes[kWipeBufferSize];
memset(&bytes, value, kWipeBufferSize);
for (size_t offset = 0; offset < bytes_to_zero; offset += kWipeBufferSize) {
zx_status_t status = block_client::SingleWriteBytes(
fidl::UnownedClientEnd<fuchsia_hardware_block::Block>(volume.channel().borrow()), bytes,
std::min(bytes_to_zero - offset, kWipeBufferSize), offset);
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to write bytes";
return zx::error(ZX_ERR_IO);
}
}
// Now deallocate the partition. This will allow us to recreate the FVM partition the next time
// we start the VM.
//
// Note we still need to zero bytes because FVM can reallocate the same slices the next time we
// create the volume.
if (volume_action == VolumeAction::REMOVE) {
auto volume_sync = volume.BindSync();
zx_status_t fidl_status;
zx_status_t status = volume_sync->Destroy(&fidl_status);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Transport Error Destroying FVM partition";
return zx::error(status);
}
if (fidl_status != ZX_OK) {
FX_PLOGS(ERROR, fidl_status) << "FIDL Error Destroying FVM partition";
return zx::error(fidl_status);
}
}
return zx::ok();
}