Skip to content

nvx-rs: encode activation records canonically - #873

Open
Enrique Saurez (esaurez) wants to merge 1 commit into
esaurez/838-pr1-activation-record-types-20261024from
esaurez/838-pr2-activation-encoding-20261024
Open

Enrique Saurez (esaurez) wants to merge 1 commit into
esaurez/838-pr1-activation-record-types-20261024from
esaurez/838-pr2-activation-encoding-20261024

Conversation

@esaurez

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

Copy link
Copy Markdown
Contributor

Stacked on #871; this PR retargets to dev when the pull requests below it merge. The chain on dev is #866 → #882 → #883 → #876 → #871 → #873 → #874, after #863, #864, #865, #867, #868, #869, #870, #872, #875, and #878, which have merged. It builds on #871's third commit, which makes the store generation a nonzero u64, the publication ID of the template store of #839: the restore field holds it in 8 little-endian bytes and the decoder refuses 0, so the field takes 56 bytes and the largest record 602.

Part of #838: the second of its three pull requests. The host writes an activation record and the guest reads it, so they need one encoding of it. This gives an ActivationRecord one canonical encoding: ActivationRecord::encode writes it, and ActivationRecord::decode reads exactly those bytes back and refuses any others. It changes no behavior: nothing carries records yet.

What changed

The encoding (nvx-rs/src/activation/codec.rs; the module documentation's "Encoding" section defines it field by field). A record is its version, a little-endian u16, then, without padding, in the increasing order of their tags, each at most once, a field for each part that the record holds: its tag, one byte, the length of its value, one byte, and its value.

Tag Field Value Bytes
0x01 Restore Transaction ID, store generation, a u64, and identity of the template's key 56
0x10 Image Format, identity of the binding, and a GPT disk's first-partition bound 33 or 41
0x11 Image identity EROFS UUID or GPT disk GUID that the guest expects 16
0x20 Upper Tmpfs, or the scratch with its binding's identity and its journal policy 1 or 34
0x30 Overlay Overlay mode 1
0x40 Export Shape and identity of the plan 33
0x41 Mappings 1 to 4,096 2
0x50 Network Identity of the policy and the listeners' publication 33
0x51 DNS IPv4 or IPv6 address 4 or 16
0x52 Proxy Endpoint 1 to 255
0x60 Hostname Hostname 1 to 64
0x61 Machine ID Mode 1
0x62 Workload identity UID and GID 8
0x70 Resources CPU count and memory size 12

Choices are one-byte codes from 1, so that a zero byte names none, integers are little-endian, and identities, IDs, UUIDs, and addresses are their bytes. A part that a record lacks is a missing field, never a flag or an empty value. The largest record takes 602 of the 1,024 bytes that MAX_RECORD_BYTES allows.

The decoder refuses, naming the Field at fault where there is one, and in a documented order: bytes past MAX_RECORD_BYTES; bytes that end inside the version or a field; another version; an unknown, repeated, or misordered field; a value of another length than its contents take, with a code that the version doesn't define, with a mapping count or a store generation of 0, or with a proxy endpoint that isn't UTF-8; a missing field that every record holds; a DNS server or a proxy without a network; and every configuration that Configuration::check refuses, among them an export without mappings and mappings without an export. The refusals are new variants of ActivationError, and Field is public. So every record has exactly one encoding.

The encoder never writes what the decoder refuses. A record exists only once ActivationRecord::new or ActivationRecord::decode has checked it, and its fields are private, so encode returns bytes rather than a Result. The property tests check that every record that they generate reads back from its encoding.

Documentation. The module documentation defines the encoding, and its example round-trips a record. The crate's README and doc/project-structure.md mention the encoding.

Tests (nvx-rs/src/activation/codec/tests.rs), whose bytes the tests build field by field from the definition, never with the encoder:

  • a cold boot's record, a clone's, and one with every part of a configuration encode to their bytes and read back;
  • the largest record holds 602 bytes, with every field in order;
  • each refusal: every proper prefix, bytes past the limit, other versions, every unknown tag, repeated and misordered fields, every wrong length of every field, unknown codes, a mapping count or a store generation of 0, missing fields, a DNS server or a proxy without a network, and the configuration's limits, including a UID or GID of u32::MAX; and the order of the refusals;
  • the fields' tags and names, the choices' codes, and the refusals' messages;
  • properties: generated records round-trip within the limit while their proper prefixes are refused, and the decoder returns, never panics, on arbitrary bytes, arbitrary fields, and altered encodings, and accepts only the canonical encoding of the record that it returns.

Validation

At 5edab9f, rebased onto #871's head without changes to the contents of its commit, on Windows and on Linux x86_64:

  • cargo fmt --check; cargo clippy with --all-targets --all-features, --lib, --lib --no-default-features, and --lib --no-default-features --features guest, -D warnings; cargo test with all, default, and no features; cargo doc with -D warnings; cargo +1.89 check;
  • the runtime adapter's protocol-only and host-only configurations, and, on Linux, its guest-only configuration;
  • aci_edge_sandboxes' checks;
  • on Linux, also the bundled artifacts test, the macOS check of both crates, the guest binaries' lint, tests, build, and check, and the Markdown link check.

Before the rebase, on dev, the same checks passed at 7aaa36b.

Left to CI: the whole matrix, which runs once this pull request targets dev.

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 codec preserves validated record invariants, has extensive focused test coverage, and has no identified blocking issues.

No open issues 🎉

What changed in this PR

Adds canonical encoding and strict decoding for nvx::activation, completing the second step of #838. The shared codec prepares activation records for future host–guest transport and configuration identity hashing without changing runtime behavior.

Changes:

  • Adds a bounded, versioned encoding with ordered, tagged, length-prefixed fields and strict rejection of non-canonical inputs.
  • Adds byte-level, boundary, rejection, and property tests for round trips and decoder robustness.
  • Documents the wire format, public field identifiers, and decoding errors.
File Description
nvx-rs/​src/​activation/​tests.rs Exposes existing fixtures for reuse by codec tests.
nvx-rs/​src/​activation/​mod.rs Documents the encoding and adds public field identifiers and decoding errors.
nvx-rs/​src/​activation/​codec/​tests.rs Tests canonical bytes, validation failures, size limits, and round-trip properties.
nvx-rs/​src/​activation/​codec.rs Implements canonical encoding and strict decoding.
nvx-rs/​README.md Describes the codec API and expanded test coverage.
doc/​project-structure.md Adds canonical encoding to the activation module’s description.

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.

🟢 Approval recommended

The bounded codec preserves record validation, has comprehensive focused tests, and has no identified blocking issues.

No open issues 🎉

Review effort: Balanced

The host writes an activation record, and the guest reads it, so they
need one encoding of it, which also gives the configuration identity
the bytes that it covers (#838).

ActivationRecord::encode writes the canonical encoding, and
ActivationRecord::decode reads it back: the record's version, a
little-endian u16, then a tagged, length-prefixed field for each of the
record's parts, in the increasing order of their tags, each at most
once, without padding. Choices are one-byte codes from 1, integers,
the store generation among them, are little-endian, and identities,
IDs, and addresses are their bytes. The largest record takes 602 of
the 1,024 bytes that MAX_RECORD_BYTES allows. The module's
documentation lists every field.

The decoder refuses bytes past MAX_RECORD_BYTES, bytes that end inside
the version or a field, other versions, unknown, repeated, and
misordered fields, values of another length or with codes that the
version does not define, a mapping count or a store generation of 0,
missing fields, a DNS server or a proxy without a network, and every
configuration that Configuration::check refuses, in a documented
order, naming the Field at fault, so that every record has exactly one
encoding. The tests build their bytes from the definition, not with
the encoder, and property tests check that generated records
round-trip and that the decoder accepts only the canonical encoding of
what it returns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8f7bd07b-b09f-48d5-bc49-ad06f47ef3f9
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/838-pr1-activation-record-types-20261024 branch from dc4517f to 3032e6d Compare October 27, 2026 01:11
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/838-pr2-activation-encoding-20261024 branch from 7aaa36b to 5edab9f Compare October 27, 2026 01:11
@esaurez
Enrique Saurez (esaurez) requested a balanced review from Copilot October 27, 2026 01:13

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 bounded codec preserves validated-record invariants and has extensive targeted coverage, with no unresolved review findings.

No open issues 🎉

Review effort: Balanced

This branch has not been deployed

No deployments
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