Repository navigation
nvx-rs: decode the MAPS entries once for both payload decoders - #616
Merged
Merged
Conversation
decode_maps_payload, the text decoder that the fake OpenVMM uses, and decode_guest_maps_payload, the byte decoder that the guest agent uses, each carried a copy of the loop that reads and checks the entries of a MAPS payload: their flags and lengths, their sources and targets, the path rules, and the trailing bytes. The copies differed only in how they read a path, as UTF-8 text or as bytes, and in the entry that they build. The path rules were written twice as well, as acceptable for text and acceptable_bytes for bytes, whose checks that a path is not empty and that a source is not `.` its other checks already made. - decode_maps_entries reads and checks the entries once. Each decoder passes it a function that converts a path as soon as it is read and one that builds its entry, and keeps what is its own: the guest decoder's bound on the number of entries, checked before any entry is read, and the allocation that it sizes from that bound. - acceptable checks the bytes of a path, so the encoder and both decoders share it. `/` and NUL occur in UTF-8 only as themselves, so text passes the byte rules exactly when it passed the text rules. What the encoder and each decoder accept, their error messages, and the order of their checks are unchanged: the host decoder still requires a source to be UTF-8 before it reads the target. The host decoder now advances its offset with the guest decoder's checked additions, which follow successful bounds checks and so never fail. A new protocol test pins the messages that both decoders share and the two that differ, and passes before and after the change. A throwaway differential test compared the old and new code on 14.7 million payloads and 332,000 mapping tables: the results of both decoders and of the encoder, messages included, were identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The focused refactor preserves existing behavior, adds regression coverage, and has no identified blocking issues.
No open issues 🎉
What changed in this PR
Consolidates MAPS decoding in nvx-rs, following the EXEC refactor in #603 while preserving existing behavior and intentional host/guest differences.
Changes:
- Shares entry decoding, byte-level path validation, and truncation errors.
- Adds regression coverage for shared error messages and decoder-specific validation order.
| File | Description |
|---|---|
nvx-rs/src/protocol/tests.rs |
Adds decoder error-message regression tests. |
nvx-rs/src/protocol/maps.rs |
Consolidates decoding and path validation without changing public interfaces. |
Review effort: Balanced
This was referenced Oct 20, 2026
This was referenced Oct 20, 2026
This was referenced Oct 21, 2026
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.
Summary
decode_maps_payload, the text decoder that the fake OpenVMM uses, anddecode_guest_maps_payload, the byte decoder that the guest agent uses, each carried a copy of the loop that reads and checks the entries of aMAPSpayload. The guest agent's path rules were also written twice, asacceptablefor text andacceptable_bytesfor bytes. This change moves the loop into one private function and keeps a single byte-levelacceptable, in the same way that #603 consolidated theEXECheader. The two decoders can no longer drift apart, and each public decoder now holds only what is its own.Changes
All of the code changes are in
nvx-rs/src/protocol/maps.rs. The test is innvx-rs/src/protocol/tests.rs.decode_maps_entries(new, private) reads and checks the entries once: the flags and lengths, the source and target, the path rules, and the trailing bytes. Each caller passes a function that converts a path as soon as it is read and a function that builds its entry. These sites now share it:decode_maps_payloadpasses a UTF-8 conversion and buildsHostMaps intoVec::new(), as before.decode_guest_maps_payloadpasses a byte copy and buildsGuestHostMaps. It keeps its ownMAX_HOST_MAPPINGSbound ("too many entries"), which it checks before reading any entry, and the preallocation that it sizes from that bound.acceptablenow checks the bytes of a path and replacesacceptable_bytes. These sites now share it:encode_maps_payloads, throughas_bytes().decode_maps_entries.The extra checks in
acceptable_bytes,!path.is_empty()andsource != b".", are dropped because they were redundant:normalalready refuses an empty source and any.component, andstrip_prefix(b"/")refuses an empty target.truncated()is a module-level function that replaces the two identical closures.maps_decoders_refuse_entries_with_the_same_messages(new test) pins the messages that both decoders share: truncated header, truncated entry, trailing bytes, unknown flags, and refused path. It also pins the two messages that differ by design. The host decoder reports a non-UTF-8 source before it reads a truncated target, which the guest decoder reports as truncated. The guest decoder's bound refuses 4097 entries before it reads any. The existing tests only checkedis_err(). The new test passes both on BASE and with this change.Behavior
encode_maps_payloads,decode_maps_payload, anddecode_guest_maps_payload. The path conversion still runs as soon as each path is read, so the host decoder still fails a non-UTF-8 source before it reads the target.checked_addandsaturating_addwhere it used+. Each of these follows a successful bounds check, because the offset never exceedspayload.len(). Neither can fail or produce a different value./(0x2F) and NUL occur in UTF-8 only as themselves, never inside a multibyte character. Splitting a string's bytes onb'/'therefore yields the same components as splitting the text on'/', so text passes the byte rules exactly when it passed the text rules.Vec, and the guest decoder still preallocates at most 4096 entries.maps.rson 14,749,578 payloads and 332,256 mapping tables. The payloads included every payload of up to 2 bytes, every pair of 46 edge-case paths with every flag, length skew, and count, both truncated and with a trailing byte, and 5 million random payloads. Both decoders and the encoder produced identical results, messages included. Every refusal message of both decoders was exercised.Follow-ups
decode_maps_payloaddoes not bound the number of entries byMAX_HOST_MAPPINGSbefore reading them, whiledecode_guest_maps_payloadrefuses more than 4096 with "too many entries". The host decoder's documentation says that it refuses what the guest agent refuses of a payload, so a payload of more than 4096 valid entries decodes in the fake OpenVMM but not in the guest. The difference may be deliberate, so this change keeps it rather than unifying it. Consider whether the host decoder should share the bound.cgroup.rs'ssetupduplicatesdispatch.rs'sdirect_system,mappings.rs'spayloadduplicatesdispatch.rs'smaps_payload, and both files carry the same host-export fixture.Validation
830536ed210e0d8bef0ca9166c13fb9636acb614(origin/dev).18948e2900a2af24cb435f232cdc8ba6c889b7d4, the tree of this pull request's commitec1c09d8f809e3be3943b2621ef0f339b7f2d2f6.v0.1.0-dev.6c4171aa3166withnvx.py download --hypervisor BACKEND. Its OpenVMM provenance isd0bdbed8f6e3b86271277dc22658109e2e633b2d, which matches the pinned gitlink.build-guest-binaries --outputandtest-guest-binaries --binaries(34 passed). The Alpine initramfs was then built withNVX_GUEST_BINARIES=… build-initramfs; its provenance records TREE'smaps.rs. The initramfs (9c36d87c…), its package manifest, and its provenance were copied to the other two hosts with matching SHA-256 digests.nvx-rsCLI: each host rebuilt it from TREE withcargo build --release --locked --features cliand recorded its provenance, so the microVM scenarios ran this change rather than the release's binary.-D warnings(all features, and the library without default features), the--all-featuresand default tests, and rustdoc with-D warnings.checkfornvx-rsandaci_edge_sandboxes, on behalf of the WHP host, which has no 1.89 toolchain.aarch64-apple-darwincheck for both crates, on behalf of the KVM host, which lacks that target.test-aci-edge-sandboxestest-microvm(default suite)check-guest-binaries(321 passed); tests 876 passed, 2 ignoredunshare --useris denied)The macOS checks for the KVM host's steps and the MSRV checks for the WHP host's steps ran locally, as listed above.
Left to CI:
test-openvmm-unitandtest-openvmm.test-microvmscenarios that run only when named:smp-lapic,managed-exec-config, andsandbox-lifecycle.