From 51bd9d24f367ea1a7add609ce92a027cdbab29e1 Mon Sep 17 00:00:00 2001 From: Leander Kohler Date: Wed, 18 Mar 2026 13:48:42 +0100 Subject: [PATCH] vmm, vm-migration: 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 | 37 +++++++++++++++++++++++++++++++++++- vmm/src/lib.rs | 7 ++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/vm-migration/src/protocol.rs b/vm-migration/src/protocol.rs index e3f899759..00ca79952 100644 --- a/vm-migration/src/protocol.rs +++ b/vm-migration/src/protocol.rs @@ -91,6 +91,7 @@ use std::mem::size_of; use std::ops::RangeInclusive; use std::{mem, slice}; +use anyhow::anyhow; use itertools::Itertools; use serde::{Deserialize, Serialize}; use vm_memory::ByteValued; @@ -226,6 +227,26 @@ impl Request { &self.command_headers } + /// Returns the sender protocol version from a `Start` request if it is supported. + pub fn sender_protocol_version(&self) -> Result { + assert_eq!( + self.command(), + Command::Start, + "sender_protocol_version() must only be called for Start requests", + ); + + // The protocol version is stored in the first two header bytes, the remaining bytes are ignored. + let sender_version = u16::from_le_bytes([self.command_headers[0], self.command_headers[1]]); + if !supported_protocol_versions().any(|version| version == sender_version) { + let supported_versions = supported_protocol_versions().join(", "); + return Err(MigratableError::MigrateReceive(anyhow!( + "Migration protocol version {sender_version} doesn't match supported versions: {supported_versions}" + ))); + } + + Ok(sender_version) + } + pub fn read_from(fd: &mut dyn Read) -> Result { let mut request = Request::default(); fd.read_exact(Self::as_mut_slice(&mut request)) @@ -522,7 +543,9 @@ impl MemoryRangeTable { #[cfg(test)] mod unit_tests { - use crate::protocol::{Command, MemoryRange, MemoryRangeTable, Request}; + use crate::protocol::{ + CURRENT_PROTOCOL_VERSION, Command, MemoryRange, MemoryRangeTable, Request, + }; #[test] fn test_start_request_ignores_residual_command_headers_bytes() { @@ -538,6 +561,18 @@ mod unit_tests { ); } + #[test] + fn test_sender_protocol_version_rejects_unsupported_version() { + let request = Request { + command: Command::Start, + command_headers: [255, 0, 0, 0, 0, 0], + length: 0, + }; + + const { assert!(CURRENT_PROTOCOL_VERSION < 255) }; + request.sender_protocol_version().unwrap_err(); + } + #[test] fn test_memory_range_table_from_dirty_ranges_iter() { let input = [0b1111_1110_1110, 0b1_0000]; diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 8d9746138..267c1ef34 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -937,7 +937,11 @@ impl Vmm { let state_name = state.variant_name(); match state { Established => match req.command() { - Command::Start => Ok(Started), + Command::Start => { + let migration_protocol_version = req.sender_protocol_version()?; + debug!("Using migration protocol {migration_protocol_version}"); + Ok(Started) + } c => invalid_command(state_name, c), }, Started => match req.command() { @@ -1420,6 +1424,7 @@ impl Vmm { Request::start(), MigratableError::MigrateSend(anyhow!("Error starting migration")), )?; + debug!("Using migration protocol {CURRENT_PROTOCOL_VERSION}"); // Send config let vm_config = vm.get_config();