Repository navigation
nvx-rs: assemble a guest root from a GPT disk's layers - #776
Merged
Pedro Henrique Penna (ppenna) merged 4 commits intoOct 23, 2026
Merged
Pedro Henrique Penna (ppenna) merged 4 commits into
Pedro Henrique Penna (ppenna) merged 4 commits into
Conversation
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Privileged GPT/ext4 validation and multi-layer mount cleanup warrant human review, with KVM and full backend-matrix validation left to CI.
No open issues 🎉
What changed in this PR
Extends the guest root library to assemble GPT-backed layered roots alongside EROFS images, building on the existing upper and overlay-mode support. Kernel partition reconciliation remains deferred to #770.
Changes:
- Validates GPT tables and layer superblocks, returns bounded first-partition bytes, and mounts ordered ext4 layers.
- Adds runtime-adapter configuration and probing for GPT roots.
- Expands unit and VM scenarios for assembly, validation failures, overlay behavior, and teardown.
| File | Description |
|---|---|
tests/runtime-adapter/tests/vm_layered.rs |
Adds GPT fixtures and end-to-end layered-root scenarios. |
tests/runtime-adapter/tests/layered_launch.rs |
Tests first-partition-bound CLI validation. |
tests/runtime-adapter/tests/guest_session.rs |
Tests GPT configuration forwarding and layout rejection. |
tests/runtime-adapter/src/worker/probes.rs |
Reports first-partition bytes and GPT layer mounts. |
tests/runtime-adapter/src/worker.rs |
Assembles GPT roots through the guest library. |
tests/runtime-adapter/src/root.rs |
Adds the optional first-partition bound to root configuration. |
tests/runtime-adapter/src/bin/runtime-adapter-host.rs |
Parses and forwards --first-partition-bound. |
tests/runtime-adapter/README.md |
Documents GPT configuration, probes, and scenarios. |
nvx-rs/src/guest/root/tests.rs |
Covers GPT validation, layer ordering, bounds, and cleanup. |
nvx-rs/src/guest/root/kernel.rs |
Adds real and fake GPT table-reading operations. |
nvx-rs/src/guest/root/block.rs |
Resolves partition paths and reads ext4 recovery flags. |
nvx-rs/src/guest/root.rs |
Implements GPT-backed assembly and exposes partition metadata. |
nvx-rs/README.md |
Documents the expanded guest root API. |
Review effort: Balanced
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt
branch
from
October 22, 2026 23:57
c774416 to
d76b474
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021
branch
from
October 23, 2026 00:30
27a196b to
24ecfbd
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt
branch
from
October 23, 2026 00:30
d76b474 to
0c115f8
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt
branch
from
October 23, 2026 03:16
0c115f8 to
834725c
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021
branch
from
October 23, 2026 03:16
24ecfbd to
5e8ec70
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021
branch
from
October 23, 2026 04:49
5e8ec70 to
28524ce
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt
branch
from
October 23, 2026 04:49
834725c to
0654e39
Compare
Base automatically changed from
esaurez/a3-3-gpt-tmpfs-overlay-20261021
to
dev
October 23, 2026 05:39
A guest root's image can now be a GPT disk, in an Image::Gpt with the caller's expected identities, its disk GUID and its partitions' unique GUIDs and sizes, each if it knows them, and a bound on the size of its first partition. The assembly reads and checks the disk's table with nvx::guest::gpt::read_disk, at the device's logical block size and size, and uses the partitions that the kernel created when it found the disk, without comparing them with the table or asking the kernel to reread it. The first partition holds opaque bytes, which the assembly reads up to the caller's bound, without their trailing padding, and returns through Root::first_partition. It never probes or mounts the first partition. Every later partition holds a layer, an ext4 file system, which the assembly mounts read-only, nosuid and nodev, at layers/pN, for the partition's number N, from the node that devtmpfs created for its partition under the kernel's name, such as vda2, and the overlay stacks the layers with partition 2's at its base and the last partition's at its top. The layers mount without noload, and the assembly refuses a layer whose superblock isn't ext4's, or has the needs_recovery feature: its read-only device can't take the recovery, which noload or norecovery among the superblock's own default mount options would make the kernel skip. The disk, its first partition, the partitions' nodes, and the layers' superblocks are checked before anything is mounted, as are the overlay's options, whose length limits a disk's layers: below /run/nvx, a root mounts up to 195 layers, those of partitions 2 through 196, whatever the overlay mode and the upper. Each Lower reports its partition, and the teardown unmounts the layers from the top down. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30bab0d7-7c3e-4120-9add-6c9cc1dc9e7e
The runtime adapter's worker refused a GPT image at the layout stage. It now also assembles the layers of a GPT disk, through nvx::guest::root's Image::Gpt, so that every combination of the guest root's images and uppers gets real-kernel coverage: - The root configuration takes an optional first-partition-bound field, which the host program's launch sets from --first-partition-bound, and which only a GPT disk takes. Without it, the guest bounds the disk's first partition at 64 KiB. The worker expects the disk GUID that the boot tokens name, and refuses a configuration that bounds a first partition for an EROFS image. - The first-partition probe reports the bytes of the first partition that the assembly returned, in hexadecimal. - A GPT scenario writes its disk first: a protective MBR, the primary and backup tables, the first partition, of bytes that are not UTF-8 followed by padding, a copy of the scratch template as the base layer, and a small ext2-format layer, which the kernel's ext4 driver mounts, as the top layer, with a file and a directory. The test writes that layer itself, since mkfs.ext4 is not available everywhere that the tests run, and checks the disk with nvx::gpt before it boots it. - New scenarios cover the disk over a scratch and over RAM in both overlay modes: the first partition, intact; each layer, mounted read-only from its partition without noload; the overlay's order of the layers; copy-up and rename in each mode; and the guest's disks. A disk of another GUID than the launch expects, and a first partition one byte larger than its bound, fail the assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30bab0d7-7c3e-4120-9add-6c9cc1dc9e7e
…ounted A root of an EROFS image mounts its layer at layers/distro, and one of a GPT disk mounts a layer at layers/pN for each partition after the first. An assembly checked only its own layers' mount points, so a layer that a failed teardown left mounted, of the other image or of a disk with more layers, went unnoticed, as an upper of the other kind did before. The assembly now also checks every entry of layers, in order, and refuses the runtime directory when a file system, or a bind mount of a directory, is mounted at one. Tests cover a GPT disk's top layer that refuses an EROFS image's root and a root of a disk with fewer layers, and an EROFS image that refuses a GPT disk's root. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30bab0d7-7c3e-4120-9add-6c9cc1dc9e7e
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt
branch
from
October 23, 2026 05:52
0654e39 to
e8c330a
Compare
Enrique Saurez (esaurez)
marked this pull request as ready for review
October 23, 2026 06:28
A layer's ext4 superblock chooses how the kernel responds to an error that it finds in the layer: its error policy, or errors=panic among its default mount options, makes the kernel panic, read-only device or not. A corrupt layer would then end the guest instead of failing the assembly with a typed error. The assembly now mounts each layer of a GPT disk with errors=remount-ro, which overrides both, so an error fails the operation that found it. A new scenario of the adapter's layered VMs boots a GPT disk whose top layer's top directory has no mode and whose superblock asks for a panic: the kernel refuses to mount the layer, and the assembly fails, while the guest keeps running. Without this change, the guest panics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30bab0d7-7c3e-4120-9add-6c9cc1dc9e7e
Pedro Henrique Penna (ppenna)
deleted the
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt
branch
October 23, 2026 11:47
This was referenced Oct 23, 2026
Akhil Gupta (akhil9tiet)
pushed a commit
to akhil9tiet/nvx
that referenced
this pull request
Oct 23, 2026
Mount GPT lower layers with noload as well as errors=remount-ro. Without noload, ext4 can scan a nonempty journal even when its needs_recovery bit is clear; a cyclic journal can hold assembly there indefinitely. Keep rejecting the recovery bit before any mount, rather than allowing noload to admit a recovery-required layer. Add adapter VM scenarios with a cyclic internal journal, with and without the recovery bit. Check that the former assembles, serves reads and copy-up, and tears down, while the latter fails before mounting. Assert journal suppression in every GPT scenario and document the deliberate change from microsoft#776's journal-loading policy. EROFS lowers and writable scratches retain their existing mount options. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30bab0d7-7c3e-4120-9add-6c9cc1dc9e7e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A guest root's image can now be a GPT disk.
Image::Gptnames the disk's format and what the caller expects of it: the disk's GUID, and its partitions' unique GUIDs and sizes, each if the caller knows it, as annvx::gpt::Expected, and the most bytes that its first partition may hold. The assembly probes nothing: the caller names the format, as it does for an EROFS image.nvx::guest::gpt::read_disk(nvx-rs: read a GPT disk's table through the guest's block device #771): the primary header and entries, at the device's logical block size and size, with no backup fallback and at most 255 partitions. As nvx-rs: read a GPT disk's table through the guest's block device #771 does, it uses the partitions that the kernel created when it found the disk, and neither compares them with the table nor asks the kernel to reread it; nvx-rs: check a GPT disk's kernel partitions in the guest #770 tracks that.Root::first_partition. A larger first partition is refused before any of it is read. The assembly never probes or mounts it.noloadornorecoveryamong the superblock's own default mount options would make the kernel skip. The assembly mounts each layer from the node that devtmpfs created for its partition, under the name that the kernel gives it, such asvda2, ornvme0n1p2for a disk whose name ends with a digit, atlayers/pN, for the partition's number N,ro,nosuid,nodev, withoutnoload, and witherrors=remount-ro, which overrides the panic on an error that a layer's superblock may ask for, with its error policy or among its default mount options: an error that the kernel finds in a layer fails the operation that found it instead. The overlay stacks the layers with partition 2's at its base and the last partition's at its top. A partition whose node is missing fails the assembly at once. A disk has at least one layer.The disk, its first partition, the partitions' nodes, and the layers' superblocks are checked before anything is mounted, as are the upper and the overlay's options. So is the runtime directory: a layer that a failed teardown left mounted at any entry of
layers, of either image, refuses it. The overlay's options limit a disk's layers: below/run/nvx, a root mounts up to 195 layers, those of partitions 2 through 196, whatever the overlay mode and the upper. EachLowerreports its partition, and the teardown unmounts the layers from the top down.The runtime adapter's layered VMs now also boot a GPT disk that each scenario writes: a first partition of opaque bytes that are not UTF-8, then padding; a copy of the scratch template, which
mkfs.ext4made with a journal, as the base layer; and a small ext2-format layer, which the kernel's ext4 driver mounts, as the top layer. The test writes the top layer itself, sincemkfs.ext4is not available everywhere that the tests run, and checks the disk withnvx::gptbefore it boots it. One scenario corrupts the top layer: its top directory's inode has no mode, and its superblock asks the kernel to panic on an error. The host names the bound of the first partition with--first-partition-bound, and a newfirst-partitionprobe reports its bytes.Part of #621. Refs #622 and #535.
Validation
On this branch's head, locally:
python scripts/nvx.py verify.nvx-rs, on Linux withNVX_BUNDLE=skip:cargo clippy --locked --all-targets --all-features -- -D warnings,cargo clippy --locked --lib --no-default-features -- -D warnings,cargo test --locked --all-features(1,213 passed),cargo test --locked(580 passed),cargo doc --locked --no-deps --all-featureswithRUSTDOCFLAGS=-D warnings,cargo +1.89 check --locked --all-targets --all-features, andcheck-nvx-rs's macOS check and bundling test;cargo fmt --all --checkon Windows.check-guest-binaries,build-guest-binariesandtest-guest-binaries(34 passed). The stripped guest binaries keep their sizes, and eight of the nine are byte-identical before and after this change.nvx-managed-agent, which calls none of this change's code and holds none of its strings, differs in its bytes at the same size. The Alpine initramfs has 8,223,221 bytes before and 8,223,168 after, 19,707,392 uncompressed in both.tests/runtime-adapter: Clippy, build, tests and the MSRV check in the protocol-only, host and guest configurations on Linux, the guest configuration's MSRV check for the GNU target, and fmt, Clippy, build, tests and the MSRV check in the protocol-only and host configurations on Windows.cargo test --locked --features host --test vm_layered --test vm_restart -- --ignored: 19 layered scenarios, the 12 thatdevhas and this PR's 7, and 2 restart tests pass on MSHV and WHP, covering the EROFS image and the GPT disk, each over a scratch and over a tmpfs, in both overlay modes. On the GPT disk, they show the layers mounted from the disk's partitions 2 and 3,ro,nosuid,nodevwith nonoloadornorecovery, stacked aslowerdir=.../layers/p3:.../layers/p2; copy-up of a file of the top layer, metadata alone or with its data as the mode sets, and the rename of one of its directories, allowed or refused withEXDEV; the first partition's 21 bytes returned intact; and only the GPT disk in the guest over a tmpfs. A disk of another GUID, and a first partition one byte over its bound, fail the assembly before the guest mounts anything, while a bound that the first partition meets exactly passes. The corrupt top layer fails the assembly when the kernel refuses to mount it, withEUCLEAN, and the guest reports the failure; withouterrors=remount-ro, the same disk crashed the guest on WHP. Uncorrupted, the written disk passessgdisk -vande2fsck -fnof both layers.python scripts/nvx.py test-microvm --backend <mshv|whp> --processors 1withmanaged-exec-config,sandbox-lifecycle,sandbox-blocks,scratch-snapshot,snapshot-tiersandmanaged-lifecycle, on MSHV and WHP, with the Alpine initramfs built from this branch.MSHV ran as root and with
--cpu-profile host, since its test host can't present a built-in CPU profile. KVM and the full backend matrix are left to CI.