Repository navigation
nvx-rs: write the NVXS record header with one function - #705
Merged
Merged
Conversation
encode_outer, in protocol/outer.rs, and nvxs::encode_record, in protocol/nvxs/record.rs, each wrote the 44-byte header of an NVXS record in the same eight statements: the magic, the version, the record type, a zero flags byte, the instance ID, the epoch, the sequence number, and the payload length. So the header's wire layout had a definition in each encoder. A private function of protocol/outer.rs, encode_header, now returns the header as an array, with each field at the offset at which decode_outer_header and the record parser read it, and both encoders append it before the payload. Each encoder keeps its own checks, which differ: encode_outer checks only the payload length of a record of any type, and encode_record everything that RecordParser checks, so neither encoder can call the other. Behavior is unchanged: both encoders write the same bytes after the same checks, into a vector of the same capacity, and encode_record still leaves its output unchanged on an error, since every check runs before its first write. No public item changes. Golden byte tests pin both encoders independently of the codec: those of protocol/tests.rs for encode_outer, and the broker's vectors of every record type in protocol/nvxs/tests/record.rs and the golden attachments of the session tests for encode_record. The managed agent's dispatch tests and the CLI's fake-OpenVMM scenarios run each encoder against the other's decoder, and the hypervisor tests ran the guest agent and the host, built from this change, through OpenVMM's own NVXS broker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The focused refactor preserves encoding behavior and validation, with existing golden-vector coverage and no unresolved findings.
No open issues 🎉
What changed in this PR
Centralizes NVXS record-header encoding so both encoders share one wire-layout implementation.
Changes:
- Adds a protocol-scoped helper that returns the 44-byte header.
- Updates both encoders while preserving validation, byte order, and payload handling.
| File | Description |
|---|---|
nvx-rs/src/protocol/outer.rs |
Defines the shared header helper and uses it in encode_outer. |
nvx-rs/src/protocol/nvxs/record.rs |
Uses the shared helper in encode_record. |
Review effort: Balanced
Pedro Henrique Penna (ppenna)
deleted the
nvx-rs-quality/record-header
branch
October 22, 2026 03:17
This was referenced Oct 22, 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
encode_outer, innvx-rs/src/protocol/outer.rs, andnvxs::encode_record, innvx-rs/src/protocol/nvxs/record.rs, each wrote the 44-byte header of an NVXS record in the same eight statements:So the wire layout of the header, which OpenVMM's broker parses, had a definition in each encoder. A private function of
outer.rs,encode_header(record_type, instance_id, epoch, sequence, length), now returns the header as a[u8; OUTER_HEADER_LEN]array, with each field at the offset at whichdecode_outer_headerand the record parser read it, and both encoders append it before the payload.This is the twenty-ninth
nvx-rs-quality/pull request. The 19 from #603 to #637 merged, as did #678, and #679 through #685 and #689 are open. Each had a Copilot overview review that recommended approval with no comment to follow, #636 and #637 also have approvals without comments to follow, and none was closed without merging. This run surveyed the crate by reading, and with a throwaway script, kept outside the repository, that reports every window of six or more whitespace-normalized lines that repeats insrcandtests, ignoring blank lines, comments, and lines of punctuation, exactly and with names and literals abstracted. It skipped every file that an open pull request todevchanged when the run started, among themREADME.md,Cargo.toml,src/lib.rs,host/mod.rs,guest/mod.rs, most of the managed agent,guest/sys,guest/time,guest/time_probe, the guest programs' modules,guest/nvxs.rs,guest/spawn.rs,host/instance.rs,host/process.rs,host/filesystem.rs,host/platform,host/store/{mod,tests}.rs,host/sandbox/{mod,mount,options,distro,golden}.rs,host/cli/session.rs,host/cli/sandbox/{lifecycle,mod,state}.rs,host/network/{lowering,policy/mod}.rs,host/python/golden.rs,tests/cli.rs,tests/managed_lifecycle.rs, andtests/supervisor.rs.No pull request has changed
outer.rsornvxs/record.rssince the NVXS session was added, and this candidate gives the most production code one definition per changed line among those in the remaining files: 44 changed lines for the wire layout that two public encoders write. The other candidates either repeat only test fixtures, such as thepolicyandnativehelpers of the sandbox tests, thekernel_addresshelper of the ingress tests, and thenvx-ingress test DIRECTORYcommand lines oftests/guest_tools.rs, or share a few statements behind a helper whose signature costs about as many lines as it saves, such as the port selectors ofEgressRule's builders, the container loops ofhost/python/dumps.rs, and the Windowsstatandlstatofhost/python/fs.rs. Follow-ups lists them.Where the choice was open, I took the conservative option:
encode_headerispub(super), visible inprotocolalone, and checks nothing. Each encoder keeps its own checks, which differ:encode_outerchecks only the payload length of a record of any type byte, which tests use to send malformed records, andencode_recordchecks everything thatRecordParserchecks. So neither encoder calls the other:encode_recordcannot take a record of any type byte, and callingencode_outerfromencode_recordwould allocate a second vector for every record.encode_headerfits one line. Writing each field at its offset also mirrors the decoders. Both encoders still reserve the same capacity, then append the header and the payload.ProtocolErrormessages andRecordErrorvariants, so a shared reader wants a change of its own. Follow-ups lists it.Changes
All changes are in
nvx-rs/src/protocol/outer.rsandnvx-rs/src/protocol/nvxs/record.rs: 28 insertions and 16 deletions.encode_header(record_type, instance_id, epoch, sequence, length) -> [u8; OUTER_HEADER_LEN],pub(super)inprotocol::outer, with a doc comment: the magic at offset 0, the version at 4, the record type at 6, a zero flags byte at 7, the instance ID at 8, the epoch at 24, the sequence number at 32, and the payload length at 40, with the integers little-endian.encode_outerinprotocol/outer.rs(nvx::protocol::encode_outer), which appends the header and then the payload to the vector that it returns;encode_recordinprotocol/nvxs/record.rs(nvx::protocol::nvxs::encode_record), which reserves room inoutputand appends the header and then the payload to it.No public item, message, error, protocol byte, fixture, or other file changes.
Behavior
Unchanged:
OUTER_HEADER_LEN + payload.len().encode_recordstill leavesoutputunchanged on an error.encode_headercannot fail.encode_outerinprotocol/tests.rs:host_attach_matches_the_python_client,data_record_matches_the_python_client,golden_outer_headers_decode_as_the_broker_sends_them, andguest_outer_records_match_golden_bytes;encode_record:records_match_the_brokers_golden_vectorsinprotocol/nvxs/tests/record.rs, with every record type, and the golden attachments ofthe_guest_attaches_acknowledges_receives_and_creditsandthe_host_attaches_after_waiting_and_exchanges_datain the session tests, besides the encoder's checks inrecord.rs's other tests;encode_outerto the agent'sGuestSessionand reads its records withdecode_outer_header; the tests ofhost/session.rs; and the scenarios oftests/cli.rsandtests/fake_openvmm.rs, in which the host'sHostSessiontalks to the fake OpenVMM, which encodes and decodes withencode_outeranddecode_outer_header;nvx-rsandaci_edge_sandboxes, all built from TREE, exchanged NVXS records through OpenVMM's own broker intest-aci-edge-sandboxes, themanaged-lifecyclescenario, and the other managed scenarios oftest-microvm.Follow-ups
Not in this pull request, to keep it to one change. I found no suspected bug.
decode_outer_headerinouter.rsand the record parser'sparse_headerinnvxs/record.rsstill each read the header's fields at the same offsets. A reader of the raw fields besideencode_headerwould give the layout one definition in both directions, while each decoder kept its own checks, their order, and its errors.outer.rs:encode_ack_payloadanddecode_ack_payloadrepeat their window check and message, asencode_credit_payloadanddecode_credit_payloadrepeat theirs, andcheck_payloadinnvxs/record.rschecks the same ranges with errors of its own.protocol/nvxs/guest.rsandhost.rs: each role'ssendrepeats the check of the next sequence number, the DATA encoding, and the advance, and the guest'screditdoes the same for CREDIT;encode_datacould take the sequence number by reference and advance it.policyandnativehelpers ofhost/sandbox/tests.rsandhost/sandbox/mount/tests.rs, thekernel_addresshelper ofguest/ingress/address.rsandcollect.rs, and the fivenvx-ingress test DIRECTORYcommand lines oftests/guest_tools.rs.EgressRule::on_port,on_port_range, andon_protocolinhost/network/mod.rseach build and push aPortSelector;host/python/dumps.rs'swrite_valuerepeats its bracket, comma, and indentation loop for lists and dicts; andhost/python/fs.rs's Windowsstatandlstatbuild the sameStat.receivein both NVXS roles, the bounded merges ofguest/ingress, the IPv4ADDRESS/PREFIXparsing ofnetandhost/network/mod.rs, and those that wait on files that open pull requests change.Validation
BASE:
8b585d722d4ade4146291ed7d4e123889d8bbbd2,origin/devwhen the run started. Before this pull request was opened,origin/devadvanced to74c56f430by the merges of ci: recover Windows runners after VMSS image upgrades #669 and tests: define the guest command once #677 and two baseline updates, which changescripts/,tests/runtime-adapter/, anddata/, none of this pull request's files.TREE:
ac6777dcf885a5dfa9527cf87c18cc5e21500c09, the tree of this commit. Every host applied the patch to BASE and checked thatgit write-treeequals TREE.Release on every host:
v0.1.0-dev.8b585d722d4a, built from BASE itself. Its OpenVMM,d0bdbed8f6e3b86271277dc22658109e2e633b2d, is the pinned revision, as each host checked inbuild/openvmm.provenance.json.nvx-rs:downloadinstalled the release'snvx-rs, which BASE's sources built, so each host removed it and rebuilt it from TREE with the Cargo command with whichnvx.pybuilds it. Cargo recompilednvx, the executable's digest changed, andrecord-nvx-rs-provenancerecorded it; each host checked that the record names TREE's sources (source_sha25607c3a52dba58608a34600178bd7430feca6d7f66fca4791dc669ed0a30f5218b) and the new executable. Each host then built theopenvmm_e2eandmanaged_lifecycletest binaries, after its other Cargo work and before its hypervisor tests, whose logs show no compilation.Guest artifacts were rebuilt from TREE on prometheus30, whose user namespaces let Alpine's apk run its scripts (prometheus32 denies them):
build-guest-binaries --output;test-guest-binaries --binaries, where 34 tests passed;build-initramfswithNVX_GUEST_BINARIES.initramfs.cpio.gz(SHA-256c7d2859c829b0da62a3ad677525cd90d3c41e805c20af73b061f97dc0bd23c12), its packages manifest (b5d0268f2e22ff6c6d67d883c79a330e86afd5536be893915bfd9fe0270f2572), and its provenance (9f7ea99e6c085999b091a97f536b21cd13ab28af914acb8bcb3a1a1901dc082f) went through this machine to the other two hosts, where the SHA-256 digests matched. On every host, the 191 source files that the provenance records matched the worktree's, the two changed files with TREE's digests, and on prometheus30 the guest binaries' build manifest names TREE's guest-binary sources (035d2b528a60b49e5ef4ef8801b35cf4aa9058c3161a93b252709d32e73761ce).Local checks with Rust 1.95.0 on Windows: rustfmt; Clippy with
-D warningson all targets with all features and on the library without default features;cargo testwith all features (425 tests in 11 test targets) and with the default ones (354 in 9); and rustdoc with-D warnings. This machine also ran the steps that no host could run, because the hosts lack the toolchain or target: on Windows, the MSRV (1.89) checks ofnvx,aci_edge_sandboxes, and the runtime adapter's protocol-only and host-only configurations; and, on WSL Ubuntu, the macOS (aarch64-apple-darwin) checks ofnvxandaci_edge_sandboxesand the MSRV check of the runtime adapter's guest-only configuration, which ran as the action defines it, starting with itsrustup target add, which gave the local 1.89 the musl target. I removed that target again afterwards. A first attempt at that last check ran nothing: my script read the MSRV as empty from this machine's CRLF checkout, and rustup refused the empty toolchain name.The change adds, moves, and deletes no file and changes no Markdown, so the CLI tests of validate-nvx and the link check had nothing to cover.
The hosts ran each action's steps with the toolchains and targets that they have, skipping the actions' installation commands; nothing was installed on them.
test-aci-edge-sandboxestest-microvm, default suitenvxtests: 755 in 11 test targets with all features and 355 in 9 with the default ones, besides the runs of single tests that some tests make of their own binary; artifact bundling (15);check-guest-binaries(412); the runtime adapter's protocol-only, host-only, and musl guest-only configurations (13, 14, and 21 tests); 158 and 85aci_edge_sandboxestests over the two feature sets; agent bundling (4). The macOS checks and the guest-only MSRV check ran locallynvxtests: 425 in 11 test targets with all features and 354 in 9 with the default ones; artifact bundling (15); the runtime adapter's protocol-only and host-only configurations (13 and 14 tests); 158 and 85aci_edge_sandboxestests over the two feature sets; agent bundling (4). The MSRV checks ran locallyEvery gate and hypervisor test passed at its first run. The Linux gates ran in the foreground of their SSH session, whose shell ignored only
SIGQUIT(SigIgn: 0x4), notSIGINT, so the CLI's Ctrl-C tests ran as intended. Each suite'smanaged-lifecyclescenario ran themanaged_lifecycletest with Cargo, which passed. OpenVMM warned of a host CPU-profile fallback only in thecpu-profile-fallbackscenario, which forces one. The time-ABI evidence lines counted no capture or restore over its CPU-time budget on any host, and no boot on prometheus32 and prometheus28; on prometheus30, they counted 1 of 58 boots, which the evidence reports without failing the suite, innvx-time's boot checks, which encode no NVXS record. On every host, oneopenvmm_e2etest printedskipped: the host has no second volumefor a check that needs one, and on the Linux hosts the filesystem caller-ownership scenario reported that OpenVMM cannot assume other host identities, so it checked that guest root and foreign guest callers fail withEPERM; both are properties of the hosts, not of this change.Left to CI:
test-openvmm-unitandtest-openvmm, since OpenVMM is unchanged;test-microvmscenarios that run only when named:smp-lapic, andmanaged-exec-configandsandbox-lifecycle, which need artifacts that packages omit;verify-guest-determinism;