Skip to content

block: add read-only VMDK disk image support - #5741

Open
ChengyuZhu6 wants to merge 5 commits into
firecracker-microvm:mainfrom
ChengyuZhu6:vmdk
Open

ChengyuZhu6 wants to merge 5 commits into
firecracker-microvm:mainfrom
ChengyuZhu6:vmdk

Conversation

@ChengyuZhu6

@ChengyuZhu6 ChengyuZhu6 commented Mar 8, 2026 •

Copy link
Copy Markdown

This change adds read-only VMDK support to the virtio-block path, and the primary motivation is the EROFS fsmerge scenario described in containerd/nerdbox#30. In that workflow, multiple layer blobs are referenced through one VMDK descriptor, so the guest can consume them as one logical block device instead of attaching many devices.

Why this is needed:
The main driver is the EROFS fsmerge workflow (see containerd/nerdbox#30). In that setup, many layer blobs are referenced by a small metadata image. For VM-based runtimes, attaching one block device per layer does not scale well.

What is included in this patch:
This patch adds read-only VMDK support so those layers can be presented as one logical disk through a standard descriptor format. That removes the need to pass a large number of block devices or add an extra image conversion step before boot.

How this solves the problem:

  • when a disk is opened, Firecracker detects VMDK and selects the VMDK engine;
  • read requests are served through the VMDK backend and mapped into guest memory;
  • write requests fail with a clear unsupported error, preserving read-only semantics;
  • raw image behavior remains unchanged.
image

Changes

...

Reason

...

License Acceptance

By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache 2.0 license. For more information on following Developer
Certificate of Origin and signing off your commits, please check
CONTRIBUTING.md.

PR Checklist

  • I have read and understand CONTRIBUTING.md.
  • I have run tools/devtool checkbuild --all to verify that the PR passes
    build checks on all supported architectures.
  • I have run tools/devtool checkstyle to verify that the PR passes the
    automated style checks.
  • I have described what is done in these changes, why they are needed, and
    how they are solving the problem in a clear and encompassing way.
  • I have updated any relevant documentation (both in code and in the docs)
    in the PR.
  • I have mentioned all user-facing changes in CHANGELOG.md.
  • If a specific issue led to this PR, this PR closes the issue.
  • When making API changes, I have followed the
    Runbook for Firecracker API changes.
  • I have tested all new and changed functionalities in unit tests and/or
    integration tests.
  • I have linked an issue to every new TODO.

  • This functionality cannot be added in rust-vmm.

@hsiangkao

Copy link
Copy Markdown

@ChengyuZhu6 what prevents this as a draft?

@ShadowCurse

Copy link
Copy Markdown
Contributor

Hi @ChengyuZhu6, unfortunately we don't have enough bandwidth to review this for now.

@hsiangkao

Copy link
Copy Markdown

Hi @ChengyuZhu6, unfortunately we don't have enough bandwidth to review this for now.

@ShadowCurse I guess this one is not complicated and it seems almost half the change is test cases.
Since it uses imago library to handle vmdk format, I guess the remaining logic should be straight-forward?

Also I guess the commit message should be improved to make the usage clear at least.

@Manciukic

Copy link
Copy Markdown
Contributor

Hey @hsiangkao, @ChengyuZhu6,

first of all, thank you very much for the contribution! I think this is a very promising solution for container workloads. Do you have any examples on how this would integrate with containerd?

However, we take every new feature very seriously and we need to undergo an internal security review for every new feature, which means that despite the change itself being small, it requires a non-trivial amount of time from our side that we don't have at the moment.

In any case, having a very quick look at the PR:

  • do we really need a dependency on tokio?
  • the dependency on imago is fine, I think (but I didn't check that crate yet), but it should be from crates.io and not gitlab.

@hsiangkao

Copy link
Copy Markdown

Hey @hsiangkao, @ChengyuZhu6,

first of all, thank you very much for the contribution! I think this is a very promising solution for container workloads. Do you have any examples on how this would integrate with containerd?

Hi @Manciukic, thank you very much for the reply! currently I don't have some free slot to run an end-to-end example using containerd + kata-containers + firecracker, but I will find (or I hope @ChengyuZhu6 could finish the pull request message at least) time to run an e2e later.

but the story is much similar to what we did for nerdbox + libkrun:
containerd/nerdbox#30

and my final interest is that to apply VMDK to virtio-pmem as well so that each backing file can be passthrough into guests and shared in a finer way: of course, virtio-pmem could bring some security concern, but depends on the workload, it could also be useful if memory sharing is not an issue.

However, we take every new feature very seriously and we need to undergo an internal security review for every new feature, which means that despite the change itself being small, it requires a non-trivial amount of time from our side that we don't have at the moment.

In any case, having a very quick look at the PR:

  • do we really need a dependency on tokio?

I will defer this question to @ChengyuZhu6 .

  • the dependency on imago is fine, I think (but I didn't check that crate yet), but it should be from crates.io and not gitlab.

yes, imago has published on crates.io with qcow2 and this limited vmdk support.

@ChengyuZhu6
ChengyuZhu6 force-pushed the vmdk branch 2 times, most recently from 2d5fc9b to 726beb2 Compare April 22, 2026 13:36
@ChengyuZhu6

ChengyuZhu6 commented Apr 22, 2026 •

Copy link
Copy Markdown
Author

In any case, having a very quick look at the PR:

  • do we really need a dependency on tokio?

Good catch. I removed the direct tokio dependency, tokio is still present transitively via imago’s sync-wrappers feature, so behavior is unchanged.

@ChengyuZhu6

Copy link
Copy Markdown
Author
  • the dependency on imago is fine, I think (but I didn't check that crate yet), but it should be from crates.io and not gitlab.

Agreed. I switched imago from gitlab to crates.io (imago = "0.2.3" with sync-wrappers).

@ChengyuZhu6
ChengyuZhu6 force-pushed the vmdk branch 2 times, most recently from a42850f to 86a6e35 Compare April 22, 2026 14:05
@hsiangkao

Copy link
Copy Markdown

@ChengyuZhu6 @ShadowCurse @Manciukic could we consider enabling qcow2 using imago too?
Since qcow2 natively supports disk snapshots (and most cloud vendors already support qcow2 disks)and particularly useful for agent sandboxes.

@ChengyuZhu6

Copy link
Copy Markdown
Author

@ChengyuZhu6 @ShadowCurse @Manciukic could we consider enabling qcow2 using imago too? Since qcow2 natively supports disk snapshots (and most cloud vendors already support qcow2 disks)and particularly useful for agent sandboxes.

I think this is a good idea. I suggest including it in the following PR, since it would make review easier.

Comment thread src/vmm/src/devices/virtio/block/virtio/io/format/vmdk.rs Outdated
Comment thread src/vmm/src/devices/virtio/block/virtio/io/format/vmdk.rs
Comment thread Cargo.lock Outdated
Comment thread resources/seccomp/aarch64-unknown-linux-musl.json Outdated
Comment thread src/vmm/src/devices/virtio/block/virtio/io/format/vmdk.rs Outdated
Comment thread src/vmm/src/devices/virtio/block/virtio/io/mod.rs
Comment thread src/vmm/src/devices/virtio/block/virtio/io/mod.rs Outdated
@JackThomson2

Copy link
Copy Markdown
Contributor

Thank you @ChengyuZhu6 for your contribution, left a few comment :)

Comment thread src/vmm/Cargo.toml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change the dependencies in a single commit? Otherwise we have an intermediary step in our repo with a set of dependencies we've never checked and never audited

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

/// Detects the format of a disk image by reading its first bytes.
pub fn detect_disk_format(file: &mut File) -> io::Result<DiskImageFormat> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is overwritten in later commits. Can you refactor your commits in a way that each change builds on top of each other?

PS: this is a mechanical tasks where AI agents are really good at

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +177 to +178
let can_reuse_engine = format == block_io::DiskImageFormat::Raw
&& !matches!(self.file_engine, FileEngine::Vmdk(_));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about letting the engine return an UnsupportedOperation error or something on update_file_path?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, done.


let disk_size = if can_reuse_engine {
let disk_size = Self::file_size(&disk_image_path, &mut disk_image)?;
match &mut self.file_engine {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we're replacing a call to update_file_path with this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VMDK engine owns state built when the image was opened (descriptor + extent fds), and the size comes from the descriptor — the descriptor file itself is a few hundred bytes, so file_size() would report a bogus capacity.

if config.blk_size.is_none() && config.topology.is_none() {
if config.blk_size.is_none()
&& config.topology.is_none()
&& !matches!(disk_properties.file_engine, FileEngine::Vmdk(_))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition should be more along the lines of "the DiskImageFormat is raw".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's now just if image_format == DiskImageFormat::Raw

Comment thread tests/host_tools/drive.py Outdated
stem, _ = os.path.splitext(self.path)
return stem + "-flat.vmdk"

def _build(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH: I'm not sure about this: we're creating ourselves a file in this format, so we're risking validating in this test our wrong assumptions. For example, I'm not a VMDK expert, but isn't the extent supposed to have an header?

Is it possible to test against a real file?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

@marco-marangoni

Copy link
Copy Markdown
Contributor

Hey, I looked at the PR. I think we should also add some performance tests for this, since now the read speed is completely dictated by the imago library. We need a way to keep an eye on it.

@ChengyuZhu6

Copy link
Copy Markdown
Author

Hey, I looked at the PR. I think we should also add some performance tests for this, since now the read speed is completely dictated by the imago library. We need a way to keep an eye on it.

Added test_block_vmdk_performance.

@hsiangkao

Copy link
Copy Markdown

Hey, I looked at the PR. I think we should also add some performance tests for this, since now the read speed is completely dictated by the imago library. We need a way to keep an eye on it.

Added test_block_vmdk_performance.

I guess raw disk performance shouldn't be impacted by imago? but yes, for vmdk it's better to have a test for this.

@ChengyuZhu6

Copy link
Copy Markdown
Author

kindly ping @JackThomson2 @marco-marangoni

@hsiangkao

Copy link
Copy Markdown

kindly ping @JackThomson2 @marco-marangoni

... @marco-marangoni does it look good to you now?

@marco-marangoni

Copy link
Copy Markdown
Contributor

Sorry, I came back from Holidays today. I'll take a look today or tomorrow.

Thanks for your patience :)

@hsiangkao

Copy link
Copy Markdown

Sorry, I came back from Holidays today. I'll take a look today or tomorrow.

Thanks for your patience :)

Thanks what I am interested is also the following qcow2 support since I would like to use it for diskfile incremental updates as a Part of the vm sandbox solution.

@marco-marangoni

Copy link
Copy Markdown
Contributor

Thanks, the change looks good for me.

I think supporting qcow2 would be a nice addition

@hsiangkao

hsiangkao commented Oct 27, 2026 •

Copy link
Copy Markdown

Thanks, the change looks good for me.

I think supporting qcow2 would be a nice addition

yes, I want to use qcow2 as a local writable block delta, and use it to form multi-layer EROFS VM templates (disk as a file inside; memory and other states as other files).

@marco-marangoni could you trigger the remaining tests as well?

@hsiangkao

Copy link
Copy Markdown

@JackThomson2 could you approve again and merge this? or could others help?.. many thanks!

Comment thread tests/host_tools/drive.py Outdated
def _build(self):
"""Create the image with ``qemu-img`` and fill the extent with the pattern."""
utils.check_output(
f"qemu-img create -f vmdk -o subformat=monolithicFlat {self.path} {self.size}M"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see the CI fails because qemu-img isn't available.

Maybe you can generate the file offline, and include it as a fixture, with a comment explaining how it was generated, so I can validate.


Alternatively we could include the utility in the container, but this approach seems easier.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

@ChengyuZhu6

Copy link
Copy Markdown
Author

@JackThomson2 @marco-marangoni I have fixed ci fails for the dependency of qemu-img. PTAL, thank you!

.get_slice(addr, count as usize)
.map_err(VmdkIoError::GuestMemory)?;
let slices = [slice];
let (bufv, _guard) = IoVectorMut::from_volatile_slice(&slices);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, there's an half-bug in imago that I didn't see before: on error, it marks the whole range as dirty.

That's okay for other project, but we're working on a feature that requires for the dirty tracking to be correct (as in, only mark pages that were actually written to).

SyncFileEngine::read had a similar problem, and we ended up passing a bitmap-less slice, and did the marking ourselves.


Unfortunately, that's a blocker

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add this and other offending calls of imago to disallowed-methods in clippy.toml (we already have some there for the same reason)

impl TryFrom<&BlockDeviceConfig> for VhostUserBlockConfig {
type Error = VhostUserBlockError;

fn try_from(value: &BlockDeviceConfig) -> Result<Self, Self::Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably also check for image_format here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// size of the descriptor file, so the engine has to be rebuilt instead.
let image_format = DiskImageFormat::from(&self.file_engine);
let disk_size = if image_format == DiskImageFormat::Raw {
let disk_size = Self::file_size(&disk_image_path, &mut disk_image)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're returning an error here, however we already mutated self.image_id a few lines above. We should restructure the code such that errors result in no mutations

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}

fn open_extent(&self, path: &Path) -> io::Result<ImagoFile> {
// VMDK descriptors are untrusted input. Extents are read-only, direct children of the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rephrase the "VMDK descriptors are untrusted input" claim. It's a bit strong, since it's the system administrator that supplies them

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rephrased.

)
})?;
// SAFETY: descriptor_dir is an open directory FD and filename is a NUL-terminated component.
let fd = unsafe {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also check that it's a "normal" file, not a FIFO/block/char/etc.?

I'm asking because if that's the case, then Firecracker will either hang or panic because of syscalls not allowed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added O_NONBLOCK to openat and the returned FD is immediately wrapped in a File, and its metadata is checked to ensure it is a regular file.

@marco-marangoni

Copy link
Copy Markdown
Contributor

Sorry, I had another look with fresh eyes, and found some other issues. Most are small, however the dirty tracking is pretty important.

Add the imago crate (sync and vm-memory features) used to read VMDK
disk images, and allow the syscalls the backend needs after sandboxing:
openat, readlink/readlinkat and preadv.

Signed-off-by: Chengyu Zhu <hudsonzhu@tencent.com>
Add a VMDK file engine backed by imago and select it through a new
`image_format` field on the drive API, which defaults to Raw. VMDK
images are read-only and only work with the Sync IO engine, as imago
reads them synchronously; anything else is rejected when the drive is
configured.

The engine opens the descriptor and its extents through a gate that only
allows read-only, direct children of the descriptor directory, so an
untrusted descriptor cannot escape it. Snapshots record the format so a
restored drive is reopened as VMDK.

Signed-off-by: Chengyu Zhu <hudsonzhu@tencent.com>
Patching a raw drive swaps the file the engine points at. A VMDK engine
instead owns state parsed when the image was opened, and its size comes
from the descriptor rather than from the descriptor file, so rebuild it
on patch.

Signed-off-by: Chengyu Zhu <hudsonzhu@tencent.com>
Cover booting with a read-only VMDK data disk: the guest must read back
exactly the extent contents, see the expected device size, and fail
writes. The image is created with qemu-img, so the descriptor and the
extent are what a real VMDK producer emits.

Signed-off-by: Chengyu Zhu <hudsonzhu@tencent.com>
Reads from a VMDK image are served by imago instead of a plain file, so
track their throughput and latency the same way as the raw block device
benchmark.

Signed-off-by: Chengyu Zhu <hudsonzhu@tencent.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Awaiting assignee Indicates that an issue or pull request is awaiting action from its assignee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants