Skip to content

nvx-rs: decode the EXEC header once for both payload decoders - #603

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
nvx-rs-quality/exec-header
Oct 19, 2026
Merged

Pedro Henrique Penna (ppenna) merged 1 commit into
devfrom
nvx-rs-quality/exec-header

Conversation

@ppenna

Copy link
Copy Markdown
Contributor

Summary

decode_exec_payload, the UTF-8 decoder that the fake OpenVMM uses, and decode_guest_exec_payload, the byte decoder that the guest agent uses, each carried the same 42 lines that read and check the fixed fields of an EXEC payload. The text readers prefixed_text and text_at also repeated the length, truncation, and NUL checks of the byte readers prefixed_bytes and bytes_at. This change decodes the header in one private function and builds the text readers on the byte readers. The wire header's checks now live in one place, so the host and guest decoders cannot drift apart when a flag or limit changes.

This is the first nvx-rs-quality/ run, and no earlier pull request in the series had feedback to follow. It picked the largest duplicate block in nvx-rs/src and nvx-rs/tests in a file that no open pull request to dev changes.

Changes

All changes are in nvx-rs/src/protocol/exec.rs: 68 insertions and 73 deletions. No public item, test, fixture, or other file changes.

  • The new private ExecHeader and decode_exec_header(payload, max_cwd) read and check the timeout, the number of arguments, the extension, and an extended payload's flags, environment count, and working-directory length. Two decoders now share this code:
    • decode_exec_payload passes MAX_ARGUMENT_BYTES as the working-directory bound, as it checked before.
    • decode_guest_exec_payload passes MAX_GUEST_CWD_BYTES, as it checked before.
  • prefixed_text now reads with prefixed_bytes, and text_at reads with bytes_at. Both then convert through the new utf8_text, which holds the exec {what} must be valid UTF-8 message once.

Behavior

  • Each decoder accepts and refuses the same payloads as before, with the same messages and checks in the same order. The header code moved as it was, except that it takes its working-directory bound as an argument and returns its fields as a struct. The two bound constants are both 4096 today, and they stay separate.

  • The byte readers advance offset with checked additions, where the text readers used +=. Each addition follows a successful bounds check, so neither can fail. On a UTF-8 error, a text reader now leaves offset past the field. Every caller returns that error at once and never reads offset again.

  • I wrote a throwaway differential test, which is not committed. It compiled the original and new exec.rs side by side and compared the Debug form of both decoders' results, error messages included, on three sets of payloads:

    • every payload of up to 3 bytes, about 16.8 million;
    • 3 million structured payloads, with lengths, counts, flags, extensions, and fields that were sometimes wrong, mutated, or truncated;
    • 3 million random payloads of 0 to 39 bytes.

    All results were identical. The inputs reached all 35 distinct error messages of the two decoders, and each decoder accepted tens of thousands of payloads.

Follow-ups

  • The documentation of decode_guest_exec_payload says that it differs from decode_exec_payload "at the C guest's 4096-byte working-directory boundary". Both decoders accept a 4096-byte working directory, though, because MAX_GUEST_CWD_BYTES is MAX_ARGUMENT_BYTES. Either the comment is stale, or the host decoder was meant to use MAX_CWD_BYTES (4095). Either fix changes documented behavior or acceptance, so this change leaves it alone.
  • In protocol/maps.rs, decode_maps_payload and decode_guest_maps_payload repeat their header and entry loop in the same way. The guest copy also refuses more than MAX_HOST_MAPPINGS entries up front and uses checked offset arithmetic, while the host copy does neither. Sharing that code needs a decision on whether the difference is intended.
  • The rest of the two EXEC decoders, which read the arguments, working directory, and environment, still mirror each other. Their environment messages and inherit_default_env rules differ on purpose, so this change keeps them apart.

Validation

  • BASE: 7e96632b3d6da4200b09b4102296cd0b11cdeb83, which was origin/dev when the run started. Before this pull request was opened, no later dev commit and no open pull request to dev changed nvx-rs/src/protocol/exec.rs.

  • TREE: 1568601977449421a4dbb2b629f589d9dda1d46e, the tree of this commit. Every host applied the patch to BASE and checked that git write-tree equals TREE.

  • Release on every host: v0.1.0-dev.3e740c0e1c9a. Its OpenVMM, 5ab8a84621524f1777b2cb5e65a70dab42c8b453, is the pinned revision.

  • Guest artifacts were rebuilt from TREE on prometheus30, with these steps:

    • build-guest-binaries --output;
    • test-guest-binaries --binaries, where 34 tests passed;
    • the Alpine build-initramfs with NVX_GUEST_BINARIES.

    initramfs.cpio.gz (SHA-256 83d4218cb082db3038a659075cae8d4634086470c6a14f569077e65ad98f3d22), its packages manifest, and its provenance went to the other two hosts, where the SHA-256 digests matched.

  • Local checks on Windows with Rust 1.95.0:

    • rustfmt;
    • Clippy with -D warnings, for all targets and features and for the library without default features;
    • cargo test with all features (181 library tests, 6 fake-OpenVMM tests, and 1 doctest) and with the default features (178 and 1);
    • rustdoc with -D warnings;
    • the steps that no host could run, because the hosts lack the toolchain or target: the Windows MSRV (1.89) checks and the macOS (aarch64-apple-darwin) checks, for both nvx-rs and aci_edge_sandboxes;
    • the differential test above.
Host profile Backend Host type check-nvx-rs and check-aci-edge-sandboxes without a hypervisor Guest artifacts test-aci-edge-sandboxes test-microvm, default suite
prometheus32 kvm baremetal Pass: every Linux step, including MSRV 1.89, check-guest-binaries (277 + 34 musl tests), artifact bundling, and agent bundling; the macOS checks ran locally Copied in, digests match Pass (18/18) Pass
prometheus30 mshv baremetal None; prometheus32 and prometheus28 ran the gates Built and tested here Pass (18/18) Pass
prometheus28 whp baremetal Pass: every Windows step, including artifact bundling and agent bundling; the MSRV checks ran locally Copied in, digests match Pass (18/18) Pass

Left to CI:

  • the OpenVMM unit and VMM suites, test-openvmm-unit and test-openvmm;
  • the microVM scenarios that run only when named: smp-lapic, managed-exec-config, and sandbox-lifecycle;
  • the Ubuntu and Azure Linux guests, and guest determinism;
  • the benchmarks;
  • the virtual-machine runners;
  • check-quality and validate-nvx, whose inputs this change does not touch.

decode_exec_payload, the text decoder that the fake OpenVMM uses, and
decode_guest_exec_payload, the byte decoder that the guest agent uses,
each carried the same 42 lines that read and check the fixed fields of
an EXEC payload: the timeout, the number of arguments, the extension,
and an extended payload's flags, environment count, and
working-directory length. The copies differed only in the constant that
bounds the working directory, MAX_ARGUMENT_BYTES in the host decoder
and MAX_GUEST_CWD_BYTES in the guest decoder. The text readers,
prefixed_text and text_at, also repeated the length, truncation, and
NUL checks of the byte readers, prefixed_bytes and bytes_at.

- decode_exec_header reads and checks the fixed fields once and returns
  them as an ExecHeader, which each decoder destructures. Each decoder
  passes its own working-directory bound, so the two constants stay
  separate.
- prefixed_text and text_at read their bytes with prefixed_bytes and
  bytes_at, then require UTF-8 through utf8_text.

What each decoder accepts, its error messages, and the order of its
checks are unchanged. The byte readers advance the offset with checked
additions where the text readers used plain ones, but each addition
follows a successful bounds check, so neither can fail or overflow. The
protocol tests pin both decoders' acceptances and refusals and the
round trip through the guest decoder. A throwaway differential test
compared the old and new decoders on every payload of up to 3 bytes
and on 6 million structured and random payloads: their results,
messages included, were identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 19, 2026 13:28

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.

🟢 Approval recommended

The focused refactor preserves validation order, limits, and error behavior, with no unresolved findings.

No open issues 🎉

What changed in this PR

Refactors EXEC payload decoding to share validation logic between the host and guest decoders without changing public APIs or accepted payloads.

Changes:

  • Extracts shared header validation while preserving each decoder’s working-directory limit.
  • Builds text readers on byte readers and centralizes UTF-8 conversion.
File Description
nvx-rs/​src/​protocol/​exec.rs Consolidates header and field parsing while preserving validation behavior.

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit f4b4b8d into dev Oct 19, 2026
33 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the nvx-rs-quality/exec-header branch October 19, 2026 14:38
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.

2 participants