From 57ff6cf11f445d885591607075faa677d3b1339b Mon Sep 17 00:00:00 2001 From: Jonas Savulionis Date: Sun, 7 Jun 2026 16:37:03 +0300 Subject: [PATCH 1/2] vmm: add opt-in KSM mergeable memory Allow operators to mark anonymous guest memory as mergeable with Linux Kernel Samepage Merging through the machine-config API. The option is disabled by default and rejected together with hugetlbfs- backed memory. Signed-off-by: Jonas Savulionis --- CHANGELOG.md | 3 + docs/device-api.md | 2 + docs/hugepages.md | 4 ++ docs/prod-host-setup.md | 5 ++ .../request/machine_configuration.rs | 13 ++++- src/firecracker/swagger/firecracker.yaml | 11 +++- .../src/devices/virtio/block/virtio/io/mod.rs | 1 + src/vmm/src/devices/virtio/mem/device.rs | 1 + src/vmm/src/devices/virtio/vhost_user.rs | 1 + src/vmm/src/persist.rs | 4 +- src/vmm/src/resources.rs | 15 +++++ src/vmm/src/test_utils/mod.rs | 4 +- src/vmm/src/vmm_config/machine_config.rs | 34 +++++++++++- src/vmm/src/vstate/memory.rs | 55 +++++++++++++++---- tests/framework/vm_config.json | 3 +- tests/framework/vm_config_network.json | 3 +- tests/framework/vm_config_with_mmdsv1.json | 3 +- tests/framework/vm_config_with_mmdsv2.json | 3 +- .../integration_tests/functional/test_api.py | 2 + 19 files changed, 146 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1be039674..228b2e3ec87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to ### Added +- Added `ksm_mergeable` to `/machine-config` to let operators explicitly mark + anonymous guest memory as mergeable with Linux Kernel Samepage Merging. + ### Changed ### Deprecated diff --git a/docs/device-api.md b/docs/device-api.md index f638cc889f3..695ab50a4e8 100644 --- a/docs/device-api.md +++ b/docs/device-api.md @@ -75,6 +75,7 @@ specification: | | mem_size_mib | O | O | O | O | O | O | O | O | O | | | track_dirty_pages | O | O | O | O | O | O | O | O | O | | | vcpu_count | O | O | O | O | O | O | O | O | O | +| | ksm_mergeable | O | O | O | O | O | O | O | O | O | | `Metrics` | metrics_path | O | O | O | O | O | O | O | O | O | | `MmdsConfig` | network_interfaces | O | O | O | O | **R** | O | O | O | O | | | version | O | O | O | O | **R** | O | O | O | O | @@ -142,6 +143,7 @@ specification: | | mem_size_mib | O | O | O | O | O | O | O | | | track_dirty_pages | O | O | O | O | O | O | O | | | vcpu_count | O | O | O | O | O | O | O | +| | ksm_mergeable | O | O | O | O | O | O | O | | | vmm_version | O | O | O | O | O | O | O | | `MemoryHotplugStatus ` | total_size_mib | O | O | O | O | O | O | **R** | | | slot_size_mib | O | O | O | O | O | O | **R** | diff --git a/docs/hugepages.md b/docs/hugepages.md index a1c1a761953..2bd9e88bce0 100644 --- a/docs/hugepages.md +++ b/docs/hugepages.md @@ -38,6 +38,10 @@ benefits of using huge pages. This is because KVM will unconditionally establish guest page tables at 4K granularity if dirty page tracking is enabled, even if the host uses huge mappings. +KSM mergeable memory cannot be enabled with hugetlbfs-backed guest memory. +Requests that set both `huge_pages` to `2M` and `ksm_mergeable` to `true` are +rejected. + The traditional balloon device reports free pages at 4k granularity, this means the device is unable to reclaim the hugepage backing of the guest and drop RSS. However, the balloon can still be inflated and used to restrict memory usage in diff --git a/docs/prod-host-setup.md b/docs/prod-host-setup.md index 8939b56a965..69e2f4147f3 100644 --- a/docs/prod-host-setup.md +++ b/docs/prod-host-setup.md @@ -340,6 +340,11 @@ to mitigate [side channel issues](https://eprint.iacr.org/2013/448.pdf) that rely on page deduplication for revealing what memory pages are accessed by another process. +Firecracker does not mark guest memory as mergeable by default. Operators that +explicitly enable `ksm_mergeable` in `/machine-config` should only do so for +deployments where the page-deduplication side channel risk is acceptable, for +example when tenant separation is not required. + ##### Use memory with Rowhammer mitigation support Rowhammer is a memory side-channel issue that can lead to unauthorized cross- diff --git a/src/firecracker/src/api_server/request/machine_configuration.rs b/src/firecracker/src/api_server/request/machine_configuration.rs index 2e8addffb74..f1797ab14f9 100644 --- a/src/firecracker/src/api_server/request/machine_configuration.rs +++ b/src/firecracker/src/api_server/request/machine_configuration.rs @@ -123,6 +123,7 @@ mod tests { cpu_template: None, track_dirty_pages: Some(false), huge_pages: Some(expected), + ksm_mergeable: Some(false), #[cfg(feature = "gdb")] gdb_socket_path: None, }; @@ -144,6 +145,7 @@ mod tests { cpu_template: Some(StaticCpuTemplate::None), track_dirty_pages: Some(false), huge_pages: Some(HugePageConfig::None), + ksm_mergeable: Some(false), #[cfg(feature = "gdb")] gdb_socket_path: None, }; @@ -165,6 +167,7 @@ mod tests { cpu_template: None, track_dirty_pages: Some(true), huge_pages: Some(HugePageConfig::None), + ksm_mergeable: Some(false), #[cfg(feature = "gdb")] gdb_socket_path: None, }; @@ -190,6 +193,7 @@ mod tests { cpu_template: Some(StaticCpuTemplate::T2), track_dirty_pages: Some(true), huge_pages: Some(HugePageConfig::None), + ksm_mergeable: Some(false), #[cfg(feature = "gdb")] gdb_socket_path: None, }; @@ -208,7 +212,8 @@ mod tests { "vcpu_count": 8, "mem_size_mib": 1024, "smt": true, - "track_dirty_pages": true + "track_dirty_pages": true, + "ksm_mergeable": true }"#; let expected_config = MachineConfigUpdate { vcpu_count: Some(8), @@ -217,6 +222,7 @@ mod tests { cpu_template: None, track_dirty_pages: Some(true), huge_pages: Some(HugePageConfig::None), + ksm_mergeable: Some(true), #[cfg(feature = "gdb")] gdb_socket_path: None, }; @@ -245,6 +251,11 @@ mod tests { }"#; parse_patch_machine_config(&Body::new(body)).unwrap(); + let body = r#"{ + "ksm_mergeable": true + }"#; + parse_patch_machine_config(&Body::new(body)).unwrap(); + // On aarch64, CPU template is also not patch compatible. let body = r#"{ "cpu_template": "T2" diff --git a/src/firecracker/swagger/firecracker.yaml b/src/firecracker/swagger/firecracker.yaml index d1ac91bdb6a..72a47b9e67d 100644 --- a/src/firecracker/swagger/firecracker.yaml +++ b/src/firecracker/swagger/firecracker.yaml @@ -452,7 +452,8 @@ paths: If 2M hugetlbfs pages are specified, then `mem_size_mib` must be a multiple of 2. If any of the parameters has an incorrect value, the whole update fails. All parameters that are optional and are not specified are set to their default values - (smt = false, track_dirty_pages = false, cpu_template = None, huge_pages = None). + (smt = false, track_dirty_pages = false, cpu_template = None, huge_pages = None, + ksm_mergeable = false). operationId: putMachineConfiguration parameters: - name: body @@ -1444,6 +1445,14 @@ definitions: - None - 2M description: Which huge pages configuration (if any) should be used to back guest memory. + ksm_mergeable: + type: boolean + description: + Marks anonymous guest memory as mergeable with Linux Kernel Samepage Merging (KSM). + This can improve host memory efficiency when KSM is enabled by the host operator. + It is disabled by default because page deduplication can enable side channels, and it + cannot be used with hugetlbfs-backed guest memory. + default: false MemoryBackend: type: object diff --git a/src/vmm/src/devices/virtio/block/virtio/io/mod.rs b/src/vmm/src/devices/virtio/block/virtio/io/mod.rs index b7aa8061d76..9090e41fb85 100644 --- a/src/vmm/src/devices/virtio/block/virtio/io/mod.rs +++ b/src/vmm/src/devices/virtio/block/virtio/io/mod.rs @@ -227,6 +227,7 @@ pub mod tests { [(GuestAddress(0), MEM_LEN)].into_iter(), true, HugePageConfig::None, + false, ) .unwrap() .into_iter() diff --git a/src/vmm/src/devices/virtio/mem/device.rs b/src/vmm/src/devices/virtio/mem/device.rs index c6c0ea443e4..c43797c906a 100644 --- a/src/vmm/src/devices/virtio/mem/device.rs +++ b/src/vmm/src/devices/virtio/mem/device.rs @@ -721,6 +721,7 @@ pub(crate) mod test_utils { std::iter::once((addr, mib_to_bytes(1024))), false, HugePageConfig::None, + false, ) .unwrap() .pop() diff --git a/src/vmm/src/devices/virtio/vhost_user.rs b/src/vmm/src/devices/virtio/vhost_user.rs index 831595f0940..3fd4497fa0c 100644 --- a/src/vmm/src/devices/virtio/vhost_user.rs +++ b/src/vmm/src/devices/virtio/vhost_user.rs @@ -487,6 +487,7 @@ pub(crate) mod tests { libc::MAP_PRIVATE, Some(file), false, + false, ) .unwrap() .into_iter() diff --git a/src/vmm/src/persist.rs b/src/vmm/src/persist.rs index feb53c0e19e..40dc85155d1 100644 --- a/src/vmm/src/persist.rs +++ b/src/vmm/src/persist.rs @@ -429,6 +429,7 @@ pub fn restore_from_snapshot( cpu_template: Some(microvm_state.vm_info.cpu_template), track_dirty_pages: Some(track_dirty_pages), huge_pages: Some(microvm_state.vm_info.huge_pages), + ksm_mergeable: Some(false), #[cfg(feature = "gdb")] gdb_socket_path: None, }) @@ -572,7 +573,8 @@ fn create_guest_memory( track_dirty_pages: bool, huge_pages: HugePageConfig, ) -> Result<(Vec, Vec), GuestMemoryFromUffdError> { - let guest_memory = memory::anonymous(mem_state.regions(), track_dirty_pages, huge_pages)?; + let guest_memory = + memory::anonymous(mem_state.regions(), track_dirty_pages, huge_pages, false)?; let mut backend_mappings = Vec::with_capacity(guest_memory.len()); let mut offset = 0; for mem_region in guest_memory.iter() { diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index 64254170e76..dce1a9222bf 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -501,6 +501,10 @@ impl VmResources { // a single way of backing guest memory for vhost-user and non-vhost-user cases, // that would not be worth the effort. if vhost_user_device_used { + if self.machine_config.ksm_mergeable { + return Err(MemoryError::KsmWithSharedMemory); + } + memory::memfd_backed( regions, self.machine_config.track_dirty_pages, @@ -511,6 +515,7 @@ impl VmResources { regions.iter().copied(), self.machine_config.track_dirty_pages, self.machine_config.huge_pages, + self.machine_config.ksm_mergeable, ) } } @@ -1428,6 +1433,7 @@ mod tests { cpu_template: Some(StaticCpuTemplate::V1N1), track_dirty_pages: Some(false), huge_pages: Some(HugePageConfig::None), + ksm_mergeable: Some(false), #[cfg(feature = "gdb")] gdb_socket_path: None, }; @@ -1517,6 +1523,15 @@ mod tests { // trigger the "ballooning incompatible with huge pages" check. vm_resources.balloon = BalloonBuilder::new(); vm_resources.update_machine_config(&aux_vm_config).unwrap(); + + // KSM mergeable memory is incompatible with hugetlbfs-backed memory. + aux_vm_config.ksm_mergeable = Some(true); + assert_eq!( + vm_resources + .update_machine_config(&aux_vm_config) + .unwrap_err(), + MachineConfigError::KsmWithHugePages + ); } #[test] diff --git a/src/vmm/src/test_utils/mod.rs b/src/vmm/src/test_utils/mod.rs index 41809b71b34..fd78fa9cc47 100644 --- a/src/vmm/src/test_utils/mod.rs +++ b/src/vmm/src/test_utils/mod.rs @@ -44,7 +44,7 @@ pub fn single_region_mem_at_raw(at: u64, size: usize) -> Vec { /// Creates a [`GuestMemoryMmap`] with multiple regions and without dirty page tracking. pub fn multi_region_mem(regions: &[(GuestAddress, usize)]) -> GuestMemoryMmap { GuestRegionCollection::from_regions( - memory::anonymous(regions.iter().copied(), false, HugePageConfig::None) + memory::anonymous(regions.iter().copied(), false, HugePageConfig::None, false) .expect("Cannot initialize memory") .into_iter() .map(|region| GuestRegionMmapExt::dram_from_mmap_region(region, 0)) @@ -54,7 +54,7 @@ pub fn multi_region_mem(regions: &[(GuestAddress, usize)]) -> GuestMemoryMmap { } pub fn multi_region_mem_raw(regions: &[(GuestAddress, usize)]) -> Vec { - memory::anonymous(regions.iter().copied(), false, HugePageConfig::None) + memory::anonymous(regions.iter().copied(), false, HugePageConfig::None, false) .expect("Cannot initialize memory") } diff --git a/src/vmm/src/vmm_config/machine_config.rs b/src/vmm/src/vmm_config/machine_config.rs index a975f5217ad..e3f133bd597 100644 --- a/src/vmm/src/vmm_config/machine_config.rs +++ b/src/vmm/src/vmm_config/machine_config.rs @@ -29,6 +29,8 @@ pub enum MachineConfigError { SmtNotSupported, /// Could not determine host kernel version when checking hugetlbfs compatibility KernelVersion, + /// KSM mergeable memory cannot be enabled with hugetlbfs-backed guest memory. + KsmWithHugePages, } /// Describes the possible (huge)page configurations for a microVM's memory. @@ -113,6 +115,9 @@ pub struct MachineConfig { /// Configures what page size Firecracker should use to back guest memory. #[serde(default)] pub huge_pages: HugePageConfig, + /// Marks anonymous guest memory as mergeable by KSM. + #[serde(default)] + pub ksm_mergeable: bool, /// GDB socket address. #[cfg(feature = "gdb")] #[serde(default, skip_serializing_if = "Option::is_none")] @@ -155,6 +160,7 @@ impl Default for MachineConfig { cpu_template: None, track_dirty_pages: false, huge_pages: HugePageConfig::None, + ksm_mergeable: false, #[cfg(feature = "gdb")] gdb_socket_path: None, } @@ -188,6 +194,9 @@ pub struct MachineConfigUpdate { /// Configures what page size Firecracker should use to back guest memory. #[serde(default)] pub huge_pages: Option, + /// Marks anonymous guest memory as mergeable by KSM. + #[serde(default)] + pub ksm_mergeable: Option, /// GDB socket address. #[cfg(feature = "gdb")] #[serde(default)] @@ -212,6 +221,7 @@ impl From for MachineConfigUpdate { cpu_template: cfg.static_template(), track_dirty_pages: Some(cfg.track_dirty_pages), huge_pages: Some(cfg.huge_pages), + ksm_mergeable: Some(cfg.ksm_mergeable), #[cfg(feature = "gdb")] gdb_socket_path: cfg.gdb_socket_path, } @@ -261,11 +271,16 @@ impl MachineConfig { let mem_size_mib = update.mem_size_mib.unwrap_or(self.mem_size_mib); let page_config = update.huge_pages.unwrap_or(self.huge_pages); + let ksm_mergeable = update.ksm_mergeable.unwrap_or(self.ksm_mergeable); if mem_size_mib == 0 || !page_config.is_valid_mem_size(mem_size_mib) { return Err(MachineConfigError::InvalidMemorySize); } + if ksm_mergeable && page_config.is_hugetlbfs() { + return Err(MachineConfigError::KsmWithHugePages); + } + let cpu_template = match update.cpu_template { None => self.cpu_template.clone(), Some(StaticCpuTemplate::None) => None, @@ -279,6 +294,7 @@ impl MachineConfig { cpu_template, track_dirty_pages: update.track_dirty_pages.unwrap_or(self.track_dirty_pages), huge_pages: page_config, + ksm_mergeable, #[cfg(feature = "gdb")] gdb_socket_path: update.gdb_socket_path.clone(), }) @@ -288,7 +304,9 @@ impl MachineConfig { #[cfg(test)] mod tests { use crate::cpu_config::templates::{CpuTemplateType, CustomCpuTemplate, StaticCpuTemplate}; - use crate::vmm_config::machine_config::MachineConfig; + use crate::vmm_config::machine_config::{ + HugePageConfig, MachineConfig, MachineConfigError, MachineConfigUpdate, + }; // Ensure the special (de)serialization logic for the cpu_template field works: // only static cpu templates can be specified via the machine-config endpoint, but @@ -335,4 +353,18 @@ mod tests { assert!(deserialized.cpu_template.is_none()); } + + #[test] + fn test_ksm_mergeable_with_huge_pages_fails() { + let update = MachineConfigUpdate { + ksm_mergeable: Some(true), + huge_pages: Some(HugePageConfig::Hugetlbfs2M), + ..Default::default() + }; + + assert_eq!( + MachineConfig::default().update(&update), + Err(MachineConfigError::KsmWithHugePages) + ); + } } diff --git a/src/vmm/src/vstate/memory.rs b/src/vmm/src/vstate/memory.rs index 7bff6ad070d..e273fd8b6ed 100644 --- a/src/vmm/src/vstate/memory.rs +++ b/src/vmm/src/vstate/memory.rs @@ -47,6 +47,10 @@ pub enum MemoryError { VmMemoryError, /// Cannot create memfd: {0} Memfd(memfd::Error), + /// KSM mergeable memory is not supported with shared memfd-backed guest memory + KsmWithSharedMemory, + /// Cannot mark guest memory as mergeable by KSM: {0} + MadviseMergeable(std::io::Error), /// Cannot resize memfd file: {0} MemfdSetLen(std::io::Error), /// Total sum of memory regions exceeds largest possible file offset @@ -507,6 +511,7 @@ pub fn create( mmap_flags: libc::c_int, file: Option, track_dirty_pages: bool, + ksm_mergeable: bool, ) -> Result, MemoryError> { let mut offset = 0; let file = file.map(Arc::new); @@ -533,15 +538,33 @@ pub fn create( Some(new_off) => new_off, }; - GuestRegionMmap::new( + let region = GuestRegionMmap::new( builder.build().map_err(MemoryError::MmapRegionError)?, start, ) - .ok_or(MemoryError::VmMemoryError) + .ok_or(MemoryError::VmMemoryError)?; + + if ksm_mergeable { + mark_region_mergeable(®ion)?; + } + + Ok(region) }) .collect::, _>>() } +fn mark_region_mergeable(region: &GuestRegionMmap) -> Result<(), MemoryError> { + // SAFETY: the region is a valid mmap owned by `GuestRegionMmap`. + let ret = unsafe { libc::madvise(region.as_ptr().cast(), region.size(), libc::MADV_MERGEABLE) }; + if ret != 0 { + return Err(MemoryError::MadviseMergeable( + std::io::Error::last_os_error(), + )); + } + + Ok(()) +} + /// Creates a GuestMemoryMmap with `size` in MiB backed by a memfd. pub fn memfd_backed( regions: &[(GuestAddress, usize)], @@ -556,6 +579,7 @@ pub fn memfd_backed( libc::MAP_SHARED | huge_pages.mmap_flags(), Some(memfd_file), track_dirty_pages, + false, ) } @@ -564,12 +588,14 @@ pub fn anonymous( regions: impl Iterator, track_dirty_pages: bool, huge_pages: HugePageConfig, + ksm_mergeable: bool, ) -> Result, MemoryError> { create( regions, libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | huge_pages.mmap_flags(), None, track_dirty_pages, + ksm_mergeable, ) } @@ -598,6 +624,7 @@ pub fn snapshot_file( libc::MAP_PRIVATE, Some(file), track_dirty_pages, + false, ) } @@ -903,6 +930,7 @@ mod tests { regions.into_iter(), dirty_page_tracking, HugePageConfig::None, + false, ) .unwrap(); guest_memory.iter().for_each(|region| { @@ -968,8 +996,9 @@ mod tests { (GuestAddress(region_size as u64), region_size), // pages 3-5 (GuestAddress(region_size as u64 * 2), region_size), // pages 6-8 ]; - let guest_memory = - into_region_ext(anonymous(regions.into_iter(), true, HugePageConfig::None).unwrap()); + let guest_memory = into_region_ext( + anonymous(regions.into_iter(), true, HugePageConfig::None, false).unwrap(), + ); let dirty_map = [ // page 0: not dirty @@ -1029,6 +1058,7 @@ mod tests { [(GuestAddress(0), region_size)].into_iter(), false, HugePageConfig::None, + false, ) .unwrap(), ); @@ -1040,8 +1070,9 @@ mod tests { (GuestAddress(region_size as u64), region_size), // pages 3-5 (GuestAddress(region_size as u64 * 2), region_size), // pages 6-8 ]; - let guest_memory = - into_region_ext(anonymous(regions.into_iter(), true, HugePageConfig::None).unwrap()); + let guest_memory = into_region_ext( + anonymous(regions.into_iter(), true, HugePageConfig::None, false).unwrap(), + ); check_serde(&guest_memory); } @@ -1055,7 +1086,7 @@ mod tests { (GuestAddress(page_size as u64 * 2), page_size), ]; let guest_memory = into_region_ext( - anonymous(mem_regions.into_iter(), true, HugePageConfig::None).unwrap(), + anonymous(mem_regions.into_iter(), true, HugePageConfig::None, false).unwrap(), ); let expected_memory_state = GuestMemoryState { @@ -1084,7 +1115,7 @@ mod tests { (GuestAddress(page_size as u64 * 4), page_size * 3), ]; let guest_memory = into_region_ext( - anonymous(mem_regions.into_iter(), true, HugePageConfig::None).unwrap(), + anonymous(mem_regions.into_iter(), true, HugePageConfig::None, false).unwrap(), ); let expected_memory_state = GuestMemoryState { @@ -1121,7 +1152,7 @@ mod tests { (region_2_address, region_size), ]; let guest_memory = into_region_ext( - anonymous(mem_regions.into_iter(), true, HugePageConfig::None).unwrap(), + anonymous(mem_regions.into_iter(), true, HugePageConfig::None, false).unwrap(), ); // Check that Firecracker bitmap is clean. guest_memory.iter().for_each(|r| { @@ -1173,7 +1204,7 @@ mod tests { (region_2_address, region_size), ]; let guest_memory = into_region_ext( - anonymous(mem_regions.into_iter(), true, HugePageConfig::None).unwrap(), + anonymous(mem_regions.into_iter(), true, HugePageConfig::None, false).unwrap(), ); // Check that Firecracker bitmap is clean. guest_memory.iter().for_each(|r| { @@ -1338,7 +1369,7 @@ mod tests { (region_2_address, region_size), ]; let guest_memory = into_region_ext( - anonymous(mem_regions.into_iter(), true, HugePageConfig::None).unwrap(), + anonymous(mem_regions.into_iter(), true, HugePageConfig::None, false).unwrap(), ); // Check that Firecracker bitmap is clean. @@ -1489,6 +1520,7 @@ mod tests { std::iter::once((base, region_size)), false, HugePageConfig::None, + false, ) .unwrap() .into_iter() @@ -1674,6 +1706,7 @@ mod tests { [(GuestAddress(next_addr), region_size)].into_iter(), true, HugePageConfig::None, + false, ) .unwrap(); diff --git a/tests/framework/vm_config.json b/tests/framework/vm_config.json index b2bac4066d5..3c44c2718d8 100644 --- a/tests/framework/vm_config.json +++ b/tests/framework/vm_config.json @@ -22,7 +22,8 @@ "mem_size_mib": 1024, "smt": false, "track_dirty_pages": false, - "huge_pages": "None" + "huge_pages": "None", + "ksm_mergeable": false }, "cpu-config": null, "balloon": null, diff --git a/tests/framework/vm_config_network.json b/tests/framework/vm_config_network.json index 7e25823cd66..2b2fb120586 100644 --- a/tests/framework/vm_config_network.json +++ b/tests/framework/vm_config_network.json @@ -20,7 +20,8 @@ "vcpu_count": 2, "mem_size_mib": 1024, "smt": false, - "track_dirty_pages": false + "track_dirty_pages": false, + "ksm_mergeable": false }, "cpu-config": null, "balloon": null, diff --git a/tests/framework/vm_config_with_mmdsv1.json b/tests/framework/vm_config_with_mmdsv1.json index 30f67ff5bfa..f819d7ee912 100644 --- a/tests/framework/vm_config_with_mmdsv1.json +++ b/tests/framework/vm_config_with_mmdsv1.json @@ -20,7 +20,8 @@ "machine-config": { "vcpu_count": 2, "mem_size_mib": 1024, - "track_dirty_pages": false + "track_dirty_pages": false, + "ksm_mergeable": false }, "balloon": null, "network-interfaces": [ diff --git a/tests/framework/vm_config_with_mmdsv2.json b/tests/framework/vm_config_with_mmdsv2.json index f766129f02f..98e20edaace 100644 --- a/tests/framework/vm_config_with_mmdsv2.json +++ b/tests/framework/vm_config_with_mmdsv2.json @@ -21,7 +21,8 @@ "vcpu_count": 2, "mem_size_mib": 1024, "smt": false, - "track_dirty_pages": false + "track_dirty_pages": false, + "ksm_mergeable": false }, "balloon": null, "network-interfaces": [ diff --git a/tests/integration_tests/functional/test_api.py b/tests/integration_tests/functional/test_api.py index 5fc32105231..873ffbda46e 100644 --- a/tests/integration_tests/functional/test_api.py +++ b/tests/integration_tests/functional/test_api.py @@ -1285,6 +1285,7 @@ def test_get_full_config_after_restoring_snapshot(microvm_factory, uvm_configure "smt": True, "track_dirty_pages": False, "huge_pages": "None", + "ksm_mergeable": False, } if cpu_vendor == utils_cpuid.CpuVendor.ARM: @@ -1430,6 +1431,7 @@ def test_get_full_config(uvm): "smt": False, "track_dirty_pages": False, "huge_pages": "None", + "ksm_mergeable": False, } expected_cfg["cpu-config"] = None expected_cfg["boot-source"] = { From 9e990e03fbd9fc5b64c25ffda9a20388b5c4b5f2 Mon Sep 17 00:00:00 2001 From: Rekas Date: Fri, 12 Jun 2026 19:40:01 +0300 Subject: [PATCH 2/2] tests: cover ksm mergeable memory mapping Add an integration test that boots a microVM with ksm_mergeable=true and verifies that the Firecracker process has a guest memory mapping marked with VmFlags: mg in smaps. Signed-off-by: Jonas Savulionis --- .../integration_tests/functional/test_api.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/integration_tests/functional/test_api.py b/tests/integration_tests/functional/test_api.py index 873ffbda46e..e248f9c7ee6 100644 --- a/tests/integration_tests/functional/test_api.py +++ b/tests/integration_tests/functional/test_api.py @@ -34,6 +34,32 @@ ) +def smaps_has_mergeable_region(pid, min_size_kib): + """ + Return whether a process has a mergeable mapping of at least min_size_kib. + """ + current_size_kib = 0 + current_flags = [] + + def current_region_matches(): + return current_size_kib >= min_size_kib and "mg" in current_flags + + for line in Path(f"/proc/{pid}/smaps").read_text(encoding="utf-8").splitlines(): + if re.match(r"^[0-9a-f]+-[0-9a-f]+", line): + if current_region_matches(): + return True + current_size_kib = 0 + current_flags = [] + continue + + if line.startswith("Size:"): + current_size_kib = int(line.split()[1]) + elif line.startswith("VmFlags:"): + current_flags = line.split()[1:] + + return current_region_matches() + + @pin_guest_kernel(GUEST_KERNEL_DEFAULT) def test_api_happy_start(uvm): """ @@ -52,6 +78,26 @@ def test_api_happy_start(uvm): assert "Kernel loaded using PVH boot protocol" in test_microvm.log_data +@pin_guest_kernel(GUEST_KERNEL_DEFAULT) +def test_ksm_mergeable_machine_config_marks_guest_memory(uvm): + """ + Test that ksm_mergeable marks anonymous guest memory as mergeable. + """ + test_microvm = uvm + mem_size_mib = 128 + + test_microvm.spawn() + test_microvm.basic_config(mem_size_mib=mem_size_mib) + test_microvm.api.machine_config.patch(ksm_mergeable=True) + test_microvm.start() + + assert test_microvm.api.machine_config.get().json()["ksm_mergeable"] is True + assert smaps_has_mergeable_region( + test_microvm.firecracker_pid, + mem_size_mib * 1024, + ) + + @pin_guest_kernel(GUEST_KERNEL_DEFAULT) def test_drive_io_engine(uvm, io_engine): """