Skip to content

consomme: answer guest UDP to host loopback from the gateway - #137

Merged
ppenna merged 2 commits into
mainfrom
agents/fix-issue-560-gateway-udp-replies
Oct 16, 2026
Merged

ppenna merged 2 commits into
mainfrom
agents/fix-issue-560-gateway-udp-replies

Conversation

@ppenna

@ppenna ppenna commented Oct 16, 2026 •

Copy link
Copy Markdown

Summary

The first commit fixes microsoft/nvx#560. The second fixes the root cause of microsoft/nvx#393, an intermittent abort that failed microsoft/nvx#583's first CI run.

consomme: answer guest UDP to host loopback from the gateway

Guest-initiated connected UDP to a host-loopback service timed out over IPv4 and IPv6 (microsoft/nvx#560). Consomme translates guest UDP to the gateway onto host loopback, but UdpConnection::poll_conn passed every datagram that a flow's host socket received through translate_remote_address, which presents host-local peers at virtual addresses. The host service's reply therefore came from 10.0.0.254 or fd00::a00:ff instead of the gateway that the guest addressed, 10.0.0.1 or fd00::a00:1 for 10.0.0.2/24. Unconnected guest sockets accepted it; connected ones, which accept datagrams only from the peer to which they are connected, dropped it. TCP was unaffected.

  • each UdpConnection remembers, by host address, the peers that the guest addresses at another address than the one that its datagrams reach, such as the gateway, and poll_conn answers their datagrams from the address that the guest addressed
  • a datagram to the host address itself forgets the peer; peers expire with the flow's UDP timeout unless datagrams refresh them, and each flow keeps at most 64 (MAX_TRANSLATED_PEERS), forgetting expired peers and then the oldest, so a guest cannot grow the table without bound
  • host-local peers that the guest did not address, such as the host clients of published listeners, keep their virtual addresses, and the egress policy's admission of replies is unchanged
  • tests: replies through the gateway from host services on IPv4 and IPv6 loopback, from one guest port that alternates between two services, and from another host peer at its virtual address (both regression tests fail without the fix, with the issue's addresses), plus the expiry and bound of the remembered peers; the hop limit of the frames that the tests build is now a named constant
  • the Guide describes the source of these replies

openvmm_core: stop the devices before tearing down the VM

On Linux/MSHV CI runners, ttrpc::test_ttrpc_interface intermittently failed with channel closed from TeardownVm, because OpenVMM aborted while its VM worker stopped after a guest power-off (microsoft/nvx#393). The runner kept the core dump of the latest abort (SIGABRT). Symbolized against a build of the same source and toolchain, it shows a SIGSEGV on a netvsp queue thread in IncomingRing::verify_interrupts_unmasked, reached from NetChannel::main_loop through ReadHalf::poll_ready, which trycopy's signal handler turned into an abort without a message.

  • after WorkerRpc::Stop, LoadedVm::run tore down the partition, removed the vmbus server, and dropped the VM without stopping its state units, so device workers on their own threads kept running while teardown invalidated the guest memory mapping
  • netvsp reads the control pages of its vmbus rings through LockedPages, which point into that mapping; VaMapper does not implement lock_gpns, so locked pages do not keep the mapping in place
  • LoadedVm::run now stops the state units first, as the restart path already does, so that the device workers stop before the partition and guest memory go away

Part of microsoft/nvx#560 and microsoft/nvx#393.

Revision

Validation

eae7da6f3

On Windows, at that commit:

  • cargo clippy --all-targets for consomme and net_consomme: no lints
  • cargo doc --no-deps with -D warnings for the same packages
  • cargo nextest run --profile agent for the same packages: 200 passed
  • cargo xtask fmt --fix: clean

On Linux (Azure Linux 3.0) with Rust 1.95.0, at that commit:

  • cargo clippy --all-targets for consomme and net_consomme: no lints
  • cargo nextest run --profile agent for the same packages: 213 passed, including the Unix-only UDP tests

On bare-metal Linux/KVM, Linux/MSHV, and Windows/WHP hosts, with OpenVMM built from this change before its test-only hop-limit refactor (so with the same production code), against the released build of c6d97c52 (NVX v0.1.0-dev.36fc8aedcb39), Alpine guest on 10.0.0.2/24 with host loopback allowed:

  • the issue's probe, connected and unconnected over IPv4 and IPv6: with c6d97c52, unconnected replies came from 10.0.0.254 and fd00::a00:ff and connected sockets timed out; with this change, all four replies came from the gateway
  • NVX's host-loopback-policy microVM scenario, extended with connected UDP exchanges through the gateway: fails with c6d97c52 and passes with this change on all three
  • with this change, virtio-net, directional-network-policy, private-network-ingress, ipv6-egress-policy, l3-l4-egress-policy, l3-l4-egress-protocols, l3-l4-egress-port-ranges, network-snapshot, and endpoint-policy-snapshot pass on all three

5ab8a8462

At the head, for openvmm_core, on Windows and on Linux (Azure Linux 3.0) with Rust 1.95.0: cargo check, cargo clippy --all-targets, cargo doc --no-deps, cargo nextest run --profile agent (106 passed), and cargo xtask fmt --fix: clean.

On bare-metal Linux/MSHV, with cargo xflowey vmm-tests-run --release --ci-profile from a tree identical to the head (tree a533cd46):

  • without this commit, the abort did not reproduce in 200 runs of ttrpc::test_ttrpc_interface in 8 concurrent loops; a temporary, environment-gated delay on the netvsp queue thread right after its Consomme endpoint's port request channel closes, where the CI runner's thread stalled, reproduces it (1/1), with the same faulting frame in the core
  • with this commit: that reproduction passes 3/3, the stop waiting for the delayed worker; ttrpc::test_ttrpc_interface passes 200/200 in 8 concurrent loops; and 32/32 of the ttrpc, Linux direct-boot, and x86_64_exclusive VMM tests pass

Not run for this commit: KVM, WHP, VMM tests that need VHD images or the x86_64-unknown-uefi target, and a natural reproduction on a virtual-machine runner; microsoft/nvx#583's CI runs the VMM tests on all three backends.

Part of microsoft/nvx#560.

Consomme translates guest UDP to the gateway onto host loopback, and
presents host-local peers to the guest at virtual addresses, such as
10.0.0.254 and fd00::a00:ff for 10.0.0.2/24, so that the guest replies
through its NIC. It applied that presentation to every datagram that a
guest flow's host socket received, including the replies of the host
service that the guest had addressed at the gateway. The reply to a
datagram that the guest sent to 10.0.0.1 therefore came from
10.0.0.254, and the reply to one sent to fd00::a00:1 from fd00::a00:ff.
An unconnected guest socket accepted the reply from the unexpected
address, but a connected one, which accepts datagrams only from the
address and port to which it is connected, dropped it, so connected UDP
to a host-loopback service timed out over IPv4 and IPv6.

Have each guest UDP flow remember, by host address, the peers that the
guest addresses at another address than the host address that its
datagrams reach, such as the gateway, and answer the datagrams of those
peers from the address that the guest addressed. A datagram that the
guest sends to the host address itself forgets the peer. A peer expires
with the flow's UDP timeout unless datagrams keep refreshing it, and
each flow remembers at most 64 peers, forgetting expired peers and then
the oldest, so a guest cannot grow the table without bound. Host-local
peers that the guest did not address, such as the host clients of
published listeners, still appear at virtual addresses, and the egress
policy's admission of replies is unchanged.

Cover with tests the replies of host services on IPv4 and IPv6 loopback
that the guest reaches at the gateway, from one guest port that
alternates between two services as an unconnected socket may, the
presentation of another host peer at a virtual address and the guest's
reply there, and the expiry and bound of the remembered peers. Name the
hop limit of the frames that the tests build, and update the Guide.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ppenna added a commit to microsoft/nvx that referenced this pull request Oct 16, 2026
Fixes #560.

Pin nanvix/openvmm#137 at eae7da6f314ec0eacb98da8091dbd557972757ce,
one commit on c6d97c52.

OpenVMM answered the guest's UDP to host-loopback services, which the
guest reaches at its gateway, from the addresses at which it presents
other host peers: 10.0.0.254 instead of 10.0.0.1, and fd00::a00:ff
instead of fd00::a00:1, for 10.0.0.2/24. Unconnected guest sockets
accepted those replies, but connected ones, which accept datagrams only
from the peer to which they are connected, dropped them, so connected
UDP to host loopback timed out over IPv4 and IPv6 on every backend.
Replies on the guest's UDP flows now come from the address and port
that the guest addressed, while host peers that the guest did not
address keep their virtual addresses.

Extend the allow phase of the host-loopback-policy microVM scenario:
under its egress default of deny, with rules that admit only these
datagrams, the guest's connected nc exchanges a datagram with host UDP
services on IPv4 and IPv6 loopback through its gateway. The phase fails
with the previous pin. Describe the source of these replies in the run
guide and the network design, and index the new pin in the roadmap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Part of microsoft/nvx#393.

When the VM worker stopped, LoadedVm::run tore down the partition,
removed the vmbus server, and dropped the VM without stopping its state
units, so device workers that run on their own threads kept running
while teardown invalidated the guest memory mapping. netvsp's queue
workers read the control pages of their vmbus rings through locked
pages, which point into that mapping, and the VA mapper does not
implement page locking, so locked pages do not keep the mapping in
place. A queue worker polled after the invalidation therefore faulted
outside trycopy, whose signal handler aborted the process without a
message. ttrpc::test_ttrpc_interface hit this intermittently on
Linux/MSHV runners after a guest power-off: a core dump from one shows
the fault in IncomingRing::verify_interrupts_unmasked on a netvsp queue
thread, tens of milliseconds after the Consomme endpoint of that queue
saw its port request channel close.

Stop the state units first, as the restart path already does, so that
the device workers stop before the partition and guest memory go away.
On bare-metal Linux/MSHV, delaying the queue worker after the port
request channel closes aborts OpenVMM without this change; with it,
the stop waits for the worker and the VM tears down cleanly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ppenna added a commit to microsoft/nvx that referenced this pull request Oct 16, 2026
Fixes #560.
Part of #393.

Pin nanvix/openvmm#137 at 5ab8a84621524f1777b2cb5e65a70dab42c8b453,
two commits on c6d97c52.

OpenVMM answered the guest's UDP to host-loopback services, which the
guest reaches at its gateway, from the addresses at which it presents
other host peers: 10.0.0.254 instead of 10.0.0.1, and fd00::a00:ff
instead of fd00::a00:1, for 10.0.0.2/24. Unconnected guest sockets
accepted those replies, but connected ones, which accept datagrams only
from the peer to which they are connected, dropped them, so connected
UDP to host loopback timed out over IPv4 and IPv6 on every backend.
Replies on the guest's UDP flows now come from the address and port
that the guest addressed, while host peers that the guest did not
address keep their virtual addresses.

OpenVMM also tore down the partition and guest memory without stopping
the VM's devices, so a netvsp queue worker that ran during teardown
could read its vmbus ring from guest memory that was already unmapped,
and OpenVMM aborted. ttrpc::test_ttrpc_interface failed this way on
Linux/MSHV, including in this change's first CI run. The second commit
stops the devices first.

Extend the allow phase of the host-loopback-policy microVM scenario:
under its egress default of deny, with rules that admit only these
datagrams, the guest's connected nc exchanges a datagram with host UDP
services on IPv4 and IPv6 loopback through its gateway. The phase fails
with the previous pin. Describe the source of these replies in the run
guide and the network design, and index both OpenVMM commits in the
roadmap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
ppenna marked this pull request as ready for review October 16, 2026 21:10
Copilot AI balanced review requested due to automatic review settings October 16, 2026 21:10
@ppenna
ppenna merged commit 5ab8a84 into main Oct 16, 2026
@ppenna
ppenna deleted the agents/fix-issue-560-gateway-udp-replies branch October 16, 2026 21:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

6 open issues
What changed in this PR

Fixes Consomme UDP source-address translation for guest-connected sockets targeting host loopback via the gateway, and hardens OpenVMM teardown ordering to prevent device threads from touching unmapped guest memory.

Changes:

  • Track per-flow “translated peers” so host-loopback UDP replies egress with the gateway address/port the guest addressed.
  • Add regression/unit tests for UDP gateway/loopback behavior (IPv4/IPv6), translated-peer expiry, and bounding.
  • Pause/stop device workers earlier during VM teardown to avoid teardown races and crashes.
File Description
vm/​devices/​net/​net_consomme/​consomme/​src/​udp.rs Adds translated-peer tracking and uses it when emitting UDP replies to the guest.
vm/​devices/​net/​net_consomme/​consomme/​src/​udp/​tests/​translated_peers.rs New unit tests covering translated-peer update/expiry/bounding behavior.
vm/​devices/​net/​net_consomme/​consomme/​src/​tests.rs Adds UDP frame build/parse helpers and an end-to-end UDP gateway reply test harness.
vm/​devices/​net/​net_consomme/​consomme/​src/​tests/​static_ipv4.rs Adds IPv4 regression test for gateway UDP replies coming from the gateway address.
vm/​devices/​net/​net_consomme/​consomme/​src/​tests/​static_ipv6.rs Adds IPv6 regression test for gateway UDP replies coming from the gateway address.
openvmm/​openvmm_core/​src/​worker/​dispatch.rs Pauses VM/device work prior to tearing down partition/guest memory.
Guide/​src/​reference/​backends/​consomme.md Documents gateway-sourced replies for connected UDP flows.

Review effort: Lite

Comment on lines +4540 to +4544
// Stop the devices before tearing down the partition and guest
// memory. Device workers run on their own threads and access guest
// memory through direct mappings (e.g., vmbus ring control pages), so
// a worker that is still running can touch memory that teardown has
// already unmapped, which aborts the process.
Comment on lines +610 to +611
let udp_len = UDP_HEADER_LEN + payload.len();
let mut frame = vec![0u8; ETHERNET_HEADER_LEN + ip_header_len + udp_len];
Comment on lines +643 to +648
let mut udp = UdpPacket::new_unchecked(udp_buffer);
udp.set_src_port(src.port());
udp.set_dst_port(dst.port());
udp.set_len(udp_len as u16);
udp.payload_mut().copy_from_slice(payload);
udp.fill_checksum(&src.ip().into(), &dst.ip().into());
Comment on lines +721 to +728
consomme
.access(client)
.send(&frame, &ChecksumState::NONE)
.unwrap();
let mut request = [0; 16];
let (len, flow) = host.recv_from(&mut request).unwrap();
assert_eq!(&request[..len], b"guest");
host.send_to(b"host:guest", flow).unwrap();
Comment on lines +313 to +319
let host = unscoped(host);
if address == host {
if !self.translated_peers.is_empty() {
self.translated_peers.remove(&host);
}
return;
}
Comment on lines +338 to +339
self.translated_peers
.insert(host, TranslatedPeer { address, seen: now });
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.

microVM: connected UDP to host loopback times out over IPv4 and IPv6

2 participants