| // Copyright 2018 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 "fvm.h" |
| |
| #include <dirent.h> |
| #include <fcntl.h> |
| #include <fidl/fuchsia.device/cpp/fidl.h> |
| #include <fidl/fuchsia.hardware.block.partition/cpp/wire.h> |
| #include <fidl/fuchsia.hardware.block.volume/cpp/wire.h> |
| #include <fidl/fuchsia.hardware.block/cpp/wire.h> |
| #include <lib/component/incoming/cpp/protocol.h> |
| #include <lib/device-watcher/cpp/device-watcher.h> |
| #include <lib/fdio/cpp/caller.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fit/defer.h> |
| #include <lib/fzl/vmo-mapper.h> |
| #include <lib/zx/channel.h> |
| #include <lib/zx/fifo.h> |
| #include <lib/zx/time.h> |
| #include <lib/zx/vmo.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <memory> |
| |
| #include <fbl/algorithm.h> |
| #include <fbl/array.h> |
| #include <fbl/string_buffer.h> |
| #include <fbl/unique_fd.h> |
| #include <ramdevice-client/ramdisk.h> |
| #include <safemath/safe_math.h> |
| |
| #include "pave-logging.h" |
| #include "src/lib/uuid/uuid.h" |
| #include "src/security/lib/zxcrypt/client.h" |
| #include "src/storage/fvm/format.h" |
| #include "src/storage/fvm/fvm_sparse.h" |
| #include "src/storage/lib/block_client/cpp/client.h" |
| #include "src/storage/lib/fs_management/cpp/fvm.h" |
| |
| namespace paver { |
| namespace { |
| |
| namespace block = fuchsia_hardware_block; |
| namespace partition = fuchsia_hardware_block_partition; |
| namespace volume = fuchsia_hardware_block_volume; |
| namespace device = fuchsia_device; |
| |
| using fuchsia_hardware_block_volume::wire::VolumeManagerInfo; |
| |
| constexpr const char kDevPath[] = "/dev/"; |
| constexpr const char kFvmName[] = "fvm"; |
| constexpr const char kFvmControllerPath[] = "fvm/device_controller"; |
| |
| // The number of additional slices a partition will need to become |
| // zxcrypt'd. |
| // |
| // TODO(aarongreen): Replace this with a value supplied by ulib/zxcrypt. |
| constexpr size_t kZxcryptExtraSlices = 1; |
| |
| zx::result<std::string> GetTopoPath(fidl::UnownedClientEnd<fuchsia_device::Controller> controller) { |
| fidl::Result result = fidl::Call(controller)->GetTopologicalPath(); |
| if (result.is_error()) { |
| if (result.error_value().is_domain_error()) { |
| return zx::error(result.error_value().domain_error()); |
| } |
| if (result.error_value().is_framework_error()) { |
| return zx::error(result.error_value().framework_error().status()); |
| } |
| } |
| return zx::ok(std::move(result.value().path())); |
| } |
| |
| // Confirm that the file descriptor to the underlying partition exists within an |
| // FVM, not, for example, a GPT or MBR. |
| // |
| // |out| is true if |fd| is a VPartition, else false. |
| zx::result<bool> FvmIsVirtualPartition( |
| fidl::UnownedClientEnd<fuchsia_device::Controller> controller) { |
| zx::result path = GetTopoPath(controller); |
| if (path.is_error()) { |
| return path.take_error(); |
| } |
| return zx::ok(path.value().find("fvm") != std::string::npos); |
| } |
| |
| // Describes the state of a partition actively being written |
| // out to disk. |
| struct PartitionInfo { |
| fvm::PartitionDescriptor* pd = nullptr; |
| fvm::PartitionDescriptor aligned_pd = {}; |
| fidl::ClientEnd<fuchsia_device::Controller> controller; |
| fidl::ClientEnd<partition::Partition> partition; |
| bool active = false; |
| |
| zx::result<> SetPartition(fidl::ClientEnd<fuchsia_device::Controller> controller) { |
| fidl::ClientEnd<partition::Partition> partition; |
| zx::result server = fidl::CreateEndpoints(&partition); |
| if (server.is_error()) { |
| ERROR("Failed creating endpoints: %s", server.status_string()); |
| return server.take_error(); |
| } |
| if (fidl::OneWayStatus status = |
| fidl::WireCall(controller)->ConnectToDeviceFidl(server->TakeChannel()); |
| !status.ok()) { |
| ERROR("Failed to connect to device fidl: %s", status.FormatDescription().c_str()); |
| return zx::error(status.status()); |
| } |
| this->controller = std::move(controller); |
| this->partition = std::move(partition); |
| return zx::ok(); |
| } |
| }; |
| |
| ptrdiff_t GetExtentOffset(size_t extent) { |
| return safemath::checked_cast<ptrdiff_t>(sizeof(fvm::PartitionDescriptor) + |
| extent * sizeof(fvm::ExtentDescriptor)); |
| } |
| |
| fvm::ExtentDescriptor GetExtent(fvm::PartitionDescriptor* pd, size_t extent) { |
| fvm::ExtentDescriptor descriptor = {}; |
| const auto* descriptor_ptr = reinterpret_cast<uint8_t*>(pd) + GetExtentOffset(extent); |
| memcpy(&descriptor, descriptor_ptr, sizeof(fvm::ExtentDescriptor)); |
| return descriptor; |
| } |
| |
| zx_status_t FlushClient(block_client::Client& client) { |
| block_fifo_request_t request; |
| request.group = 0; |
| request.vmoid = block::wire::kVmoidInvalid; |
| request.command = {.opcode = BLOCK_OPCODE_FLUSH, .flags = 0}; |
| request.length = 0; |
| request.vmo_offset = 0; |
| request.dev_offset = 0; |
| |
| return client.Transaction(&request, 1); |
| } |
| |
| // Stream an FVM partition to disk. |
| zx_status_t StreamFvmPartition(fvm::SparseReader* reader, PartitionInfo* part, |
| const fzl::VmoMapper& mapper, block_client::Client& client, |
| size_t block_size, block_fifo_request_t* request) { |
| size_t slice_size = reader->Image()->slice_size; |
| const size_t vmo_cap = mapper.size(); |
| for (size_t e = 0; e < part->aligned_pd.extent_count; e++) { |
| LOG("Writing extent %zu... \n", e); |
| fvm::ExtentDescriptor ext = GetExtent(part->pd, e); |
| size_t offset = ext.slice_start * slice_size; |
| size_t bytes_left = ext.extent_length; |
| |
| // Write real data |
| while (bytes_left > 0) { |
| size_t actual; |
| zx_status_t status = reader->ReadData(reinterpret_cast<uint8_t*>(mapper.start()), |
| std::min(bytes_left, vmo_cap), &actual); |
| if (status != ZX_OK) { |
| ERROR("Error reading extent data with %zu bytes of %zu remaining: %s\n", bytes_left, |
| ext.extent_length, zx_status_get_string(status)); |
| return status; |
| } |
| |
| const size_t vmo_sz = actual; |
| bytes_left -= actual; |
| |
| if (vmo_sz == 0) { |
| ERROR("Read nothing from src_fd; %zu bytes left\n", bytes_left); |
| return ZX_ERR_IO; |
| } |
| if (vmo_sz % block_size != 0) { |
| ERROR("Cannot write non-block size multiple: %zu\n", vmo_sz); |
| return ZX_ERR_IO; |
| } |
| |
| uint64_t length = vmo_sz / block_size; |
| if (length > UINT32_MAX) { |
| ERROR("Error writing partition: Too large\n"); |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| request->length = static_cast<uint32_t>(length); |
| request->vmo_offset = 0; |
| request->dev_offset = offset / block_size; |
| |
| if (zx_status_t status = client.Transaction(request, 1); status != ZX_OK) { |
| ERROR("Error writing partition data\n"); |
| return status; |
| } |
| |
| offset += vmo_sz; |
| } |
| |
| // Write trailing zeroes (which are implied, but were omitted from |
| // transfer). |
| bytes_left = (ext.slice_count * slice_size) - ext.extent_length; |
| if (bytes_left > 0) { |
| LOG("%zu bytes written, %zu zeroes left\n", ext.extent_length, bytes_left); |
| memset(mapper.start(), 0, vmo_cap); |
| } |
| while (bytes_left > 0) { |
| uint64_t length = std::min(bytes_left, vmo_cap) / block_size; |
| if (length > UINT32_MAX) { |
| ERROR("Error writing trailing zeroes: Too large(%lu)\n", length); |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| request->length = static_cast<uint32_t>(length); |
| request->vmo_offset = 0; |
| request->dev_offset = offset / block_size; |
| |
| if (zx_status_t status = client.Transaction(request, 1); status != ZX_OK) { |
| ERROR("Error writing trailing zeroes length:%u dev_offset:%lu vmo_offset:%lu\n", |
| request->length, request->dev_offset, request->vmo_offset); |
| return status; |
| } |
| |
| offset += request->length * block_size; |
| bytes_left -= request->length * block_size; |
| } |
| } |
| return ZX_OK; |
| } |
| |
| } // namespace |
| |
| zx::result<fidl::ClientEnd<fuchsia_device::Controller>> TryBindToFvmDriver( |
| const fbl::unique_fd& devfs_root, fidl::UnownedClientEnd<fuchsia_device::Controller> partition, |
| zx::duration timeout) { |
| zx::result topo_path = GetTopoPath(partition); |
| if (topo_path.is_error()) { |
| ERROR("Failed to get topological path: %s\n", topo_path.status_string()); |
| return topo_path.take_error(); |
| } |
| std::string path = std::move(topo_path.value()); |
| if (!cpp20::starts_with(std::string_view(path), kDevPath)) { |
| ERROR("Topo path does not start with '%s': %s\n", kDevPath, path.c_str()); |
| return zx::error(ZX_ERR_BAD_STATE); |
| } |
| path.erase(0, std::string_view(kDevPath).length()); |
| |
| // Check if fvm already exists, and return it if it does. |
| int fd; |
| if (zx_status_t status = |
| fdio_open_fd_at(devfs_root.get(), path.c_str(), |
| static_cast<uint32_t>(fuchsia_io::OpenFlags::kDirectory), &fd); |
| status != ZX_OK) { |
| ERROR("Failed to open %s: %s\n", path.c_str(), zx_status_get_string(status)); |
| return zx::error(status); |
| } |
| DIR* dir = fdopendir(fd); |
| auto cleanup = fit::defer([dir]() { closedir(dir); }); |
| |
| fdio_cpp::UnownedFdioCaller dir_caller(dirfd(dir)); |
| while (true) { |
| struct dirent* de = readdir(dir); |
| if (!de) { |
| break; |
| } |
| std::string_view name = de->d_name; |
| if (name != kFvmName) { |
| continue; |
| } |
| return component::ConnectAt<fuchsia_device::Controller>(dir_caller.directory(), |
| kFvmControllerPath); |
| } |
| |
| LOG("Could not find fvm: proceeding to rebind...\n"); |
| |
| constexpr char kFvmDriverLib[] = "fvm.cm"; |
| fidl::WireResult result = fidl::WireCall(partition)->Rebind(fidl::StringView(kFvmDriverLib)); |
| if (!result.ok()) { |
| ERROR("Could not call rebind driver: %s\n", zx_status_get_string(result.status())); |
| return zx::error(result.status()); |
| } |
| if (result.value().is_error() && result.value().error_value() != ZX_ERR_ALREADY_BOUND) { |
| ERROR("Could not rebind fvm driver: %s\n", zx_status_get_string(result.value().error_value())); |
| return zx::error(result.value().error_value()); |
| } |
| |
| auto fvm_controller_path = std::string(kFvmName) + "/device_controller"; |
| zx::result channel = |
| device_watcher::RecursiveWaitForFile(dirfd(dir), fvm_controller_path.c_str(), timeout); |
| if (channel.is_error()) { |
| ERROR("Error waiting for fvm driver to bind: %s\n", channel.status_string()); |
| return channel.take_error(); |
| } |
| return zx::ok(fidl::ClientEnd<fuchsia_device::Controller>(std::move(channel.value()))); |
| } |
| |
| zx::result<fidl::ClientEnd<fuchsia_device::Controller>> FvmPartitionFormat( |
| const fbl::unique_fd& devfs_root, |
| fidl::UnownedClientEnd<fuchsia_hardware_block::Block> partition, |
| fidl::UnownedClientEnd<fuchsia_device::Controller> partition_controller, |
| const fvm::SparseImage& header, BindOption option, FormatResult* format_result) { |
| // Although the format (based on the magic in the FVM superblock) |
| // indicates this is (or at least was) an FVM image, it may be invalid. |
| // |
| // Attempt to bind the FVM driver to this partition, but fall-back to |
| // reinitializing the FVM image so the rest of the paving |
| // process can continue successfully. |
| if (format_result != nullptr) { |
| *format_result = FormatResult::kUnknown; |
| } |
| if (option == BindOption::TryBind) { |
| fs_management::DiskFormat df = fs_management::DetectDiskFormat(partition); |
| if (df == fs_management::kDiskFormatFvm) { |
| zx::result fvm = TryBindToFvmDriver(devfs_root, partition_controller, zx::sec(3)); |
| if (fvm.is_ok()) { |
| LOG("Found already formatted FVM.\n"); |
| auto [volume, volume_server] = fidl::Endpoints<volume::VolumeManager>::Create(); |
| if (fidl::OneWayError status = |
| fidl::WireCall(fvm.value())->ConnectToDeviceFidl(volume_server.TakeChannel()); |
| !status.ok()) { |
| return zx::error(status.status()); |
| } |
| fidl::WireResult result = fidl::WireCall(volume)->GetInfo(); |
| if (result.ok()) { |
| auto get_maximum_slice_count = [](const fvm::SparseImage& header) { |
| return fvm::Header::FromDiskSize(fvm::kMaxUsablePartitions, header.maximum_disk_size, |
| header.slice_size) |
| .GetAllocationTableAllocatedEntryCount(); |
| }; |
| if (result.value().info->slice_size != header.slice_size) { |
| ERROR("Mismatched slice size. Reinitializing FVM.\n"); |
| } else if (header.maximum_disk_size > 0 && |
| result.value().info->maximum_slice_count < get_maximum_slice_count(header)) { |
| ERROR("Mismatched maximum slice count. Reinitializing FVM.\n"); |
| } else { |
| if (format_result != nullptr) { |
| *format_result = FormatResult::kPreserved; |
| } |
| return fvm; |
| } |
| } else { |
| ERROR("Could not query FVM for info. Reinitializing FVM.\n"); |
| } |
| } else { |
| ERROR( |
| "Saw fs_management::kDiskFormatFvm, but could not bind driver. Reinitializing FVM.\n"); |
| } |
| } |
| } |
| |
| LOG("Initializing partition as FVM\n"); |
| { |
| if (format_result != nullptr) { |
| *format_result = FormatResult::kReformatted; |
| } |
| |
| const fidl::WireResult result = fidl::WireCall(partition)->GetInfo(); |
| if (!result.ok()) { |
| ERROR("Failed to query block info: %s\n", result.FormatDescription().c_str()); |
| return zx::error(result.status()); |
| } |
| const fit::result response = result.value(); |
| if (response.is_error()) { |
| ERROR("Failed to query block info: %s\n", zx_status_get_string(response.error_value())); |
| return zx::error(response.error_value()); |
| } |
| const fuchsia_hardware_block::wire::BlockInfo& info = response.value()->info; |
| |
| uint64_t initial_disk_size = info.block_count * info.block_size; |
| uint64_t max_disk_size = |
| (header.maximum_disk_size == 0) ? initial_disk_size : header.maximum_disk_size; |
| |
| zx_status_t status = fs_management::FvmInitPreallocated(partition, initial_disk_size, |
| max_disk_size, header.slice_size); |
| if (status != ZX_OK) { |
| ERROR("Failed to initialize fvm: %s\n", zx_status_get_string(status)); |
| return zx::error(status); |
| } |
| } |
| |
| return TryBindToFvmDriver(devfs_root, partition_controller, zx::sec(3)); |
| } |
| |
| namespace { |
| |
| // Formats a block device as a zxcrypt volume. |
| // |
| // On success, returns the VolumeManager for the zxcrypt instance, and writes the inner block |
| // device's into |part|. |
| zx::result<zxcrypt::VolumeManager> ZxcryptCreate(PartitionInfo* part) { |
| // TODO(security): https://fxbug.dev/42106007. We need to bind with channel in order to pass a key |
| // here. |
| // TODO(security): https://fxbug.dev/42106740. The created volume must marked as needing key |
| // rotation. |
| |
| fbl::unique_fd devfs_root; |
| if (zx_status_t status = fdio_open_fd("/dev", 0, devfs_root.reset_and_get_address()); |
| status != ZX_OK) { |
| return zx::error(status); |
| } |
| |
| zxcrypt::VolumeManager zxcrypt_manager(std::move(part->controller), std::move(devfs_root)); |
| zx::channel client_chan; |
| if (zx_status_t status = zxcrypt_manager.OpenClient(zx::sec(3), client_chan); status != ZX_OK) { |
| ERROR("Could not open zxcrypt volume manager\n"); |
| return zx::error(status); |
| } |
| zxcrypt::EncryptedVolumeClient zxcrypt_client(std::move(client_chan)); |
| uint8_t slot = 0; |
| if (zx_status_t status = zxcrypt_client.FormatWithImplicitKey(slot); status != ZX_OK) { |
| ERROR("Could not create zxcrypt volume\n"); |
| return zx::error(status); |
| } |
| |
| if (zx_status_t status = zxcrypt_client.UnsealWithImplicitKey(slot); status != ZX_OK) { |
| ERROR("Could not unseal zxcrypt volume\n"); |
| return zx::error(status); |
| } |
| |
| zx::result controller = zxcrypt_manager.OpenInnerBlockDevice(zx::sec(3)); |
| if (controller.is_error()) { |
| ERROR("Could not open zxcrypt volume: %s\n", controller.status_string()); |
| return controller.take_error(); |
| } |
| if (zx::result status = part->SetPartition(std::move(controller.value())); status.is_error()) { |
| return status.take_error(); |
| } |
| |
| return zx::ok(std::move(zxcrypt_manager)); |
| } |
| |
| zx::result<bool> FvmPartitionIsChild(fidl::UnownedClientEnd<fuchsia_device::Controller> fvm, |
| fidl::UnownedClientEnd<fuchsia_device::Controller> partition) { |
| zx::result fvm_path = GetTopoPath(fvm); |
| if (fvm_path.is_error()) { |
| ERROR("Couldn't get topological path of FVM: %s\n", fvm_path.status_string()); |
| return fvm_path.take_error(); |
| } |
| zx::result partition_path = GetTopoPath(partition); |
| if (partition_path.is_error()) { |
| ERROR("Couldn't get topological path of partition: %s\n", partition_path.status_string()); |
| return partition_path.take_error(); |
| } |
| if (cpp20::starts_with(std::string_view(fvm_path.value()), |
| std::string_view(partition_path.value()))) { |
| ERROR("Partition does not exist within FVM: partition='%s' fvm='%s'\n", |
| partition_path.value().c_str(), fvm_path.value().c_str()); |
| return zx::ok(false); |
| } |
| return zx::ok(true); |
| } |
| |
| void RecommendWipe(const char* problem) { |
| Warn(problem, "Please run 'install-disk-image wipe' to wipe your partitions"); |
| } |
| |
| // Calculate the amount of space necessary for the incoming partitions, |
| // validating the header along the way. Additionally, deletes any old partitions |
| // which match the type GUID of the provided partition. |
| // |
| // Parses the information from the |reader| into |parts|. |
| zx_status_t PreProcessPartitions(fidl::UnownedClientEnd<fuchsia_device::Controller> fvm, |
| const std::unique_ptr<fvm::SparseReader>& reader, |
| const fbl::Array<PartitionInfo>& parts, |
| size_t* out_requested_slices) { |
| fvm::PartitionDescriptor* part = reader->Partitions(); |
| fvm::SparseImage* hdr = reader->Image(); |
| |
| // Validate the header and determine the necessary slice requirements for |
| // all partitions and all offsets. |
| size_t requested_slices = 0; |
| for (size_t p = 0; p < hdr->partition_count; p++) { |
| parts[p].pd = part; |
| memcpy(&parts[p].aligned_pd, part, sizeof(fvm::PartitionDescriptor)); |
| if (parts[p].pd->magic != fvm::kPartitionDescriptorMagic) { |
| ERROR("Bad partition magic\n"); |
| return ZX_ERR_IO; |
| } |
| |
| if (zx_status_t status = WipeAllFvmPartitionsWithGuid(fvm, parts[p].pd->type); |
| status != ZX_OK) { |
| ERROR("Failure wiping old partitions matching this GUID: %s\n", zx_status_get_string(status)); |
| return status; |
| } |
| |
| fvm::ExtentDescriptor ext = GetExtent(parts[p].pd, 0); |
| if (ext.magic != fvm::kExtentDescriptorMagic) { |
| ERROR("Bad extent magic\n"); |
| return ZX_ERR_IO; |
| } |
| if (ext.slice_start != 0) { |
| ERROR("First slice must start at zero\n"); |
| return ZX_ERR_IO; |
| } |
| if (ext.slice_count == 0) { |
| ERROR("Extents must have > 0 slices\n"); |
| return ZX_ERR_IO; |
| } |
| if (ext.extent_length > ext.slice_count * hdr->slice_size) { |
| ERROR("Extent length(%lu) must fit within allocated slice count(%lu * %lu)\n", |
| ext.extent_length, ext.slice_count, hdr->slice_size); |
| return ZX_ERR_IO; |
| } |
| |
| // Filter drivers may require additional space. |
| if ((parts[p].aligned_pd.flags & fvm::kSparseFlagZxcrypt) != 0) { |
| requested_slices += kZxcryptExtraSlices; |
| } |
| |
| for (size_t e = 1; e < parts[p].aligned_pd.extent_count; e++) { |
| ext = GetExtent(parts[p].pd, e); |
| if (ext.magic != fvm::kExtentDescriptorMagic) { |
| ERROR("Bad extent magic\n"); |
| return ZX_ERR_IO; |
| } |
| if (ext.slice_count == 0) { |
| ERROR("Extents must have > 0 slices\n"); |
| return ZX_ERR_IO; |
| } |
| if (ext.extent_length > ext.slice_count * hdr->slice_size) { |
| char name[sizeof(parts[p].aligned_pd.name) + 1]; |
| name[sizeof(parts[p].aligned_pd.name)] = '\0'; |
| memcpy(name, parts[p].aligned_pd.name, sizeof(parts[p].aligned_pd.name)); |
| ERROR("Partition(%s) extent length(%lu) must fit within allocated slice count(%lu * %lu)\n", |
| name, ext.extent_length, ext.slice_count, hdr->slice_size); |
| return ZX_ERR_IO; |
| } |
| |
| requested_slices += ext.slice_count; |
| } |
| part = reinterpret_cast<fvm::PartitionDescriptor*>( |
| reinterpret_cast<uint8_t*>(parts[p].pd) + sizeof(fvm::PartitionDescriptor) + |
| parts[p].aligned_pd.extent_count * sizeof(fvm::ExtentDescriptor)); |
| } |
| |
| *out_requested_slices = requested_slices; |
| return ZX_OK; |
| } |
| |
| struct BoundZxcryptDevice { |
| public: |
| BoundZxcryptDevice() = default; |
| BoundZxcryptDevice(BoundZxcryptDevice&&) = default; |
| BoundZxcryptDevice(const BoundZxcryptDevice&) = delete; |
| explicit BoundZxcryptDevice(zxcrypt::VolumeManager&& manager) : manager(std::move(manager)) {} |
| ~BoundZxcryptDevice() { |
| if (manager) { |
| if (zx_status_t status = manager->Unbind(); status != ZX_OK) { |
| ERROR("Failed to unbind Zxcrypt: %s. The driver may be unavailable.", |
| zx_status_get_string(status)); |
| } |
| } |
| } |
| std::optional<zxcrypt::VolumeManager> manager; |
| }; |
| |
| // Allocates the space requested by the partitions by creating new |
| // partitions and filling them with extents. This guarantees that |
| // streaming the data to the device will not run into "no space" issues |
| // later. |
| // Partitions which are zxcrypt-enabled will have their driver bound. A set of all such drivers is |
| // returned so the caller can unbind them when finished writing into them. |
| zx::result<std::vector<BoundZxcryptDevice>> AllocatePartitions( |
| const fbl::unique_fd& devfs_root, |
| fidl::UnownedClientEnd<fuchsia_hardware_block_volume::VolumeManager> fvm_device, |
| fbl::Array<PartitionInfo>* parts) { |
| fdio_cpp::UnownedFdioCaller devfs_caller(devfs_root); |
| fidl::UnownedClientEnd devfs_client_end = devfs_caller.directory(); |
| |
| std::vector<BoundZxcryptDevice> bound_devices; |
| for (PartitionInfo& part_info : *parts) { |
| fvm::ExtentDescriptor ext = GetExtent(part_info.pd, 0); |
| // Allocate this partition as inactive so it gets deleted on the next |
| // reboot if this stream fails. |
| uint32_t flags = part_info.active ? 0 : volume::wire::kAllocatePartitionFlagInactive; |
| uint64_t slice_count = ext.slice_count; |
| uuid::Uuid type_guid(part_info.pd->type); |
| uuid::Uuid instance_guid = uuid::Uuid::Generate(); |
| const char* name = reinterpret_cast<const char*>(part_info.pd->name); |
| std::string_view name_view(name, strnlen(name, sizeof(part_info.pd->name))); |
| { |
| char name[sizeof(part_info.pd->name) + 1]; |
| name[sizeof(part_info.pd->name)] = '\0'; |
| memcpy(name, part_info.pd->name, sizeof(part_info.pd->name)); |
| LOG("Allocating partition %s consisting of %zu slices\n", name, slice_count); |
| } |
| if (zx::result channel = fs_management::FvmAllocatePartitionWithDevfs( |
| devfs_client_end, fvm_device, slice_count, type_guid, instance_guid, name_view, flags); |
| channel.is_error()) { |
| ERROR("Couldn't allocate partition: %s\n", channel.status_string()); |
| return zx::error(ZX_ERR_NO_SPACE); |
| } else { |
| if (zx::result status = part_info.SetPartition(std::move(channel.value())); |
| status.is_error()) { |
| return status.take_error(); |
| } |
| } |
| |
| // Add filter drivers. |
| if ((part_info.pd->flags & fvm::kSparseFlagZxcrypt) != 0) { |
| LOG("Creating zxcrypt volume\n"); |
| auto volume_manager = ZxcryptCreate(&part_info); |
| if (volume_manager.is_error()) { |
| return volume_manager.take_error(); |
| } |
| bound_devices.emplace_back(std::move(*volume_manager)); |
| } |
| |
| // The 0th index extent is allocated alongside the partition, so we |
| // begin indexing from the 1st extent here. |
| for (size_t e = 1; e < part_info.pd->extent_count; e++) { |
| ext = GetExtent(part_info.pd, e); |
| uint64_t offset = ext.slice_start; |
| uint64_t length = ext.slice_count; |
| |
| fidl::UnownedClientEnd<volume::Volume> volume(part_info.partition.channel().borrow()); |
| auto result = fidl::WireCall(volume)->Extend(offset, length); |
| auto status = result.ok() ? result.value().status : result.status(); |
| if (status != ZX_OK) { |
| ERROR("Failed to extend partition: %s\n", zx_status_get_string(status)); |
| return zx::error(status); |
| } |
| } |
| } |
| |
| return zx::ok(std::move(bound_devices)); |
| } |
| |
| // Holds the description of a partition with a single extent. Note that even though some code asks |
| // for a PartitionDescriptor, in reality it treats that as a descriptor followed by a bunch of |
| // extents, so this copes with that de-facto pattern. |
| struct FvmPartition { |
| // Returns an FVM partition with no real information about extents. In order to |
| // use the partitions, they should be formatted with the appropriate filesystem. |
| static FvmPartition Make(const std::array<uint8_t, fvm::kGuidSize> partition_type, |
| std::string_view name) { |
| FvmPartition partition{.extent = { |
| .slice_count = 1, |
| }}; |
| std::copy(std::begin(partition_type), std::end(partition_type), |
| std::begin(partition.descriptor.type)); |
| std::copy(name.begin(), name.end(), std::begin(partition.descriptor.name)); |
| return partition; |
| } |
| |
| fvm::PartitionDescriptor descriptor; |
| fvm::ExtentDescriptor extent; |
| }; |
| |
| } // namespace |
| |
| // Deletes all partitions within the FVM with a type GUID matching |type_guid| |
| // until there are none left. |
| zx_status_t WipeAllFvmPartitionsWithGuid(fidl::UnownedClientEnd<fuchsia_device::Controller> fvm, |
| const uint8_t type_guid[]) { |
| zx::result fvm_topo_path = GetTopoPath(fvm); |
| if (fvm_topo_path.is_error()) { |
| ERROR("Couldn't get topological path of FVM! %s\n", fvm_topo_path.status_string()); |
| return fvm_topo_path.error_value(); |
| } |
| |
| fs_management::PartitionMatcher matcher{ |
| .type_guids = {uuid::Uuid(&type_guid[0])}, |
| .parent_device = fvm_topo_path->c_str(), |
| }; |
| for (;;) { |
| zx::result old_partition = fs_management::OpenPartition(matcher, /* wait=*/false); |
| if (old_partition.is_error()) { |
| if (old_partition.error_value() == ZX_ERR_NOT_FOUND) { |
| return ZX_OK; |
| } |
| return old_partition.error_value(); |
| } |
| zx::result is_vpartition = FvmIsVirtualPartition(*old_partition); |
| if (is_vpartition.is_error()) { |
| ERROR("Couldn't confirm old vpartition type: %s\n", is_vpartition.status_string()); |
| return ZX_ERR_IO; |
| } |
| |
| if (zx::result result = FvmPartitionIsChild(fvm, *old_partition); result.is_ok()) { |
| if (!result.value()) { |
| RecommendWipe("Streaming a partition type which also exists outside the target FVM"); |
| return ZX_ERR_BAD_STATE; |
| } |
| } else { |
| std::string error = std::string("Failed to check if partition type is a child of the FVM: ") + |
| result.status_string(); |
| RecommendWipe(error.c_str()); |
| return result.error_value(); |
| } |
| if (!is_vpartition.value()) { |
| RecommendWipe("Streaming a partition type which also exists in a GPT"); |
| return ZX_ERR_BAD_STATE; |
| } |
| |
| // We're paving a partition that already exists within the FVM: let's |
| // destroy it before we pave anew. |
| |
| auto [volume, volume_server] = fidl::Endpoints<volume::Volume>::Create(); |
| if (fidl::OneWayError status = |
| fidl::WireCall(*old_partition)->ConnectToDeviceFidl(volume_server.TakeChannel()); |
| !status.ok()) { |
| return status.status(); |
| } |
| fidl::WireResult result = fidl::WireCall(volume)->Destroy(); |
| zx_status_t status = result.ok() ? result.value().status : result.status(); |
| if (status != ZX_OK) { |
| ERROR("Couldn't destroy partition: %s\n", zx_status_get_string(status)); |
| return status; |
| } |
| } |
| } |
| |
| zx::result<> AllocateEmptyPartitions( |
| const fbl::unique_fd& devfs_root, |
| fidl::UnownedClientEnd<fuchsia_hardware_block_volume::VolumeManager> fvm_device) { |
| FvmPartition fvm_partitions[] = { |
| FvmPartition::Make(std::array<uint8_t, fvm::kGuidSize>(GUID_BLOB_VALUE), |
| paver::kBlobfsPartitionLabel), |
| FvmPartition::Make(std::array<uint8_t, fvm::kGuidSize>(GUID_DATA_VALUE), |
| paver::kDataPartitionLabel)}; |
| fbl::Array<PartitionInfo> partitions(new PartitionInfo[2]{{ |
| .pd = &fvm_partitions[0].descriptor, |
| .active = true, |
| }, |
| { |
| .pd = &fvm_partitions[1].descriptor, |
| .active = true, |
| }}, |
| 2); |
| zx::result res = AllocatePartitions(devfs_root, fvm_device, &partitions); |
| if (res.is_error()) { |
| return res.take_error(); |
| } |
| return zx::ok(); |
| } |
| |
| zx::result<> FvmStreamPartitions(const fbl::unique_fd& devfs_root, |
| std::unique_ptr<PartitionClient> partition_client, |
| std::unique_ptr<fvm::ReaderInterface> payload) { |
| if (!partition_client->SupportsBlockPartition()) { |
| ERROR("tlp partition is not a block device\n"); |
| return zx::error(ZX_ERR_NOT_SUPPORTED); |
| } |
| BlockPartitionClient* block = reinterpret_cast<BlockPartitionClient*>(partition_client.get()); |
| |
| std::unique_ptr<fvm::SparseReader> reader; |
| zx::result<> status = zx::ok(); |
| if (status = zx::make_result(fvm::SparseReader::Create(std::move(payload), &reader)); |
| status.is_error()) { |
| return status.take_error(); |
| } |
| |
| LOG("Header Validated - OK\n"); |
| |
| fvm::SparseImage* hdr = reader->Image(); |
| // Acquire an fd to the FVM, either by finding one that already |
| // exists, or formatting a new one. |
| zx::result fvm = FvmPartitionFormat(devfs_root, block->block_channel(), |
| block->controller_channel(), *hdr, BindOption::TryBind); |
| if (fvm.is_error()) { |
| ERROR("Couldn't find FVM partition: %s\n", fvm.status_string()); |
| return fvm.take_error(); |
| } |
| |
| fbl::Array<PartitionInfo> parts(new PartitionInfo[hdr->partition_count], hdr->partition_count); |
| |
| // Parse the incoming image and calculate its size. |
| // |
| // Additionally, delete the old versions of any new partitions. |
| size_t requested_slices = 0; |
| if (status = zx::make_result(PreProcessPartitions(fvm.value(), reader, parts, &requested_slices)); |
| status.is_error()) { |
| ERROR("Failed to validate partitions: %s\n", status.status_string()); |
| return status.take_error(); |
| } |
| // Get a connection to the fvm's volume manager. |
| fidl::ClientEnd<volume::VolumeManager> volume_manager; |
| { |
| zx::result volume_server = fidl::CreateEndpoints(&volume_manager); |
| if (volume_server.is_error()) { |
| return volume_server.take_error(); |
| } |
| if (fidl::OneWayError status = |
| fidl::WireCall(fvm.value())->ConnectToDeviceFidl(volume_server.value().TakeChannel()); |
| !status.ok()) { |
| return zx::error(status.status()); |
| } |
| } |
| |
| // Contend with issues from an image that may be too large for this device. |
| VolumeManagerInfo info; |
| { |
| fidl::WireResult result = fidl::WireCall(volume_manager)->GetInfo(); |
| if (!result.ok()) { |
| ERROR("Failed to call FVM info: %s\n", zx_status_get_string(result.error().status())); |
| return zx::error(result.error().status()); |
| } |
| fidl::WireResponse response = result.value(); |
| if (response.status != ZX_OK) { |
| ERROR("Failed to get FVM info: %s\n", zx_status_get_string(response.status)); |
| return zx::error(response.status); |
| } |
| info = *response.info.get(); |
| } |
| size_t free_slices = info.slice_count - info.assigned_slice_count; |
| if (info.slice_count < requested_slices) { |
| char buf[256]; |
| snprintf(buf, sizeof(buf), "Image size (%zu) > Storage size (%zu)", |
| requested_slices * hdr->slice_size, info.slice_count * hdr->slice_size); |
| Warn(buf, "Image is too large to be paved to device"); |
| return zx::error(ZX_ERR_NO_SPACE); |
| } |
| if (free_slices < requested_slices) { |
| Warn("Not enough space to non-destructively pave", |
| "Automatically reinitializing FVM; Expect data loss"); |
| fvm = FvmPartitionFormat(devfs_root, block->block_channel(), block->controller_channel(), *hdr, |
| BindOption::Reformat); |
| if (fvm.is_error()) { |
| ERROR("Couldn't reformat FVM partition: %s\n", fvm.status_string()); |
| return zx::error(ZX_ERR_IO); |
| } |
| LOG("FVM Reformatted successfully.\n"); |
| } |
| |
| LOG("Partitions pre-validated successfully: Enough space exists to pave.\n"); |
| |
| // Actually allocate the storage for the incoming image. |
| zx::result devices = AllocatePartitions(devfs_root, volume_manager, &parts); |
| if (devices.is_error()) { |
| ERROR("Failed to allocate partitions: %s\n", devices.status_string()); |
| return devices.take_error(); |
| } |
| |
| LOG("Partition space pre-allocated successfully.\n"); |
| |
| constexpr size_t vmo_size = 1 << 20; |
| |
| fzl::VmoMapper mapping; |
| zx::vmo vmo; |
| if (mapping.CreateAndMap(vmo_size, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, nullptr, &vmo) != ZX_OK) { |
| ERROR("Failed to create stream VMO\n"); |
| return zx::error(ZX_ERR_NO_MEMORY); |
| } |
| |
| // Now that all partitions are preallocated, begin streaming data to them. |
| for (size_t p = 0; p < parts.size(); p++) { |
| const fidl::WireResult result = fidl::WireCall(parts[p].partition)->GetInfo(); |
| if (!result.ok()) { |
| ERROR("Couldn't get partition block info: %s\n", result.FormatDescription().c_str()); |
| return zx::error(result.status()); |
| } |
| fit::result response = result.value(); |
| if (response.is_error()) { |
| ERROR("Couldn't get partition 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; |
| |
| auto [session, server] = fidl::Endpoints<block::Session>::Create(); |
| if (fidl::Status result = fidl::WireCall(parts[p].partition)->OpenSession(std::move(server)); |
| !result.ok()) { |
| return zx::error(result.status()); |
| } |
| const fidl::WireResult fifo_result = fidl::WireCall(session)->GetFifo(); |
| if (!fifo_result.ok()) { |
| return zx::error(fifo_result.status()); |
| } |
| fit::result fifo_response = fifo_result.value(); |
| if (fifo_response.is_error()) { |
| return fifo_response.take_error(); |
| } |
| block_client::Client client(std::move(session), std::move(fifo_response.value()->fifo)); |
| zx::result vmoid = client.RegisterVmo(vmo); |
| if (vmoid.is_error()) { |
| return vmoid.take_error(); |
| } |
| |
| block_fifo_request_t request = { |
| .command = {.opcode = BLOCK_OPCODE_WRITE, .flags = 0}, |
| .group = 0, |
| .vmoid = vmoid->get(), |
| }; |
| |
| LOG("Streaming partition %zu\n", p); |
| status = zx::make_result( |
| StreamFvmPartition(reader.get(), &parts[p], mapping, client, info.block_size, &request)); |
| LOG("Done streaming partition %zu\n", p); |
| if (status.is_error()) { |
| ERROR("Failed to stream partition status=%d\n", status.error_value()); |
| return status.take_error(); |
| } |
| if (status = zx::make_result(FlushClient(client)); status.is_error()) { |
| ERROR("Failed to flush client\n"); |
| return status.take_error(); |
| } |
| LOG("Done flushing partition %zu\n", p); |
| } |
| |
| for (const PartitionInfo& part_info : parts) { |
| // Upgrade the old partition (currently active) to the new partition (currently |
| // inactive) so the new partition persists. |
| auto result = fidl::WireCall(part_info.partition)->GetInstanceGuid(); |
| if (!result.ok() || result.value().status != ZX_OK) { |
| ERROR("Failed to get unique GUID of new partition\n"); |
| return zx::error(ZX_ERR_BAD_STATE); |
| } |
| auto* guid = result.value().guid.get(); |
| |
| fidl::WireResult result2 = fidl::WireCall(volume_manager)->Activate(*guid, *guid); |
| if (result2.status() != ZX_OK || result2.value().status != ZX_OK) { |
| ERROR("Failed to upgrade partition\n"); |
| return zx::error(ZX_ERR_IO); |
| } |
| } |
| |
| return zx::ok(); |
| } |
| |
| // Unbinds the FVM driver from the given device. Assumes that the driver is either |
| // loaded or not (but not in the process of being loaded). |
| zx_status_t FvmUnbind(const fbl::unique_fd& devfs_root, const char* device) { |
| size_t len = strnlen(device, PATH_MAX); |
| constexpr const char* kDevPath = "/dev/"; |
| constexpr size_t kDevPathLen = std::char_traits<char>::length(kDevPath); |
| |
| if (len == PATH_MAX || len <= kDevPathLen) { |
| ERROR("Invalid device name: %s\n", device); |
| return ZX_ERR_INVALID_ARGS; |
| } |
| fbl::StringBuffer<PATH_MAX - 1> name_buffer; |
| name_buffer.Append(device + kDevPathLen); |
| name_buffer.Append("/fvm/device_controller"); |
| |
| fdio_cpp::UnownedFdioCaller caller(devfs_root.get()); |
| zx::result channel = component::ConnectAt<device::Controller>(caller.directory(), name_buffer); |
| if (channel.is_error()) { |
| ERROR("Unable to connect to FVM service: %s on device %s\n", channel.status_string(), |
| name_buffer.c_str()); |
| return channel.status_value(); |
| } |
| auto resp = fidl::WireCall(channel.value())->ScheduleUnbind(); |
| if (resp.status() != ZX_OK) { |
| ERROR("Failed to schedule FVM unbind: %s on device %s\n", zx_status_get_string(resp.status()), |
| name_buffer.data()); |
| return resp.status(); |
| } |
| if (resp->is_error()) { |
| ERROR("FVM unbind failed: %s on device %s\n", zx_status_get_string(resp->error_value()), |
| name_buffer.data()); |
| return resp->error_value(); |
| } |
| return ZX_OK; |
| } |
| |
| } // namespace paver |