vmm: config: add memory_restore_mode to RestoreConfig

Add a MemoryRestoreMode enum (Copy | OnDemand) to RestoreConfig so
the restore path can be selected at restore time. Copy preserves the
existing eager read-copy behavior. OnDemand enables userfaultfd-based
demand paging and fails restore if the kernel does not support it.

Validate that prefault=on is not combined with OnDemand mode.

Update the OpenAPI spec with the new enum field.

Signed-off-by: Shayon Mukherjee <shayonj@gmail.com>
This commit is contained in:
Shayon Mukherjee
2026-03-13 05:47:16 -07:00
committed by Rob Bradford
parent 8340307ace
commit bf85af907e
3 changed files with 121 additions and 3 deletions

View File

@@ -93,6 +93,28 @@ start using it.
At this point, the VM is fully restored and is identical to the VM which was At this point, the VM is fully restored and is identical to the VM which was
snapshot earlier. snapshot earlier.
Restore also supports selecting how guest memory is populated:
```bash
./cloud-hypervisor \
--api-socket /tmp/cloud-hypervisor.sock \
--restore source_url=file:///home/foo/snapshot,memory_restore_mode=ondemand
```
If `memory_restore_mode` is omitted, Cloud Hypervisor uses the eager-copy
restore path (`copy`).
With `memory_restore_mode=ondemand`, restore uses `userfaultfd` to fault snapshot
pages in on first access instead of copying the full `memory-ranges` file into
guest RAM before restore completes. This mode is strict: if Cloud Hypervisor
cannot enable the `userfaultfd` restore path, restore fails instead of falling
back to `copy`.
Current constraints for `memory_restore_mode=ondemand`:
- `prefault=on` is not supported
- the snapshot memory ranges must be page-aligned
## Restore a VM with new Net FDs ## Restore a VM with new Net FDs
For a VM created with FDs explicitly passed to NetConfig, a set of valid FDs For a VM created with FDs explicitly passed to NetConfig, a set of valid FDs
need to be provided along with the VM restore command in the following syntax: need to be provided along with the VM restore command in the following syntax:

View File

@@ -1342,6 +1342,11 @@ components:
destination_url: destination_url:
type: string type: string
MemoryRestoreMode:
type: string
enum: [Copy, OnDemand]
default: Copy
RestoreConfig: RestoreConfig:
required: required:
- source_url - source_url
@@ -1351,6 +1356,8 @@ components:
type: string type: string
prefault: prefault:
type: boolean type: boolean
memory_restore_mode:
$ref: "#/components/schemas/MemoryRestoreMode"
ReceiveMigrationData: ReceiveMigrationData:
required: required:

View File

@@ -356,6 +356,9 @@ pub enum ValidationError {
/// Number of FDs passed during Restore are incorrect to the NetConfig /// Number of FDs passed during Restore are incorrect to the NetConfig
#[error("Number of Net FDs passed for '{0}' during Restore: {1}. Expected: {2}")] #[error("Number of Net FDs passed for '{0}' during Restore: {1}. Expected: {2}")]
RestoreNetFdCountMismatch(String, usize, usize), RestoreNetFdCountMismatch(String, usize, usize),
/// Prefault cannot be combined with on-demand restore
#[error("'prefault' cannot be combined with 'memory_restore_mode=ondemand'")]
InvalidRestorePrefaultWithOnDemand,
/// Path provided in landlock-rules doesn't exist /// Path provided in landlock-rules doesn't exist
#[error("Path {0:?} provided in landlock-rules does not exist")] #[error("Path {0:?} provided in landlock-rules does not exist")]
LandlockPathDoesNotExist(PathBuf), LandlockPathDoesNotExist(PathBuf),
@@ -2564,27 +2567,61 @@ where
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub enum MemoryRestoreMode {
/// Restore by eagerly copying the snapshot into guest RAM before resume.
#[default]
Copy,
/// Restore lazily by faulting snapshot pages into guest RAM on demand.
OnDemand,
}
#[derive(Debug, Error)]
pub enum MemoryRestoreModeParseError {
#[error("Invalid value: {0}")]
InvalidValue(String),
}
impl FromStr for MemoryRestoreMode {
type Err = MemoryRestoreModeParseError;
fn from_str(s: &str) -> result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"copy" => Ok(Self::Copy),
"ondemand" => Ok(Self::OnDemand),
_ => Err(MemoryRestoreModeParseError::InvalidValue(s.to_owned())),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoreConfig { pub struct RestoreConfig {
pub source_url: PathBuf, pub source_url: PathBuf,
#[serde(default)] #[serde(default)]
pub prefault: bool, pub prefault: bool,
#[serde(default)] #[serde(default)]
pub memory_restore_mode: MemoryRestoreMode,
#[serde(default)]
pub net_fds: Option<Vec<RestoredNetConfig>>, pub net_fds: Option<Vec<RestoredNetConfig>>,
} }
impl RestoreConfig { impl RestoreConfig {
pub const SYNTAX: &'static str = "Restore from a VM snapshot. \ pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
\nRestore parameters \"source_url=<source_url>,prefault=on|off,\ \nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
net_fds=<list_of_net_ids_with_their_associated_fds>\" \ net_fds=<list_of_net_ids_with_their_associated_fds>\" \
\n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \ \n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \
\n`prefault` brings memory pages in when enabled (disabled by default) \ \n`prefault` controls eager prefaulting for the copy-based restore path (disabled by default) \
\n`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, while `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable \
\n`net_fds` is a list of net ids with new file descriptors. \ \n`net_fds` is a list of net ids with new file descriptors. \
Only net devices backed by FDs directly are needed as input."; Only net devices backed by FDs directly are needed as input.";
pub fn parse(restore: &str) -> Result<Self> { pub fn parse(restore: &str) -> Result<Self> {
let mut parser = OptionParser::new(); let mut parser = OptionParser::new();
parser.add("source_url").add("prefault").add("net_fds"); parser
.add("source_url")
.add("prefault")
.add("memory_restore_mode")
.add("net_fds");
parser.parse(restore).map_err(Error::ParseRestore)?; parser.parse(restore).map_err(Error::ParseRestore)?;
let source_url = parser let source_url = parser
@@ -2596,6 +2633,10 @@ impl RestoreConfig {
.map_err(Error::ParseRestore)? .map_err(Error::ParseRestore)?
.unwrap_or(Toggle(false)) .unwrap_or(Toggle(false))
.0; .0;
let memory_restore_mode = parser
.convert::<MemoryRestoreMode>("memory_restore_mode")
.map_err(Error::ParseRestore)?
.unwrap_or_default();
let net_fds = parser let net_fds = parser
.convert::<Tuple<String, Vec<u64>>>("net_fds") .convert::<Tuple<String, Vec<u64>>>("net_fds")
.map_err(Error::ParseRestore)? .map_err(Error::ParseRestore)?
@@ -2612,6 +2653,7 @@ impl RestoreConfig {
Ok(RestoreConfig { Ok(RestoreConfig {
source_url, source_url,
prefault, prefault,
memory_restore_mode,
net_fds, net_fds,
}) })
} }
@@ -2620,6 +2662,10 @@ impl RestoreConfig {
// corresponding 'RestoreNetConfig' with a matched 'id' and expected // corresponding 'RestoreNetConfig' with a matched 'id' and expected
// number of FDs. // number of FDs.
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
if self.memory_restore_mode == MemoryRestoreMode::OnDemand && self.prefault {
return Err(ValidationError::InvalidRestorePrefaultWithOnDemand);
}
let mut restored_net_with_fds = HashMap::new(); let mut restored_net_with_fds = HashMap::new();
for n in self.net_fds.iter().flatten() { for n in self.net_fds.iter().flatten() {
assert_eq!( assert_eq!(
@@ -4498,6 +4544,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
RestoreConfig { RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"), source_url: PathBuf::from("/path/to/snapshot"),
prefault: false, prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None, net_fds: None,
} }
); );
@@ -4508,6 +4555,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
RestoreConfig { RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"), source_url: PathBuf::from("/path/to/snapshot"),
prefault: false, prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: Some(vec![ net_fds: Some(vec![
RestoredNetConfig { RestoredNetConfig {
id: "net0".to_string(), id: "net0".to_string(),
@@ -4522,11 +4570,39 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
]), ]),
} }
); );
assert_eq!(
RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=ondemand")?,
RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: false,
memory_restore_mode: MemoryRestoreMode::OnDemand,
net_fds: None,
}
);
// Parsing should fail as source_url is a required field // Parsing should fail as source_url is a required field
RestoreConfig::parse("prefault=off").unwrap_err(); RestoreConfig::parse("prefault=off").unwrap_err();
RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=bogus").unwrap_err();
Ok(()) Ok(())
} }
#[test]
fn test_restore_config_serde() {
assert_eq!(
serde_json::from_str::<RestoreConfig>(r#"{"source_url":"/path/to/snapshot"}"#)
.unwrap()
.memory_restore_mode,
MemoryRestoreMode::Copy
);
assert_eq!(
serde_json::from_str::<RestoreConfig>(
r#"{"source_url":"/path/to/snapshot","memory_restore_mode":"OnDemand"}"#
)
.unwrap()
.memory_restore_mode,
MemoryRestoreMode::OnDemand
);
}
#[test] #[test]
fn test_restore_config_validation() { fn test_restore_config_validation() {
// interested in only VmConfig.net, so set rest to default values // interested in only VmConfig.net, so set rest to default values
@@ -4589,6 +4665,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
let valid_config = RestoreConfig { let valid_config = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"), source_url: PathBuf::from("/path/to/snapshot"),
prefault: false, prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: Some(vec![ net_fds: Some(vec![
RestoredNetConfig { RestoredNetConfig {
id: "net0".to_string(), id: "net0".to_string(),
@@ -4663,6 +4740,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
let another_valid_config = RestoreConfig { let another_valid_config = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"), source_url: PathBuf::from("/path/to/snapshot"),
prefault: false, prefault: false,
memory_restore_mode: MemoryRestoreMode::Copy,
net_fds: None, net_fds: None,
}; };
snapshot_vm_config.net = Some(vec![NetConfig { snapshot_vm_config.net = Some(vec![NetConfig {
@@ -4671,6 +4749,17 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
..net_fixture() ..net_fixture()
}]); }]);
another_valid_config.validate(&snapshot_vm_config).unwrap(); another_valid_config.validate(&snapshot_vm_config).unwrap();
let invalid_restore_mode = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: true,
memory_restore_mode: MemoryRestoreMode::OnDemand,
net_fds: None,
};
assert_eq!(
invalid_restore_mode.validate(&snapshot_vm_config),
Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
);
} }
fn platform_fixture() -> PlatformConfig { fn platform_fixture() -> PlatformConfig {