From 33bb2916110b8ba72f713e861c0c6030a90872df Mon Sep 17 00:00:00 2001 From: Leander Kohler Date: Wed, 18 Mar 2026 13:36:33 +0100 Subject: [PATCH 1/3] vm-migration: add protocol versioning Add protocol-side support for migration protocol versioning. Use the existing 6-byte Start command header, which was previously zero padding, to carry the sender's migration protocol version without changing the wire layout. Store the version as a little-endian u16 in the first two bytes and ignore the remaining four bytes. A zeroed command header continues to mean a legacy v0 sender. This keeps the message flow unchanged for rollout: Start is still followed by plain OK or Error (Aborted), and no new command is needed. On-behalf-of: SAP leander.kohler@sap.com Signed-off-by: Leander Kohler --- vm-migration/src/protocol.rs | 91 +++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/vm-migration/src/protocol.rs b/vm-migration/src/protocol.rs index 012284f858..805e2e0697 100644 --- a/vm-migration/src/protocol.rs +++ b/vm-migration/src/protocol.rs @@ -73,6 +73,19 @@ //! Source->>Destination: Complete //! Destination-->>Source: OK //! ``` +//! +//! ## Protocol Versioning +//! +//! `Start` carries the sender's migration protocol version. +//! A zeroed version field is treated as legacy protocol `v0`. +//! +//! The destination validates that version and replies with a plain `OK` or +//! `Error`. +//! +//! Only the current and immediately previous protocol versions are +//! supported. Compatibility is one-way, from older protocol versions +//! to newer ones. +//! use std::io::{Read, Write}; @@ -124,11 +137,25 @@ pub enum Command { KeepAlive, } +pub type MigrationProtocolVersion = u16; + +pub const CURRENT_PROTOCOL_VERSION: MigrationProtocolVersion = 0; +pub const PRIOR_PROTOCOL_VERSION: MigrationProtocolVersion = + CURRENT_PROTOCOL_VERSION.saturating_sub(1); + +/// Encodes a sender protocol version into the `command_headers` bytes of a +/// `Start` request. +fn encode_sender_version(version: MigrationProtocolVersion) -> [u8; 6] { + let mut command_headers = [0; 6]; + command_headers[..2].copy_from_slice(&version.to_le_bytes()); + command_headers +} + #[repr(C)] #[derive(Default, Copy, Clone, Immutable, IntoBytes, KnownLayout, TryFromBytes)] pub struct Request { command: Command, - padding: [u8; 6], + command_headers: [u8; 6], length: u64, // Length of payload for command excluding the Request struct } @@ -145,6 +172,14 @@ impl Request { Self::new(Command::Start, 0) } + pub fn start_with_version() -> Self { + Self { + command: Command::Start, + command_headers: encode_sender_version(CURRENT_PROTOCOL_VERSION), + length: 0, + } + } + pub fn state(length: u64) -> Self { Self::new(Command::State, length) } @@ -181,6 +216,36 @@ impl Request { self.length } + pub fn command_headers(&self) -> &[u8; 6] { + &self.command_headers + } + + /// Returns the sender protocol version carried by a `Start` request. + /// + /// Only the first two bytes are used. The remaining bytes are ignored. + pub fn sender_protocol_version(&self) -> MigrationProtocolVersion { + u16::from_le_bytes([self.command_headers[0], self.command_headers[1]]) + } + + pub fn validate_start_protocol_version( + &self, + ) -> Result { + debug_assert_eq!( + self.command(), + Command::Start, + "validate_start_protocol_version() must only be called for Start requests", + ); + + let sender_version = self.sender_protocol_version(); + if sender_version != PRIOR_PROTOCOL_VERSION && sender_version != CURRENT_PROTOCOL_VERSION { + return Err(MigratableError::MigrateReceive(anyhow!( + "Migration protocol version {sender_version} doesn't match supported versions: {PRIOR_PROTOCOL_VERSION}, {CURRENT_PROTOCOL_VERSION}" + ))); + } + + Ok(sender_version) + } + pub fn read_from(fd: &mut dyn Read) -> Result { /// A byte buffer that matches `Self` in size and alignment to allow deserializing `Self` into. #[repr(C, align(8))] @@ -502,7 +567,29 @@ impl MemoryRangeTable { #[cfg(test)] mod unit_tests { - use crate::protocol::{MemoryRange, MemoryRangeTable}; + use crate::protocol::{Command, MemoryRange, MemoryRangeTable, Request}; + + #[test] + fn test_start_request_ignores_residual_command_headers_bytes() { + let request = Request { + command: Command::Start, + command_headers: [1, 0, 0xaa, 0xbb, 0xcc, 0xdd], + length: 0, + }; + + assert_eq!(request.sender_protocol_version(), 1); + } + + #[test] + fn test_validate_start_protocol_version_rejects_unsupported_version() { + let request = Request { + command: Command::Start, + command_headers: [2, 0, 0, 0, 0, 0], + length: 0, + }; + + request.validate_start_protocol_version().unwrap_err(); + } #[test] fn test_memory_range_table_from_dirty_ranges_iter() { From e2ef489c9b7b3e5ff48768adddae9ae53257c94a Mon Sep 17 00:00:00 2001 From: Leander Kohler Date: Wed, 18 Mar 2026 13:48:42 +0100 Subject: [PATCH 2/3] vmm: validate protocol version at start Validate the sender's migration protocol version when handling the initial Start request. Read the version from the Start command header, accept only the supported version window n-1..=n, and reject unsupported versions with Error. A rejected Start moves the receiver to the aborted state. This keeps compatibility one-way, from older protocol versions to newer ones, and leaves later version-based branching on the receiver side. Log the protocol version on both sender and receiver to make the active migration path visible. On-behalf-of: SAP leander.kohler@sap.com Signed-off-by: Leander Kohler --- vm-migration/src/protocol.rs | 4 ---- vmm/src/lib.rs | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/vm-migration/src/protocol.rs b/vm-migration/src/protocol.rs index 805e2e0697..d68a83b50a 100644 --- a/vm-migration/src/protocol.rs +++ b/vm-migration/src/protocol.rs @@ -169,10 +169,6 @@ impl Request { } pub fn start() -> Self { - Self::new(Command::Start, 0) - } - - pub fn start_with_version() -> Self { Self { command: Command::Start, command_headers: encode_sender_version(CURRENT_PROTOCOL_VERSION), diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 85c57d9190..b83018bcc2 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -2179,7 +2179,11 @@ impl Vmm { match state { Established => match req.command() { - Command::Start => Ok(Started), + Command::Start => { + let migration_protocol_version = req.validate_start_protocol_version()?; + info!("Using migration protocol v{migration_protocol_version} on the receiver"); + Ok(Started) + } _ => invalid_command(), }, Started => match req.command() { @@ -2769,6 +2773,7 @@ impl Vmm { &mut socket, MigratableError::MigrateSend(anyhow!("Error starting migration (got bad response)")), )?; + info!("Using migration protocol v{CURRENT_PROTOCOL_VERSION} on the sender"); return_if_cancelled_cb(&mut socket)?; From d22e787f2922c456efffa6591095a9b7bbd443a0 Mon Sep 17 00:00:00 2001 From: Leander Kohler Date: Wed, 18 Mar 2026 13:49:21 +0100 Subject: [PATCH 3/3] main: print supported protocol versions Print the supported vm-migration protocol version range in cloud-hypervisor --version as an extra line: vm-migration protocol versions v0-v1 This makes the currently supported compatibility window visible without having to inspect the migration code. On-behalf-of: SAP leander.kohler@sap.com Signed-off-by: Leander Kohler --- cloud-hypervisor/src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cloud-hypervisor/src/main.rs b/cloud-hypervisor/src/main.rs index dfac20138f..fed616aa66 100644 --- a/cloud-hypervisor/src/main.rs +++ b/cloud-hypervisor/src/main.rs @@ -881,6 +881,11 @@ fn main() { if cmd_arguments.get_flag("version") { println!("{} {}", env!("CARGO_BIN_NAME"), env!("BUILD_VERSION")); + println!( + "vm-migration protocol versions v{}-v{}", + vm_migration::protocol::PRIOR_PROTOCOL_VERSION, + vm_migration::protocol::CURRENT_PROTOCOL_VERSION + ); if cmd_arguments.get_count("v") != 0 { println!("Enabled features: {:?}", vmm::feature_list());