static int vhdx_open(BlockDriverState *bs, QDict *options, int flags,
                     Error **errp)
{
    BDRVVHDXState *s = bs->opaque;
    int ret = 0;
    uint32_t i;
    uint64_t signature;
    Error *local_err = NULL;

    s->bat = NULL;
    s->first_visible_write = true;

    qemu_co_mutex_init(&s->lock);
    QLIST_INIT(&s->regions);

    /* validate the file signature */
    ret = bdrv_pread(bs->file, 0, &signature, sizeof(uint64_t));
    if (ret < 0) {
        goto fail;
    }
    if (memcmp(&signature, "vhdxfile", 8)) {
        ret = -EINVAL;
        goto fail;
    }

    /* This is used for any header updates, for the file_write_guid.
     * The spec dictates that a new value should be used for the first
     * header update */
    vhdx_guid_generate(&s->session_guid);

    vhdx_parse_header(bs, s, &local_err);
    if (local_err != NULL) {
        error_propagate(errp, local_err);
        ret = -EINVAL;
        goto fail;
    }

    ret = vhdx_parse_log(bs, s, &s->log_replayed_on_open, errp);
    if (ret < 0) {
        goto fail;
    }

    ret = vhdx_open_region_tables(bs, s);
    if (ret < 0) {
        goto fail;
    }

    ret = vhdx_parse_metadata(bs, s);
    if (ret < 0) {
        goto fail;
    }

    s->block_size = s->params.block_size;

    /* the VHDX spec dictates that virtual_disk_size is always a multiple of
     * logical_sector_size */
    bs->total_sectors = s->virtual_disk_size >> s->logical_sector_size_bits;

    vhdx_calc_bat_entries(s);

    s->bat_offset = s->bat_rt.file_offset;

    if (s->bat_entries > s->bat_rt.length / sizeof(VHDXBatEntry)) {
        /* BAT allocation is not large enough for all entries */
        ret = -EINVAL;
        goto fail;
    }

    /* s->bat is freed in vhdx_close() */
    s->bat = qemu_blockalign(bs, s->bat_rt.length);

    ret = bdrv_pread(bs->file, s->bat_offset, s->bat, s->bat_rt.length);
    if (ret < 0) {
        goto fail;
    }

    uint64_t payblocks = s->chunk_ratio;
    /* endian convert, and verify populated BAT field file offsets against
     * region table and log entries */
    for (i = 0; i < s->bat_entries; i++) {
        le64_to_cpus(&s->bat[i]);
        if (payblocks--) {
            /* payload bat entries */
            if ((s->bat[i] & VHDX_BAT_STATE_BIT_MASK) ==
                    PAYLOAD_BLOCK_FULLY_PRESENT) {
                ret = vhdx_region_check(s, s->bat[i] & VHDX_BAT_FILE_OFF_MASK,
                                        s->block_size);
                if (ret < 0) {
                    goto fail;
                }
            }
        } else {
            payblocks = s->chunk_ratio;
            /* Once differencing files are supported, verify sector bitmap
             * blocks here */
        }
    }

    if (flags & BDRV_O_RDWR) {
        ret = vhdx_update_headers(bs, s, false, NULL);
        if (ret < 0) {
            goto fail;
        }
    }

    /* TODO: differencing files */

    /* Disable migration when VHDX images are used */
    error_set(&s->migration_blocker,
            QERR_BLOCK_FORMAT_FEATURE_NOT_SUPPORTED,
            "vhdx", bs->device_name, "live migration");
    migrate_add_blocker(s->migration_blocker);

    return 0;
fail:
    vhdx_close(bs);
    return ret;
}