blob: 049000fa743f576441f97bfd26e2926ad825687b [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 "src/virtualization/bin/vmm/device/qcow.h"
#include <fcntl.h>
#include <lib/syslog/cpp/macros.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <fbl/ref_counted.h>
#include <fbl/ref_ptr.h>
// Implementation based on the spec located at:
//
// https://github.com/qemu/qemu/blob/HEAD/docs/interop/qcow2.txt
// Compute the number of L1 table entries required to hold all mappings for a
// disk of |disk_size|.
static size_t ComputeL1Size(size_t disk_size, uint32_t cluster_bits) {
size_t cluster_size = 1 << cluster_bits;
// Each L2 table is an array of 8b cluster addresses, so each table can hold
// |cluster_size| / 8 entries.
size_t l2_num_entries = cluster_size / sizeof(uint64_t);
size_t l1_entry_size = cluster_size * l2_num_entries;
return (disk_size + l1_entry_size - 1) / l1_entry_size;
}
// A LookupTable holds the 2-level table mapping a linear cluster address to the
// physical offset in the QCOW file.
class QcowFile::LookupTable {
public:
LookupTable(uint32_t cluster_bits, size_t disk_size)
: cluster_bits_(cluster_bits),
l2_bits_(cluster_bits - 3),
l1_size_(ComputeL1Size(disk_size, cluster_bits)) {}
// Loads the L1 table to use for cluster mapping.
//
// Note we currently load all existing L2 tables for the disk so that all
// mappings are held in memory. With a 64k cluster size this results in 1MB
// of tables per 8GB of virtual disk.
//
// TODO(tjdetwiler): Add some bound to this L2 cache.
void Load(const QcowHeader& header, BlockDispatcher* disp, BlockDispatcher::Callback callback) {
if (!l1_table_.empty()) {
callback(ZX_ERR_BAD_STATE);
return;
}
auto io_guard = fbl::MakeRefCounted<IoGuard>(std::move(callback));
uint32_t l1_size = header.l1_size;
if (l1_size < l1_size_) {
FX_LOGS(ERROR) << "Invalid QCOW header: L1 table is too small. Image size requires "
<< l1_size_ << " entries but the header specifies " << l1_size << ".";
io_guard->SetStatus(ZX_ERR_INVALID_ARGS);
return;
}
std::vector<L1Entry> l1_entries(l1_size);
auto l1_entries_ptr = l1_entries.data();
size_t l2_size = 1 << (header.cluster_bits - 3);
auto load_l1 = [this, io_guard, l1_entries = std::move(l1_entries), l2_size,
disp](zx_status_t status) {
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to read L1 table " << status;
io_guard->SetStatus(status);
return;
}
l1_table_.resize(l1_entries.size());
for (size_t l1_index = 0; l1_index < l1_entries.size(); ++l1_index) {
uint64_t off = BigToHostEndianTraits::Convert(l1_entries[l1_index]) & kTableOffsetMask;
if (off == 0) {
continue;
}
auto load_l2 = [io_guard](zx_status_t status) {
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to read L2 table " << status;
io_guard->SetStatus(status);
}
};
auto& l2_table = l1_table_[l1_index];
l2_table.resize(l2_size);
disp->ReadAt(l2_table.data(), l2_size * sizeof(L2Entry), off, load_l2);
}
};
disp->ReadAt(l1_entries_ptr, l1_size * sizeof(L1Entry), header.l1_table_offset,
std::move(load_l1));
}
// Walks the tables to find the physical offset of |linear_offset| into
// the image file. The returned value is only valid up until the next cluster
// boundary.
//
// Returns:
// |ZX_OK| - The lineary address is mapped and the physical offset into the
// QCOW file is written to |physical_offset|.
// |ZX_ERR_NOT_FOUND| - The linear offset is valid, but the cluster is not
// mapped.
// |ZX_ERR_OUT_OF_RANGE| - The linear offset is outside the bounds of the
// virtual disk.
// |ZX_ERR_NOT_SUPPORTED| - The cluster is compressed.
// |ZX_ERR_BAD_STATE| - The file has not yet been initialized with a call to
// |Load|.
zx_status_t Walk(size_t linear_offset, uint64_t* physical_offset) {
if (l1_table_.empty()) {
return ZX_ERR_BAD_STATE;
}
size_t cluster_offset = linear_offset & (1 << cluster_bits_) - 1;
linear_offset >>= cluster_bits_;
size_t l2_offset = linear_offset & (1 << l2_bits_) - 1;
linear_offset >>= l2_bits_;
size_t l1_offset = linear_offset;
if (l1_offset >= l1_size_) {
return ZX_ERR_OUT_OF_RANGE;
}
const auto& l2 = l1_table_[l1_offset];
if (l2.empty()) {
return ZX_ERR_NOT_FOUND;
}
uint64_t l2_entry = BigToHostEndianTraits::Convert(l2[l2_offset]);
if (l2_entry & kTableEntryCompressedBit) {
FX_LOGS(ERROR) << "Cluster compression not supported";
return ZX_ERR_NOT_SUPPORTED;
}
uint64_t cluster = l2_entry & kTableOffsetMask;
if (cluster == 0) {
return ZX_ERR_NOT_FOUND;
}
*physical_offset = cluster | cluster_offset;
return ZX_OK;
}
private:
size_t cluster_bits_;
size_t l2_bits_;
size_t l1_size_;
using L1Entry = uint64_t;
using L2Entry = uint64_t;
using L2Table = std::vector<L2Entry>;
using L1Table = std::vector<L2Table>;
L1Table l1_table_;
};
QcowFile::QcowFile() = default;
QcowFile::~QcowFile() = default;
void QcowFile::Load(BlockDispatcher* disp, BlockDispatcher::Callback callback) {
auto load = [this, disp, callback = std::move(callback)](zx_status_t status) mutable {
// Load QCOW header.
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to read QCOW header";
callback(ZX_ERR_WRONG_TYPE);
return;
}
header_ = header_.BigToHostEndian();
LoadLookupTable(disp, std::move(callback));
};
disp->ReadAt(&header_, sizeof(header_), 0, std::move(load));
}
void QcowFile::LoadLookupTable(BlockDispatcher* disp, BlockDispatcher::Callback callback) {
if (header_.magic != kQcowMagic) {
FX_LOGS(ERROR) << "Invalid QCOW image";
callback(ZX_ERR_WRONG_TYPE);
return;
}
// Default values for version 2.
if (header_.version == 2) {
header_.incompatible_features = 0;
header_.compatible_features = 0;
header_.autoclear_features = 0;
header_.refcount_order = 4;
header_.header_length = 72;
} else if (header_.version != 3) {
FX_LOGS(ERROR) << "QCOW version " << header_.version << " is not supported";
callback(ZX_ERR_NOT_SUPPORTED);
return;
}
// We don't support any optional features so refuse to load an image that
// requires any.
if (header_.incompatible_features) {
FX_LOGS(ERROR) << "Rejecting QCOW image with incompatible features " << std::hex << "0x"
<< header_.incompatible_features;
callback(ZX_ERR_NOT_SUPPORTED);
return;
}
// No encryption is supported.
if (header_.crypt_method) {
FX_LOGS(ERROR) << "Rejecting QCOW image with crypt method " << std::hex << "0x"
<< header_.crypt_method;
callback(ZX_ERR_NOT_SUPPORTED);
return;
}
// clang-format off
FX_VLOGS(1) << "Found QCOW header:";
FX_VLOGS(1) << "\tmagic: 0x" << std::hex << header_.magic;
FX_VLOGS(1) << "\tversion: " << std::hex << header_.version;
FX_VLOGS(1) << "\tbacking_file_offset: 0x" << std::hex << header_.backing_file_offset;
FX_VLOGS(1) << "\tbacking_file_size: 0x" << std::hex << header_.backing_file_size;
FX_VLOGS(1) << "\tcluster_bits: " << header_.cluster_bits;
FX_VLOGS(1) << "\tsize: 0x" << std::hex << header_.size;
FX_VLOGS(1) << "\tcrypt_method: " << header_.crypt_method;
FX_VLOGS(1) << "\tl1_size: 0x" << std::hex << header_.l1_size;
FX_VLOGS(1) << "\tl1_table_offset: 0x" << std::hex << header_.l1_table_offset;
FX_VLOGS(1) << "\trefcount_table_offset: 0x" << std::hex << header_.refcount_table_offset;
FX_VLOGS(1) << "\trefcount_table_clusters: " << header_.refcount_table_clusters;
FX_VLOGS(1) << "\tnb_snapshots: " << header_.nb_snapshots;
FX_VLOGS(1) << "\tsnapshots_offset: 0x" << std::hex << header_.snapshots_offset;
FX_VLOGS(1) << "\tincompatible_features: 0x" << std::hex << header_.incompatible_features;
FX_VLOGS(1) << "\tcompatible_features: 0x" << std::hex << header_.compatible_features;
FX_VLOGS(1) << "\tautoclear_features: 0x" << std::hex << header_.autoclear_features;
FX_VLOGS(1) << "\trefcount_order: " << header_.refcount_order;
FX_VLOGS(1) << "\theader_length: " << header_.header_length;
// clang-format on
lookup_table_ = std::make_unique<LookupTable>(header_.cluster_bits, header_.size);
lookup_table_->Load(header_, disp, std::move(callback));
};
void QcowFile::ReadAt(BlockDispatcher* disp, void* data, uint64_t size, uint64_t off,
BlockDispatcher::Callback callback) {
if (!lookup_table_) {
callback(ZX_ERR_BAD_STATE);
return;
}
auto io_guard = fbl::MakeRefCounted<IoGuard>(std::move(callback));
auto addr = static_cast<uint8_t*>(data);
uint64_t cluster_mask = cluster_size() - 1;
while (size) {
uint64_t physical_offset;
uint64_t cluster_offset = off & cluster_mask;
uint64_t read_size = std::min(size, cluster_size() - cluster_offset);
zx_status_t status = lookup_table_->Walk(off, &physical_offset);
switch (status) {
case ZX_OK: {
auto load = [io_guard](zx_status_t status) {
if (status != ZX_OK) {
io_guard->SetStatus(status);
}
};
disp->ReadAt(addr, read_size, physical_offset, load);
break;
}
case ZX_ERR_NOT_FOUND:
// Cluster is not mapped; read as zero.
memset(addr, 0, read_size);
break;
default:
io_guard->SetStatus(status);
return;
}
off += read_size;
addr += read_size;
size -= read_size;
}
}