+ Stefan Hajnoczi I'm sorry but I didn't realize that git send-mail missed you.
Huiba Li <[email protected]> 于2026年9月21日周一 14:44写道: > > 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) >
