Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/dev-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Build and Release Dev

on:
push:
branches: [dev]

permissions:
contents: write

concurrency:
group: dev-release
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Install pinned Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.89.0"

- name: Show toolchain
run: |
rustc --version --verbose
cargo --version --verbose

- name: Build cloud-hypervisor (release)
run: |
cargo build --release

- name: Generate checksum and build metadata
run: |
artifact=target/release/cloud-hypervisor
sha256sum "$artifact" > "${artifact}.sha256"
cat > target/release/build-info.json <<EOF
{
"ref": "${GITHUB_REF_NAME}",
"commit": "${GITHUB_SHA}",
"run_id": "${GITHUB_RUN_ID}",
"run_attempt": "${GITHUB_RUN_ATTEMPT}",
"workflow": "${GITHUB_WORKFLOW}",
"rustc": "$(rustc --version --verbose | tr '\n' '; ' | sed 's/; $//')",
"cargo": "$(cargo --version --verbose | tr '\n' '; ' | sed 's/; $//')"
}
EOF

- name: Upload binary as artifact
uses: actions/upload-artifact@v7
with:
name: cloud-hypervisor
path: |
target/release/cloud-hypervisor
target/release/cloud-hypervisor.sha256
target/release/build-info.json

- name: Create dev release
uses: softprops/action-gh-release@v2
with:
tag_name: dev
target_commitish: ${{ github.sha }}
name: Dev Build
body: |
Branch: `${{ github.ref_name }}`
Commit: `${{ github.sha }}`
Workflow Run: `${{ github.run_id }}`
files: |
target/release/cloud-hypervisor
target/release/cloud-hypervisor.sha256
target/release/build-info.json
prerelease: true
make_latest: false
overwrite_files: true
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Move dev tag to current commit
run: |
git tag -f dev "${GITHUB_SHA}"
git push origin refs/tags/dev --force
29 changes: 29 additions & 0 deletions docs/snapshot_restore.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,35 @@ Current constraints for `memory_restore_mode=ondemand`:
- `prefault=on` is not supported
- the snapshot memory ranges must be page-aligned

### Mmap restore

With `memory_restore_mode=mmap`, guest RAM is created by mapping the snapshot
memory file copy-on-write before any KVM memslot or device consumes the
mapping: nothing is copied up front, pages fault in from the page cache — so
many VMs restored from the same snapshot share it — and guest writes stay
private to each VM.

Current constraints for `memory_restore_mode=mmap`:

- Plain private guest RAM only. Anything else falls back to the eager copy
(logged): `shared=on` or hugepages (global or per-zone), zones with
`host_numa_node`, `reserve`, `mergeable` or hotplug fields (a purely static
`id`+`size` zone is fine), resizable RAM (`hotplug_size`, virtio-mem), KSM
(`mergeable=on`), `--pvmemcontrol`, device passthrough
(`--device`/`--user-device`), and snapshot ranges that are not page-aligned
single-region extents. `reserve=on` and THP are re-applied to the mapped
region.
- A snapshot memory file shorter than the saved ranges is rejected up front (it
would otherwise fault `SIGBUS` at run time).
- `prefault` is rejected.
- The snapshot memory file must remain on disk **and unchanged** for the
entire lifetime of the VM. This is stronger than `ondemand`: UFFD copies each
page into the original anonymous mapping and stops needing the file once every
page is populated, and a read error there is a controlled VM exit; the mmap
region stays file-backed forever, so truncating it delivers a synchronous
`SIGBUS` and any in-place edit corrupts the guest. The length check only
rejects a file that is already short at restore time.

## Restore a VM with new Net 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:
Expand Down
2 changes: 1 addition & 1 deletion vmm/src/api/openapi/cloud-hypervisor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1484,7 +1484,7 @@ components:

MemoryRestoreMode:
type: string
enum: [Copy, OnDemand]
enum: [Copy, OnDemand, Mmap]
default: Copy

RestoreConfig:
Expand Down
26 changes: 21 additions & 5 deletions vmm/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,8 @@ pub enum ValidationError {
/// Number of FDs passed during Restore are incorrect to the NetConfig
#[error("Number of Net FDs passed for '{0}' during Restore: {1}. Expected: {2}")]
RestoreNetFdCountMismatch(String, usize, usize),
/// Prefault cannot be combined with on-demand restore
#[error("'prefault' cannot be combined with 'memory_restore_mode=ondemand'")]
/// Prefault requires the eager-copy restore mode
#[error("'prefault' requires 'memory_restore_mode=copy'")]
InvalidRestorePrefaultWithOnDemand,
/// Path provided in landlock-rules doesn't exist
#[error("Path {0:?} provided in landlock-rules does not exist")]
Expand Down Expand Up @@ -2782,6 +2782,9 @@ pub enum MemoryRestoreMode {
Copy,
/// Restore lazily by faulting snapshot pages into guest RAM on demand.
OnDemand,
/// Restore by mapping the snapshot memory file copy-on-write, sharing the
/// page cache across VMs restored from the same snapshot.
Mmap,
}

#[derive(Debug, Error)]
Expand All @@ -2797,6 +2800,7 @@ impl FromStr for MemoryRestoreMode {
match s.to_lowercase().as_str() {
"copy" => Ok(Self::Copy),
"ondemand" => Ok(Self::OnDemand),
"mmap" => Ok(Self::Mmap),
_ => Err(MemoryRestoreModeParseError::InvalidValue(s.to_owned())),
}
}
Expand All @@ -2817,11 +2821,11 @@ pub struct RestoreConfig {

impl RestoreConfig {
pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
\nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
\nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand|mmap,\
net_fds=<list_of_net_ids_with_their_associated_fds>,resume=true|false\" \
\n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \
\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`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable, and `memory_restore_mode=mmap` maps the snapshot file copy-on-write (plain private RAM only; falls back to copy otherwise) \
\n`net_fds` is a list of net ids with new file descriptors. \
Only net devices backed by FDs directly are needed as input.\
\n `resume` controls whether the VM will be directly resumed after restore ";
Expand Down Expand Up @@ -2880,7 +2884,7 @@ impl RestoreConfig {
// corresponding 'RestoreNetConfig' with a matched 'id' and expected
// number of FDs.
pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
if self.memory_restore_mode == MemoryRestoreMode::OnDemand && self.prefault {
if self.memory_restore_mode != MemoryRestoreMode::Copy && self.prefault {
return Err(ValidationError::InvalidRestorePrefaultWithOnDemand);
}

Expand Down Expand Up @@ -5333,6 +5337,18 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
invalid_restore_mode.validate(&snapshot_vm_config),
Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
);

let invalid_mmap_prefault = RestoreConfig {
source_url: PathBuf::from("/path/to/snapshot"),
prefault: true,
memory_restore_mode: MemoryRestoreMode::Mmap,
net_fds: None,
resume: false,
};
assert_eq!(
invalid_mmap_prefault.validate(&snapshot_vm_config),
Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
);
}

fn platform_fixture() -> PlatformConfig {
Expand Down
Loading
Loading