+ 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)
>

Reply via email to