Skip to content
Open
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
3 changes: 2 additions & 1 deletion hypervisor/vm-migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ pub enum SnapshotType {
/// Full snapshot - saves complete VM memory
#[default]
Full,
/// Incremental snapshot - only saves CoW anonymous pages via pagemap + kpageflags
/// Incremental snapshot - only saves CoW anonymous pages (pagemap bit 61
/// on Linux 6.6.44+ / 6.11+ / 7+; `/proc/kpageflags` on older kernels)
Incremental,
/// Soft-dirty snapshot - only saves pages written since the previous
/// soft-dirty snapshot (true delta), via /proc/self/clear_refs +
Expand Down
221 changes: 221 additions & 0 deletions hypervisor/vmm/src/kernel_release.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Copyright © 2026 Tencent Corporation
//
// SPDX-License-Identifier: Apache-2.0

//! Host kernel release (`uname -r`) and the pagemap bit-61 version gate.
//!
//! File-PMD `PM_FILE` reporting was fixed in upstream `3f9f022` (6.6.44 on
//! the 6.6 stable line, about 6.11 on mainline). A naive
//! `(major, minor, patch) >= (6, 6, 44)` compare would treat 6.7–6.10 as
//! new enough; those trees still omit `PM_FILE` on file PMDs.

use log::info;
use once_cell::sync::Lazy;
use std::fmt;

/// Parsed leading `major.minor.patch` from a `uname -r` string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct KernelRelease {
pub major: u32,
pub minor: u32,
pub patch: u32,
}

impl KernelRelease {
/// Parse leading `major.minor[.patch]` from a `uname -r` string.
///
/// Distro suffixes are ignored (`6.6.44-1.el9` → `6.6.44`). A missing
/// patch is `0` (`6.6` → `6.6.0`). Returns `None` when the string does
/// not start with at least `major.minor`.
pub(crate) fn parse(release: &str) -> Option<Self> {
let mut nums = [0u32; 3];
let mut count = 0usize;
let bytes = release.as_bytes();
let mut i = 0usize;
while count < 3 {
if i >= bytes.len() || !bytes[i].is_ascii_digit() {
break;
}
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
nums[count] = release[start..i].parse().ok()?;
count += 1;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
continue;
}
break;
}
(count >= 2).then_some(Self {
major: nums[0],
minor: nums[1],
patch: nums[2],
})
}

/// Host kernel release string (`uname -r`).
///
/// Prefers `/proc/sys/kernel/osrelease` so the gate does not depend on the
/// `libc` `utsname` layout. Falls back to `uname(2)`. Empty if both fail.
pub(crate) fn uname_string() -> String {
if let Ok(s) = std::fs::read_to_string("/proc/sys/kernel/osrelease") {
let s = s.trim();
if !s.is_empty() {
return s.to_string();
}
}
let mut uts = std::mem::MaybeUninit::<libc::utsname>::uninit();
// SAFETY: `uname` writes a complete `utsname` on success.
if unsafe { libc::uname(uts.as_mut_ptr()) } != 0 {
return String::new();
}
// SAFETY: `uname` succeeded, so `uts` is initialized and `release` is
// a NUL-terminated kernel string.
let uts = unsafe { uts.assume_init() };
let release = unsafe { std::ffi::CStr::from_ptr(uts.release.as_ptr()) };
release.to_string_lossy().into_owned()
}

/// Whether this kernel is known to set `PM_FILE` on file PMDs.
pub(crate) fn supports_pm_file_pmd(self) -> bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KernelRelease::parse takes only the leading major.minor.patch, so a string like 6.6.44-rc1 is treated as final 6.6.44, and any vendor kernel that reports ≥ 6.6.44 / ≥ 6.11 without carrying the PM_FILE backport is also gated to bit 61. Both directions are safe — a wrongly-enabled kernel only over-saves file huge pages (valid but larger snapshot), a wrongly-disabled one falls back to kpageflags — but the PR's "writes the same amount of data (within 1%)" claim assumes the gate is accurate. A one-line note that the gate means "≥ this version, assuming the vendor ships the upstream stable backport" would help future readers.

match (self.major, self.minor, self.patch) {
(6, 6, patch) => patch >= 44,
(6, minor, _) => minor >= 11,
(major, _, _) => major >= 7,
}
}
}

impl fmt::Display for KernelRelease {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}

/// Cached scan-path decision for `pagemap_anon`.
///
/// Bit 61 (`PM_FILE`) is only used when the kernel is known to set it on
/// file PMDs. Everything else, including an unparseable release, uses
/// `kpageflags`.
pub(crate) struct PagemapScanPath {
use_bit61: bool,
}

impl PagemapScanPath {
fn detect() -> Self {
let release = KernelRelease::uname_string();
let use_bit61 =
KernelRelease::parse(&release).is_some_and(KernelRelease::supports_pm_file_pmd);
info!(
"pagemap_anon: kernel={} path={}",
if release.is_empty() {
"unknown"
} else {
release.as_str()
},
if use_bit61 { "bit61" } else { "kpageflags" }
);
Self { use_bit61 }
}

/// Host decision, probed once from `uname`.
pub(crate) fn cached() -> &'static Self {
static PATH: Lazy<PagemapScanPath> = Lazy::new(PagemapScanPath::detect);
&PATH
}

pub(crate) fn use_bit61(&self) -> bool {
self.use_bit61
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_uname_release_strings() {
assert_eq!(
KernelRelease::parse("6.6.44"),
Some(KernelRelease {
major: 6,
minor: 6,
patch: 44
})
);
assert_eq!(
KernelRelease::parse("6.6.44-1.el9"),
Some(KernelRelease {
major: 6,
minor: 6,
patch: 44
})
);
assert_eq!(
KernelRelease::parse("6.6"),
Some(KernelRelease {
major: 6,
minor: 6,
patch: 0
})
);
assert_eq!(
KernelRelease::parse("7.0.0-28-generic"),
Some(KernelRelease {
major: 7,
minor: 0,
patch: 0
})
);
assert_eq!(
KernelRelease::parse("5.15.0-91-generic"),
Some(KernelRelease {
major: 5,
minor: 15,
patch: 0
})
);
assert_eq!(
KernelRelease::parse("6.6.69-opencloudos9.cubesandbox.pvm.host-gb85200d80fa2"),
Some(KernelRelease {
major: 6,
minor: 6,
patch: 69
})
);
assert_eq!(KernelRelease::parse(""), None);
assert_eq!(KernelRelease::parse("abc"), None);
assert_eq!(KernelRelease::parse("6"), None);
assert_eq!(KernelRelease::parse("linux-6.6.44"), None);
}

#[test]
fn pm_file_pmd_gate() {
let cases = [
("6.6.43", false),
("6.6.44", true),
("6.6.45", true),
(
"6.6.69-opencloudos9.cubesandbox.pvm.host-gb85200d80fa2",
true,
),
("6.6.44-1.el9", true),
("6.6", false),
("6.7.0", false),
("6.10.0", false),
("6.11.0", true),
("6.12.1", true),
("7.0.0-28-generic", true),
("5.15.0", false),
("", false),
("not-a-version", false),
];
for (release, expected) in cases {
let got =
KernelRelease::parse(release).is_some_and(KernelRelease::supports_pm_file_pmd);
assert_eq!(got, expected, "release={release}");
}
}
}
1 change: 1 addition & 0 deletions hypervisor/vmm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub mod device_tree;
#[cfg(feature = "guest_debug")]
mod gdb;
pub mod interrupt;
mod kernel_release;
pub mod memory_manager;
pub mod migration;
pub mod pagemap_anon;
Expand Down
3 changes: 2 additions & 1 deletion hypervisor/vmm/src/memory_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2396,7 +2396,8 @@ impl MemoryManager {

let guest_memory = self.guest_memory.memory();

// Use pagemap + kpageflags to filter memory ranges, keeping only anonymous pages (CoW)
// Classify CoW anonymous pages: pagemap bit 61 on kernels that set
// PM_FILE on file PMDs, otherwise /proc/kpageflags.
let (filtered_ranges, stats) =
filter_memory_ranges_by_pagemap_anon(&guest_memory, &self.snapshot_memory_ranges)
.map_err(|e| {
Expand Down
Loading
Loading