Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/check-nvx-rs/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ runs:
python3 ../scripts/nvx.py test-guest-binaries --binaries "${directory}"

# The runtime adapter builds on nvx-rs as an independent adapter does: with a lockfile of its
# own, default features off, and the public API only. Its tests check consumer-contract
# scenario C0 in each configuration, which also builds with nvx-rs's minimum Rust version.
# own, default features off, and the public API only. Its tests check that each configuration
# builds that way, and each configuration also builds with nvx-rs's minimum Rust version.
- name: Check the runtime adapter's protocol-only configuration
shell: bash
working-directory: tests/runtime-adapter
Expand All @@ -168,7 +168,7 @@ runs:
cargo "+${{ steps.toolchains.outputs.toolchain }}" test --locked --features host
cargo "+${{ steps.toolchains.outputs.msrv }}" check --locked --all-targets --features host

# The guest half runs in Linux guests, as a static executable that a test checks.
# The guest agent runs in Linux guests, as a static executable that a test checks.
- name: Check the runtime adapter's guest-only configuration
if: runner.os == 'Linux'
shell: bash
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/run-nvx-microvm-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1180,10 +1180,10 @@ jobs:
--backend "${{ inputs.backend }}"
--output-dir "build/test-results/aci-edge-sandboxes-${{ inputs.backend }}"

# The hook of the runtime adapter's scenarios on this backend: the ignored
# tests of its host-only configuration, which later steps add and which
# read the backend through `nvx::Hypervisor::from_env_or_default`. There
# are none yet, so these steps only build the adapter.
# The runtime adapter's hypervisor scenarios on this backend: any ignored
# tests of its host-only configuration, which read the backend through
# `nvx::Hypervisor::from_env_or_default`. The step builds the adapter
# whether or not it declares any.
- name: Run runtime adapter scenarios on Linux
if: runner.os != 'Windows' && !inputs.debug-kernel
shell: bash
Expand Down
24 changes: 14 additions & 10 deletions doc/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,14 @@ lockfile of its own, default features off, and the public API only. For each
of its configurations, protocol-only, host-only, and, on Linux, guest-only for
`x86_64-unknown-linux-musl`, the action runs Clippy with warnings denied, the
build, the tests, and a check with the minimum Rust version of `nvx`, after
rustfmt. The tests check consumer-contract scenario C0: each configuration
enables only its side of `nvx`, the guest-only build pulls in neither Tokio
nor a dependency that only the host side declares, and the guest binary is a
static executable.
rustfmt. The tests check that each configuration builds as an independent
adapter's does: it enables only its side of `nvx`, the guest-only build pulls
in neither Tokio nor a dependency that only the host side declares, and the
guest binary is a static executable. In the guest-only configuration, they
also check that the adapter's guest agent serves the control session between
the host and the guest with the adapter's own framing, against a test broker
that sends it messages that span records, resets, disconnects, and malformed
records.

The development release job depends on `aci-edge-sandboxes` and `nvx-rs` and
requires their combined Linux/Windows results to be successful; failed,
Expand All @@ -680,12 +684,12 @@ it keeps the OpenVMM log under `build/test-results/aci-edge-sandboxes-<backend>`
uploaded with the other microVM logs. Changes under `aci_edge_sandboxes/` or `nvx-rs/`
therefore trigger the NVX microVM test matrix.

The same jobs, except the debug-kernel ones, run the runtime adapter's
scenarios on their backend: the ignored tests of its host-only configuration,
with `NVX_HYPERVISOR` naming the backend. Later steps of
[microsoft/nvx#535](https://github.com/microsoft/nvx/issues/535) add them; until
then, the step only builds the adapter. Changes under `tests/runtime-adapter/`
therefore trigger the NVX microVM test matrix too.
The same jobs, except the debug-kernel ones, build the runtime adapter's
host-only configuration on their backend and run any ignored tests that it
declares, its hypervisor scenarios, with `NVX_HYPERVISOR` naming the backend.
The adapter's other tests, including those of its guest agent, run in the
`nvx-rs` job. Changes under `tests/runtime-adapter/` therefore trigger the NVX
microVM test matrix too.

## Adversarial campaigns

Expand Down
4 changes: 2 additions & 2 deletions doc/contribute.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ runs `tests/guest_tools.rs` against the exact guest-profile binaries. Tests of t
mechanics, such as the golden fixtures of the control protocol, belong in
`nvx`, and tests of the ACI Edge Sandboxes contract in
`aci_edge_sandboxes`. Tests of what an independent adapter observes through
the public API of `nvx` alone, such as the consumer-contract scenarios of
[microsoft/nvx#535](https://github.com/microsoft/nvx/issues/535), belong in the
the public API of `nvx` alone, such as its own framing over the control
session between the host and the guest, belong in the
[runtime adapter](../tests/runtime-adapter/README.md), whose ignored tests the
microVM jobs run on each backend. The `scripts/nvx.py test-aci-edge-sandboxes --backend
BACKEND` command runs all ignored `openvmm_e2e` tests on a real hypervisor,
Expand Down
9 changes: 5 additions & 4 deletions doc/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ nvx/
| `-- test_*.py Python tooling unit tests
|-- tests/ Tests that build on the repository's crates from outside them
| `-- runtime-adapter/ Rust crate `runtime-adapter`, an independent adapter of nvx
| |-- src/ Its library, and its host and guest skeleton binaries
| `-- tests/ Consumer-contract scenario C0, the independent build
| |-- src/ Its framing, its host program, and its guest agent
| `-- tests/ Its independent build, and its guest agent against a test broker
|-- SECURITY.md Microsoft security vulnerability reporting policy
|-- .dockerignore Docker build-context exclusions
|-- .gitattributes Git path attributes
Expand Down Expand Up @@ -335,8 +335,9 @@ Tests that build on the repository's crates from outside them.
crate with its own `Cargo.lock` that depends on `nvx` with default features
off and uses its public API only. Its protocol-only, host-only, and guest-only
configurations build in CI, and its tests check that each one builds as an
independent adapter's does. See its
[README](../tests/runtime-adapter/README.md).
independent adapter's does. Its guest agent serves the control session between
the host and the guest with the adapter's own framing, which its tests check
against a test broker. See its [README](../tests/runtime-adapter/README.md).

## Root files

Expand Down
4 changes: 2 additions & 2 deletions tests/runtime-adapter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ publish = false
# No default features: each configuration selects one side of `nvx`, as an independent adapter
# does. Without features, the adapter builds against the protocol-only `nvx`.
default = []
# The host half: the host API of `nvx` and the `runtime-adapter-host` binary.
# The host program: the host API of `nvx` and the `runtime-adapter-host` binary.
host = ["nvx/host"]
# The guest half: the guest API of `nvx` and the `runtime-adapter-guest` binary, which builds as a
# The guest agent: the guest API of `nvx` and the `runtime-adapter-guest` binary, which builds as a
# static `x86_64-unknown-linux-musl` executable.
guest = ["nvx/guest"]

Expand Down
102 changes: 79 additions & 23 deletions tests/runtime-adapter/README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,78 @@
# Runtime adapter

The runtime adapter is an independent adapter of the NVX runtime. It lives in
this repository so that NVX's own CI builds and runs it, and it depends on
[`nvx`](../../nvx-rs/README.md) the way an adapter outside the repository
does:
The runtime adapter is an independent adapter of the NVX runtime, with a host
program and a guest agent. It lives in this repository so that NVX's own CI
builds and runs it, and it depends on [`nvx`](../../nvx-rs/README.md) the way
an adapter outside the repository does:

- as a path dependency with default features off;
- with a `Cargo.lock` of its own, outside any other workspace;
- through the public API alone, with unsafe code forbidden;
- with the minimum supported Rust version of `nvx`.

[microsoft/nvx#535](https://github.com/microsoft/nvx/issues/535) sets the
contract that it proves: an adapter brings its own application framing over
the NVXS session, its own init, and its own status collector. Today its
binaries are skeletons that report the contract that they were built against.
Later steps fill its guest half
([microsoft/nvx#619](https://github.com/microsoft/nvx/issues/619)) and its host
half ([microsoft/nvx#620](https://github.com/microsoft/nvx/issues/620)), and add
the scenarios that run them on a hypervisor.
The adapter brings its own application framing to the control session between
the host and the guest, in place of NVX's `NVXC` frames. Its guest agent serves
requests in that framing. Its host program reports the contract that it was
built against.

## Configurations

Each configuration enables one side of `nvx`:

| Configuration | Cargo options | `nvx` features | Binary |
| --- | --- | --- | --- |
| Protocol-only | none | none | none |
| Host-only | `--features host` | `host` | `runtime-adapter-host` |
| Guest-only | `--features guest --target x86_64-unknown-linux-musl` | `guest` | `runtime-adapter-guest`, a static executable |
| Configuration | Cargo options | `nvx` features | Modules | Binary |
| --- | --- | --- | --- | --- |
| Protocol-only | none | none | `frame` | none |
| Host-only | `--features host` | `host` | `frame` | `runtime-adapter-host` |
| Guest-only | `--features guest --target x86_64-unknown-linux-musl` | `guest` | `frame`, `guest` | `runtime-adapter-guest`, a static executable |

## Framing

The `frame` module is the adapter's application framing, which its host program
and guest agent exchange over the control session in place of NVX's `NVXC`
frames. A message is a 5-byte header, then a body. The header holds the body's
length, a little-endian `u32`, then the message's kind:

| Kind | Message | Body |
| --- | --- | --- |
| 1 | `Ping`: asks the guest to answer with `Pong` | none |
| 2 | `Pong`: the guest's answer to `Ping` | none |
| 3 | `Echo`: bytes that the guest sends back in an `Echo` of its own | at most 256 KiB, what four of the session's largest records carry |
| 4 | `Stop`: asks the guest to stop serving | none |
| 5 | `Stopped`: the guest's confirmation of `Stop`, its last message | none |

The session keeps no message boundaries, so a message may span several of its
records, and one record may end a message and start the next. `Decoder`
reassembles the messages. It checks each header before it takes any byte of
the body, and holds at most one partial message. A malformed message ends the
stream, because the framing cannot find the next message after it.

## Guest agent

`guest::serve` serves the host program on a stream to the guest's end of the
control session, through `nvx::guest::nvxs`. It attaches, then:

- acknowledges each reset of the session with the smallest receive window that
the session allows, once it dropped the earlier epoch's partial message,
because a new epoch starts with a new message;
- credits each payload back once it holds the payload, so that a message
longer than the window still arrives whole;
- answers each `Ping` with `Pong`, sends each `Echo` back, and answers `Stop`
with `Stopped`, then returns.

A failure of the session, a malformed message, and a message that only the
guest sends end serving with an error.

`runtime-adapter-guest serve` runs the guest agent on its standard input and
output, which connect to the guest's end of the session, such as a control
console in raw mode. It exits with 0 once the host stopped it or the broker
closed the stream, and with 1, after a diagnostic on standard error, once
serving failed. Without arguments, it reports the contract that it was built
against.

## Tests

`tests/independent_build.rs` checks consumer-contract scenario C0 in every
configuration:
`tests/independent_build.rs` checks that every configuration builds as an
independent adapter's does:

- The adapter is a workspace of its own with its own lockfile, and declares
the minimum supported Rust version of `nvx`.
Expand All @@ -46,6 +86,22 @@ configuration:
guest binary is a static executable: it has no program interpreter, needs no
shared library, and keeps its stack non-executable.

The unit tests of `frame` run in every configuration. They cover each kind's
encoding, messages in pieces of any size and split at every byte, and the
headers that the framing refuses before their body arrives.

`tests/guest_session.rs` checks, in the guest-only configuration, that the
guest agent serves the control session with the adapter's own framing. A test
broker plays the broker's end of the guest's session over a Unix socket pair
with the public record API of `nvx`, for `guest::serve` and for
`runtime-adapter-guest serve`. It covers messages that span records both ways
and fill the receive window, a record whose bytes arrive one at a time, resets
in the middle of a message, with the next epoch of the instance and with a new
instance, streams that end at and inside a record, and the records and
messages that the guest agent refuses: sequence, epoch, and instance errors,
stale and identity-less resets, record lengths, bootstrap identity, ERROR
payloads, and malformed headers.

Run the checks of each configuration in this directory. The guest-only
configuration needs Linux and the musl target, which
`rustup target add x86_64-unknown-linux-musl` installs:
Expand All @@ -69,7 +125,7 @@ supported Rust version, on Linux and Windows, where it skips the guest-only
configuration. A change to the adapter is a test input of CI's
[change classification](../../doc/ci.md#change-classification), so it runs the
NVX microVM tests. Their
[workflow](../../.github/workflows/run-nvx-microvm-tests.yml) runs the
adapter's hypervisor scenarios on each backend: its ignored tests in the
host-only configuration, with `NVX_HYPERVISOR` naming the backend, which
`nvx::Hypervisor::from_env_or_default` reads. There are none yet.
[workflow](../../.github/workflows/run-nvx-microvm-tests.yml) builds the
host-only configuration on each backend and runs any ignored tests that it
declares, the adapter's hypervisor scenarios, with `NVX_HYPERVISOR` naming the
backend, which `nvx::Hypervisor::from_env_or_default` reads.
74 changes: 65 additions & 9 deletions tests/runtime-adapter/src/bin/runtime-adapter-guest.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,76 @@
//! The guest half of the runtime adapter, a static `x86_64-unknown-linux-musl` executable. This
//! skeleton reports the contract that it was built against; later steps make it serve the guest
//! role of the NVXS session with the adapter's own framing.
//! The runtime adapter's guest agent, a static `x86_64-unknown-linux-musl` executable.
//!
//! - `runtime-adapter-guest` reports the contract that it was built against.
//! - `runtime-adapter-guest serve` serves the host program with the adapter's own framing on its
//! standard input and output, which connect to the guest's end of the control session between
//! the host and the guest, such as a control console in raw mode. It exits with 0 once the host
//! stopped it or the broker closed the stream, and with 1, after a diagnostic on standard error,
//! once serving failed.
//!
//! Anything else is a usage error, with status 2.

use std::process::ExitCode;

#[cfg(target_os = "linux")]
fn main() -> ExitCode {
if std::env::args_os().len() > 1 {
eprintln!("usage: runtime-adapter-guest");
return ExitCode::from(2);
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
match arguments.as_slice() {
[] => {
println!("runtime-adapter-guest: {}", runtime_adapter::contract());
ExitCode::SUCCESS
}
[command] if command == "serve" => serve(),
Comment thread
esaurez marked this conversation as resolved.
_ => {
eprintln!("usage: runtime-adapter-guest [serve]");
ExitCode::from(2)
}
}
println!("runtime-adapter-guest: {}", runtime_adapter::contract());
ExitCode::SUCCESS
}

// The guest half runs only in Linux guests. Elsewhere this stub fails, so that every target still
/// Serves the session on standard input and output.
#[cfg(target_os = "linux")]
fn serve() -> ExitCode {
let stdio = Stdio {
input: std::io::stdin().lock(),
output: std::io::stdout().lock(),
};
match runtime_adapter::guest::serve(stdio) {
Ok(_) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("runtime-adapter-guest: {error}");
ExitCode::FAILURE
}
}
}

/// Standard input and output as one stream, which carries nothing but the session's records.
#[cfg(target_os = "linux")]
struct Stdio {
/// Standard input, from the broker.
input: std::io::StdinLock<'static>,
/// Standard output, to the broker.
output: std::io::StdoutLock<'static>,
}

#[cfg(target_os = "linux")]
impl std::io::Read for Stdio {
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
self.input.read(buffer)
}
}

#[cfg(target_os = "linux")]
impl std::io::Write for Stdio {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.output.write(bytes)
}

fn flush(&mut self) -> std::io::Result<()> {
self.output.flush()
}
}

// The guest agent runs only in Linux guests. Elsewhere this stub fails, so that every target still
// builds with every feature.
#[cfg(not(target_os = "linux"))]
fn main() -> ExitCode {
Expand Down
5 changes: 2 additions & 3 deletions tests/runtime-adapter/src/bin/runtime-adapter-host.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
//! The host half of the runtime adapter. This skeleton reports the contract that it was built
//! against and whether `nvx` supports this host; later steps make it start, reattach to, and
//! stop sandboxes through the host API.
//! The runtime adapter's host program. It reports the contract that it was built against and
//! whether `nvx` supports this host.

use std::process::ExitCode;

Expand Down
Loading
Loading