Skip to content

nvx-rs: assemble a guest root from a GPT disk's layers - #776

Merged
Pedro Henrique Penna (ppenna) merged 4 commits into
devfrom
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt
Oct 23, 2026
Merged

Pedro Henrique Penna (ppenna) merged 4 commits into
devfrom
esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt

Conversation

@esaurez

@esaurez Enrique Saurez (esaurez) commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

A guest root's image can now be a GPT disk. Image::Gpt names 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 an nvx::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.

  • The table. The assembly finds the distro device by its MMIO window, checks its access mode and expected size, and flushes it, as for an EROFS image. It then reads and checks the disk's partition table with 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.
  • The first partition holds opaque bytes, which the assembly reads up to the caller's bound, without their trailing NUL bytes and ASCII whitespace, and returns through Root::first_partition. A larger first partition is refused before any of it is read. The assembly never probes or mounts it.
  • The layers. Each later partition, from partition 2 on, holds an ext4 layer. The assembly checks in each layer's superblock that it is ext4 and that its journal needs no recovery: the layer's device is read-only, so it can't take the recovery, which noload or norecovery among 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 as vda2, or nvme0n1p2 for a disk whose name ends with a digit, at layers/pN, for the partition's number N, ro,nosuid,nodev, without noload, and with errors=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. Each Lower reports 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.ext4 made 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, since mkfs.ext4 is not available everywhere that the tests run, and checks the disk with nvx::gpt before 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 new first-partition probe 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 with NVX_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-features with RUSTDOCFLAGS=-D warnings, cargo +1.89 check --locked --all-targets --all-features, and check-nvx-rs's macOS check and bundling test; cargo fmt --all --check on Windows.
  • Guest binaries: check-guest-binaries, build-guest-binaries and test-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.
  • Real VMs, the adapter's ignored tests, cargo test --locked --features host --test vm_layered --test vm_restart -- --ignored: 19 layered scenarios, the 12 that dev has 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,nodev with no noload or norecovery, stacked as lowerdir=.../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 with EXDEV; 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, with EUCLEAN, and the guest reports the failure; without errors=remount-ro, the same disk crashed the guest on WHP. Uncorrupted, the written disk passes sgdisk -v and e2fsck -fn of both layers.
  • NVX's own layered scenarios still pass, unchanged: python scripts/nvx.py test-microvm --backend <mshv|whp> --processors 1 with managed-exec-config, sandbox-lifecycle, sandbox-blocks, scratch-snapshot, snapshot-tiers and managed-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.

Copilot AI left a comment

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.

🔵 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

Copilot AI left a comment

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.

🔵 Needs a closer look

The privileged GPT/ext4 mounting and teardown paths warrant final human review of the validation evidence, including the backend CI matrix.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

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.

🔵 Needs a closer look

Privileged partition handling and multi-layer mount lifecycle changes warrant final human review and complete backend-matrix validation.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

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.

🔵 Needs a closer look

Privileged filesystem assembly, multi-layer cleanup, and handcrafted disk fixtures warrant final human review alongside full backend CI results.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

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.

🔵 Needs a closer look

Privileged GPT/ext4 mounting and failure cleanup warrant final human review and confirmation of the remaining backend CI results.

No open issues 🎉

Review effort: Balanced

Base automatically changed from esaurez/a3-3-gpt-tmpfs-overlay-20261021 to dev October 23, 2026 05:39
Enrique Saurez and others added 3 commits October 22, 2026 22:40
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

Copilot AI left a comment

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.

🔵 Needs a closer look

Privileged GPT mounting and multi-layer cleanup warrant final human review alongside full backend CI results.

No open issues 🎉

Review effort: Balanced

@esaurez
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
Copilot AI balanced review requested due to automatic review settings October 23, 2026 07:33

Copilot AI left a comment

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.

🔵 Needs a closer look

Privileged filesystem mounting and partial-failure cleanup warrant human review and full backend CI confirmation despite no confirmed blocking defects.

No open issues 🎉

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit 19814ba into dev Oct 23, 2026
42 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the esaurez/a3-3-gpt-tmpfs-overlay-20261021-gpt branch October 23, 2026 11:47
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants