blob: 69e70c4359eb8ef13c565cd7b2352fda51d83ec1 [file] [log] [blame]
// 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 <lib/paver/paver.h>
#include <dirent.h>
#include <fcntl.h>
#include <libgen.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <block-client/cpp/client.h>
#include <crypto/bytes.h>
#include <fbl/algorithm.h>
#include <fbl/array.h>
#include <fbl/auto_call.h>
#include <fbl/unique_ptr.h>
#include <fbl/vector.h>
#include <fs-management/fvm.h>
#include <fs-management/mount.h>
#include <fuchsia/device/c/fidl.h>
#include <fuchsia/hardware/block/volume/c/fidl.h>
#include <fuchsia/hardware/skipblock/c/fidl.h>
#include <fuchsia/hardware/zxcrypt/c/fidl.h>
#include <fvm/format.h>
#include <fvm/fvm-sparse.h>
#include <fvm/sparse-reader.h>
#include <lib/cksum.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fdio.h>
#include <lib/fzl/fdio.h>
#include <lib/fzl/resizeable-vmo-mapper.h>
#include <lib/fzl/vmo-mapper.h>
#include <lib/zx/fifo.h>
#include <lib/zx/vmo.h>
#include <ramdevice-client/ramdisk.h>
#include <zircon/boot/image.h>
#include <zircon/device/block.h>
#include <zircon/status.h>
#include <zircon/syscalls.h>
#include <zxcrypt/fdio-volume.h>
#include <utility>
#include "pave-logging.h"
#include "pave-utils.h"
#include "stream-reader.h"
#include "vmo-reader.h"
#define ZXCRYPT_DRIVER_LIB "/boot/driver/zxcrypt.so"
namespace paver {
namespace {
using volume_info_t = fuchsia_hardware_block_volume_VolumeInfo;
Partition PartitionType(fuchsia_paver_Configuration configuration, fuchsia_paver_Asset asset) {
switch (asset) {
case fuchsia_paver_Asset_KERNEL: {
switch (configuration) {
case fuchsia_paver_Configuration_A:
return Partition::kZirconA;
case fuchsia_paver_Configuration_B:
return Partition::kZirconB;
case fuchsia_paver_Configuration_RECOVERY:
return Partition::kZirconR;
};
break;
}
case fuchsia_paver_Asset_VERIFIED_BOOT_METADATA: {
switch (configuration) {
case fuchsia_paver_Configuration_A:
return Partition::kVbMetaA;
case fuchsia_paver_Configuration_B:
return Partition::kVbMetaB;
case fuchsia_paver_Configuration_RECOVERY:
return Partition::kVbMetaR;
};
break;
}
};
return Partition::kUnknown;
}
// 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;
// Looks up the topological path of a device.
// |buf| is the buffer the path will be written to. |buf_len| is the total
// capcity of the buffer, including space for a null byte.
// Upon success, |buf| will contain the null-terminated topological path.
zx_status_t GetTopoPathFromFd(const fbl::unique_fd& fd, char* buf, size_t buf_len) {
fdio_t* io = fdio_unsafe_fd_to_io(fd.get());
if (io == nullptr) {
return ZX_ERR_BAD_STATE;
}
zx_status_t call_status;
size_t path_len;
zx_status_t status = fuchsia_device_ControllerGetTopologicalPath(
fdio_unsafe_borrow_channel(io), &call_status, buf, buf_len - 1, &path_len);
fdio_unsafe_release(io);
if (status != ZX_OK) {
return status;
}
if (call_status != ZX_OK) {
return call_status;
}
buf[path_len] = 0;
return ZX_OK;
}
// 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_status_t FvmIsVirtualPartition(const fbl::unique_fd& fd, bool* out) {
char path[PATH_MAX];
zx_status_t status = GetTopoPathFromFd(fd, path, sizeof(path));
if (status != ZX_OK) {
return ZX_ERR_IO;
}
*out = strstr(path, "fvm") != nullptr;
return ZX_OK;
}
// Describes the state of a partition actively being written
// out to disk.
struct PartitionInfo {
PartitionInfo()
: pd(nullptr) {}
fvm::partition_descriptor_t* pd;
fbl::unique_fd new_part;
};
inline fvm::extent_descriptor_t* GetExtent(fvm::partition_descriptor_t* pd, size_t extent) {
return reinterpret_cast<fvm::extent_descriptor_t*>(reinterpret_cast<uintptr_t>(pd) +
sizeof(fvm::partition_descriptor_t) +
extent * sizeof(fvm::extent_descriptor_t));
}
// Registers a FIFO
zx_status_t RegisterFastBlockIo(const fbl::unique_fd& fd, const zx::vmo& vmo, vmoid_t* out_vmoid,
block_client::Client* out_client) {
fzl::UnownedFdioCaller disk_connection(fd.get());
zx::unowned_channel channel(disk_connection.borrow_channel());
zx::fifo fifo;
zx_status_t status;
zx_status_t io_status =
fuchsia_hardware_block_BlockGetFifo(channel->get(), &status, fifo.reset_and_get_address());
if (io_status != ZX_OK)
return io_status;
if (status != ZX_OK)
return status;
zx::vmo dup;
if (vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup) != ZX_OK) {
ERROR("Couldn't duplicate buffer vmo\n");
return ZX_ERR_IO;
}
fuchsia_hardware_block_VmoID vmoid;
io_status =
fuchsia_hardware_block_BlockAttachVmo(channel->get(), dup.release(), &status, &vmoid);
if (io_status != ZX_OK)
return io_status;
if (status != ZX_OK)
return status;
*out_vmoid = vmoid.id;
return block_client::Client::Create(std::move(fifo), out_client);
}
// Stream an FVM partition to disk.
zx_status_t StreamFvmPartition(fvm::SparseReader* reader, PartitionInfo* part,
const fzl::VmoMapper& mapper, const 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->pd->extent_count; e++) {
LOG("Writing extent %zu... \n", e);
fvm::extent_descriptor_t* 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 vmo_sz = 0;
size_t actual;
zx_status_t status =
reader->ReadData(&reinterpret_cast<uint8_t*>(mapper.start())[vmo_sz],
fbl::min(bytes_left, vmo_cap - vmo_sz), &actual);
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;
} else if (vmo_sz % block_size != 0) {
ERROR("Cannot write non-block size multiple: %zu\n", vmo_sz);
return ZX_ERR_IO;
} else if (status != ZX_OK) {
ERROR("Error reading partition data\n");
return status;
}
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;
ssize_t r;
if ((r = client.Transaction(request, 1)) != ZX_OK) {
ERROR("Error writing partition data\n");
return static_cast<zx_status_t>(r);
}
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 = fbl::min(bytes_left, vmo_cap) / block_size;
if (length > UINT32_MAX) {
ERROR("Error writing trailing zeroes: 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;
zx_status_t status;
if ((status = client.Transaction(request, 1)) != ZX_OK) {
ERROR("Error writing trailing zeroes\n");
return status;
}
offset += request->length * block_size;
bytes_left -= request->length * block_size;
}
}
return ZX_OK;
}
// Writes a raw (non-FVM) partition to a block device from a VMO.
zx_status_t WriteVmoToBlock(const zx::vmo& vmo, size_t vmo_size, const fbl::unique_fd& partition_fd,
uint32_t block_size_bytes) {
ZX_ASSERT(vmo_size % block_size_bytes == 0);
vmoid_t vmoid;
block_client::Client client;
zx_status_t status = RegisterFastBlockIo(partition_fd, vmo, &vmoid, &client);
if (status != ZX_OK) {
ERROR("Cannot register fast block I/O\n");
return status;
}
block_fifo_request_t request;
request.group = 0;
request.vmoid = vmoid;
request.opcode = BLOCKIO_WRITE;
uint64_t length = vmo_size / block_size_bytes;
if (length > UINT32_MAX) {
ERROR("Error writing partition data: Too large\n");
return ZX_ERR_OUT_OF_RANGE;
}
request.length = static_cast<uint32_t>(length);
request.vmo_offset = 0;
request.dev_offset = 0;
if ((status = client.Transaction(&request, 1)) != ZX_OK) {
ERROR("Error writing partition data: %s\n", zx_status_get_string(status));
return status;
}
return ZX_OK;
}
// Writes a raw (non-FVM) partition to a skip-block device from a VMO.
zx_status_t WriteVmoToSkipBlock(const zx::vmo& vmo, size_t vmo_size,
const fzl::UnownedFdioCaller& caller, uint32_t block_size_bytes) {
ZX_ASSERT(vmo_size % block_size_bytes == 0);
zx::vmo dup;
zx_status_t status;
if ((status = vmo.duplicate(ZX_RIGHT_SAME_RIGHTS, &dup)) != ZX_OK) {
ERROR("Couldn't duplicate buffer vmo\n");
return status;
}
fuchsia_hardware_skipblock_ReadWriteOperation operation = {
.vmo = dup.release(),
.vmo_offset = 0,
.block = 0,
.block_count = static_cast<uint32_t>(vmo_size / block_size_bytes),
};
bool bad_block_grown;
fuchsia_hardware_skipblock_SkipBlockWrite(caller.borrow_channel(), &operation, &status,
&bad_block_grown);
if (status != ZX_OK) {
ERROR("Error writing partition data: %s\n", zx_status_get_string(status));
return status;
}
return ZX_OK;
}
#if 0
// Checks first few bytes of buffer to ensure it is a ZBI.
// Also validates architecture in kernel header matches the target.
bool ValidateKernelZbi(const uint8_t* buffer, size_t size, Arch arch) {
const auto payload = reinterpret_cast<const zircon_kernel_t*>(buffer);
const uint32_t expected_kernel =
(arch == Arch::X64) ? ZBI_TYPE_KERNEL_X64 : ZBI_TYPE_KERNEL_ARM64;
const auto crc_valid = [](const zbi_header_t* hdr) {
const uint32_t crc = crc32(0, reinterpret_cast<const uint8_t*>(hdr + 1), hdr->length);
return hdr->crc32 == crc;
};
return size >= sizeof(zircon_kernel_t) &&
// Container header
payload->hdr_file.type == ZBI_TYPE_CONTAINER &&
payload->hdr_file.extra == ZBI_CONTAINER_MAGIC &&
(payload->hdr_file.length - offsetof(zircon_kernel_t, hdr_kernel)) <= size &&
payload->hdr_file.magic == ZBI_ITEM_MAGIC &&
payload->hdr_file.flags == ZBI_FLAG_VERSION &&
payload->hdr_file.crc32 == ZBI_ITEM_NO_CRC32 &&
// Kernel header
payload->hdr_kernel.type == expected_kernel &&
(payload->hdr_kernel.length - offsetof(zircon_kernel_t, data_kernel)) <= size &&
payload->hdr_kernel.magic == ZBI_ITEM_MAGIC &&
(payload->hdr_kernel.flags & ZBI_FLAG_VERSION) == ZBI_FLAG_VERSION &&
((payload->hdr_kernel.flags & ZBI_FLAG_CRC32)
? crc_valid(&payload->hdr_kernel)
: payload->hdr_kernel.crc32 == ZBI_ITEM_NO_CRC32);
}
// Parses a partition and validates that it matches the expected format.
zx_status_t ValidateKernelPayload(const fzl::ResizeableVmoMapper& mapper, size_t vmo_size,
Partition partition_type, Arch arch) {
// TODO(surajmalhotra): Re-enable this as soon as we have a good way to
// determine whether the payload is signed or not. (Might require bootserver
// changes).
if (false) {
const auto* buffer = reinterpret_cast<uint8_t*>(mapper.start());
switch (partition_type) {
case Partition::kZirconA:
case Partition::kZirconB:
case Partition::kZirconR:
if (!ValidateKernelZbi(buffer, vmo_size, arch)) {
ERROR("Invalid ZBI payload!");
return ZX_ERR_BAD_STATE;
}
break;
default:
// TODO(surajmalhotra): Validate non-zbi payloads as well.
LOG("Skipping validation as payload is not a ZBI\n");
break;
}
}
return ZX_OK;
}
#endif
// Attempt to bind an FVM driver to a partition fd.
fbl::unique_fd TryBindToFvmDriver(const fbl::unique_fd& partition_fd, zx::duration timeout) {
char path[PATH_MAX];
zx_status_t status = GetTopoPathFromFd(partition_fd, path, sizeof(path));
if (status != ZX_OK) {
ERROR("Failed to get topological path\n");
return fbl::unique_fd();
}
fdio_t* io = fdio_unsafe_fd_to_io(partition_fd.get());
if (io == nullptr) {
ERROR("Failed to convert to io\n");
return fbl::unique_fd();
}
zx_status_t call_status;
constexpr char kFvmDriverLib[] = "/boot/driver/fvm.so";
status = fuchsia_device_ControllerBind(fdio_unsafe_borrow_channel(io), kFvmDriverLib,
strlen(kFvmDriverLib), &call_status);
fdio_unsafe_release(io);
if (status == ZX_OK) {
status = call_status;
}
if (status != ZX_OK) {
ERROR("Could not bind fvm driver\n");
return fbl::unique_fd();
}
char fvm_path[PATH_MAX];
snprintf(fvm_path, sizeof(fvm_path), "%s/fvm", path);
if (wait_for_device(fvm_path, timeout.get()) != ZX_OK) {
ERROR("Error waiting for fvm driver to bind\n");
return fbl::unique_fd();
}
return fbl::unique_fd(open(fvm_path, O_RDWR));
}
// Options for locating an FVM within a partition.
enum class BindOption {
// Bind to the FVM, if it exists already.
TryBind,
// Reformat the partition, regardless of if it already exists as an FVM.
Reformat,
};
// Formats the FVM within the provided partition if it is not already formatted.
//
// On success, returns a file descriptor to an FVM.
// On failure, returns -1
fbl::unique_fd FvmPartitionFormat(fbl::unique_fd partition_fd, size_t slice_size,
BindOption option) {
// 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.
fbl::unique_fd fvm_fd;
if (option == BindOption::TryBind) {
disk_format_t df = detect_disk_format(partition_fd.get());
if (df == DISK_FORMAT_FVM) {
fvm_fd = TryBindToFvmDriver(partition_fd, zx::sec(3));
if (fvm_fd) {
LOG("Found already formatted FVM.\n");
volume_info_t info;
zx_status_t status = fvm_query(fvm_fd.get(), &info);
if (status == ZX_OK) {
if (info.slice_size == slice_size) {
return fvm_fd;
} else {
ERROR("Mismatched slice size. Reinitializing FVM.\n");
}
} else {
ERROR("Could not query FVM for info. Reinitializing FVM.\n");
}
} else {
ERROR("Saw DISK_FORMAT_FVM, but could not bind driver. Reinitializing FVM.\n");
}
}
}
LOG("Initializing partition as FVM\n");
zx_status_t status = fvm_init(partition_fd.get(), slice_size);
if (status != ZX_OK) {
ERROR("Failed to initialize fvm: %s\n", zx_status_get_string(status));
return fbl::unique_fd();
}
{
fzl::UnownedFdioCaller partition_connection(partition_fd.get());
zx::unowned_channel partition(partition_connection.borrow_channel());
zx_status_t io_status = fuchsia_hardware_block_BlockRebindDevice(partition->get(), &status);
if (io_status != ZX_OK) {
status = io_status;
}
if (status != ZX_OK) {
ERROR("Could not rebind partition: %s\n", zx_status_get_string(status));
return fbl::unique_fd();
}
}
return TryBindToFvmDriver(partition_fd, zx::sec(3));
}
// Formats a block device as a zxcrypt volume.
//
// On success, returns a file descriptor to an FVM.
// On failure, returns -1
zx_status_t ZxcryptCreate(PartitionInfo* part) {
zx_status_t status;
char path[PATH_MAX];
status = GetTopoPathFromFd(part->new_part, path, sizeof(path));
if (status != ZX_OK) {
ERROR("Failed to get topological path\n");
return status;
}
// TODO(security): ZX-1130. We need to bind with channel in order to pass a key here.
// TODO(security): ZX-1864. The created volume must marked as needing key rotation.
fbl::unique_ptr<zxcrypt::FdioVolume> volume;
if ((status = zxcrypt::FdioVolume::CreateWithDeviceKey(std::move(part->new_part), &volume)) !=
ZX_OK) {
ERROR("Could not create zxcrypt volume\n");
return status;
}
zx::channel zxcrypt_manager_chan;
if ((status = volume->OpenManager(zx::sec(3), zxcrypt_manager_chan.reset_and_get_address())) !=
ZX_OK) {
ERROR("Could not open zxcrypt volume manager\n");
return status;
}
zxcrypt::FdioVolumeManager zxcrypt_manager(std::move(zxcrypt_manager_chan));
uint8_t slot = 0;
if ((status = zxcrypt_manager.UnsealWithDeviceKey(slot)) != ZX_OK) {
ERROR("Could not unseal zxcrypt volume\n");
return status;
}
if ((status = volume->Open(zx::sec(3), &part->new_part)) != ZX_OK) {
ERROR("Could not open zxcrypt volume\n");
return status;
}
fvm::extent_descriptor_t* ext = GetExtent(part->pd, 0);
size_t reserved = volume->reserved_slices();
// |Create| guarantees at least |reserved| + 1 slices are allocated. If the first extent had a
// single slice, we're done.
size_t allocated = fbl::max(reserved + 1, ext->slice_count);
size_t needed = reserved + ext->slice_count;
if (allocated >= needed) {
return ZX_OK;
}
// Otherwise, extend by the number of slices we stole for metadata
uint64_t offset = allocated - reserved;
uint64_t length = needed - allocated;
{
fzl::UnownedFdioCaller partition_connection(part->new_part.get());
zx::unowned_channel partition(partition_connection.borrow_channel());
zx_status_t io_status =
fuchsia_hardware_block_volume_VolumeExtend(partition->get(), offset, length, &status);
if (io_status != ZX_OK) {
status = io_status;
}
if (status != ZX_OK) {
ERROR("Failed to extend zxcrypt volume: %s\n", zx_status_get_string(status));
return status;
}
}
return ZX_OK;
}
// Returns |ZX_OK| if |partition_fd| is a child of |fvm_fd|.
zx_status_t FvmPartitionIsChild(const fbl::unique_fd& fvm_fd, const fbl::unique_fd& partition_fd) {
char fvm_path[PATH_MAX];
char part_path[PATH_MAX];
zx_status_t status;
if ((status = GetTopoPathFromFd(fvm_fd, fvm_path, sizeof(fvm_path))) != ZX_OK) {
ERROR("Couldn't get topological path of FVM\n");
return status;
} else if ((status = GetTopoPathFromFd(partition_fd, part_path, sizeof(part_path))) != ZX_OK) {
ERROR("Couldn't get topological path of partition\n");
return status;
}
if (strncmp(fvm_path, part_path, strlen(fvm_path))) {
ERROR("Partition does not exist within FVM\n");
return ZX_ERR_BAD_STATE;
}
return ZX_OK;
}
// Warn users about issues in a way that is intended to stand out from
// typical error logs. These errors typically require user intervention,
// or may result in data loss.
void Warn(const char* problem, const char* action) {
ERROR("-----------------------------------------------------\n");
ERROR("\n");
ERROR("%s:\n", problem);
ERROR("%s\n", action);
ERROR("\n");
ERROR("-----------------------------------------------------\n");
}
void RecommendWipe(const char* problem) {
Warn(problem, "Please run 'install-disk-image wipe' to wipe your partitions");
}
// Deletes all partitions within the FVM with a type GUID matching |type_guid|
// until there are none left.
zx_status_t WipeAllFvmPartitionsWithGUID(const fbl::unique_fd& fvm_fd, const uint8_t type_guid[]) {
fbl::unique_fd old_part;
while ((old_part.reset(open_partition(nullptr, type_guid, ZX_MSEC(500), nullptr))), old_part) {
bool is_vpartition;
if (FvmIsVirtualPartition(old_part, &is_vpartition) != ZX_OK) {
ERROR("Couldn't confirm old vpartition type\n");
return ZX_ERR_IO;
}
if (FvmPartitionIsChild(fvm_fd, old_part) != ZX_OK) {
RecommendWipe("Streaming a partition type which also exists outside the target FVM");
return ZX_ERR_BAD_STATE;
}
if (!is_vpartition) {
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.
fzl::UnownedFdioCaller partition_connection(old_part.get());
zx::unowned_channel partition(partition_connection.borrow_channel());
zx_status_t io_status, status;
io_status = fuchsia_hardware_block_volume_VolumeDestroy(partition->get(), &status);
if (io_status != ZX_OK) {
status = io_status;
}
if (status != ZX_OK) {
ERROR("Couldn't destroy partition: %s\n", zx_status_get_string(status));
return status;
}
}
return ZX_OK;
}
// 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(const fbl::unique_fd& fvm_fd,
const fbl::unique_ptr<fvm::SparseReader>& reader,
const fbl::Array<PartitionInfo>& parts,
size_t* out_requested_slices) {
fvm::partition_descriptor_t* part = reader->Partitions();
fvm::sparse_image_t* 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;
if (parts[p].pd->magic != fvm::kPartitionDescriptorMagic) {
ERROR("Bad partition magic\n");
return ZX_ERR_IO;
}
zx_status_t status = WipeAllFvmPartitionsWithGUID(fvm_fd, parts[p].pd->type);
if (status != ZX_OK) {
ERROR("Failure wiping old partitions matching this GUID\n");
return status;
}
fvm::extent_descriptor_t* 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 must fit within allocated slice count\n");
return ZX_ERR_IO;
}
// Filter drivers may require additional space.
if ((parts[p].pd->flags & fvm::kSparseFlagZxcrypt) != 0) {
requested_slices += kZxcryptExtraSlices;
}
for (size_t e = 1; e < parts[p].pd->extent_count; e++) {
ext = GetExtent(parts[p].pd, e);
if (ext->magic != fvm::kExtentDescriptorMagic) {
ERROR("Bad extent magic\n");
return ZX_ERR_IO;
} else if (ext->slice_count == 0) {
ERROR("Extents must have > 0 slices\n");
return ZX_ERR_IO;
} else if (ext->extent_length > ext->slice_count * hdr->slice_size) {
ERROR("Extent must fit within allocated slice count\n");
return ZX_ERR_IO;
}
requested_slices += ext->slice_count;
}
part = reinterpret_cast<fvm::partition_descriptor*>(reinterpret_cast<uintptr_t>(ext) +
sizeof(fvm::extent_descriptor_t));
}
*out_requested_slices = requested_slices;
return ZX_OK;
}
// 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.
zx_status_t AllocatePartitions(const fbl::unique_fd& fvm_fd,
const fbl::Array<PartitionInfo>& parts) {
for (size_t p = 0; p < parts.size(); p++) {
fvm::extent_descriptor_t* ext = GetExtent(parts[p].pd, 0);
alloc_req_t alloc;
// Allocate this partition as inactive so it gets deleted on the next
// reboot if this stream fails.
alloc.flags = fuchsia_hardware_block_volume_AllocatePartitionFlagInactive;
alloc.slice_count = ext->slice_count;
memcpy(&alloc.type, parts[p].pd->type, sizeof(alloc.type));
zx_cprng_draw(alloc.guid, GPT_GUID_LEN);
memcpy(&alloc.name, parts[p].pd->name, sizeof(alloc.name));
LOG("Allocating partition %s consisting of %zu slices\n", alloc.name, alloc.slice_count);
parts[p].new_part.reset(fvm_allocate_partition(fvm_fd.get(), &alloc));
if (!parts[p].new_part) {
ERROR("Couldn't allocate partition\n");
return ZX_ERR_NO_SPACE;
}
// Add filter drivers.
if ((parts[p].pd->flags & fvm::kSparseFlagZxcrypt) != 0) {
LOG("Creating zxcrypt volume\n");
zx_status_t status = ZxcryptCreate(&parts[p]);
if (status != ZX_OK) {
return status;
}
}
// The 0th index extent is allocated alongside the partition, so we
// begin indexing from the 1st extent here.
for (size_t e = 1; e < parts[p].pd->extent_count; e++) {
ext = GetExtent(parts[p].pd, e);
uint64_t offset = ext->slice_start;
uint64_t length = ext->slice_count;
fzl::UnownedFdioCaller partition_connection(parts[p].new_part.get());
zx::unowned_channel partition(partition_connection.borrow_channel());
zx_status_t status;
zx_status_t io_status = fuchsia_hardware_block_volume_VolumeExtend(
partition->get(), offset, length, &status);
if (io_status != ZX_OK) {
status = io_status;
}
if (status != ZX_OK) {
ERROR("Failed to extend partition: %s\n", zx_status_get_string(status));
return status;
}
}
}
return ZX_OK;
}
// Given an fd representing a "sparse FVM format", fill the FVM with the
// provided partitions described by |src_fd|.
//
// Decides to overwrite or create new partitions based on the type
// GUID, not the instance GUID.
zx_status_t FvmStreamPartitions(fbl::unique_fd partition_fd,
std::unique_ptr<fvm::ReaderInterface> payload) {
fbl::unique_ptr<fvm::SparseReader> reader;
zx_status_t status;
if ((status = fvm::SparseReader::Create(std::move(payload), &reader)) != ZX_OK) {
return status;
}
LOG("Header Validated - OK\n");
// Duplicate the partition fd; we may need it later if we reformat the FVM.
fbl::unique_fd partition_fd2(dup(partition_fd.get()));
if (!partition_fd2) {
ERROR("Coudln't dup partition fd\n");
return ZX_ERR_IO;
}
fvm::sparse_image_t* hdr = reader->Image();
// Acquire an fd to the FVM, either by finding one that already
// exists, or formatting a new one.
fbl::unique_fd fvm_fd(
FvmPartitionFormat(std::move(partition_fd2), hdr->slice_size, BindOption::TryBind));
if (!fvm_fd) {
ERROR("Couldn't find FVM partition\n");
return ZX_ERR_IO;
}
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 = PreProcessPartitions(fvm_fd, reader, parts, &requested_slices)) != ZX_OK) {
ERROR("Failed to validate partitions: %s\n", zx_status_get_string(status));
return status;
}
// Contend with issues from an image that may be too large for this device.
volume_info_t info;
status = fvm_query(fvm_fd.get(), &info);
if (status != ZX_OK) {
ERROR("Failed to acquire FVM info: %s\n", zx_status_get_string(status));
return status;
}
size_t free_slices = info.pslice_total_count - info.pslice_allocated_count;
if (info.pslice_total_count < requested_slices) {
char buf[256];
snprintf(buf, sizeof(buf), "Image size (%zu) > Storage size (%zu)",
requested_slices * hdr->slice_size, info.pslice_total_count * hdr->slice_size);
Warn(buf, "Image is too large to be paved to device");
return ZX_ERR_NO_SPACE;
}
if (free_slices < requested_slices) {
Warn("Not enough space to non-destructively pave",
"Automatically reinitializing FVM; Expect data loss");
fvm_fd = FvmPartitionFormat(std::move(partition_fd), hdr->slice_size, BindOption::Reformat);
if (!fvm_fd) {
ERROR("Couldn't reformat FVM partition.\n");
return 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.
if ((status = AllocatePartitions(fvm_fd, parts)) != ZX_OK) {
ERROR("Failed to allocate partitions: %s\n", zx_status_get_string(status));
return status;
}
LOG("Partition space pre-allocated successfully.\n");
constexpr size_t vmo_size = 1 << 20;
fzl::VmoMapper mapping;
zx::vmo vmo;
if ((status = 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_ERR_NO_MEMORY;
}
fzl::FdioCaller volume_manager(std::move(fvm_fd));
// Now that all partitions are preallocated, begin streaming data to them.
for (size_t p = 0; p < parts.size(); p++) {
vmoid_t vmoid;
block_client::Client client;
zx_status_t status = RegisterFastBlockIo(parts[p].new_part, vmo, &vmoid, &client);
if (status != ZX_OK) {
ERROR("Failed to register fast block IO\n");
return status;
}
fzl::UnownedFdioCaller partition_connection(parts[p].new_part.get());
zx::unowned_channel partition(partition_connection.borrow_channel());
fuchsia_hardware_block_BlockInfo block_info;
zx_status_t io_status =
fuchsia_hardware_block_BlockGetInfo(partition->get(), &status, &block_info);
if (io_status != ZX_OK) {
status = io_status;
}
if (status != ZX_OK) {
ERROR("Couldn't get partition block info: %s\n", zx_status_get_string(status));
return status;
}
size_t block_size = block_info.block_size;
block_fifo_request_t request;
request.group = 0;
request.vmoid = vmoid;
request.opcode = BLOCKIO_WRITE;
LOG("Streaming partition %zu\n", p);
status = StreamFvmPartition(reader.get(), &parts[p], mapping, client, block_size, &request);
LOG("Done streaming partition %zu\n", p);
if (status != ZX_OK) {
ERROR("Failed to stream partition\n");
return status;
}
if ((status = FlushClient(client)) != ZX_OK) {
ERROR("Failed to flush client\n");
return status;
}
LOG("Done flushing partition %zu\n", p);
}
for (size_t p = 0; p < parts.size(); p++) {
fzl::UnownedFdioCaller partition_connection(parts[p].new_part.get());
zx::unowned_channel partition(partition_connection.borrow_channel());
// Upgrade the old partition (currently active) to the new partition (currently
// inactive) so the new partition persists.
fuchsia_hardware_block_partition_GUID guid;
zx_status_t io_status = fuchsia_hardware_block_partition_PartitionGetInstanceGuid(
partition->get(), &status, &guid);
if (io_status != ZX_OK || status != ZX_OK) {
ERROR("Failed to get unique GUID of new partition\n");
return ZX_ERR_BAD_STATE;
}
zx_status_t status;
io_status = fuchsia_hardware_block_volume_VolumeManagerActivate(
volume_manager.borrow_channel(), &guid, &guid, &status);
if (io_status != ZX_OK || status != ZX_OK) {
ERROR("Failed to upgrade partition\n");
return ZX_ERR_IO;
}
}
return ZX_OK;
}
zx_status_t FvmPave(const DevicePartitioner& partitioner,
fbl::unique_ptr<fvm::ReaderInterface> payload) {
LOG("Paving partition.\n");
constexpr auto partition_type = Partition::kFuchsiaVolumeManager;
zx_status_t status;
fbl::unique_fd partition_fd;
if ((status = partitioner.FindPartition(partition_type, &partition_fd)) != ZX_OK) {
if (status != ZX_ERR_NOT_FOUND) {
ERROR("Failure looking for partition: %s\n", zx_status_get_string(status));
return status;
}
LOG("Coud not find \"%s\" Partition on device. Attemping to add new partition\n",
PartitionName(partition_type));
if ((status = partitioner.AddPartition(partition_type, &partition_fd)) != ZX_OK) {
ERROR("Failure creating partition: %s\n", zx_status_get_string(status));
return status;
}
} else {
LOG("Partition already exists\n");
}
if (partitioner.UseSkipBlockInterface()) {
LOG("Attempting to format FTL...\n");
status = partitioner.WipeFvm();
if (status != ZX_OK) {
ERROR("Failed to format FTL: %s\n", zx_status_get_string(status));
} else {
LOG("Formatted successfully!\n");
}
}
LOG("Streaming partitions...\n");
if ((status = FvmStreamPartitions(std::move(partition_fd), std::move(payload))) != ZX_OK) {
ERROR("Failed to stream partitions: %s\n", zx_status_get_string(status));
return status;
}
LOG("Completed successfully\n");
return ZX_OK;
}
// Paves an image onto the disk.
zx_status_t PartitionPave(const DevicePartitioner& partitioner,
zx::vmo payload_vmo, size_t payload_size, Partition partition_type) {
LOG("Paving partition.\n");
zx_status_t status;
fbl::unique_fd partition_fd;
if ((status = partitioner.FindPartition(partition_type, &partition_fd)) != ZX_OK) {
if (status != ZX_ERR_NOT_FOUND) {
ERROR("Failure looking for partition: %s\n", zx_status_get_string(status));
return status;
}
LOG("Coud not find \"%s\" Partition on device. Attemping to add new partition\n",
PartitionName(partition_type));
if ((status = partitioner.AddPartition(partition_type, &partition_fd)) != ZX_OK) {
ERROR("Failure creating partition: %s\n", zx_status_get_string(status));
return status;
}
} else {
LOG("Partition already exists\n");
}
uint32_t block_size_bytes;
if ((status = partitioner.GetBlockSize(partition_fd, &block_size_bytes)) != ZX_OK) {
ERROR("Couldn't get partition block size\n");
return status;
}
// Pad payload with 0s to make it block size aligned.
if (payload_size % block_size_bytes != 0) {
const size_t remaining_bytes = block_size_bytes - (payload_size % block_size_bytes);
size_t vmo_size;
if ((status = payload_vmo.get_size(&vmo_size)) != ZX_OK) {
ERROR("Couldn't get vmo size\n");
return status;
}
// Grow VMO if it's too small.
if (vmo_size < payload_size + remaining_bytes) {
const auto new_size = fbl::round_up(payload_size + remaining_bytes, ZX_PAGE_SIZE);
status = payload_vmo.set_size(new_size);
if (status != ZX_OK) {
ERROR("Couldn't grow vmo\n");
return status;
}
}
auto buffer = std::make_unique<uint8_t[]>(remaining_bytes);
memset(buffer.get(), 0, remaining_bytes);
status = payload_vmo.write(buffer.get(), payload_size, remaining_bytes);
if (status != ZX_OK) {
ERROR("Failed to write padding to vmo\n");
return status;
}
payload_size += remaining_bytes;
}
if (partitioner.UseSkipBlockInterface()) {
fzl::UnownedFdioCaller caller(partition_fd.get());
status = WriteVmoToSkipBlock(payload_vmo, payload_size, caller, block_size_bytes);
} else {
status = WriteVmoToBlock(payload_vmo, payload_size, partition_fd, block_size_bytes);
}
if (status != ZX_OK) {
ERROR("Failed to write partition to block\n");
return status;
}
if ((status = partitioner.FinalizePartition(partition_type)) != ZX_OK) {
ERROR("Failed to finalize partition\n");
return status;
}
LOG("Completed successfully\n");
return ZX_OK;
}
} // namespace
bool Paver::InitializePartitioner() {
if (!partitioner_) {
// Use global devfs if one wasn't injected via set_devfs_root.
if (!devfs_root_) {
devfs_root_ = fbl::unique_fd(open("/dev", O_RDONLY));
}
if (!sysinfo_) {
zx::channel server, client;
auto status = zx::channel::create(0, &server, &client);
if (status != ZX_OK) {
ERROR("Unable to create channel.\n");
return false;
}
status = fdio_service_connect("/dev/misc/sysinfo", server.release());
if (status != ZX_OK) {
ERROR("Unable to open sysinfo.\n");
return false;
}
sysinfo_ = std::move(client);
}
partitioner_ = DevicePartitioner::Create(devfs_root_.duplicate(), std::move(sysinfo_));
if (!partitioner_) {
ERROR("Unable to initialize a partitioner.\n");
return false;
}
}
return true;
}
zx_status_t Paver::WriteAsset(fuchsia_paver_Configuration configuration, fuchsia_paver_Asset asset,
const fuchsia_mem_Buffer& payload) {
if (!InitializePartitioner()) {
return ZX_ERR_BAD_STATE;
}
return PartitionPave(*partitioner_, zx::vmo(payload.vmo), payload.size,
PartitionType(configuration, asset));
}
zx_status_t Paver::WriteVolumes(zx::channel payload_stream) {
if (!InitializePartitioner()) {
return ZX_ERR_BAD_STATE;
}
std::unique_ptr<StreamReader> reader;
auto status = StreamReader::Create(std::move(payload_stream), &reader);
if (status != ZX_OK) {
ERROR("Unable to create stream.\n");
return status;
}
return FvmPave(*partitioner_, std::move(reader));
}
zx_status_t Paver::WriteBootloader(const fuchsia_mem_Buffer& payload) {
if (!InitializePartitioner()) {
return ZX_ERR_BAD_STATE;
}
return PartitionPave(*partitioner_, zx::vmo(payload.vmo), payload.size,
Partition::kBootloader);
}
zx_status_t Paver::WriteDataFile(fbl::String filename, const fuchsia_mem_Buffer& payload) {
const char* mount_path = "/volume/data";
const uint8_t data_guid[] = GUID_DATA_VALUE;
char minfs_path[PATH_MAX] = {0};
char path[PATH_MAX] = {0};
zx_status_t status = ZX_OK;
fbl::unique_fd part_fd(open_partition(nullptr, data_guid, ZX_SEC(1), path));
if (!part_fd) {
ERROR("DATA partition not found in FVM\n");
return ZX_ERR_NOT_FOUND;
}
auto disk_format = detect_disk_format(part_fd.get());
fbl::unique_fd mountpoint_dev_fd;
// By the end of this switch statement, mountpoint_dev_fd needs to be an
// open handle to the block device that we want to mount at mount_path.
switch (disk_format) {
case DISK_FORMAT_MINFS:
// If the disk we found is actually minfs, we can just use the block
// device path we were given by open_partition.
strncpy(minfs_path, path, PATH_MAX);
mountpoint_dev_fd.reset(open(minfs_path, O_RDWR));
break;
case DISK_FORMAT_ZXCRYPT: {
fbl::unique_ptr<zxcrypt::FdioVolume> zxc_volume;
uint8_t slot = 0;
if ((status =
zxcrypt::FdioVolume::UnlockWithDeviceKey(std::move(part_fd),
static_cast<zxcrypt::key_slot_t>(slot),
&zxc_volume)) != ZX_OK) {
ERROR("Couldn't unlock zxcrypt volume: %s\n", zx_status_get_string(status));
return status;
}
// Most of the time we'll expect the volume to actually already be
// unsealed, because we created it and unsealed it moments ago to
// format minfs.
if ((status = zxc_volume->Open(zx::sec(0), &mountpoint_dev_fd)) == ZX_OK) {
// Already unsealed, great, early exit.
break;
}
// Ensure zxcrypt volume manager is bound.
zx::channel zxc_manager_chan;
if ((status = zxc_volume->OpenManager(zx::sec(5),
zxc_manager_chan.reset_and_get_address())) != ZX_OK) {
ERROR("Couldn't open zxcrypt volume manager: %s\n", zx_status_get_string(status));
return status;
}
// Unseal.
zxcrypt::FdioVolumeManager zxc_manager(std::move(zxc_manager_chan));
if ((status = zxc_manager.UnsealWithDeviceKey(slot)) != ZX_OK) {
ERROR("Couldn't unseal zxcrypt volume: %s\n", zx_status_get_string(status));
return status;
}
// Wait for the device to appear, and open it.
if ((status = zxc_volume->Open(zx::sec(5), &mountpoint_dev_fd)) != ZX_OK) {
ERROR("Couldn't open block device atop unsealed zxcrypt volume: %s\n",
zx_status_get_string(status));
return status;
}
} break;
default:
ERROR("unsupported disk format at %s\n", path);
return ZX_ERR_NOT_SUPPORTED;
}
mount_options_t opts(default_mount_options);
opts.create_mountpoint = true;
if ((status = mount(mountpoint_dev_fd.get(), mount_path, DISK_FORMAT_MINFS, &opts,
launch_logs_async)) != ZX_OK) {
ERROR("mount error: %s\n", zx_status_get_string(status));
return status;
}
// mkdir any intermediate directories between mount_path and basename(filename).
snprintf(path, sizeof(path), "%s/%s", mount_path, filename.c_str());
size_t cur = strlen(mount_path);
size_t max = strlen(path) - strlen(basename(path));
// note: the call to basename above modifies path, so it needs reconstruction.
snprintf(path, sizeof(path), "%s/%s", mount_path, filename.c_str());
while (cur < max) {
++cur;
if (path[cur] == '/') {
path[cur] = 0;
// errors ignored, let the open() handle that later.
mkdir(path, 0700);
path[cur] = '/';
}
}
// We append here, because the primary use case here is to send SSH keys
// which can be appended, but we may want to revisit this choice for other
// files in the future.
{
uint8_t buf[8192];
fbl::unique_fd kfd(open(path, O_CREAT | O_WRONLY | O_APPEND, 0600));
if (!kfd) {
umount(mount_path);
ERROR("open %s error: %s\n", filename.c_str(), strerror(errno));
return ZX_ERR_IO;
}
VmoReader reader(payload);
size_t actual;
while ((status = reader.Read(buf, sizeof(buf), &actual)) == ZX_OK && actual > 0) {
if (write(kfd.get(), buf, actual) != static_cast<ssize_t>(actual)) {
umount(mount_path);
ERROR("write %s error: %s\n", filename.c_str(), strerror(errno));
return ZX_ERR_IO;
}
}
fsync(kfd.get());
}
if ((status = umount(mount_path)) != ZX_OK) {
ERROR("unmount %s failed: %s\n", mount_path, zx_status_get_string(status));
return status;
}
LOG("Wrote %s\n", filename.c_str());
return ZX_OK;
}
zx_status_t Paver::WipeVolumes() {
if (!InitializePartitioner()) {
return ZX_ERR_BAD_STATE;
}
return partitioner_->WipeFvm();
}
} // namespace paver