Overlaybd (overlay block device) is a novel layered image format based on block-diff representation. It was originally designed for fast launching of containers, and it is also feasible for virtual machines.
It features: (1) small index with variable-length block size; (2) fast lookup with linearized B+ tree and SIMD; (binary search for now); (3) O(1) lookup at any number of depth (internal backing chain); (4) optionally fast decompression with lz4 or zstd; (5) OCI-compatible layering and manifest. Signed-off-by: Huiba Li <[email protected]> --- MAINTAINERS | 7 + block/meson.build | 5 +- block/overlaybd.c | 1962 ++++++++++++++++++++++++ docs/system/qemu-block-drivers.rst.inc | 37 + meson.build | 9 + meson_options.txt | 4 + qapi/block-core.json | 39 + scripts/meson-buildoptions.sh | 6 + tests/qemu-iotests/tests/overlaybd | 795 ++++++++++ tests/qemu-iotests/tests/overlaybd.out | 10 + 10 files changed, 2873 insertions(+), 1 deletion(-) create mode 100644 block/overlaybd.c create mode 100755 tests/qemu-iotests/tests/overlaybd create mode 100644 tests/qemu-iotests/tests/overlaybd.out diff --git a/MAINTAINERS b/MAINTAINERS index 6be9c5725b..a71264490b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4436,6 +4436,13 @@ L: [email protected] S: Supported F: block/dmg.c +overlaybd +M: Huiba Li <[email protected]> +L: [email protected] +S: Supported +F: block/overlaybd.c +F: tests/qemu-iotests/tests/overlaybd* + parallels M: Stefan Hajnoczi <[email protected]> M: Denis V. Lunev <[email protected]> diff --git a/block/meson.build b/block/meson.build index bc419aebf0..48fdac8b0b 100644 --- a/block/meson.build +++ b/block/meson.build @@ -39,7 +39,7 @@ block_ss.add(files( 'throttle.c', 'throttle-groups.c', 'write-threshold.c', -), zstd, zlib) +), zstd, zlib, lz4) system_ss.add(when: 'CONFIG_TCG', if_true: files('blkreplay.c')) system_ss.add(files('block-ram-registrar.c')) @@ -69,6 +69,9 @@ endif if get_option('bochs').allowed() block_ss.add(files('bochs.c')) endif +if get_option('overlaybd').allowed() + block_ss.add(files('overlaybd.c')) +endif if get_option('vvfat').allowed() block_ss.add(files('vvfat.c')) endif diff --git a/block/overlaybd.c b/block/overlaybd.c new file mode 100644 index 0000000000..9d3b5eb437 --- /dev/null +++ b/block/overlaybd.c @@ -0,0 +1,1962 @@ +/* + * + * Read-only block driver for OverlayBD images + * + * An OverlayBD image is a stack of layer files. Each layer is an LSMT + * blob: a 4096-byte header, a data region, + * a sorted index of 16-byte LBA mappings, and a 4096-byte trailer. The + * driver merges the per-layer indexes top-down (a top layer's mapping + * wins its whole extent; lower layers only fill holes) and serves reads + * at 512-byte sector granularity. Holes and zeroed mappings read as + * zeros. + * + * Layers may optionally be wrapped in a single-entry tar and/or + * compressed with ZFile (block-wise LZ4 or ZSTD with a jump table). + * + * The layer stack is given either by an OCI image manifest, whose layer + * digests name blobs in a local directory (manifest option, or the file + * child for -drive file=... usage), or as an explicit bottom-first + * "layers" array. + * + * See also the reference implementation: + * https://github.com/containerd/overlaybd + * + * Specifications for the on-disk formats: + * LSMT layer blob format (header, data, index, trailer) + * https://github.com/containerd/overlaybd/blob/main/src/overlaybd/lsmt/format_spec.md + * ZFile block compression format (jump table, LZ4/ZSTD) + * https://github.com/containerd/overlaybd/blob/main/src/overlaybd/zfile/format_spec.md + * + * The layer blobs are named by an OCI image manifest; the remaining OCI + * specifications are listed where the manifest is parsed: + * https://github.com/opencontainers/image-spec/blob/main/manifest.md + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "qemu/osdep.h" +#include "qapi/error.h" +#include "block/block-io.h" +#include "block/block_int.h" +#include "block/qdict.h" +#include "qemu/module.h" +#include "qemu/bswap.h" +#include "qemu/crc32c.h" +#include "qemu/cutils.h" +#include "qemu/memalign.h" +#include "qobject/qjson.h" +#include "qobject/qdict.h" +#include "qobject/qlist.h" + +#ifdef CONFIG_LZ4 +#include <lz4.h> +#endif +#ifdef CONFIG_ZSTD +#include <zstd.h> +#endif + +/* + * + * On-disk format definitions below mirror the reference implementation + * (containerd/overlaybd: lsmt/file.cpp HeaderTrailer, lsmt/index.h, + * zfile/zfile.cpp HeaderTrailer, zfile/compressor.h CompressOptions) + * field for field; per-struct citations mark the source of each layout. + * Metadata is little-endian on disk and is read through explicit + * little-endian accessors, which also sidesteps the signedness of + * ldl_le_p() for the 32-bit fields. + */ + +static const char lsmt_magic0[8] = "LSMT\0\1\2"; +static const uint8_t lsmt_magic1[16] = { + 0x65, 0x7e, 0x63, 0xd2, 0x94, 0x44, 0x08, 0x4c, + 0xa2, 0xd2, 0xc8, 0xec, 0x4f, 0xcf, 0xae, 0x8a +}; +static const char zfile_magic0[8] = "ZFile\0\1"; +static const uint8_t zfile_magic1[16] = "tuji.yyf@Alibaba"; + +/* LSMT::HeaderTrailer::SPACE, lsmt/file.cpp */ +#define LSMT_HT_SPACE 4096 +/* LSMT::MAX_LSMT_RO_INDEX_SIZE, lsmt/index.h */ +#define LSMT_MAX_RO_INDEX_ENTRIES 1000000LL +/* LSMT::MAX_LSMT_INDEX_SIZE, lsmt/index.h: cap on the merged index */ +#define LSMT_MAX_MERGED_ENTRIES 128000000LL +/* LSMT::MAX_STACK_LAYERS, lsmt/file.h */ +#define LSMT_MAX_LAYERS 255 +/* LSMT::Segment::INVALID_OFFSET, lsmt/index.h */ +#define LSMT_INVALID_OFFSET (((uint64_t)1 << 50) - 1) +#define LSMT_MOFFSET_BEGIN (LSMT_HT_SPACE / BDRV_SECTOR_SIZE) + +/* LSMT::HeaderTrailer::FLAG_SHIFT_*, lsmt/file.cpp */ +#define LSMT_FLAG_HEADER (1u << 0) +#define LSMT_FLAG_DATA_FILE (1u << 1) +#define LSMT_FLAG_SEALED (1u << 2) + +/* + * LSMT::HeaderTrailer, lsmt/file.cpp: a packed struct with byte-aligned + * fields only (no bitfields), identical to the upstream definition less + * the C++ methods, and covering the whole LSMT_HT_SPACE block that is + * read and written. Upstream stops at user_tag (sizeof == 390) and + * keeps the tail outside the type, reading the block into a raw buffer + * and casting it; format_spec.md:32 documents that tail as a header + * field, so it is declared here instead. + */ +typedef struct QEMU_PACKED OverlaybdLSMTHeader { + uint64_t magic0; + uint8_t magic1[16]; + uint32_t size; /* 390: the fields above, tail excluded */ + uint32_t flags; + uint64_t index_offset; + uint64_t index_size; /* # of 16-byte index entries */ + uint64_t virtual_size; /* bytes */ + uint8_t uuid[37]; /* UUID::String */ + uint8_t parent_uuid[37]; /* UUID::String */ + uint16_t reserved; + uint8_t version; + uint8_t sub_version; + uint8_t user_tag[256]; /* TAG_SIZE */ + uint8_t reserved_space[3706]; +} OverlaybdLSMTHeader; + +QEMU_BUILD_BUG_ON(sizeof(OverlaybdLSMTHeader) != LSMT_HT_SPACE); + +/* ZFile::HeaderTrailer::SPACE, zfile/zfile.cpp */ +#define ZFILE_HT_SPACE 512 +/* + * ZFile::MAX_READ_SIZE, zfile/zfile.h: upstream uses this one constant + * both as the block size cap and as the size of BlockReader's + * compressed-data read window. Only the block size cap is needed here -- + * the read window is sized to the request. + */ +#define ZFILE_MAX_BLOCK_SIZE 65536 +/* + * qemu-side hardening: a jump table entry is image-controlled, so cap the + * compressed size of one block to bound the read window. Upstream has no + * span cap; it ERANGEs when a block overflows its 64 KiB window instead. + */ +#define ZFILE_MAX_SPAN (1u << 20) +/* ZFile::CompressOptions::LZ4 / ZSTD, zfile/compressor.h */ +#define ZFILE_ALGO_LZ4 1 +#define ZFILE_ALGO_ZSTD 2 + +/* ZFile::HeaderTrailer::FLAG_SHIFT_*, zfile/zfile.cpp */ +#define ZFILE_FLAG_HEADER (1ULL << 0) +#define ZFILE_FLAG_DATA_FILE (1ULL << 1) +#define ZFILE_FLAG_SEALED (1ULL << 2) +#define ZFILE_FLAG_HEADER_OVERWRITE (1ULL << 3) +#define ZFILE_FLAG_DIGEST (1ULL << 4) +#define ZFILE_FLAG_INDEX_COMPRESSED (1ULL << 5) + +/* ZFile::CompressOptions, zfile/compressor.h: sizeof == 24 */ +typedef struct QEMU_PACKED OverlaybdCompressOptions { + uint32_t block_size; + uint8_t algo; + uint8_t level; + uint8_t use_dict; + uint8_t __padding_0; + uint32_t reserved; + uint32_t dict_size; + uint8_t verify; + uint8_t __padding_1[7]; +} OverlaybdCompressOptions; + +/* + * ZFile::HeaderTrailer, zfile/zfile.cpp, embedding ZFile::CompressOptions + * at offset 72: upstream is not packed, but every member is naturally + * aligned at these offsets, so a packed C struct has the identical + * layout. As with the LSMT header, the ZFILE_HT_SPACE tail that + * upstream keeps outside the type is declared here, per + * format_spec.md:39 (offset 89 ~ 511, of which opt.__padding_1 already + * covers 89 ~ 95). + */ +typedef struct QEMU_PACKED OverlaybdZFileHeader { + uint64_t magic0; + uint8_t magic1[16]; + uint32_t size; /* 96: the fields above, tail excluded */ + uint32_t digest; /* CRC32C of bytes 28..511, digest zeroed */ + uint64_t flags; + uint64_t index_offset; + uint64_t index_size; /* # of u32 jump-table entries (blocks) */ + uint64_t original_file_size; + uint32_t index_crc; + uint32_t reserved_0; + OverlaybdCompressOptions opt; + uint8_t reserved_space[416]; +} OverlaybdZFileHeader; + +QEMU_BUILD_BUG_ON(sizeof(OverlaybdCompressOptions) != 24); +QEMU_BUILD_BUG_ON(sizeof(OverlaybdZFileHeader) != ZFILE_HT_SPACE); + +/* ZFile::MAX_ZFILE_INDEX_SIZE, zfile/zfile.cpp */ +#define ZFILE_MAX_INDEX_ENTRIES 1000000000LL + +/* crc32c_salt()'s seed, zfile/zfile.cpp: per-block CRCs are salted */ +#define ZFILE_CRC_SALT 100007 + +/* + * ZFile::JumpTable, zfile/zfile.cpp: within-group offsets are uint16_t, + * so one group spans at most UINT16_MAX + 1 bytes of compressed data. + */ +#define ZFILE_JUMP_GROUP_BYTES (UINT16_MAX + 1u) + +#define ZFILE_INDEX_CHUNK_BYTES (1 << 20) /* ZFile::load_jump_table delta */ +#define ZFILE_INDEX_MAX_COROUTINES 32 + +/* + * In-memory 16-byte index entry. The field assignment mirrors the + * on-disk SegmentMapping (lo = lba:50 | length:14, hi = moffset:55 | + * zeroed:1 | tag:8, the tag slot holding the serving layer at runtime, + * like the reference implementation), but this struct deliberately does + * NOT map the disk image: bitfields are forbidden in packed or + * exact-layout structures (docs/devel/style.rst), so on-disk bytes are + * decoded by overlaybd_decode_entry() with explicit little-endian loads + * and the bit order below is compiler-managed, never exposed. + */ +typedef struct OverlaybdSegment { + uint64_t lba:50; + uint64_t length:14; + uint64_t moffset:55; + uint64_t zeroed:1; + uint64_t layer:8; +} OverlaybdSegment; + +QEMU_BUILD_BUG_ON(sizeof(OverlaybdSegment) != 16); + +typedef struct OverlaybdZfile { + uint64_t index_offset; /* jump table offset, ZFile-relative bytes */ + uint64_t index_size; + uint64_t original_file_size; + uint32_t block_size; + uint8_t algo; + bool verify; + /* + * ZFile::JumpTable, zfile/zfile.cpp: block @i starts at + * partial_offset[i >> group_shift] + deltas[i], except at a group + * boundary where the delta is zero. + */ + uint64_t *partial_offset; + uint16_t *deltas; + unsigned group_shift; +} OverlaybdZfile; + +typedef struct OverlaybdLayer { + BdrvChild *child; + bool is_zfile; + uint64_t base_offset; /* tar prefix length in the child, bytes */ + uint64_t view_size; + OverlaybdZfile zf; + /* LSMT metadata, in ZFile-virtual space for compressed layers */ + uint64_t index_offset; + OverlaybdSegment *index; + size_t index_count; + uint64_t virtual_size; +} OverlaybdLayer; + +typedef struct BDRVOverlaybdState { + OverlaybdLayer *layers; /* top-first: layers[0] is the topmost layer */ + int nb_layers; + OverlaybdSegment *merged; + size_t merged_cap; + size_t merged_count; + uint64_t virtual_size; +} BDRVOverlaybdState; + +/* + * Load one on-disk 16-byte index entry (two little-endian u64: + * lo = lba:50 | length:14, hi = moffset:55 | zeroed:1 | tag:8) into the + * host-side entry. The on-disk tag is discarded; the merge assigns + * layers. + */ +static void overlaybd_decode_entry(const uint8_t *p, OverlaybdSegment *out) +{ + uint64_t lo = ldq_le_p(p); + uint64_t hi = ldq_le_p(p + 8); + + out->lba = lo & (((uint64_t)1 << 50) - 1); + out->length = (lo >> 50) & ((1u << 14) - 1); + out->moffset = hi & (((uint64_t)1 << 55) - 1); + out->zeroed = (hi >> 55) & 1; + out->layer = 0; +} + +static bool overlaybd_has_magic(const void *ht, const void *magic0, + const void *magic1) +{ + return memcmp(ht, magic0, 8) == 0 && + memcmp((const uint8_t *)ht + 8, magic1, 16) == 0; +} + +static bool overlaybd_is_lsmt_header(const uint8_t *buf) +{ + return overlaybd_has_magic(buf, lsmt_magic0, lsmt_magic1); +} + +static bool overlaybd_is_zfile_header(const uint8_t *buf) +{ + return overlaybd_has_magic(buf, zfile_magic0, zfile_magic1); +} + +static bool overlaybd_is_tar_header(const uint8_t *hdr) +{ + return memcmp(hdr + 257, "ustar", 5) == 0 && + (hdr[156] == '0' || hdr[156] == '\0' || hdr[156] == 'x'); +} + +static bool overlaybd_verify_lsmt_block(const OverlaybdLSMTHeader *ht, + bool is_trailer) +{ + uint32_t flags; + + if (!overlaybd_has_magic(ht, lsmt_magic0, lsmt_magic1)) { + return false; + } + flags = le32_to_cpu(ht->flags); + if (is_trailer) { + return !(flags & LSMT_FLAG_HEADER) && + (flags & LSMT_FLAG_DATA_FILE) && + (flags & LSMT_FLAG_SEALED); + } + return flags & LSMT_FLAG_HEADER; +} + +/* + * Upstream's crc32::crc32c_extend() is the raw CRC-32C recurrence with no + * post-conditioning, while qemu's crc32c() inverts once on return, so XOR + * the result to recover upstream's value for any seed. A wrong convention + * here fails silently against real images: nothing but the digest mismatches. + */ +static uint32_t overlaybd_crc32c(uint32_t seed, const void *buf, size_t len) +{ + return crc32c(seed, buf, len) ^ 0xffffffff; +} + +static bool overlaybd_zfile_verify_digest(OverlaybdZFileHeader *blk) +{ + uint32_t crc, saved = le32_to_cpu(blk->digest); + + blk->digest = 0; + crc = overlaybd_crc32c(0, blk, sizeof(*blk)); + blk->digest = cpu_to_le32(saved); + return crc == saved; +} + +static bool overlaybd_zfile_verify_block(OverlaybdZFileHeader *blk, + bool is_trailer) +{ + uint64_t flags; + + if (!overlaybd_has_magic(blk, zfile_magic0, zfile_magic1)) { + return false; + } + flags = le64_to_cpu(blk->flags); + if (is_trailer) { + if ((flags & ZFILE_FLAG_HEADER) || !(flags & ZFILE_FLAG_DATA_FILE) || + !(flags & ZFILE_FLAG_SEALED)) { + return false; + } + } else { + if (!(flags & ZFILE_FLAG_HEADER)) { + return false; + } + } + if (flags & ZFILE_FLAG_DIGEST && !overlaybd_zfile_verify_digest(blk)) { + return false; + } + return true; +} + +/* + * Locate a header/trailer block of @ht_space bytes near the end of the + * layer and return its offset, relative to the child, or -errno. The + * block found is also copied to @ht_out, which must hold @ht_space bytes: + * it has already been read and validated here, so re-reading it at the + * returned offset would be a second I/O for the same bytes. + * + * The end of the layer is not the end of the child: a tar wrapper pads + * its entry to a blocking factor, so the scan must stop at + * base_offset + view_size. That end is still not exact for a bare layer, + * whose view_size is just bdrv_getlength() rounded up to 512 bytes, so + * scan the last @ht_space + 511 bytes for the magic (the same approach + * as dmg_find_koly_offset()), taking the match closest to the end. + */ +static int64_t GRAPH_RDLOCK +overlaybd_scan_trailer(OverlaybdLayer *l, size_t ht_space, bool is_zfile, + void *ht_out) +{ + int64_t end = l->base_offset + l->view_size; + int64_t window, off, found = -ENOENT; + uint8_t *buf; + int ret; + + window = (int64_t)ht_space + 511; + off = end > window ? end - window : 0; + window = end - off; + + buf = g_malloc(window); + ret = bdrv_pread(l->child, off, window, buf, 0); + if (ret < 0) { + g_free(buf); + return ret; + } + for (int64_t i = (int64_t)window - ht_space; i >= 0; i--) { + bool ok; + + /* copy the whole block: the ZFile digest covers all 512 bytes */ + if (is_zfile) { + OverlaybdZFileHeader blk; + + memcpy(&blk, buf + i, sizeof(blk)); + ok = overlaybd_zfile_verify_block(&blk, true); + } else { + OverlaybdLSMTHeader blk; + + memcpy(&blk, buf + i, sizeof(blk)); + ok = overlaybd_verify_lsmt_block(&blk, true); + } + if (ok) { + memcpy(ht_out, buf + i, ht_space); + found = off + i; + break; + } + } + g_free(buf); + return found; +} + +/* + * ZFile::JumpTable::build(), zfile/zfile.cpp: turn the on-disk u32 + * block lengths into one absolute offset per group plus uint16_t + * within-group prefix sums. @offset_begin is where block 0 lives, + * relative to the ZFile. + */ +static int overlaybd_zfile_build_table(OverlaybdZfile *zf, + const uint32_t *table, + uint64_t offset_begin, Error **errp) +{ + uint64_t group_size = ZFILE_JUMP_GROUP_BYTES / zf->block_size; + uint64_t min_span = zf->verify ? 4 : 0; + uint64_t raw_offset = offset_begin; + uint32_t span; + + zf->group_shift = ctz64(group_size); + /* + * g_try_new, not g_new: both sizes come from the image, and a hostile + * header can ask for gigabytes (at block_size 65536, group_size is 1 + * and partial_offset costs 8 bytes per entry). + */ + zf->partial_offset = g_try_new(uint64_t, zf->index_size / group_size + 1); + zf->deltas = g_try_new(uint16_t, zf->index_size + 1); + if (!zf->partial_offset || !zf->deltas) { + return -ENOMEM; + } + zf->partial_offset[0] = raw_offset; + zf->deltas[0] = 0; + + for (uint64_t i = 1; i <= zf->index_size; i++) { + span = ldl_le_p((const uint8_t *)&table[i - 1]); + + if (span <= min_span || span > ZFILE_MAX_SPAN) { + error_setg(errp, "invalid ZFile block size in jump table"); + return -EINVAL; + } + raw_offset += span; + if (i % group_size == 0) { + zf->partial_offset[i / group_size] = raw_offset; + zf->deltas[i] = 0; + continue; + } + if ((uint64_t)zf->deltas[i - 1] + span >= UINT16_MAX) { + error_setg(errp, "ZFile compressed blocks in one jump table " + "group exceed %d bytes", UINT16_MAX); + return -ERANGE; + } + zf->deltas[i] = zf->deltas[i - 1] + span; + } + return 0; +} + +/* ZFile::JumpTable::operator[], zfile/zfile.cpp */ +static uint64_t overlaybd_zfile_block_off(const OverlaybdZfile *zf, + uint64_t idx) +{ + uint64_t part = zf->partial_offset[idx >> zf->group_shift]; + + if (idx & ((1ULL << zf->group_shift) - 1)) { + return part + zf->deltas[idx]; + } + return part; +} + +typedef struct OverlaybdZfileLoad { + BdrvChild *child; + int64_t offset; /* table start, relative to the child */ + uint8_t *table; + uint64_t bytes; /* table size */ + uint64_t next; /* first byte no coroutine has claimed yet */ + int nb_done; + int ret; +} OverlaybdZfileLoad; + +/* + * The caller holds the graph rdlock across the whole open, so these + * coroutines take no further lock. They share the one cursor in @load: + * claiming a chunk reads load->next then advances it with no yield between, + * and coroutines in one AioContext never run concurrently, so the chunks are + * partitioned with no locking. Inputs are copied to locals here; only next, + * ret and nb_done stay shared (a private next would spin on chunk 0 forever). + */ +static void coroutine_fn GRAPH_RDLOCK +overlaybd_zfile_load_co(void *opaque) +{ + OverlaybdZfileLoad *load = opaque; + BdrvChild *child = load->child; + int64_t offset = load->offset; + uint8_t *table = load->table; + uint64_t bytes = load->bytes; + + while (load->next < bytes) { + uint64_t off = load->next; + uint64_t len = MIN(bytes - off, (uint64_t)ZFILE_INDEX_CHUNK_BYTES); + int ret; + + load->next = off + len; + + ret = bdrv_co_pread(child, offset + off, len, table + off, 0); + if (ret < 0) { + if (load->ret == 0) { + load->ret = ret; + } + break; + } + } + load->nb_done++; +} + +static int GRAPH_RDLOCK +overlaybd_zfile_load_table(OverlaybdLayer *l, uint32_t *table, Error **errp) +{ + OverlaybdZfile *zf = &l->zf; + uint64_t bytes = zf->index_size * sizeof(uint32_t); + int ret; + + if (bytes <= ZFILE_INDEX_CHUNK_BYTES) { + ret = bdrv_pread(l->child, l->base_offset + zf->index_offset, + bytes, table, 0); + } else { + OverlaybdZfileLoad load = { + .child = l->child, + .offset = l->base_offset + zf->index_offset, + .table = (uint8_t *)table, + .bytes = bytes, + }; + int nb_chunks = DIV_ROUND_UP(bytes, ZFILE_INDEX_CHUNK_BYTES); + int nb_cos = MIN(nb_chunks, ZFILE_INDEX_MAX_COROUTINES); + + for (int i = 0; i < nb_cos; i++) { + qemu_coroutine_enter(qemu_coroutine_create(overlaybd_zfile_load_co, + &load)); + } + BDRV_POLL_WHILE(l->child->bs, load.nb_done < nb_cos); + ret = load.ret; + } + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read ZFile jump table"); + } + return ret; +} + +static int GRAPH_RDLOCK +overlaybd_zfile_open(OverlaybdLayer *l, Error **errp) +{ + BdrvChild *child = l->child; + OverlaybdZfile *zf = &l->zf; + OverlaybdZFileHeader ht; + uint64_t flags, index_offset, index_size, original_file_size; + uint32_t *table = NULL; + uint32_t block_size; + uint8_t algo, verify; + int64_t child_size, zfile_end; + uint64_t end_reserve; + int ret; + + child_size = bdrv_getlength(child->bs); + if (child_size < 0) { + error_setg_errno(errp, -child_size, "could not get layer file size"); + return child_size; + } + child_size -= l->base_offset; + if (child_size < 2 * ZFILE_HT_SPACE) { + error_setg(errp, "layer file too small to be a ZFile"); + return -EINVAL; + } + + ret = bdrv_pread(child, l->base_offset, sizeof(ht), &ht, 0); + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read ZFile header"); + return ret; + } + if (!overlaybd_zfile_verify_block(&ht, false)) { + error_setg(errp, "invalid ZFile header"); + return -EINVAL; + } + + flags = le64_to_cpu(ht.flags); + /* + * The layer's own end, not the child's: a tar wrapper pads its entry. + * bdrv_getlength() also rounds up, so when there is a trailer the + * scanned trailer position gives the exact end instead. + */ + zfile_end = l->view_size; + end_reserve = 0; + if (!(flags & ZFILE_FLAG_HEADER_OVERWRITE)) { + /* + * Without HEADER_OVERWRITE the trailer carries the final metadata, + * so the header is dead once its flags have been read and the + * trailer is read into the same block -- as upstream does. + */ + int64_t trl_off = overlaybd_scan_trailer(l, ZFILE_HT_SPACE, true, + &ht); + + if (trl_off < 0) { + error_setg(errp, "invalid ZFile trailer"); + return -EINVAL; + } + flags = le64_to_cpu(ht.flags); + zfile_end = trl_off + ZFILE_HT_SPACE - (int64_t)l->base_offset; + end_reserve = ZFILE_HT_SPACE; + } + + if (flags & ZFILE_FLAG_INDEX_COMPRESSED) { + error_setg(errp, "compressed ZFile jump table is not supported"); + return -ENOTSUP; + } + + index_offset = le64_to_cpu(ht.index_offset); + index_size = le64_to_cpu(ht.index_size); + original_file_size = le64_to_cpu(ht.original_file_size); + block_size = le32_to_cpu(ht.opt.block_size); + algo = ht.opt.algo; + verify = ht.opt.verify; + + if (ht.opt.use_dict != 0) { + error_setg(errp, "ZFile with dictionary is not supported"); + return -ENOTSUP; + } + if (block_size == 0 || block_size > ZFILE_MAX_BLOCK_SIZE || + !is_power_of_2(block_size)) { + error_setg(errp, "invalid ZFile block size %" PRIu32, block_size); + return -EINVAL; + } + if (algo != ZFILE_ALGO_LZ4 && algo != ZFILE_ALGO_ZSTD) { + error_setg(errp, "unknown ZFile compression algorithm %" PRIu8, algo); + return -EINVAL; + } +#ifndef CONFIG_LZ4 + if (algo == ZFILE_ALGO_LZ4) { + error_setg(errp, "LZ4-compressed layer, but qemu was compiled " + "without lz4 support"); + return -ENOTSUP; + } +#endif +#ifndef CONFIG_ZSTD + if (algo == ZFILE_ALGO_ZSTD) { + error_setg(errp, "ZSTD-compressed layer, but qemu was compiled " + "without zstd support"); + return -ENOTSUP; + } +#endif + + if (index_size > ZFILE_MAX_INDEX_ENTRIES) { + error_setg(errp, "ZFile index size %" PRIu64 " exceeds maximum " + "%lld", index_size, ZFILE_MAX_INDEX_ENTRIES); + return -EINVAL; + } + /* + * order the checks so each subtraction is guarded by the previous + * condition (no unsigned wraparound on adversarial values) + */ + if (index_offset < ZFILE_HT_SPACE || + index_offset > (uint64_t)zfile_end - end_reserve || + index_size * 4 > (uint64_t)zfile_end - end_reserve - index_offset) { + error_setg(errp, "invalid ZFile jump table location"); + return -EINVAL; + } + if ((index_size - 1) * block_size >= original_file_size || + original_file_size > index_size * block_size) { + error_setg(errp, "ZFile block count does not match content size"); + return -EINVAL; + } + + zf->index_offset = index_offset; + zf->index_size = index_size; + zf->original_file_size = original_file_size; + zf->block_size = block_size; + zf->algo = algo; + zf->verify = verify; + + table = qemu_try_blockalign(l->child->bs, + index_size * sizeof(uint32_t)); + if (!table) { + return -ENOMEM; + } + ret = overlaybd_zfile_load_table(l, table, errp); + if (ret < 0) { + goto out; + } + /* + * like the reference implementation, the jump table CRC is only + * checked when the digest flag is set (it is garbage otherwise) + */ + if ((flags & ZFILE_FLAG_DIGEST) && le32_to_cpu(ht.index_crc) != + overlaybd_crc32c(0, table, index_size * sizeof(uint32_t))) { + error_setg(errp, "ZFile jump table CRC mismatch"); + ret = -EIO; + goto out; + } + + ret = overlaybd_zfile_build_table(zf, table, + ZFILE_HT_SPACE + + le32_to_cpu(ht.opt.dict_size), errp); + if (ret < 0) { + goto out; + } + if (overlaybd_zfile_block_off(zf, zf->index_size) > zf->index_offset) { + error_setg(errp, "ZFile data region overlaps the jump table"); + ret = -EINVAL; + goto out; + } + + l->view_size = zf->original_file_size; + ret = 0; +out: + qemu_vfree(table); + return ret; +} + +/* + * Forward-only cursor into a QEMUIOVector. Seeding it and committing each + * block's bytes are the same advance, so they share one. The blocks of a + * request are written contiguously in increasing order, so a full pass costs + * O(niov) rather than the O(niov * nblocks) of qemu_iovec_subvec_niov(), + * which rescans from iov[0] -- expensive for the one-iovec-per-guest-page + * qiov that virtio-blk normally hands us. + */ +typedef struct OverlaybdQiovCursor { + struct iovec *iov; + struct iovec *end; + uint8_t *cur; + size_t left; +} OverlaybdQiovCursor; + +static void overlaybd_qiov_cursor_advance(OverlaybdQiovCursor *c, size_t n) +{ + while (c->cur && n >= c->left) { + n -= c->left; + if (++c->iov >= c->end) { + c->cur = NULL; + c->left = 0; + return; + } + c->cur = (uint8_t *)c->iov->iov_base; + c->left = c->iov->iov_len; + } + if (c->cur) { + c->cur += n; + c->left -= n; + } +} + +/* + * Point @c at byte 0 of @qiov. @qiov is NULL on the buf path, which + * leaves the cursor exhausted, so every test of it fails closed. + */ +static void overlaybd_qiov_cursor_init(OverlaybdQiovCursor *c, + QEMUIOVector *qiov) +{ + c->iov = qiov ? qiov->iov : NULL; + c->end = qiov ? qiov->iov + qiov->niov : NULL; + c->cur = c->iov < c->end ? (uint8_t *)c->iov->iov_base : NULL; + c->left = c->cur ? c->iov->iov_len : 0; +} + +/* + * All I/O goes through bdrv_pread() (a mixed wrapper), so this is + * callable from both GS and coroutine contexts. + */ +static int coroutine_mixed_fn GRAPH_RDLOCK +overlaybd_zfile_read(OverlaybdLayer *l, uint64_t off, uint64_t bytes, + void *buf, QEMUIOVector *qiov, size_t qiov_off) +{ + OverlaybdZfile *zf = &l->zf; + uint32_t bsz = zf->block_size; + uint64_t first, last, base, need; + uint8_t *cbuf = NULL; + uint8_t *scratch = NULL; + OverlaybdQiovCursor c; + int ret; + + assert(off + bytes <= zf->original_file_size); + + /* + * also keeps "off + bytes - 1" below from wrapping when both are 0, + * which a caller reading an empty index can legitimately ask for + */ + if (bytes == 0) { + return 0; + } + + first = off / bsz; + last = (off + bytes - 1) / bsz; + /* + * One read covers the compressed bytes of every block the request spans, + * so the loop below never refills. A hostile jump table can still make + * @need many times larger than @bytes, hence qemu_try_blockalign() rather + * than the aborting qemu_blockalign(); aligning to the child's + * opt_mem_alignment lets an already-aligned request reach the protocol + * driver without being bounced. + */ + base = overlaybd_zfile_block_off(zf, first); + need = overlaybd_zfile_block_off(zf, last + 1) - base; + + /* + * One allocation for both: the compressed window, then the one-block + * decompression scratch behind it. bdrv_pread() writes only the first + * @need bytes so the two never overlap, and only the window goes to the + * block layer, so only its start needs the alignment. + */ + cbuf = qemu_try_blockalign(l->child->bs, need + bsz); + if (!cbuf) { + return -ENOMEM; + } + scratch = cbuf + need; + + ret = bdrv_pread(l->child, l->base_offset + base, need, cbuf, 0); + if (ret < 0) { + goto out; + } + + overlaybd_qiov_cursor_init(&c, qiov); + overlaybd_qiov_cursor_advance(&c, qiov_off); + + for (uint64_t bi = first; bi <= last; bi++) { + uint64_t bstart = bi * (uint64_t)bsz; + uint64_t expected = MIN((uint64_t)bsz, + zf->original_file_size - bstart); + uint64_t cp_begin = bstart > off ? 0 : off - bstart; + uint64_t cp_len = MIN(expected, off + bytes - bstart) - cp_begin; + uint64_t dst_off = bstart + cp_begin - off; + uint64_t off_blk = overlaybd_zfile_block_off(zf, bi); + uint64_t span = overlaybd_zfile_block_off(zf, bi + 1) - off_blk; + uint64_t clen = span - (zf->verify ? 4 : 0); + const uint8_t *cdata; + uint8_t *dst = NULL, *dec; + + cdata = cbuf + (off_blk - base); + + /* + * BlockReader::crc32_code(), zfile/zfile.cpp: the salted CRC trails + * the compressed data and covers exactly those bytes. Upstream + * retries three times before failing, which cannot help against a + * read-only child. + */ + if (zf->verify) { + uint32_t want = ldl_le_p(cdata + clen); + + if (want != overlaybd_crc32c(ZFILE_CRC_SALT, cdata, clen)) { + ret = -EIO; + goto out; + } + } + + if (cp_begin == 0 && cp_len == expected) { + if (buf) { + dst = (uint8_t *)buf + dst_off; + } else if (c.cur && cp_len <= c.left) { + dst = c.cur; + } + } + dec = dst ? dst : scratch; + + ret = -EIO; +#ifdef CONFIG_LZ4 + if (zf->algo == ZFILE_ALGO_LZ4) { + int r = LZ4_decompress_safe((const char *)cdata, (char *)dec, + clen, expected); + + if (r >= 0 && (uint64_t)r == expected) { + ret = 0; + } + } +#endif +#ifdef CONFIG_ZSTD + if (zf->algo == ZFILE_ALGO_ZSTD) { + size_t r = ZSTD_decompress(dec, expected, cdata, clen); + + if (!ZSTD_isError(r) && r == expected) { + ret = 0; + } + } +#endif + if (ret < 0) { + goto out; + } + if (!dst) { + if (buf) { + memcpy((uint8_t *)buf + dst_off, scratch + cp_begin, cp_len); + } else { + qemu_iovec_from_buf(qiov, qiov_off + dst_off, + scratch + cp_begin, cp_len); + } + } + + overlaybd_qiov_cursor_advance(&c, cp_len); + } + + ret = 0; +out: + qemu_vfree(cbuf); + return ret; +} + +static int GRAPH_RDLOCK +overlaybd_pread_view(OverlaybdLayer *l, uint64_t off, int64_t bytes, void *buf) +{ + if (l->is_zfile) { + return overlaybd_zfile_read(l, off, bytes, buf, NULL, 0); + } + return bdrv_pread(l->child, l->base_offset + off, bytes, buf, 0); +} + +/* + * Load and validate the LSMT header/trailer/index of a layer. All + * metadata comes from the trailer; the header is only a format sniff + * (real images may have garbage header fields). + */ +static int GRAPH_RDLOCK +overlaybd_lsmt_open(OverlaybdLayer *l, Error **errp) +{ + OverlaybdLSMTHeader ht; + OverlaybdSegment *index = NULL; + uint8_t *raw = NULL; + uint64_t index_offset, index_size, virtual_size, moffset_end; + uint64_t index_bound; + uint64_t prev_end = 0; + size_t count = 0; + int ret; + + if (l->view_size < 2 * LSMT_HT_SPACE) { + error_setg(errp, "layer too small to be an LSMT image"); + return -EINVAL; + } + + ret = overlaybd_pread_view(l, 0, LSMT_HT_SPACE, &ht); + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read LSMT header"); + return ret; + } + if (!overlaybd_verify_lsmt_block(&ht, false)) { + error_setg(errp, "invalid LSMT header"); + return -EINVAL; + } + + /* + * The header is only a format sniff, so reading the trailer into the + * same block is safe. index_bound is where the index must end: the + * trailer start, which for a bare layer is not view_size - SPACE + * because bdrv_getlength() over-reports (tar padding, rounding). + */ + if (l->is_zfile) { + /* in the ZFile virtual space the content size is exact */ + ret = overlaybd_pread_view(l, l->view_size - LSMT_HT_SPACE, + LSMT_HT_SPACE, &ht); + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read LSMT trailer"); + return ret; + } + index_bound = l->view_size - LSMT_HT_SPACE; + } else { + int64_t trl_off = overlaybd_scan_trailer(l, LSMT_HT_SPACE, false, + &ht); + + if (trl_off < 0) { + error_setg(errp, "invalid LSMT trailer (not a sealed data " + "layer?)"); + return trl_off < -ENOENT ? trl_off : -EINVAL; + } + index_bound = (uint64_t)(trl_off - (int64_t)l->base_offset); + } + if (!overlaybd_verify_lsmt_block(&ht, true)) { + error_setg(errp, "invalid LSMT trailer (not a sealed data layer?)"); + return -EINVAL; + } + + index_offset = le64_to_cpu(ht.index_offset); + index_size = le64_to_cpu(ht.index_size); + virtual_size = le64_to_cpu(ht.virtual_size); + + if (index_size > LSMT_MAX_RO_INDEX_ENTRIES) { + error_setg(errp, "LSMT index of %" PRIu64 " entries exceeds maximum " + "%" PRId64, index_size, LSMT_MAX_RO_INDEX_ENTRIES); + return -EINVAL; + } + if (index_offset < LSMT_HT_SPACE || + index_offset > index_bound || + index_size * 16 > index_bound - index_offset) { + error_setg(errp, "invalid LSMT index location"); + return -EINVAL; + } + + /* + * both sizes come from the image, so the allocating variants that + * abort on OOM are not acceptable here; out: frees either pointer + */ + index = g_try_new(OverlaybdSegment, index_size); + raw = qemu_try_blockalign(l->child->bs, index_size * 16); + if (!index || !raw) { + ret = -ENOMEM; + goto out; + } + ret = overlaybd_pread_view(l, index_offset, index_size * 16, raw); + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read LSMT index"); + goto out; + } + + moffset_end = index_offset / BDRV_SECTOR_SIZE; + for (uint64_t i = 0; i < index_size; i++) { + OverlaybdSegment seg, *out; + + overlaybd_decode_entry(raw + i * 16, &seg); + if (seg.lba == LSMT_INVALID_OFFSET || seg.length == 0) { + continue; + } + if (count > 0 && seg.lba < prev_end) { + error_setg(errp, "LSMT index is disordered or overlapping"); + ret = -EINVAL; + goto out; + } + if (seg.zeroed) { + if (seg.moffset < LSMT_MOFFSET_BEGIN || + seg.moffset > moffset_end) { + error_setg(errp, "LSMT zeroed entry moffset out of range"); + ret = -EINVAL; + goto out; + } + } else { + if (seg.moffset < LSMT_MOFFSET_BEGIN || + seg.moffset >= moffset_end || + seg.moffset + seg.length > moffset_end) { + error_setg(errp, "LSMT entry moffset out of range"); + ret = -EINVAL; + goto out; + } + } + out = &index[count++]; + *out = seg; + prev_end = seg.lba + seg.length; + } + + l->index_offset = index_offset; + l->index = index; + index = NULL; + l->index_count = count; + l->virtual_size = virtual_size; + ret = 0; +out: + qemu_vfree(raw); + g_free(index); + return ret; +} + +static int GRAPH_RDLOCK +overlaybd_detect_tar(OverlaybdLayer *l, int64_t child_size, Error **errp) +{ + uint8_t hdr[512]; + uint64_t hoff = 0; + int ret; + + l->base_offset = 0; + l->view_size = child_size; + + if (child_size < 1024) { + return 0; + } + ret = bdrv_pread(l->child, 0, sizeof(hdr), hdr, 0); + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read layer file header"); + return ret; + } + if (memcmp(hdr + 257, "ustar", 5) != 0) { + return 0; + } + if (!overlaybd_is_tar_header(hdr)) { + error_setg(errp, "unsupported tar layer layout"); + return -EINVAL; + } + if (hdr[156] == 'x') { + /* + * pax extended header: its size field gives the length of the + * attribute block that precedes the real ustar header + */ + char size[13]; + unsigned long long pax_size; + + memcpy(size, hdr + 124, 12); + size[12] = 0; + if (qemu_strtou64(size, NULL, 8, &pax_size) < 0) { + pax_size = 0; + } + hoff = ZFILE_HT_SPACE + + DIV_ROUND_UP(pax_size, ZFILE_HT_SPACE) * ZFILE_HT_SPACE; + if (hoff + 2 * ZFILE_HT_SPACE > (uint64_t)child_size) { + error_setg(errp, "tar layer too small"); + return -EINVAL; + } + ret = bdrv_pread(l->child, hoff, sizeof(hdr), hdr, 0); + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read tar file header"); + return ret; + } + if (memcmp(hdr + 257, "ustar", 5) != 0 || + (hdr[156] != '0' && hdr[156] != '\0')) { + error_setg(errp, "unsupported tar layer layout"); + return -EINVAL; + } + } + + l->base_offset = hoff + ZFILE_HT_SPACE; + l->view_size = child_size - l->base_offset; + if (hdr[124] < 0x80) { + /* + * prefer the tar entry size: it is authoritative when the tar + * entry is padded + */ + char size[13]; + unsigned long long tar_size; + + memcpy(size, hdr + 124, 12); + size[12] = 0; + if (!qemu_strtou64(size, NULL, 8, &tar_size) && + tar_size > 0 && tar_size <= l->view_size) { + l->view_size = tar_size; + } + } + return 0; +} + +static int GRAPH_RDLOCK +overlaybd_open_layer(OverlaybdLayer *l, Error **errp) +{ + uint8_t buf[512]; + int64_t child_size; + int ret; + + child_size = bdrv_getlength(l->child->bs); + if (child_size < 0) { + error_setg_errno(errp, -child_size, "could not get layer file size"); + return child_size; + } + + ret = overlaybd_detect_tar(l, child_size, errp); + if (ret < 0) { + return ret; + } + if (l->view_size < 2 * ZFILE_HT_SPACE) { + error_setg(errp, "layer file too small"); + return -EINVAL; + } + + ret = bdrv_pread(l->child, l->base_offset, sizeof(buf), buf, 0); + if (ret < 0) { + error_setg_errno(errp, -ret, "could not read layer file header"); + return ret; + } + if (overlaybd_is_zfile_header(buf) && + (ldq_le_p(buf + 32) & ZFILE_FLAG_HEADER)) { + l->is_zfile = true; + ret = overlaybd_zfile_open(l, errp); + if (ret < 0) { + return ret; + } + } + + return overlaybd_lsmt_open(l, errp); +} + +/* + * First index whose entry covers or follows @sector: entries are sorted + * and non-overlapping, so entry ends are monotonically increasing. + * This matches the reference implementation's lower_bound(), which + * searches by entry end so that an entry covering @sector is found. + */ +static size_t overlaybd_find_segment(const OverlaybdSegment *a, size_t n, + uint64_t sector) +{ + size_t lo = 0, hi = n; + + while (lo < hi) { + size_t mid = (lo + hi) / 2; + + if (a[mid].lba + a[mid].length <= sector) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; +} + +/* + * Merge the per-layer indexes into s->merged: layers[0] is the topmost + * layer; a layer's mapping wins its whole extent, lower layers fill the + * holes. Entries pushed for [begin, end) are edge-trimmed to that range. + */ +static bool overlaybd_merge_level(BDRVOverlaybdState *s, int level, + uint64_t begin, uint64_t end, size_t *count) +{ + OverlaybdLayer *l; + size_t i, size0 = *count; + uint64_t begin0 = begin; + + if (level >= s->nb_layers || begin >= end) { + return true; + } + l = &s->layers[level]; + for (i = overlaybd_find_segment(l->index, l->index_count, begin); + i < l->index_count && l->index[i].lba < end; i++) { + OverlaybdSegment *e = &l->index[i]; + + if (e->lba > begin && + !overlaybd_merge_level(s, level + 1, begin, e->lba, count)) { + return false; + } + if (*count == s->merged_cap) { + /* + * One input entry can be emitted several times when it is + * split by ranges covered (or zeroed) at upper layers, so the + * output can exceed the sum of the input indexes -- hence the + * cap, which upstream enforces at the same point. Grow into a + * temporary so that a failed realloc leaves s->merged valid + * for overlaybd_close() to free. + */ + size_t cap = s->merged_cap ? s->merged_cap * 2 : 64; + OverlaybdSegment *grown; + + if (cap > LSMT_MAX_MERGED_ENTRIES) { + return false; + } + grown = g_try_renew(OverlaybdSegment, s->merged, cap); + if (!grown) { + return false; + } + s->merged = grown; + s->merged_cap = cap; + } + s->merged[*count] = *e; + s->merged[*count].layer = level; + (*count)++; + begin = e->lba + e->length; + } + if (begin < end && + !overlaybd_merge_level(s, level + 1, begin, end, count)) { + return false; + } + if (*count > size0) { + OverlaybdSegment *first = &s->merged[size0]; + OverlaybdSegment *last = &s->merged[*count - 1]; + + if (first->lba < begin0) { + uint64_t delta = begin0 - first->lba; + + first->length -= delta; + first->lba = begin0; + if (!first->zeroed) { + first->moffset += delta; + } + } + if (last->lba + last->length > end) { + last->length = end - last->lba; + } + } + return true; +} + +static int coroutine_fn GRAPH_RDLOCK +overlaybd_co_preadv(BlockDriverState *bs, int64_t offset, int64_t bytes, + QEMUIOVector *qiov, BdrvRequestFlags flags) +{ + BDRVOverlaybdState *s = bs->opaque; + uint64_t cur = offset >> BDRV_SECTOR_BITS; + uint64_t end = cur + (bytes >> BDRV_SECTOR_BITS); + size_t done = 0; + int ret = 0; + + assert(QEMU_IS_ALIGNED(offset, BDRV_SECTOR_SIZE)); + assert(QEMU_IS_ALIGNED(bytes, BDRV_SECTOR_SIZE)); + + while (cur < end) { + OverlaybdSegment *e; + OverlaybdLayer *l; + uint64_t run, run_end; + size_t idx = overlaybd_find_segment(s->merged, s->merged_count, cur); + + if (idx == s->merged_count || s->merged[idx].lba >= end) { + qemu_iovec_memset(qiov, done, 0, (end - cur) * BDRV_SECTOR_SIZE); + break; + } + e = &s->merged[idx]; + if (e->lba > cur) { + run = (e->lba - cur) * BDRV_SECTOR_SIZE; + qemu_iovec_memset(qiov, done, 0, run); + done += run; + cur = e->lba; + } + run_end = MIN(end, e->lba + e->length); + run = (run_end - cur) * BDRV_SECTOR_SIZE; + if (e->zeroed) { + qemu_iovec_memset(qiov, done, 0, run); + } else { + int64_t off = (e->moffset + (cur - e->lba)) * BDRV_SECTOR_SIZE; + + l = &s->layers[e->layer]; + if (l->is_zfile) { + ret = overlaybd_zfile_read(l, off, run, NULL, qiov, done); + } else { + ret = bdrv_co_preadv_part(l->child, l->base_offset + off, run, + qiov, done, 0); + } + if (ret < 0) { + return ret; + } + } + done += run; + cur = run_end; + } + return ret; +} + +static void overlaybd_refresh_limits(BlockDriverState *bs, Error **errp) +{ + bs->bl.request_alignment = BDRV_SECTOR_SIZE; +} + +static int overlaybd_probe(const uint8_t *buf, int buf_size, + const char *filename) +{ + if (buf_size < 24) { + return 0; + } + /* + * Only a bare LSMT or ZFile magic is distinctive enough to claim a file. + * A tar-wrapped layer shows just its entry header here, the payload + * starting past the 512-byte probe window, and tar is far too generic a + * container to claim on that evidence -- every unrelated archive would + * then fail to open instead of being read as raw. So a tar-wrapped layer + * must be named explicitly (-f overlaybd, file.driver=, + * layers.<n>.driver= or a manifest), all of which bypass probing. + */ + if (overlaybd_is_lsmt_header(buf) || overlaybd_is_zfile_header(buf)) { + return 100; + } + return 0; +} + +static void overlaybd_close(BlockDriverState *bs) +{ + BDRVOverlaybdState *s = bs->opaque; + int i; + + bdrv_graph_wrlock_drained(); + for (i = 0; i < s->nb_layers; i++) { + if (s->layers[i].child) { + bdrv_unref_child(bs, s->layers[i].child); + s->layers[i].child = NULL; + } + } + bdrv_graph_wrunlock(); + + for (i = 0; i < s->nb_layers; i++) { + g_free(s->layers[i].zf.partial_offset); + g_free(s->layers[i].zf.deltas); + g_free(s->layers[i].index); + } + g_free(s->layers); + s->layers = NULL; + s->nb_layers = 0; + g_free(s->merged); + s->merged = NULL; + s->merged_count = 0; +} + +/* + * Manifest handling: the layer stack comes either from an OCI image manifest + * (manifest option, or the file child content) or from an explicit + * bottom-first "layers" array. Of a manifest only "schemaVersion" (must be 2) + * and "layers" (non-empty, bottom-first) are read, and of each layer + * descriptor only "digest"; everything else -- the top-level "mediaType", + * "annotations", "subject" and the whole "config" descriptor -- is ignored, + * so the image config blob is never fetched. + * + * Blobs are named after the root they live under: ROOT/<alg>/<hex> for an OCI + * image layout's blobs/ or an explicit blob-path, or ROOT/<hex> when ROOT is + * itself an <alg> directory, i.e. the manifest is a sibling of the layers it + * names (an OCI layout read from inside blobs/sha256/, or a containerd + * content store). + * + * Specifications: + * image manifest (schemaVersion, layers) + * https://github.com/opencontainers/image-spec/blob/main/manifest.md + * descriptor "digest" grammar, narrowed by overlaybd_parse_digest() + * https://github.com/opencontainers/image-spec/blob/main/descriptor.md + * the blobs/<alg>/<hex> directory layout + * https://github.com/opencontainers/image-spec/blob/main/image-layout.md + */ + +typedef enum { + OBD_BLOB_DIR, + OBD_BLOB_SIBLING, +} OverlaybdBlobKind; + +typedef struct OverlaybdBlobRoot { + char *path; + OverlaybdBlobKind kind; + GPtrArray *blobs; +} OverlaybdBlobRoot; + +#define OCI_DIGEST_ALG_MAX 32 +#define OCI_DIGEST_HEX_MAX 128 +/* an OCI image manifest is a few kilobytes; anything larger is not one */ +#define OCI_MANIFEST_MAX_SIZE (1u << 20) + +static char *overlaybd_blob_path(const OverlaybdBlobRoot *root, + const char *alg, const char *hex) +{ + switch (root->kind) { + case OBD_BLOB_SIBLING: + return g_strdup_printf("%s/%s", root->path, hex); + case OBD_BLOB_DIR: + return g_strdup_printf("%s/%s/%s", root->path, alg, hex); + } + g_assert_not_reached(); +} + +/* + * Validate a digest and split it into algorithm and hex parts. Both are used + * verbatim as path components, so anything outside the OCI grammar -- in + * particular "." and "/" -- is rejected. The grammar also allows separators + * in the algorithm and uppercase hex; both are excluded on purpose, since + * every tool in practice emits a lowercase "sha256" and narrowing the set + * costs nothing. + */ +static int overlaybd_parse_digest(const char *digest, char **alg, char **hex, + Error **errp) +{ + static const char alg_chars[] = "abcdefghijklmnopqrstuvwxyz0123456789"; + static const char hex_chars[] = "0123456789abcdef"; + const char *colon = strchr(digest, ':'); + size_t alen, hlen; + + if (!colon || strchr(colon + 1, ':')) { + goto invalid; + } + alen = colon - digest; + hlen = strlen(colon + 1); + if (alen < 1 || alen > OCI_DIGEST_ALG_MAX || + hlen < 1 || hlen > OCI_DIGEST_HEX_MAX || + strspn(digest, alg_chars) != alen || + strspn(colon + 1, hex_chars) != hlen) { + goto invalid; + } + + *alg = g_strndup(digest, alen); + *hex = g_strdup(colon + 1); + return 0; + +invalid: + error_setg(errp, "invalid layer digest '%s'", digest); + return -EINVAL; +} + +static int overlaybd_reject_url(const char *what, const char *ref, + Error **errp) +{ + if (path_has_protocol(ref)) { + error_setg(errp, "%s '%s' is a URL; only local files are supported", + what, ref); + return -ENOTSUP; + } + return 0; +} + +/* + * Put an explicit file child reference under @key, so that the blob is + * opened without format probing. A bare string reference would instead be + * looked up as a node name. + */ +static void overlaybd_put_child(QDict *options, const char *key, + const char *value) +{ + /* two variables: reassigning a g_autofree one leaks the old value */ + g_autofree char *drv_key = g_strdup_printf("%s.driver", key); + g_autofree char *ref = g_strdup_printf("%s.filename", key); + + qdict_put_str(options, drv_key, "file"); + qdict_put_str(options, ref, value); +} + +static int overlaybd_blob_root_from_option(const char *blob_path, + OverlaybdBlobRoot *root, + Error **errp) +{ + char *end; + int ret; + + ret = overlaybd_reject_url("blob-path", blob_path, errp); + if (ret < 0) { + return ret; + } + + root->path = g_strdup(blob_path); + end = root->path + strlen(root->path); + while (end > root->path && end[-1] == '/') { + *--end = '\0'; + } + if (!*root->path) { + error_setg(errp, "blob-path is empty"); + g_free(root->path); + root->path = NULL; + return -EINVAL; + } + root->kind = OBD_BLOB_DIR; + return 0; +} + +static int overlaybd_derive_blob_root(const char *manifest, const char *alg, + OverlaybdBlobRoot *root, Error **errp) +{ + g_autofree char *dir = NULL; + g_autofree char *base = NULL; + g_autofree char *alg_dir = NULL; + + root->path = NULL; + + dir = g_path_get_dirname(manifest); + base = g_path_get_basename(dir); + if (strcmp(base, alg) == 0) { + root->path = g_strdup(dir); + root->kind = OBD_BLOB_SIBLING; + return 0; + } + + /* + * Otherwise the blobs are grouped by algorithm in a directory beside + * the manifest, either directly or under the blobs/ of an OCI image + * layout. The root is that directory's parent, since a directory root + * composes ROOT/<alg>/<hex>. + */ + alg_dir = g_build_filename(dir, alg, NULL); + if (g_file_test(alg_dir, G_FILE_TEST_IS_DIR)) { + root->path = g_strdup(dir); + } else { + root->path = g_build_filename(dir, "blobs", NULL); + if (!g_file_test(root->path, G_FILE_TEST_IS_DIR)) { + error_setg(errp, "could not find the blobs of manifest '%s': no " + "%s/ or blobs/ directory beside it; give blob-path", + manifest, alg); + g_free(root->path); + root->path = NULL; + return -ENOENT; + } + } + root->kind = OBD_BLOB_DIR; + return 0; +} + +static int overlaybd_parse_manifest(const char *content, QDict **manifest_out, + Error **errp) +{ + QDict *manifest; + QObject *obj; + int64_t version; + + obj = qobject_from_json(content, errp); + if (!obj) { + return -EINVAL; + } + manifest = qobject_to(QDict, obj); + if (!manifest) { + error_setg(errp, "overlaybd manifest root is not a JSON object"); + qobject_unref(obj); + return -EINVAL; + } + + version = qdict_get_try_int(manifest, "schemaVersion", 0); + if (version != 2) { + error_setg(errp, "unsupported OCI schemaVersion %" PRId64 ", " + "expected 2", version); + qobject_unref(obj); + return -ENOTSUP; + } + if (!qobject_to(QList, qdict_get(manifest, "layers"))) { + error_setg(errp, "overlaybd manifest has no \"layers\" array"); + qobject_unref(obj); + return -EINVAL; + } + *manifest_out = manifest; + return 0; +} + +static int overlaybd_paths_from_manifest(QDict *manifest, const char *location, + OverlaybdBlobRoot *root, Error **errp) +{ + QList *layers = qobject_to(QList, qdict_get(manifest, "layers")); + const QListEntry *entry; + int i = 0, ret; + + for (entry = qlist_first(layers); entry; entry = qlist_next(entry)) { + QDict *desc = qobject_to(QDict, entry->value); + const char *digest = desc ? qdict_get_try_str(desc, "digest") : NULL; + g_autofree char *alg = NULL; + g_autofree char *hex = NULL; + + if (!digest) { + error_setg(errp, "layers[%d] has no digest", i); + return -EINVAL; + } + ret = overlaybd_parse_digest(digest, &alg, &hex, errp); + if (ret < 0) { + return ret; + } + if (i == 0 && !root->path) { + ret = overlaybd_derive_blob_root(location, alg, root, errp); + if (ret < 0) { + return ret; + } + } + g_ptr_array_add(root->blobs, overlaybd_blob_path(root, alg, hex)); + i++; + } + if (i == 0) { + error_setg(errp, "overlaybd manifest has no layers"); + return -EINVAL; + } + return 0; +} + +static int overlaybd_layers_from_options(QDict *options, int nb_layers, + Error **errp) +{ + int i; + + for (i = 0; i < nb_layers; i++) { + char key[32]; + const char *val; + int n, ret; + + n = snprintf(key, sizeof(key), "layers.%d", i); + assert(n < (int)sizeof(key)); + + val = qdict_get_try_str(options, key); + if (val) { + g_autofree char *path = g_strdup(val); + + ret = overlaybd_reject_url("layer", path, errp); + if (ret < 0) { + return ret; + } + qdict_del(options, key); + overlaybd_put_child(options, key, path); + } + } + return 0; +} + +static int overlaybd_open_layers(BlockDriverState *bs, QDict *options, + const OverlaybdBlobRoot *root, int nb_layers, + BdrvChild *file_layer, Error **errp) +{ + BDRVOverlaybdState *s = bs->opaque; + int ret; + int i; + + if (nb_layers > LSMT_MAX_LAYERS) { + error_setg(errp, "too many overlaybd layers (%d, maximum %d)", + nb_layers, LSMT_MAX_LAYERS); + return -EINVAL; + } + + s->layers = g_new0(OverlaybdLayer, nb_layers); + s->nb_layers = nb_layers; + + if (file_layer) { + s->layers[0].child = file_layer; + } else { + for (i = 0; i < nb_layers; i++) { + BdrvChild *child; + char key[32]; + int n; + + if (root) { + n = snprintf(key, sizeof(key), "layer.%d", i); + assert(n < (int)sizeof(key)); + overlaybd_put_child(options, key, root->blobs->pdata[i]); + } else { + n = snprintf(key, sizeof(key), "layers.%d", i); + assert(n < (int)sizeof(key)); + } + + child = bdrv_open_child(NULL, options, key, bs, &child_of_bds, + BDRV_CHILD_DATA | BDRV_CHILD_METADATA, + false, errp); + if (!child) { + ret = -EINVAL; + goto fail; + } + /* + * fill top-first: the last path (bottom-first) is the top + * layer + */ + s->layers[nb_layers - 1 - i].child = child; + } + } + + bdrv_graph_rdlock_main_loop(); + for (i = 0; i < nb_layers; i++) { + ret = overlaybd_open_layer(&s->layers[i], errp); + if (ret < 0) { + goto unlock_fail; + } + } + + s->merged_cap = 64; + s->merged = g_new(OverlaybdSegment, s->merged_cap); + if (!overlaybd_merge_level(s, 0, 0, UINT64_MAX, &s->merged_count)) { + error_setg(errp, "failed to merge overlaybd layer indexes"); + ret = -EINVAL; + goto unlock_fail; + } + + s->virtual_size = 0; + for (i = 0; i < nb_layers; i++) { + if (s->layers[i].virtual_size != 0) { + s->virtual_size = s->layers[i].virtual_size; + break; + } + } + if (s->virtual_size == 0) { + error_setg(errp, "overlaybd image has zero virtual size"); + ret = -EINVAL; + goto unlock_fail; + } + /* DIV_ROUND_UP() below would wrap around to a zero-length image */ + if (s->virtual_size > UINT64_MAX - (BDRV_SECTOR_SIZE - 1)) { + error_setg(errp, "overlaybd image virtual size %" PRIu64 " is too " + "large", s->virtual_size); + ret = -EINVAL; + goto unlock_fail; + } + + bs->total_sectors = DIV_ROUND_UP(s->virtual_size, BDRV_SECTOR_SIZE); + bdrv_graph_rdunlock_main_loop(); + return 0; + +unlock_fail: + bdrv_graph_rdunlock_main_loop(); +fail: + return ret; +} + +static int overlaybd_open(BlockDriverState *bs, QDict *options, int flags, + Error **errp) +{ + g_autofree char *manifest_opt = NULL; + g_autofree char *blob_path = NULL; + OverlaybdBlobRoot root = { 0 }; + const OverlaybdBlobRoot *blobs = NULL; + QDict *manifest = NULL; + const char *location = NULL; + char *content = NULL; + BdrvChild *file_layer = NULL; + bool use_file_child; + int nb_layers = 0; + int ret; + + GLOBAL_STATE_CODE(); + + bdrv_graph_rdlock_main_loop(); + ret = bdrv_apply_auto_read_only(bs, NULL, errp); + bdrv_graph_rdunlock_main_loop(); + if (ret < 0) { + return ret; + } + + qdict_flatten(options); + + /* copy before deleting: qdict_get_try_str() borrows from the entry */ + manifest_opt = g_strdup(qdict_get_try_str(options, "manifest")); + blob_path = g_strdup(qdict_get_try_str(options, "blob-path")); + qdict_del(options, "manifest"); + qdict_del(options, "blob-path"); + use_file_child = qdict_haskey(options, "file"); + nb_layers = qdict_array_entries(options, "layers."); + if (nb_layers < 0) { + error_setg(errp, "option layers is not a valid array"); + return -EINVAL; + } + if ((manifest_opt != NULL) + use_file_child + (nb_layers > 0) > 1) { + error_setg(errp, "only one of manifest, file or layers may be given"); + return -EINVAL; + } + if (!manifest_opt && !use_file_child && nb_layers == 0) { + error_setg(errp, "either manifest, file or layers is required"); + return -EINVAL; + } + if (blob_path && !manifest_opt && !use_file_child) { + error_setg(errp, "blob-path requires a manifest"); + return -EINVAL; + } + + if (manifest_opt) { + gsize len; + + ret = overlaybd_reject_url("manifest", manifest_opt, errp); + if (ret < 0) { + return ret; + } + if (!g_file_get_contents(manifest_opt, &content, &len, NULL)) { + error_setg(errp, "could not read overlaybd manifest '%s'", + manifest_opt); + return -ENOENT; + } + location = manifest_opt; + } else if (use_file_child) { + uint8_t sniff[512]; + const uint8_t *p, *end; + int64_t len; + + ret = bdrv_open_file_child(NULL, options, "file", bs, errp); + if (ret < 0) { + return ret; + } + /* + * The file child is either an OCI image manifest or a single layer, + * possibly inside a tar and/or ZFile shell. No layer magic starts + * with '{', so a JSON document is a manifest and anything else is a + * layer; overlaybd_open_layer() peels the shells and requires an + * LSMT core. + */ + bdrv_graph_rdlock_main_loop(); + len = bdrv_getlength(bs->file->bs); + if (len < 0) { + bdrv_graph_rdunlock_main_loop(); + error_setg_errno(errp, -len, "could not get the file child size"); + ret = len; + goto out; + } + memset(sniff, 0, sizeof(sniff)); + ret = bdrv_pread(bs->file, 0, MIN((uint64_t)len, sizeof(sniff)), + sniff, 0); + if (ret < 0) { + bdrv_graph_rdunlock_main_loop(); + error_setg_errno(errp, -ret, "could not read the file child"); + goto out; + } + location = bs->file->bs->filename; + end = sniff + MIN((uint64_t)len, sizeof(sniff)); + for (p = sniff; p < end && g_ascii_isspace(*p); p++) { + } + if (p == end || *p != '{') { + file_layer = bs->file; + nb_layers = 1; + bdrv_graph_rdunlock_main_loop(); + } else { + /* + * @len comes from the child rather than from overlaybd, so it + * cannot be handed to g_malloc() unchecked; a real manifest is + * a few kilobytes. + */ + if (len > OCI_MANIFEST_MAX_SIZE) { + bdrv_graph_rdunlock_main_loop(); + error_setg(errp, "file child starts like a JSON manifest but " + "is %" PRId64 " bytes, above the %u byte limit", + len, OCI_MANIFEST_MAX_SIZE); + ret = -EINVAL; + goto out; + } + content = g_malloc(len + 1); + ret = bdrv_pread(bs->file, 0, len, content, 0); + if (ret >= 0) { + content[len] = 0; + ret = 0; + } else { + error_setg_errno(errp, -ret, + "could not read overlaybd manifest"); + } + bdrv_graph_rdunlock_main_loop(); + if (ret < 0) { + goto out; + } + } + } + + if (!file_layer) { + if (content) { + ret = overlaybd_parse_manifest(content, &manifest, errp); + if (ret < 0) { + goto out; + } + root.blobs = g_ptr_array_new_with_free_func(g_free); + if (blob_path) { + ret = overlaybd_blob_root_from_option(blob_path, &root, errp); + if (ret < 0) { + goto out; + } + } + ret = overlaybd_paths_from_manifest(manifest, location, &root, + errp); + if (ret < 0) { + goto out; + } + nb_layers = root.blobs->len; + blobs = &root; + } else { + ret = overlaybd_layers_from_options(options, nb_layers, errp); + if (ret < 0) { + goto out; + } + } + } + + ret = overlaybd_open_layers(bs, options, blobs, nb_layers, file_layer, + errp); + +out: + g_free(root.path); + if (root.blobs) { + g_ptr_array_free(root.blobs, TRUE); + } + qobject_unref(manifest); + g_free(content); + if (ret < 0) { + overlaybd_close(bs); + } + return ret; +} + +static BlockDriver bdrv_overlaybd = { + .format_name = "overlaybd", + .instance_size = sizeof(BDRVOverlaybdState), + .bdrv_probe = overlaybd_probe, + .bdrv_open = overlaybd_open, + .bdrv_child_perm = bdrv_default_perms, + .bdrv_refresh_limits = overlaybd_refresh_limits, + .bdrv_co_preadv = overlaybd_co_preadv, + .bdrv_close = overlaybd_close, + .is_format = true, +}; + +static void bdrv_overlaybd_init(void) +{ + bdrv_register(&bdrv_overlaybd); +} + +block_init(bdrv_overlaybd_init); diff --git a/docs/system/qemu-block-drivers.rst.inc b/docs/system/qemu-block-drivers.rst.inc index 675daa72f9..2569f42e52 100644 --- a/docs/system/qemu-block-drivers.rst.inc +++ b/docs/system/qemu-block-drivers.rst.inc @@ -391,6 +391,43 @@ More disk image file formats are supported in a read-only mode. Apple disk image. +.. program:: image-formats +.. option:: overlaybd + + Overlaybd image, the layered format used by containerd's overlaybd + snapshotter. Each layer holds over-written data blocks, optionally + compressed block-wise with LZ4 or ZSTD and optionally wrapped in a + single-file tar. Only sealed layers can be read; compressed layers + need QEMU built with lz4 / zstd support respectively. See + https://github.com/containerd/overlaybd for the format specification. + + The layer stack is given by exactly one of: + + - ``manifest=<path>``: an OCI image manifest whose ``layers`` array + (bottom-first, of each entry only ``digest`` is used) names the layer + blobs, plus an optional ``blob-path=<root>`` saying where they are. + The root defaults to the blobs beside the manifest, which covers an + OCI image layout and a containerd content store. + + - ``layers.<n>.driver=file,layers.<n>.filename=<path>``: an explicit + bottom-first list, the first element the base layer and the last the + top layer. + + - a ``file`` child holding a manifest or a single sealed layer, as + ``-drive file=<path>`` provides. A manifest must be named with + ``format=overlaybd``, since probing cannot recognise JSON. + + A bare layer is detected by probing; a tar-wrapped one is not, as + probing sees only the tar entry header, so it must be named explicitly + via ``-f overlaybd``, a ``file`` child, ``layers.<n>`` or a manifest. + Tar-wrapped layers are usually shipped gzip-compressed and must be + unpacked first, since the driver reads an uncompressed tar. + + For example:: + + qemu-img info --image-opts driver=overlaybd,manifest=manifest.json,\ + blob-path=/var/lib/containerd/io.containerd.content.v1.content/blobs + .. program:: image-formats .. option:: parallels diff --git a/meson.build b/meson.build index cfac634cf1..d7468db2a7 100644 --- a/meson.build +++ b/meson.build @@ -1391,6 +1391,12 @@ if not get_option('zstd').auto() or have_block required: get_option('zstd'), method: 'pkg-config') endif +lz4 = not_found +if not get_option('lz4').auto() or have_block + lz4 = dependency('liblz4', version: '>=1.8.0', + required: get_option('lz4'), + method: 'pkg-config') +endif qpl = not_found if not get_option('qpl').auto() or have_system qpl = dependency('qpl', version: '>=1.5.0', @@ -2542,6 +2548,7 @@ config_host_data.set('CONFIG_CRYPTO_SM3', crypto_sm3.found()) config_host_data.set('CONFIG_HOGWEED', hogweed.found()) config_host_data.set('CONFIG_MALLOC_TRIM', has_malloc_trim) config_host_data.set('CONFIG_ZSTD', zstd.found()) +config_host_data.set('CONFIG_LZ4', lz4.found()) config_host_data.set('CONFIG_QPL', qpl.found()) config_host_data.set('CONFIG_UADK', uadk.found()) config_host_data.set('CONFIG_QATZIP', qatzip.found()) @@ -4856,6 +4863,7 @@ if have_block summary_info += {'bochs support': get_option('bochs').allowed()} summary_info += {'cloop support': get_option('cloop').allowed()} summary_info += {'dmg support': get_option('dmg').allowed()} + summary_info += {'overlaybd support': get_option('overlaybd').allowed()} summary_info += {'qcow v1 support': get_option('qcow1').allowed()} summary_info += {'vdi support': get_option('vdi').allowed()} summary_info += {'vhdx support': get_option('vhdx').allowed()} @@ -4989,6 +4997,7 @@ summary_info += {'snappy support': snappy} summary_info += {'bzip2 support': libbzip2} summary_info += {'lzfse support': liblzfse} summary_info += {'zstd support': zstd} +summary_info += {'lz4 support': lz4} summary_info += {'Query Processing Library support': qpl} summary_info += {'UADK Library support': uadk} summary_info += {'qatzip support': qatzip} diff --git a/meson_options.txt b/meson_options.txt index 6c2de6296f..a1b2ca6e56 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -270,6 +270,8 @@ option('xkbcommon', type : 'feature', value : 'auto', description: 'xkbcommon support') option('zstd', type : 'feature', value : 'auto', description: 'zstd compression support') +option('lz4', type : 'feature', value : 'auto', + description: 'lz4 compression support') option('qpl', type : 'feature', value : 'auto', description: 'Query Processing Library support') option('uadk', type : 'feature', value : 'auto', @@ -339,6 +341,8 @@ option('cloop', type: 'feature', value: 'auto', description: 'cloop image format support') option('dmg', type: 'feature', value: 'auto', description: 'dmg image format support') +option('overlaybd', type: 'feature', value: 'auto', + description: 'overlaybd image format support') option('qcow1', type: 'feature', value: 'auto', description: 'qcow1 image format support') option('vdi', type: 'feature', value: 'auto', diff --git a/qapi/block-core.json b/qapi/block-core.json index aed2888147..18a3fe8a95 100644 --- a/qapi/block-core.json +++ b/qapi/block-core.json @@ -3390,6 +3390,7 @@ 'iscsi', 'luks', 'nbd', 'nfs', 'null-aio', 'null-co', 'nvme', { 'name': 'nvme-io_uring', 'if': 'CONFIG_BLKIO' }, + 'overlaybd', 'parallels', 'preallocate', 'qcow', 'qcow2', 'qed', 'quorum', 'raw', 'rbd', { 'name': 'replication', 'if': 'CONFIG_REPLICATION' }, @@ -4182,6 +4183,43 @@ 'data': { 'path': 'str' }, 'if': 'CONFIG_BLKIO' } +## +# @BlockdevOptionsOverlaybd: +# +# Driver specific block device options for overlaybd (read-only). +# +# @manifest: an OCI image manifest naming the layer stack. Its +# "layers" array is read bottom-first and only the "digest" of +# each entry is used; the image config blob is never fetched. +# +# @blob-path: directory the layer digests are resolved against. +# Optional, and derived from @manifest when absent. Blobs are +# looked up as BLOB-PATH/<alg>/<hex>, i.e. the blobs/ of an OCI +# image layout. A derived root is the blobs/ directory beside +# the manifest, except when the manifest is itself a blob, as in +# an OCI image layout addressed by digest or a containerd content +# store, in which case the layers are its siblings, +# BLOB-PATH/<hex>. +# +# @layers: explicit layer list, bottom-first: the first element is the +# base layer, the last element is the top layer (the same order as +# the "layers" array of an OCI image manifest) +# +# @file: the image to open, which may be either an OCI image manifest +# or a bare sealed layer file, i.e. a single-layer image +# +# Exactly one of @manifest, @layers and @file must be given, and +# @blob-path only with a manifest. Every reference is a local file; +# a URL is rejected. +# +# Since: 11.2 +## +{ 'struct': 'BlockdevOptionsOverlaybd', + 'data': { '*manifest': 'str', + '*blob-path': 'str', + '*layers': [ 'BlockdevRef' ], + '*file': 'BlockdevRef' } } + ## # @BlockdevOptionsVirtioBlkVfioPci: # @@ -4905,6 +4943,7 @@ 'nvme': 'BlockdevOptionsNVMe', 'nvme-io_uring': { 'type': 'BlockdevOptionsNvmeIoUring', 'if': 'CONFIG_BLKIO' }, + 'overlaybd': 'BlockdevOptionsOverlaybd', 'parallels': 'BlockdevOptionsGenericFormat', 'preallocate':'BlockdevOptionsPreallocate', 'qcow2': 'BlockdevOptionsQcow2', diff --git a/scripts/meson-buildoptions.sh b/scripts/meson-buildoptions.sh index 08c9fbb62c..a7bfcce254 100644 --- a/scripts/meson-buildoptions.sh +++ b/scripts/meson-buildoptions.sh @@ -153,6 +153,7 @@ meson_options_help() { printf "%s\n" ' libvduse build VDUSE Library' printf "%s\n" ' linux-aio Linux AIO support' printf "%s\n" ' linux-io-uring Linux io_uring support' + printf "%s\n" ' lz4 lz4 compression support' printf "%s\n" ' lzfse lzfse support for DMG images' printf "%s\n" ' lzo lzo compression support' printf "%s\n" ' malloc-trim enable libc malloc_trim() for memory optimization' @@ -168,6 +169,7 @@ meson_options_help() { printf "%s\n" ' nvmm NVMM acceleration support' printf "%s\n" ' opengl OpenGL support' printf "%s\n" ' oss OSS sound support' + printf "%s\n" ' overlaybd overlaybd image format support' printf "%s\n" ' pa PulseAudio sound support' printf "%s\n" ' parallels parallels image format support' printf "%s\n" ' passt passt network backend support' @@ -404,6 +406,8 @@ _meson_option_parse() { --disable-linux-io-uring) printf "%s" -Dlinux_io_uring=disabled ;; --localedir=*) quote_sh "-Dlocaledir=$2" ;; --localstatedir=*) quote_sh "-Dlocalstatedir=$2" ;; + --enable-lz4) printf "%s" -Dlz4=enabled ;; + --disable-lz4) printf "%s" -Dlz4=disabled ;; --enable-lzfse) printf "%s" -Dlzfse=enabled ;; --disable-lzfse) printf "%s" -Dlzfse=disabled ;; --enable-lzo) printf "%s" -Dlzo=enabled ;; @@ -438,6 +442,8 @@ _meson_option_parse() { --disable-opengl) printf "%s" -Dopengl=disabled ;; --enable-oss) printf "%s" -Doss=enabled ;; --disable-oss) printf "%s" -Doss=disabled ;; + --enable-overlaybd) printf "%s" -Doverlaybd=enabled ;; + --disable-overlaybd) printf "%s" -Doverlaybd=disabled ;; --enable-pa) printf "%s" -Dpa=enabled ;; --disable-pa) printf "%s" -Dpa=disabled ;; --enable-parallels) printf "%s" -Dparallels=enabled ;; diff --git a/tests/qemu-iotests/tests/overlaybd b/tests/qemu-iotests/tests/overlaybd new file mode 100755 index 0000000000..7ce102426f --- /dev/null +++ b/tests/qemu-iotests/tests/overlaybd @@ -0,0 +1,795 @@ +#!/usr/bin/env python3 +# group: rw quick +# +# Test the read-only overlaybd block driver +# +# Builds a 3-layer stack (plain sealed LSMT base, LZ4 zfile middle layer +# with a zeroed range, ZSTD zfile top layer inside a tar wrapper with +# dirty header fields) plus a standalone LZ4 zfile of 16-byte blocks +# whose jump table exceeds 1 MiB, with pure-Python encoders -- LZ4 +# literal-only and match-based blocks and ZSTD Raw_Blocks are valid +# without any compression library -- and verifies byte-exact reads +# through all interface forms plus the error paths. +# +# Copyright (c) 2026 Huiba Li <[email protected]> +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import hashlib +import json +import os +import shutil +import struct + +import iotests +from iotests import file_path, log, qemu_img + +BLOCK = 4096 +NBLOCKS = 256 # 1 MiB virtual image +SECTORS_PER_BLOCK = BLOCK // 512 + +LSMT_MAGIC0 = b'LSMT\x00\x01\x02\x00' +LSMT_MAGIC1 = bytes([ + 0x65, 0x7e, 0x63, 0xd2, 0x94, 0x44, 0x08, 0x4c, + 0xa2, 0xd2, 0xc8, 0xec, 0x4f, 0xcf, 0xae, 0x8a +]) +ZFILE_MAGIC0 = b'ZFile\x00\x01\x00' +ZFILE_MAGIC1 = b'tuji.yyf@Alibaba' + + +def _crc32c_table() -> list: + table = [] + for i in range(256): + c = i + for _ in range(8): + c = (0x82f63b78 ^ (c >> 1)) if (c & 1) else (c >> 1) + table.append(c) + return table + + +CRC32C_TABLE = _crc32c_table() + +# zfile.cpp's NOI_WELL_KNOWN_PRIME: per-block CRCs are computed with this +# seed rather than 0 +CRC32C_SALT = 100007 + + +def crc32c(data: bytes, crc: int = 0) -> int: + """Raw CRC-32C accumulation -- no init inversion and no final xor. + + This is overlaybd's crc32::crc32c_extend(), which is NOT the standard + init-0xFFFFFFFF/xorout-0xFFFFFFFF CRC-32C. The two disagree on every + input, and a fixture that used the standard form would agree with a + driver that made the same mistake, so both would look correct while + rejecting every real image. + """ + table = CRC32C_TABLE + for b in data: + crc = table[(crc ^ b) & 0xff] ^ (crc >> 8) + return crc + + +def lz4_block_literals(data: bytes) -> bytes: + out = bytearray() + n = len(data) + if n < 15: + out.append(n << 4) + else: + out.append(15 << 4) + rest = n - 15 + while rest >= 255: + out.append(255) + rest -= 255 + out.append(rest) + out += data + return bytes(out) + + +def lz4_block_zeros(n: int) -> bytes: + """LZ4 block decoding to @n zero bytes. + + One literal, one offset-1 match, then the five trailing literals the + format requires; a block that ends in a match is rejected by + LZ4_decompress_safe, so this only works for n >= 12. + """ + match = n - 6 + assert match >= 4 + out = bytearray() + ml = match - 4 + nib = min(ml, 15) + out.append((1 << 4) | nib) + out.append(0) + out += struct.pack('<H', 1) + ml -= nib + if nib == 15: + while ml >= 255: + out.append(255) + ml -= 255 + out.append(ml) + out.append(5 << 4) + out += bytes(5) + return bytes(out) + + +def zstd_frame_raw(data: bytes) -> bytes: + out = bytearray(b'\x28\xb5\x2f\xfd\xe0') + out += struct.pack('<Q', len(data)) + off = 0 + while True: + chunk = data[off:off + 128 * 1024] + last = 1 if off + len(chunk) >= len(data) else 0 + out += struct.pack('<I', (len(chunk) << 3) | last)[:3] + out += chunk + off += len(chunk) + if last: + break + return bytes(out) + + +def fill_base(b: int) -> bytes: + return bytes((b * 7 + 13 + i) & 0xff for i in range(BLOCK)) + + +def fill_mid(b: int) -> bytes: + return bytes((b * 11 + 29 + i * 3) & 0xff for i in range(BLOCK)) + + +def fill_top(b: int) -> bytes: + return bytes((b * 5 + 101 + i * 7) & 0xff for i in range(BLOCK)) + + +def lsmt_header(flags: int, index_off: int, index_size: int, + virtual_size: int) -> bytes: + ht = bytearray(4096) + ht[0:8] = LSMT_MAGIC0 + ht[8:24] = LSMT_MAGIC1 + struct.pack_into('<I', ht, 24, 390) + struct.pack_into('<I', ht, 28, flags) + struct.pack_into('<Q', ht, 32, index_off) + struct.pack_into('<Q', ht, 40, index_size) + struct.pack_into('<Q', ht, 48, virtual_size) + return bytes(ht) + + +def build_lsmt(spec: dict, pattern) -> bytes: + """spec maps 4K block index -> 1 (data) or 2 (zeroed); missing = hole.""" + out = bytearray(4096) + data = bytearray() + slot_of = {} + for b in sorted(k for k, v in spec.items() if v == 1): + slot_of[b] = (4096 >> 9) + len(data) // BLOCK * SECTORS_PER_BLOCK + data += pattern(b) + index_off = 4096 + len(data) + + index = bytearray() + keys = sorted(spec.keys()) + i = 0 + while i < len(keys): + j = i + while j + 1 < len(keys) and keys[j + 1] == keys[j] + 1 \ + and spec[keys[j + 1]] == spec[keys[i]]: + j += 1 + first, count = keys[i], keys[j] - keys[i] + 1 + assert count * SECTORS_PER_BLOCK <= (1 << 14) - 1 + lba = first * SECTORS_PER_BLOCK + length = count * SECTORS_PER_BLOCK + if spec[keys[i]] == 2: + moffset, zeroed = 8, 1 + else: + moffset, zeroed = slot_of[first], 0 + lo = lba | (length << 50) + hi = moffset | (zeroed << 55) + index += struct.pack('<QQ', lo, hi) + i = j + 1 + + out += data + out += index + + for flags in (2 | 4, 1 | 2): # trailer first, then header + ht = lsmt_header(flags, index_off, len(index) // 16, NBLOCKS * BLOCK) + if flags & 1: # header goes over the placeholder + out[0:4096] = ht + else: + out += ht + return bytes(out) + + +def wrap_zfile(content: bytes, algo: int, garbage: bool = False, + block: int = BLOCK, overwrite: bool = False, + bad_block_crc: bool = False) -> bytes: + """Wrap into a ZFile blob: algo 1 = LZ4, 2 = ZSTD. With garbage, the + digest flag is clear and the digest/crc fields carry junk, like real + baselayer files. With overwrite, the header carries the final metadata + and the trailer is left out -- upstream's HEADER_OVERWRITE shape, which + is what zfile_compress produces when it rewrites the header in place. + + With bad_block_crc the first block's trailing CRC is wrong while its + compressed bytes stay valid, so only the driver's per-block checksum + can notice; corrupting the compressed stream instead would be caught + by the decompressor and would prove nothing about the checksum. + + All-zero blocks use the compact match encoding: the reference jump + table stores in-group offsets as uint16, so the compressed blocks of + a group must together stay under 64 KiB, which literal-only encoding + cannot manage at a small block size.""" + zero_chunk = bytes(block) + zero_c = lz4_block_zeros(block) if algo == 1 else None + body = bytearray() + jump = bytearray() + for off in range(0, len(content), block): + chunk = content[off:off + block] + if algo == 1: + if chunk == zero_chunk: + cdata = zero_c + else: + cdata = lz4_block_literals(chunk) + else: + cdata = zstd_frame_raw(chunk) + body += cdata + crc = crc32c(cdata, CRC32C_SALT) + if bad_block_crc and off == 0: + crc ^= 0xffffffff + body += struct.pack('<I', crc) + jump += struct.pack('<I', len(cdata) + 4) + + out = bytearray() + index_off = 512 + len(body) + for is_header in (True, False): + if overwrite and not is_header: + break # the header already carries the final metadata + ht = bytearray(512) + ht[0:8] = ZFILE_MAGIC0 + ht[8:24] = ZFILE_MAGIC1 + struct.pack_into('<I', ht, 24, 96) + flags = (1 if is_header else 0) | 2 | 4 | (1 << 4) + if overwrite: + flags |= 1 << 3 + struct.pack_into('<Q', ht, 32, flags) + struct.pack_into('<Q', ht, 40, index_off) + struct.pack_into('<Q', ht, 48, len(content) // block + + (1 if len(content) % block else 0)) + struct.pack_into('<Q', ht, 56, len(content)) + struct.pack_into('<I', ht, 64, crc32c(bytes(jump))) + struct.pack_into('<I', ht, 72, block) + ht[76] = algo + ht[88] = 1 + if garbage: + struct.pack_into('<Q', ht, 32, + (1 if is_header else 0) | 2 | 4) + struct.pack_into('<I', ht, 28, + 0xfeedface if is_header else 0xdeadbeef) + struct.pack_into('<I', ht, 64, 0xdeadbeef) + ht[300:304] = b'\x01\x02\x03\x04' + else: + struct.pack_into('<I', ht, 28, crc32c(bytes(ht))) + out += ht + if is_header: + out += body + out += jump + return bytes(out) + + +# tar's default blocking factor: 20 x 512-byte records +TAR_BLOCKING = 10240 + + +def wrap_tar(content: bytes) -> bytes: + """Tar wrapper with the padding a real archiver writes, so that the + archive ends well past the layer and the driver has to locate the + layer trailer from the entry size rather than from the file size.""" + hdr = bytearray(512) + hdr[0:9] = b'layer.bin' + hdr[156] = ord('0') + # slice assignment lengths must match exactly, or the bytearray + # silently shrinks: 11 octal digits into the 12-byte size field + # (the 12th byte stays NUL), 6 digits into the 8-byte checksum + hdr[124:135] = b'%011o' % len(content) + hdr[257:263] = b'ustar\x00' + hdr[263:265] = b'00' + hdr[148:156] = b' ' * 8 + hdr[148:154] = b'%06o' % (sum(hdr) & 0o777777) + out = bytes(hdr) + content + out += bytes(-len(out) % 512) + out += bytes(1024) + out += bytes(-len(out) % TAR_BLOCKING) + return out + + +# A ZFile of 16-byte blocks needs a jump table above 1 MiB (the point +# where the driver switches to parallel loading) once the content passes +# 4 MiB. 16 is the smallest block size whose all-zero LZ4 block still +# fits the uint16 in-group deltas, so the data region is that big. +BIG_BLOCK = 16 +BIG_DATA = 4259840 +BIG_VIRT_SECTORS = 24 + + +def fill_big(tag: int) -> bytes: + return bytes((tag * 31 + i * 5 + 7) & 0xff for i in range(4096)) + + +def build_big_lsmt() -> tuple: + """LSMT blob with two mapped 4K blocks, at the start and at the far + end of an otherwise zero data region, plus the expected image. + + The two blocks are 4 MiB apart, so reading them back exercises jump + table lookups in two different groups. + """ + index_off = 4096 + BIG_DATA + far_moff = (index_off - 4096) // 512 + index = struct.pack('<QQ', 8 << 50, 8) + index += struct.pack('<QQ', 8 | (8 << 50), far_moff) + + blob = bytearray(4096) + blob += fill_big(1) + blob += bytes(BIG_DATA - 2 * 4096) + blob += fill_big(2) + blob += index + assert len(blob) == index_off + len(index) + + for flags in (2 | 4, 1 | 2): + ht = lsmt_header(flags, index_off, len(index) // 16, + BIG_VIRT_SECTORS * 512) + if flags & 1: + blob[0:4096] = ht + else: + blob += ht + + expected = fill_big(1) + fill_big(2) + bytes(4096) + return bytes(blob), expected + + +def corrupt_jump_table(src: str, dst: str, span: int) -> None: + """Copy a ZFile with every jump table entry rewritten to @span, fixing + up the index CRC and both header/trailer digests so that only the + entry values differ.""" + with open(src, 'rb') as f: + img = bytearray(f.read()) + + index_off = struct.unpack_from('<Q', img, 40)[0] + index_size = struct.unpack_from('<Q', img, 48)[0] + for i in range(index_size): + struct.pack_into('<I', img, index_off + i * 4, span) + + crc = crc32c(bytes(img[index_off:index_off + index_size * 4])) + for ht_off in (0, len(img) - 512): + ht = img[ht_off:ht_off + 512] + struct.pack_into('<I', ht, 28, 0) + struct.pack_into('<I', ht, 64, crc) + struct.pack_into('<I', ht, 28, crc32c(bytes(ht))) + img[ht_off:ht_off + 512] = ht + + with open(dst, 'wb') as f: + f.write(img) + + +mid_img = file_path('mid_lz4.zfile') +top_img = file_path('top_zstd.tar') +top_plain_img = file_path('top_plain.lsmt') +oci_dir = file_path('oci.d') +oci_blobs = os.path.join(oci_dir, 'blobs') +oci_exported = os.path.join(oci_dir, 'manifest.json') +alg_dir = file_path('algdir.d') +alg_manifest = os.path.join(alg_dir, 'manifest.json') +probe_img = file_path('probe.zfile') +converted = file_path('converted.raw') +garbage_img = file_path('garbage.bin') +garbage_zfile = file_path('garbage.zfile') +big_img = file_path('big_index.zfile') +small_img = file_path('small_lz4.zfile') +overwrite_img = file_path('overwrite_lz4.zfile') +huge_img = file_path('huge_vsize.lsmt') +overrun_img = file_path('overrun_index.lsmt') +corrupt_img = file_path('corrupt_index.zfile') +badcrc_img = file_path('bad_block_crc.zfile') +junk_tar = file_path('junk.tar') +badver_json = file_path('bad_schema_version.json') +nolayers_json = file_path('no_layers.json') +traverse_json = file_path('digest_traversal.json') +missing_json = file_path('missing_blob.json') + +TOP_BLOCKS = {b: 1 for b in list(range(0, 51)) + list(range(150, 201))} +MID_BLOCKS = {b: 1 for b in range(60, 161)} +MID_BLOCKS.update({b: 2 for b in range(210, 221)}) + + +def expected_block(b: int) -> bytes: + if b in TOP_BLOCKS: + return fill_top(b) + if b in MID_BLOCKS: + return fill_mid(b) if MID_BLOCKS[b] == 1 else bytes(BLOCK) + return fill_base(b) + + +def expected_image() -> bytes: + return b''.join(expected_block(b) for b in range(NBLOCKS)) + + +def top_only_image() -> bytes: + return b''.join(fill_top(b) if b in TOP_BLOCKS else bytes(BLOCK) + for b in range(NBLOCKS)) + + +OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json' +OCI_LAYER_MEDIA_TYPE = 'application/vnd.oci.image.layer.v1.tar' +OCI_EMPTY_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json' + + +def write_blob(data: bytes) -> str: + digest = 'sha256:' + hashlib.sha256(data).hexdigest() + alg, hexsum = digest.split(':', 1) + d = os.path.join(oci_blobs, alg) + os.makedirs(d, exist_ok=True) + with open(os.path.join(d, hexsum), 'wb') as f: + f.write(data) + return digest + + +def digest_path(digest: str) -> str: + alg, hexsum = digest.split(':', 1) + return os.path.join(oci_blobs, alg, hexsum) + + +def build_oci_manifest(layers: list) -> tuple: + """Write @layers (bottom-first) as blobs plus a manifest listing them. + Returns the manifest blob's path and the layer blob paths. + + The image config blob is written as well, since the driver has to + ignore it, but index.json is left out: the tests name the manifest + directly instead of resolving it through the index.""" + cfg = b'{}' + digests = [write_blob(data) for data in layers] + manifest = json.dumps({ + 'schemaVersion': 2, + 'mediaType': OCI_MANIFEST_MEDIA_TYPE, + 'config': {'mediaType': OCI_EMPTY_MEDIA_TYPE, + 'digest': write_blob(cfg), 'size': len(cfg)}, + 'layers': [{'mediaType': OCI_LAYER_MEDIA_TYPE, + 'digest': d, 'size': len(data)} + for d, data in zip(digests, layers)], + 'annotations': {'org.opencontainers.image.ref.name': 'latest'}, + }).encode() + return digest_path(write_blob(manifest)), [digest_path(d) for d in digests] + + +def mutated_manifest(src: str, dst: str, mutate) -> str: + with open(src) as f: + m = json.load(f) + mutate(m) + with open(dst, 'w') as f: + json.dump(m, f) + return dst + + +def huge_virtual_size(src: str, dst: str) -> None: + """Copy an LSMT with virtual_size set to the value that makes the + driver's sectors conversion wrap around to a zero-length image. An + LSMT header carries no digest, so both copies can be patched raw.""" + with open(src, 'rb') as f: + img = bytearray(f.read()) + for off in (48, len(img) - 4096 + 48): + struct.pack_into('<Q', img, off, (1 << 64) - 1) + with open(dst, 'wb') as f: + f.write(img) + + +def overrun_lsmt_index(src: str, dst: str, pad: int = 256) -> None: + """Copy a bare LSMT with @pad bytes of trailing junk after its trailer, + whose trailer then declares index entries reaching into that junk. + + The padding is what makes the case reachable: the driver's view of the + layer ends at the child's length, so a bound taken at view_size - 4096 + accepts the overrun, while the trailer position the backward scan + actually found is @pad bytes earlier. @pad must stay under 512 or the + scan window no longer reaches the trailer. An LSMT header carries no + digest, so the copy can be patched raw.""" + assert pad < 512 and pad % 16 == 0 + with open(src, 'rb') as f: + img = bytearray(f.read()) + for off in (40, len(img) - 4096 + 40): + n = struct.unpack_from('<Q', img, off)[0] + struct.pack_into('<Q', img, off, n + pad // 16) + img += bytes(pad) + with open(dst, 'wb') as f: + f.write(img) + + +def make_fixtures() -> tuple: + base = build_lsmt({b: 1 for b in range(NBLOCKS)}, fill_base) + mid = wrap_zfile(build_lsmt(MID_BLOCKS, fill_mid), algo=1) + top = wrap_tar(wrap_zfile(build_lsmt(TOP_BLOCKS, fill_top), + algo=2, garbage=True)) + + with open(mid_img, 'wb') as f: + f.write(mid) + with open(top_img, 'wb') as f: + f.write(top) + with open(top_plain_img, 'wb') as f: + f.write(build_lsmt(TOP_BLOCKS, fill_top)) + with open(garbage_img, 'wb') as f: + f.write(os.urandom(32 * 1024)) + with open(garbage_zfile, 'wb') as f: + f.write(wrap_zfile(os.urandom(64 * 1024), algo=1)) + with open(junk_tar, 'wb') as f: + f.write(wrap_tar(os.urandom(64 * 1024))) + with open(small_img, 'wb') as f: + f.write(wrap_zfile(build_lsmt({0: 1}, fill_base), algo=1)) + with open(overwrite_img, 'wb') as f: + f.write(wrap_zfile(build_lsmt(TOP_BLOCKS, fill_top), algo=1, + overwrite=True)) + huge_virtual_size(top_plain_img, huge_img) + overrun_lsmt_index(top_plain_img, overrun_img) + + os.mkdir(oci_dir) + manifest, blobs = build_oci_manifest([base, mid, top]) + shutil.copyfile(manifest, oci_exported) + + os.mkdir(alg_dir) + os.mkdir(os.path.join(alg_dir, 'sha256')) + for blob in blobs: + shutil.copyfile(blob, os.path.join(alg_dir, 'sha256', + os.path.basename(blob))) + shutil.copyfile(manifest, alg_manifest) + + big_blob, big_expected = build_big_lsmt() + with open(big_img, 'wb') as f: + f.write(wrap_zfile(big_blob, algo=1, block=BIG_BLOCK)) + return big_expected, manifest, blobs + + +def codec_supported(algo: int) -> bool: + """ + True unless the driver says this codec was not compiled in. + + Keyed on the driver's own message, not on the exit status: a probe + that only checked the status would also report "not compiled in" + when the read path is merely broken, turning a real failure into a + silent not-run. + """ + name = {1: 'lz4', 2: 'zstd'}[algo] + with open(probe_img, 'wb') as f: + f.write(wrap_zfile(build_lsmt({0: 1}, fill_base), algo=algo)) + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,layers.0.driver=file,' + f'layers.0.filename={probe_img}', check=False) + return f'without {name} support' not in res.stdout + + +def convert(opts: str, out: str): + return qemu_img('convert', '--image-opts', '-O', 'raw', opts, out, + check=False) + + +for algo in (1, 2): + if not codec_supported(algo): + iotests.notrun(f'zfile algo {algo} not compiled in') + + +def read_converted(path: str) -> bytes: + with open(path, 'rb') as f: + return f.read() + + +def probed_format(path: str) -> str: + res = qemu_img('info', path, check=False) + for line in res.stdout.splitlines(): + if line.startswith('file format:'): + return line.split(':', 1)[1].strip() + return '' + + +def expect(cond: bool, msg: str) -> None: + if not cond: + raise AssertionError(msg) + + +def test_overlaybd() -> None: + big_expected, manifest, blobs = make_fixtures() + # the manifest is itself a blob in blobs/sha256/, so the blob root is + # derived from its own path -- the OCI image layout and containerd + # content store shapes + cfg = f'driver=overlaybd,manifest={manifest}' + + res = qemu_img('info', '--image-opts', cfg) + expect('virtual size: 1 MiB' in res.stdout, 'unexpected info output') + + ret = convert(cfg, converted) + expect(ret.returncode == 0, ret.stderr or 'convert failed') + expect(read_converted(converted) == expected_image(), + 'manifest mode: content mismatch') + log('manifest mode: byte-exact') + + for name, opts in ( + ('explicit blob-path', + f'driver=overlaybd,manifest={manifest},blob-path={oci_blobs}'), + ('exported manifest', + f'driver=overlaybd,manifest={oci_exported},' + f'blob-path={oci_blobs}'), + ('exported manifest, derived blob-path', + f'driver=overlaybd,manifest={oci_exported}'), + ('manifest beside an <alg> directory', + f'driver=overlaybd,manifest={alg_manifest}')): + ret = convert(opts, converted) + expect(ret.returncode == 0, ret.stderr or f'{name}: convert failed') + expect(read_converted(converted) == expected_image(), + f'{name}: content mismatch') + log('blob-path forms: byte-exact') + + layers = (f'driver=overlaybd,' + f'layers.0.driver=file,layers.0.filename={blobs[0]},' + f'layers.1.driver=file,layers.1.filename={blobs[1]},' + f'layers.2.driver=file,layers.2.filename={blobs[2]}') + ret = convert(layers, converted) + expect(ret.returncode == 0, ret.stderr or 'convert failed') + expect(read_converted(converted) == expected_image(), + 'layers mode: content mismatch') + log('layers mode: byte-exact') + + single = f'driver=overlaybd,layers.0.driver=file,' \ + f'layers.0.filename={top_plain_img}' + ret = convert(single, converted) + expect(ret.returncode == 0, ret.stderr or 'convert failed') + expect(read_converted(converted) == top_only_image(), + 'single layer: content mismatch') + log('single plain layer: byte-exact') + + single = f'driver=overlaybd,layers.0.driver=file,' \ + f'layers.0.filename={overwrite_img}' + ret = convert(single, converted) + expect(ret.returncode == 0, ret.stderr or 'convert failed') + expect(read_converted(converted) == top_only_image(), + 'overwrite header: content mismatch') + log('header-overwrite zfile: byte-exact') + + # the file child given as a dict reference carries the manifest + # (a bare string "file" would be a node-name reference, which is + # why -drive file=... passes the filename as a parameter); the blob + # root has to be derived from the child's own filename + ret = convert(f'driver=overlaybd,file.driver=file,' + f'file.filename={manifest}', converted) + expect(ret.returncode == 0, ret.stderr or 'convert failed') + expect(read_converted(converted) == expected_image(), + 'file-manifest mode: content mismatch') + log('file child as manifest: byte-exact') + + ret = convert(f'driver=overlaybd,file.driver=file,' + f'file.filename={top_img}', converted) + expect(ret.returncode == 0, ret.stderr or 'convert failed') + expect(read_converted(converted) == top_only_image(), + 'file layer mode: content mismatch') + log('file child as tar layer: byte-exact') + + # probing sees only the first 512 bytes: a bare LSMT and a bare ZFile + # match on their magic, but a tar shell shows nothing there except a + # generic entry header, so tars are deliberately not claimed -- every + # unrelated tar would then fail to open, and probing has no fallback + expect(probed_format(top_plain_img) == 'overlaybd', 'LSMT not probed') + expect(probed_format(mid_img) == 'overlaybd', 'ZFile not probed') + expect(probed_format(top_img) == 'raw', 'tar layer claimed by probing') + expect(probed_format(garbage_img) == 'raw', 'garbage not probed as raw') + log('format probing verified') + + big = f'driver=overlaybd,layers.0.driver=file,' \ + f'layers.0.filename={big_img}' + res = qemu_img('info', '--image-opts', big) + expect('virtual size: 12 KiB' in res.stdout, 'unexpected info output') + ret = convert(big, converted) + expect(ret.returncode == 0, ret.stderr or 'convert failed') + expect(read_converted(converted) == big_expected, + 'big jump table: content mismatch') + log('1 MiB jump table: byte-exact') + + # error paths (qemu_img() merges stdout and stderr) + res = qemu_img('info', '--image-opts', 'driver=overlaybd', check=False) + expect('either manifest, file or layers is required' in res.stdout, + 'missing options: wrong error') + res = qemu_img('info', '--image-opts', + 'driver=overlaybd,manifest=/nonexistent.json', check=False) + expect('could not read overlaybd manifest' in res.stdout, + 'missing manifest: wrong error') + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,manifest={manifest},' + f'layers.0.driver=file,' + f'layers.0.filename={mid_img}', check=False) + expect('only one of manifest, file or layers may be given' in res.stdout, + 'conflicting options: wrong error') + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,blob-path={oci_blobs},' + f'layers.0.driver=file,' + f'layers.0.filename={mid_img}', check=False) + expect('blob-path requires a manifest' in res.stdout, + 'blob-path without manifest: wrong error') + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,manifest={manifest},blob-path=/', + check=False) + expect('blob-path is empty' in res.stdout, + 'empty blob-path: wrong error') + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,layers.0.driver=file,' + f'layers.0.filename={garbage_img}', check=False) + expect('invalid LSMT header' in res.stdout, + 'garbage layer: wrong error') + res = qemu_img('info', '-f', 'overlaybd', junk_tar, check=False) + expect('invalid LSMT header' in res.stdout, + 'tar of garbage: wrong error') + # probing cannot see past a shell: it claims this on the ZFile magic, + # so the payload is only verified once the ZFile is open + res = qemu_img('info', garbage_zfile, check=False) + expect('invalid LSMT header' in res.stdout, + 'ZFile of garbage: wrong error') + + mutated_manifest(manifest, badver_json, + lambda m: m.update(schemaVersion=1)) + mutated_manifest(manifest, nolayers_json, lambda m: m.pop('layers')) + mutated_manifest(manifest, traverse_json, + lambda m: m['layers'][0].update( + digest='sha256:../../../../../../../etc/passwd')) + mutated_manifest(manifest, missing_json, + lambda m: m['layers'][0].update( + digest='sha256:' + '0' * 64)) + for name, path, want in ( + ('schemaVersion', badver_json, 'unsupported OCI schemaVersion'), + ('no layers', nolayers_json, 'has no "layers" array'), + ('digest traversal', traverse_json, 'invalid layer digest')): + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,manifest={path},' + f'blob-path={oci_blobs}', check=False) + expect(want in res.stdout, f'{name}: wrong error: {res.stdout}') + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,manifest={missing_json},' + f'blob-path={oci_blobs}', check=False) + expect(f'{oci_blobs}/sha256/' + '0' * 64 in res.stdout, + f'missing blob: wrong error: {res.stdout}') + + for name, opts in ( + ('manifest URL', + 'driver=overlaybd,manifest=http://127.0.0.1:1/manifest.json'), + ('blob-path URL', + f'driver=overlaybd,manifest={manifest},' + 'blob-path=http://127.0.0.1:1/blobs'), + ('layer URL', + 'driver=overlaybd,layers.0=http://127.0.0.1:1/blobs/sha256/0')): + res = qemu_img('info', '--image-opts', opts, check=False) + expect('only local files are supported' in res.stdout, + f'{name}: wrong error: {res.stdout[-200:]}') + + # 30000-byte blocks: the running in-group offset no longer fits u16 + corrupt_jump_table(small_img, corrupt_img, 30000) + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,layers.0.driver=file,' + f'layers.0.filename={corrupt_img}', check=False) + expect('jump table group exceed' in res.stdout, + 'oversized jump table group: wrong error') + # A valid compressed block whose trailing CRC is wrong. Opening the LSMT + # layer reads its header, which is ZFile block 0, so only the driver's + # per-block checksum can turn this into an error; without it the block + # decodes fine and the corruption is never noticed. + with open(badcrc_img, 'wb') as f: + f.write(wrap_zfile(build_lsmt({0: 1}, fill_base), algo=1, + bad_block_crc=True)) + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,layers.0.driver=file,' + f'layers.0.filename={badcrc_img}', check=False) + expect(res.returncode != 0 and 'Input/output error' in res.stdout, + f'corrupt block CRC: rc={res.returncode} {res.stdout[-200:]}') + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,layers.0.driver=file,' + f'layers.0.filename={huge_img}', check=False) + expect('virtual size 18446744073709551615 is too large' in res.stdout, + f'huge virtual size: wrong error: {res.stdout}') + res = qemu_img('info', '--image-opts', + f'driver=overlaybd,layers.0.driver=file,' + f'layers.0.filename={overrun_img}', check=False) + expect('invalid LSMT index location' in res.stdout, + f'overrunning LSMT index: wrong error: {res.stdout[-200:]}') + log('error paths verified') + + # file_path() only removes plain files, so the layout directories go + # explicitly (as tests/vvfat does) + shutil.rmtree(oci_dir) + shutil.rmtree(alg_dir) + + +if __name__ == '__main__': + iotests.script_main(test_overlaybd, supported_fmts=['raw'], + supported_protocols=['file']) diff --git a/tests/qemu-iotests/tests/overlaybd.out b/tests/qemu-iotests/tests/overlaybd.out new file mode 100644 index 0000000000..be1352fbce --- /dev/null +++ b/tests/qemu-iotests/tests/overlaybd.out @@ -0,0 +1,10 @@ +manifest mode: byte-exact +blob-path forms: byte-exact +layers mode: byte-exact +single plain layer: byte-exact +header-overwrite zfile: byte-exact +file child as manifest: byte-exact +file child as tar layer: byte-exact +format probing verified +1 MiB jump table: byte-exact +error paths verified -- 2.54.0 (Apple Git-157)
