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 <leander.kohler@cyberus-technology.de>
This commit is contained in:
Leander Kohler
2026-03-18 13:48:42 +01:00
committed by Rob Bradford
parent b27faaaa45
commit 51bd9d24f3
2 changed files with 42 additions and 2 deletions

View File

@@ -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<u16, MigratableError> {
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<Request, MigratableError> {
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];

View File

@@ -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();