mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vm-memory 0.18 has renamed GuestMemory to GuestMemoryBackend, and made GuestMemory refer to something less specific. For simplicity, we keep using GuestMemoryBackend (formerly GuestMemory) everywhere for now. We can adjust bounds to be less specific later if we find ourselves needing the newly enabled flexibility. Signed-off-by: Alyssa Ross <hi@alyssa.is> Signed-off-by: Rob Bradford <rbradford@meta.com>
3441 lines
125 KiB
Rust
3441 lines
125 KiB
Rust
// Copyright © 2019 Intel Corporation
|
||
//
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
//
|
||
|
||
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
||
use std::collections::BTreeMap;
|
||
use std::collections::HashMap;
|
||
use std::fs::{File, OpenOptions};
|
||
use std::io::{self, Seek, SeekFrom};
|
||
use std::mem::{MaybeUninit, zeroed};
|
||
use std::num::NonZeroUsize;
|
||
use std::ops::{BitAnd, Not, Sub};
|
||
use std::os::fd::{AsFd, OwnedFd};
|
||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||
use std::path::{Path, PathBuf};
|
||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||
use std::sync::mpsc::{self, Receiver, SyncSender};
|
||
use std::sync::{Arc, Barrier, Mutex};
|
||
use std::{cmp, ffi, panic, result, thread, time};
|
||
|
||
use acpi_tables::{Aml, aml};
|
||
use anyhow::{Context, anyhow};
|
||
use arch::{RegionType, layout};
|
||
#[cfg(target_arch = "x86_64")]
|
||
use devices::ioapic;
|
||
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
|
||
use hypervisor::HypervisorVmError;
|
||
use log::{debug, error, info, warn};
|
||
use serde::{Deserialize, Serialize};
|
||
use thiserror::Error;
|
||
use tracer::trace_scoped;
|
||
use virtio_devices::BlocksState;
|
||
use virtio_devices::mem::Error as VirtioMemError;
|
||
#[cfg(target_arch = "x86_64")]
|
||
use vm_allocator::GsiApic;
|
||
use vm_allocator::{AddressAllocator, MemorySlotAllocator, SystemAllocator};
|
||
use vm_device::BusDevice;
|
||
use vm_memory::bitmap::AtomicBitmap;
|
||
use vm_memory::guest_memory::{Error as MmapError, FileOffset};
|
||
use vm_memory::mmap::MmapRegionError;
|
||
use vm_memory::{
|
||
Address, Bytes, GuestAddress, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryBackend,
|
||
GuestMemoryError, GuestMemoryRegion, GuestUsize, MmapRegion,
|
||
};
|
||
use vm_migration::protocol::{MemoryRange, MemoryRangeTable};
|
||
use vm_migration::{
|
||
Migratable, MigratableError, Pausable, Snapshot, SnapshotData, Snapshottable, Transportable,
|
||
UffdError,
|
||
};
|
||
use vmm_sys_util::eventfd::EventFd;
|
||
|
||
use crate::config::MemoryRestoreMode;
|
||
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
||
use crate::coredump::{
|
||
CoredumpMemoryRegion, CoredumpMemoryRegions, DumpState, GuestDebuggableError,
|
||
};
|
||
use crate::migration::transport::SocketStream;
|
||
use crate::migration::url_to_path;
|
||
use crate::sparse::{next_data_extent, write_region_sparse};
|
||
use crate::uffd::{
|
||
self, FaultResolution, FileUffdMemorySource, SocketUffdMemorySource, UffdMemorySource,
|
||
UffdRange,
|
||
};
|
||
use crate::vm_config::{HotplugMethod, MemoryConfig, MemoryZoneConfig};
|
||
use crate::{GuestMemoryMmap, GuestRegionMmap, MEMORY_MANAGER_SNAPSHOT_ID, userfaultfd};
|
||
|
||
struct UffdHandler {
|
||
stop_event: EventFd,
|
||
result_rx: Receiver<Result<(), io::Error>>,
|
||
handle: thread::JoinHandle<()>,
|
||
fault_socket_fd: Option<OwnedFd>,
|
||
prefault_complete: Arc<AtomicBool>,
|
||
}
|
||
|
||
pub const MEMORY_MANAGER_ACPI_SIZE: usize = 0x18;
|
||
|
||
const DEFAULT_MEMORY_ZONE: &str = "mem0";
|
||
|
||
const SNAPSHOT_FILENAME: &str = "memory-ranges";
|
||
|
||
#[cfg(target_arch = "x86_64")]
|
||
const X86_64_IRQ_BASE: u32 = 5;
|
||
|
||
const HOTPLUG_COUNT: usize = 8;
|
||
|
||
// Memory policy constants
|
||
const MPOL_BIND: u32 = 2;
|
||
const MPOL_MF_STRICT: u32 = 1;
|
||
const MPOL_MF_MOVE: u32 = 1 << 1;
|
||
|
||
// Reserve 1 MiB for platform MMIO devices (e.g. ACPI control devices)
|
||
const PLATFORM_DEVICE_AREA_SIZE: u64 = 1 << 20;
|
||
|
||
const MAX_PREFAULT_THREAD_COUNT: usize = 16;
|
||
|
||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||
struct HotPlugState {
|
||
base: u64,
|
||
length: u64,
|
||
active: bool,
|
||
inserting: bool,
|
||
removing: bool,
|
||
}
|
||
|
||
pub struct VirtioMemZone {
|
||
region: Arc<GuestRegionMmap>,
|
||
virtio_device: Option<Arc<Mutex<virtio_devices::Mem>>>,
|
||
hotplugged_size: u64,
|
||
hugepages: bool,
|
||
blocks_state: Arc<Mutex<BlocksState>>,
|
||
}
|
||
|
||
impl VirtioMemZone {
|
||
pub fn region(&self) -> &Arc<GuestRegionMmap> {
|
||
&self.region
|
||
}
|
||
pub fn set_virtio_device(&mut self, virtio_device: Arc<Mutex<virtio_devices::Mem>>) {
|
||
self.virtio_device = Some(virtio_device);
|
||
}
|
||
pub fn hotplugged_size(&self) -> u64 {
|
||
self.hotplugged_size
|
||
}
|
||
pub fn hugepages(&self) -> bool {
|
||
self.hugepages
|
||
}
|
||
pub fn blocks_state(&self) -> &Arc<Mutex<BlocksState>> {
|
||
&self.blocks_state
|
||
}
|
||
pub fn plugged_ranges(&self) -> MemoryRangeTable {
|
||
self.blocks_state
|
||
.lock()
|
||
.unwrap()
|
||
.memory_ranges(self.region.start_addr().raw_value(), true)
|
||
}
|
||
}
|
||
|
||
pub struct MemoryZone {
|
||
regions: Vec<Arc<GuestRegionMmap>>,
|
||
virtio_mem_zone: Option<VirtioMemZone>,
|
||
shared: bool,
|
||
hugepages: bool,
|
||
backing_page_size: u64,
|
||
mergeable: bool,
|
||
}
|
||
|
||
impl MemoryZone {
|
||
fn new(shared: bool, hugepages: bool, backing_page_size: u64, mergeable: bool) -> Self {
|
||
Self {
|
||
regions: Vec::new(),
|
||
virtio_mem_zone: None,
|
||
shared,
|
||
hugepages,
|
||
backing_page_size,
|
||
mergeable,
|
||
}
|
||
}
|
||
|
||
pub fn regions(&self) -> &Vec<Arc<GuestRegionMmap>> {
|
||
&self.regions
|
||
}
|
||
pub fn virtio_mem_zone(&self) -> &Option<VirtioMemZone> {
|
||
&self.virtio_mem_zone
|
||
}
|
||
pub fn virtio_mem_zone_mut(&mut self) -> Option<&mut VirtioMemZone> {
|
||
self.virtio_mem_zone.as_mut()
|
||
}
|
||
|
||
fn backing_page_size_for_gpa(&self, gpa: u64) -> Option<u64> {
|
||
if self.regions.iter().any(|region| {
|
||
let start = region.start_addr().raw_value();
|
||
gpa >= start && gpa < start + region.len()
|
||
}) {
|
||
return Some(self.backing_page_size);
|
||
}
|
||
|
||
self.virtio_mem_zone.as_ref().and_then(|virtio_mem_zone| {
|
||
let start = virtio_mem_zone.region.start_addr().raw_value();
|
||
(gpa >= start && gpa < start + virtio_mem_zone.region.len())
|
||
.then_some(self.backing_page_size)
|
||
})
|
||
}
|
||
}
|
||
|
||
pub type MemoryZones = HashMap<String, MemoryZone>;
|
||
|
||
#[derive(Clone, Serialize, Deserialize)]
|
||
struct GuestRamMapping {
|
||
slot: u32,
|
||
gpa: u64,
|
||
size: u64,
|
||
zone_id: String,
|
||
virtio_mem: bool,
|
||
file_offset: u64,
|
||
}
|
||
|
||
#[derive(Clone, Serialize, Deserialize)]
|
||
struct ArchMemRegion {
|
||
base: u64,
|
||
size: usize,
|
||
r_type: RegionType,
|
||
}
|
||
|
||
pub struct MemoryManager {
|
||
boot_guest_memory: GuestMemoryMmap,
|
||
guest_memory: GuestMemoryAtomic<GuestMemoryMmap>,
|
||
next_memory_slot: Arc<AtomicU32>,
|
||
memory_slot_free_list: Arc<Mutex<Vec<u32>>>,
|
||
start_of_device_area: GuestAddress,
|
||
end_of_device_area: GuestAddress,
|
||
end_of_ram_area: GuestAddress,
|
||
pub vm: Arc<dyn hypervisor::Vm>,
|
||
hotplug_slots: Vec<HotPlugState>,
|
||
selected_slot: usize,
|
||
mergeable: bool,
|
||
allocator: Arc<Mutex<SystemAllocator>>,
|
||
hotplug_method: HotplugMethod,
|
||
boot_ram: u64,
|
||
current_ram: u64,
|
||
next_hotplug_slot: usize,
|
||
shared: bool,
|
||
hugepages: bool,
|
||
hugepage_size: Option<u64>,
|
||
prefault: bool,
|
||
reserve: bool,
|
||
thp: bool,
|
||
user_provided_zones: bool,
|
||
snapshot_memory_ranges: MemoryRangeTable,
|
||
memory_zones: MemoryZones,
|
||
log_dirty: bool, // Enable dirty logging for created RAM regions
|
||
arch_mem_regions: Vec<ArchMemRegion>,
|
||
ram_allocator: AddressAllocator,
|
||
dynamic: bool,
|
||
|
||
// Keep track of calls to create_userspace_mapping() for guest RAM.
|
||
// This is useful for getting the dirty pages as we need to know the
|
||
// slots that the mapping is created in.
|
||
guest_ram_mappings: Vec<GuestRamMapping>,
|
||
uffd_handler: Option<UffdHandler>,
|
||
|
||
pub acpi_address: Option<GuestAddress>,
|
||
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
|
||
uefi_flash: Option<GuestMemoryAtomic<GuestMemoryMmap>>,
|
||
}
|
||
|
||
#[derive(Error, Debug)]
|
||
pub enum Error {
|
||
/// Failed to create shared file.
|
||
#[error("Failed to create shared file")]
|
||
SharedFileCreate(#[source] io::Error),
|
||
|
||
/// Failed to set shared file length.
|
||
#[error("Failed to set shared file length")]
|
||
SharedFileSetLen(#[source] io::Error),
|
||
|
||
/// Mmap backed guest memory error
|
||
#[error("Mmap backed guest memory error")]
|
||
GuestMemory(#[source] MmapError),
|
||
|
||
/// Guest region collection error
|
||
#[error("Guest region collection error")]
|
||
GuestRegionCollection(#[source] vm_memory::GuestRegionCollectionError),
|
||
|
||
/// Failed to allocate a memory range.
|
||
#[error("Failed to allocate a memory range")]
|
||
MemoryRangeAllocation,
|
||
|
||
/// Error from region creation
|
||
#[error("Error from region creation")]
|
||
GuestMemoryRegion(#[source] MmapRegionError),
|
||
|
||
/// No ACPI slot available
|
||
#[error("No ACPI slot available")]
|
||
NoSlotAvailable,
|
||
|
||
/// Not enough space in the hotplug RAM region
|
||
#[error("Not enough space in the hotplug RAM region")]
|
||
InsufficientHotplugRam,
|
||
|
||
/// The requested hotplug memory addition is not a valid size
|
||
#[error("The requested hotplug memory addition is not a valid size")]
|
||
InvalidSize,
|
||
|
||
/// Failed to create the user memory region.
|
||
#[error("Failed to create the user memory region")]
|
||
CreateUserMemoryRegion(#[source] hypervisor::HypervisorVmError),
|
||
|
||
/// Failed to remove the user memory region.
|
||
#[error("Failed to remove the user memory region")]
|
||
RemoveUserMemoryRegion(#[source] hypervisor::HypervisorVmError),
|
||
|
||
/// Failed to EventFd.
|
||
#[error("Failed to EventFd")]
|
||
EventFdFail(#[source] io::Error),
|
||
|
||
/// Eventfd write error
|
||
#[error("Eventfd write error")]
|
||
EventfdError(#[source] io::Error),
|
||
|
||
/// Failed to virtio-mem resize
|
||
#[error("Failed to virtio-mem resize")]
|
||
VirtioMemResizeFail(#[source] VirtioMemError),
|
||
|
||
/// Cannot restore VM
|
||
#[error("Cannot restore VM")]
|
||
Restore(#[source] MigratableError),
|
||
|
||
/// Cannot restore VM because source URL is missing
|
||
#[error("Cannot restore VM because source URL is missing")]
|
||
RestoreMissingSourceUrl,
|
||
|
||
/// Cannot create the system allocator
|
||
#[error("Cannot create the system allocator")]
|
||
CreateSystemAllocator,
|
||
|
||
/// Failed creating a new MmapRegion instance.
|
||
#[cfg(target_arch = "x86_64")]
|
||
#[error("Failed creating a new MmapRegion instance")]
|
||
NewMmapRegion(#[source] MmapRegionError),
|
||
|
||
/// No memory zones found.
|
||
#[error("No memory zones found")]
|
||
MissingMemoryZones,
|
||
|
||
/// Memory configuration is not valid.
|
||
#[error("Memory configuration is not valid")]
|
||
InvalidMemoryParameters,
|
||
|
||
/// Memory backing path contains an interior NUL byte.
|
||
#[error("Memory backing path contains an interior NUL byte")]
|
||
InvalidMemoryPath(#[source] ffi::NulError),
|
||
|
||
/// Forbidden operation. Impossible to resize guest memory if it is
|
||
/// backed by user defined memory regions.
|
||
#[error("Impossible to resize guest memory if it is backed by user defined memory regions")]
|
||
InvalidResizeWithMemoryZones,
|
||
|
||
/// Forbidden operation. Impossible to resize guest memory below the boot
|
||
/// RAM as it is not removable with virtio-mem.
|
||
#[error("Invalid resize request: desired_size = {0} below boot size = {1}")]
|
||
InvalidResizeBelowBootSize(u64, u64),
|
||
|
||
/// It's invalid to try applying a NUMA policy to a memory zone that is
|
||
/// memory mapped with MAP_SHARED.
|
||
#[error(
|
||
"Invalid to try applying a NUMA policy to a memory zone that is memory mapped with MAP_SHARED"
|
||
)]
|
||
InvalidSharedMemoryZoneWithHostNuma,
|
||
|
||
/// Failed applying NUMA memory policy.
|
||
#[error("Failed applying NUMA memory policy")]
|
||
ApplyNumaPolicy(#[source] io::Error),
|
||
|
||
/// Memory zone identifier is not unique.
|
||
#[error("Memory zone identifier is not unique")]
|
||
DuplicateZoneId,
|
||
|
||
/// No virtio-mem resizing handler found.
|
||
#[error("No virtio-mem resizing handler found")]
|
||
MissingVirtioMemHandler,
|
||
|
||
/// Unknown memory zone.
|
||
#[error("Unknown memory zone")]
|
||
UnknownMemoryZone,
|
||
|
||
/// Invalid size for resizing. Can be anything except 0.
|
||
#[error("Invalid size for resizing. Can be anything except 0")]
|
||
InvalidHotplugSize,
|
||
|
||
/// Invalid hotplug method associated with memory zones resizing capability.
|
||
#[error("Invalid hotplug method associated with memory zones resizing capability")]
|
||
InvalidHotplugMethodWithMemoryZones,
|
||
|
||
/// Could not find specified memory zone identifier from hash map.
|
||
#[error("Could not find specified memory zone identifier from hash map")]
|
||
MissingZoneIdentifier,
|
||
|
||
/// Resizing the memory zone failed.
|
||
#[error("Resizing the memory zone failed")]
|
||
ResizeZone,
|
||
|
||
/// Guest address overflow
|
||
#[error("Guest address overflow")]
|
||
GuestAddressOverFlow,
|
||
|
||
/// Error opening snapshot file
|
||
#[error("Error opening snapshot file")]
|
||
SnapshotOpen(#[source] io::Error),
|
||
|
||
/// Error reading from snapshot file
|
||
#[error("Error reading from snapshot file")]
|
||
SnapshotRead(#[source] io::Error),
|
||
|
||
// Error copying snapshot into region
|
||
#[error("Error copying snapshot into region")]
|
||
SnapshotCopy(#[source] GuestMemoryError),
|
||
|
||
/// Failed to allocate MMIO address
|
||
#[error("Failed to allocate MMIO address")]
|
||
AllocateMmioAddress,
|
||
|
||
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
|
||
/// Failed to create UEFI flash
|
||
#[error("Failed to create UEFI flash")]
|
||
CreateUefiFlash(#[source] HypervisorVmError),
|
||
|
||
/// Using a directory as a backing file for memory is not supported
|
||
#[error("Using a directory as a backing file for memory is not supported")]
|
||
DirectoryAsBackingFileForMemory,
|
||
|
||
/// Failed to stat filesystem
|
||
#[error("Failed to stat filesystem")]
|
||
GetFileSystemBlockSize(#[source] io::Error),
|
||
|
||
/// Memory size is misaligned with default page size or its hugepage size
|
||
#[error("Memory size is misaligned with default page size or its hugepage size")]
|
||
MisalignedMemorySize,
|
||
|
||
/// Failed to prefault memory
|
||
#[error("Failed to prefault memory")]
|
||
PrefaultMemory(#[source] io::Error),
|
||
}
|
||
|
||
impl From<UffdError> for Error {
|
||
fn from(e: UffdError) -> Self {
|
||
Error::Restore(MigratableError::OnDemandRestore(e))
|
||
}
|
||
}
|
||
|
||
const ENABLE_FLAG: usize = 0;
|
||
const INSERTING_FLAG: usize = 1;
|
||
const REMOVING_FLAG: usize = 2;
|
||
const EJECT_FLAG: usize = 3;
|
||
|
||
const BASE_OFFSET_LOW: u64 = 0;
|
||
const BASE_OFFSET_HIGH: u64 = 0x4;
|
||
const LENGTH_OFFSET_LOW: u64 = 0x8;
|
||
const LENGTH_OFFSET_HIGH: u64 = 0xC;
|
||
const STATUS_OFFSET: u64 = 0x14;
|
||
const SELECTION_OFFSET: u64 = 0;
|
||
|
||
// The MMIO address space size is subtracted with 64k. This is done for the
|
||
// following reasons:
|
||
// - Reduce the addressable space size by at least 4k to workaround a Linux
|
||
// bug when the VMM allocates devices at the end of the addressable space
|
||
// - Windows requires the addressable space size to be 64k aligned
|
||
fn mmio_address_space_size(phys_bits: u8) -> u64 {
|
||
(1 << phys_bits) - (1 << 16)
|
||
}
|
||
|
||
// The `statfs` function can get information of hugetlbfs, and the hugepage size is in the
|
||
// `f_bsize` field.
|
||
//
|
||
// See: https://github.com/torvalds/linux/blob/v6.3/fs/hugetlbfs/inode.c#L1169
|
||
fn statfs_get_bsize(path: &str) -> Result<u64, Error> {
|
||
let path = ffi::CString::new(path).map_err(Error::InvalidMemoryPath)?;
|
||
let mut buf = MaybeUninit::<libc::statfs>::uninit();
|
||
|
||
// SAFETY: FFI call with a valid path and buffer
|
||
let ret = unsafe { libc::statfs(path.as_ptr(), buf.as_mut_ptr()) };
|
||
if ret != 0 {
|
||
return Err(Error::GetFileSystemBlockSize(io::Error::last_os_error()));
|
||
}
|
||
|
||
// SAFETY: `buf` is valid at this point
|
||
// Because this value is always positive, just convert it directly.
|
||
// Note that the `f_bsize` is `i64` in glibc and `u64` in musl, using `as u64` will be warned
|
||
// by `clippy` on musl target. To avoid the warning, there should be `as _` instead of
|
||
// `as u64`.
|
||
let bsize = unsafe { (*buf.as_ptr()).f_bsize } as _;
|
||
Ok(bsize)
|
||
}
|
||
|
||
fn memory_zone_get_align_size(zone: &MemoryZoneConfig) -> Result<u64, Error> {
|
||
// SAFETY: FFI call. Trivially safe.
|
||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as u64 };
|
||
|
||
// There is no backend file and the `hugepages` is disabled, just use system page size.
|
||
if zone.file.is_none() && !zone.hugepages {
|
||
return Ok(page_size);
|
||
}
|
||
|
||
// The `hugepages` is enabled and the `hugepage_size` is specified, just use it directly.
|
||
if let Some(hugepage_size) = zone.hugepage_size
|
||
&& zone.hugepages
|
||
{
|
||
return Ok(hugepage_size);
|
||
}
|
||
|
||
// There are two scenarios here:
|
||
// - `hugepages` is enabled but `hugepage_size` is not specified:
|
||
// Call `statfs` for `/dev/hugepages` for getting the default size of hugepage
|
||
// - The backing file is specified:
|
||
// Call `statfs` for the file and get its `f_bsize`. If the value is larger than the page
|
||
// size of normal page, just use the `f_bsize` because the file is in a hugetlbfs. If the
|
||
// value is less than or equal to the page size, just use the page size.
|
||
let path = zone.file.as_ref().map_or(Ok("/dev/hugepages"), |pathbuf| {
|
||
pathbuf.to_str().ok_or(Error::InvalidMemoryParameters)
|
||
})?;
|
||
|
||
let align_size = cmp::max(page_size, statfs_get_bsize(path)?);
|
||
|
||
Ok(align_size)
|
||
}
|
||
|
||
#[inline]
|
||
fn align_down<T>(val: T, align: T) -> T
|
||
where
|
||
T: BitAnd<Output = T> + Not<Output = T> + Sub<Output = T> + From<u8>,
|
||
{
|
||
val & !(align - 1u8.into())
|
||
}
|
||
|
||
#[inline]
|
||
fn is_aligned<T>(val: T, align: T) -> bool
|
||
where
|
||
T: BitAnd<Output = T> + Sub<Output = T> + From<u8> + PartialEq,
|
||
{
|
||
(val & (align - 1u8.into())) == 0u8.into()
|
||
}
|
||
|
||
impl BusDevice for MemoryManager {
|
||
fn read(&mut self, _base: u64, offset: u64, data: &mut [u8]) {
|
||
if self.selected_slot < self.hotplug_slots.len() {
|
||
let state = &self.hotplug_slots[self.selected_slot];
|
||
if matches!(
|
||
offset,
|
||
BASE_OFFSET_LOW | BASE_OFFSET_HIGH | LENGTH_OFFSET_LOW | LENGTH_OFFSET_HIGH
|
||
) && data.len() != 4
|
||
{
|
||
warn!(
|
||
"Invalid sized read ({}) of memory manager register {offset:#x}",
|
||
data.len()
|
||
);
|
||
return;
|
||
}
|
||
match offset {
|
||
BASE_OFFSET_LOW => {
|
||
data.copy_from_slice(&state.base.to_le_bytes()[..4]);
|
||
}
|
||
BASE_OFFSET_HIGH => {
|
||
data.copy_from_slice(&state.base.to_le_bytes()[4..]);
|
||
}
|
||
LENGTH_OFFSET_LOW => {
|
||
data.copy_from_slice(&state.length.to_le_bytes()[..4]);
|
||
}
|
||
LENGTH_OFFSET_HIGH => {
|
||
data.copy_from_slice(&state.length.to_le_bytes()[4..]);
|
||
}
|
||
STATUS_OFFSET => {
|
||
// The Linux kernel, quite reasonably, doesn't zero the memory it gives us.
|
||
data.fill(0);
|
||
if state.active {
|
||
data[0] |= 1 << ENABLE_FLAG;
|
||
}
|
||
if state.inserting {
|
||
data[0] |= 1 << INSERTING_FLAG;
|
||
}
|
||
if state.removing {
|
||
data[0] |= 1 << REMOVING_FLAG;
|
||
}
|
||
}
|
||
_ => {
|
||
warn!("Unexpected offset for accessing memory manager device: {offset:#}");
|
||
}
|
||
}
|
||
} else {
|
||
warn!("Out of range memory slot: {}", self.selected_slot);
|
||
}
|
||
}
|
||
|
||
fn write(&mut self, _base: u64, offset: u64, data: &[u8]) -> Option<Arc<Barrier>> {
|
||
match offset {
|
||
SELECTION_OFFSET => {
|
||
self.selected_slot = usize::from(data[0]);
|
||
}
|
||
STATUS_OFFSET => {
|
||
if self.selected_slot < self.hotplug_slots.len() {
|
||
let state = &mut self.hotplug_slots[self.selected_slot];
|
||
// The ACPI code writes back a 1 to acknowledge the insertion
|
||
if (data[0] & (1 << INSERTING_FLAG) == 1 << INSERTING_FLAG) && state.inserting {
|
||
state.inserting = false;
|
||
}
|
||
// Ditto for removal
|
||
if (data[0] & (1 << REMOVING_FLAG) == 1 << REMOVING_FLAG) && state.removing {
|
||
state.removing = false;
|
||
}
|
||
// Trigger removal of "DIMM"
|
||
if data[0] & (1 << EJECT_FLAG) == 1 << EJECT_FLAG {
|
||
warn!("Ejection of memory not currently supported");
|
||
}
|
||
} else {
|
||
warn!("Out of range memory slot: {}", self.selected_slot);
|
||
}
|
||
}
|
||
_ => {
|
||
warn!("Unexpected offset for accessing memory manager device: {offset:#}");
|
||
}
|
||
}
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Memory range policy.
|
||
#[derive(Clone, Copy)]
|
||
pub enum MemoryRangePolicy {
|
||
/// Send every region in full.
|
||
Full,
|
||
/// Send every region in full, except those already persisted through a
|
||
/// shared, hardlinked backing file.
|
||
SkipPersisted,
|
||
/// Send only the populated ranges of each region, skipping holes.
|
||
Sparse,
|
||
}
|
||
|
||
impl MemoryManager {
|
||
/// Creates all memory regions based on the available RAM ranges defined
|
||
/// by `ram_regions`, and based on the description of the memory zones.
|
||
/// In practice, this function can perform multiple memory mappings of the
|
||
/// same backing file if there's a hole in the address space between two
|
||
/// RAM ranges.
|
||
///
|
||
/// One example might be ram_regions containing 2 regions (0-3G and 4G-6G)
|
||
/// and zones containing two zones (size 1G and size 4G).
|
||
///
|
||
/// This function will create 3 resulting memory regions:
|
||
/// - First one mapping entirely the first memory zone on 0-1G range
|
||
/// - Second one mapping partially the second memory zone on 1G-3G range
|
||
/// - Third one mapping partially the second memory zone on 4G-6G range
|
||
///
|
||
/// Also, all memory regions are page-size aligned (e.g. their sizes must
|
||
/// be multiple of page-size), which may leave an additional hole in the
|
||
/// address space when hugepage is used.
|
||
fn create_memory_regions_from_zones(
|
||
ram_regions: &[(GuestAddress, usize)],
|
||
zones: &[MemoryZoneConfig],
|
||
prefault: Option<bool>,
|
||
thp: bool,
|
||
) -> Result<(Vec<Arc<GuestRegionMmap>>, MemoryZones), Error> {
|
||
let mut zone_iter = zones.iter();
|
||
let mut mem_regions = Vec::new();
|
||
let mut zone = zone_iter.next().ok_or(Error::MissingMemoryZones)?;
|
||
let mut zone_align_size = memory_zone_get_align_size(zone)?;
|
||
let mut zone_offset = 0u64;
|
||
let mut memory_zones = HashMap::new();
|
||
|
||
if !is_aligned(zone.size, zone_align_size) {
|
||
return Err(Error::MisalignedMemorySize);
|
||
}
|
||
|
||
// Add zone id to the list of memory zones.
|
||
memory_zones.insert(
|
||
zone.id.clone(),
|
||
MemoryZone::new(zone.shared, zone.hugepages, zone_align_size, zone.mergeable),
|
||
);
|
||
|
||
for ram_region in ram_regions.iter() {
|
||
let mut ram_region_offset = 0;
|
||
let mut exit = false;
|
||
|
||
loop {
|
||
let mut ram_region_consumed = false;
|
||
let mut pull_next_zone = false;
|
||
|
||
let ram_region_available_size =
|
||
align_down(ram_region.1 as u64 - ram_region_offset, zone_align_size);
|
||
if ram_region_available_size == 0 {
|
||
break;
|
||
}
|
||
let zone_sub_size = zone.size - zone_offset;
|
||
|
||
let file_offset = zone_offset;
|
||
let region_start = ram_region
|
||
.0
|
||
.checked_add(ram_region_offset)
|
||
.ok_or(Error::GuestAddressOverFlow)?;
|
||
let region_size = if zone_sub_size <= ram_region_available_size {
|
||
if zone_sub_size == ram_region_available_size {
|
||
ram_region_consumed = true;
|
||
}
|
||
|
||
ram_region_offset += zone_sub_size;
|
||
pull_next_zone = true;
|
||
|
||
zone_sub_size
|
||
} else {
|
||
zone_offset += ram_region_available_size;
|
||
ram_region_consumed = true;
|
||
|
||
ram_region_available_size
|
||
};
|
||
|
||
info!(
|
||
"create ram region for zone {}, region_start: {:#x}, region_size: {:#x}",
|
||
zone.id,
|
||
region_start.raw_value(),
|
||
region_size
|
||
);
|
||
let region = MemoryManager::create_ram_region(
|
||
&zone.file,
|
||
file_offset,
|
||
region_start,
|
||
region_size as usize,
|
||
prefault.unwrap_or(zone.prefault),
|
||
zone.reserve,
|
||
zone.shared,
|
||
zone.hugepages,
|
||
zone.hugepage_size,
|
||
zone.host_numa_node,
|
||
None,
|
||
thp,
|
||
)?;
|
||
|
||
// Add region to the list of regions associated with the
|
||
// current memory zone.
|
||
if let Some(memory_zone) = memory_zones.get_mut(&zone.id) {
|
||
memory_zone.regions.push(region.clone());
|
||
}
|
||
|
||
mem_regions.push(region);
|
||
|
||
if pull_next_zone {
|
||
// Get the next zone and reset the offset.
|
||
zone_offset = 0;
|
||
if let Some(z) = zone_iter.next() {
|
||
zone = z;
|
||
} else {
|
||
exit = true;
|
||
break;
|
||
}
|
||
zone_align_size = memory_zone_get_align_size(zone)?;
|
||
if !is_aligned(zone.size, zone_align_size) {
|
||
return Err(Error::MisalignedMemorySize);
|
||
}
|
||
|
||
// Check if zone id already exist. In case it does, throw
|
||
// an error as we need unique identifiers. Otherwise, add
|
||
// the new zone id to the list of memory zones.
|
||
if memory_zones.contains_key(&zone.id) {
|
||
error!(
|
||
"Memory zone identifier '{}' found more than once. \
|
||
It must be unique",
|
||
zone.id,
|
||
);
|
||
return Err(Error::DuplicateZoneId);
|
||
}
|
||
memory_zones.insert(
|
||
zone.id.clone(),
|
||
MemoryZone::new(
|
||
zone.shared,
|
||
zone.hugepages,
|
||
zone_align_size,
|
||
zone.mergeable,
|
||
),
|
||
);
|
||
}
|
||
|
||
if ram_region_consumed {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if exit {
|
||
break;
|
||
}
|
||
}
|
||
|
||
Ok((mem_regions, memory_zones))
|
||
}
|
||
|
||
// Restore both GuestMemoryBackend regions along with MemoryZone zones.
|
||
fn restore_memory_regions_and_zones(
|
||
guest_ram_mappings: &[GuestRamMapping],
|
||
zones_config: &[MemoryZoneConfig],
|
||
prefault: Option<bool>,
|
||
mut existing_memory_files: HashMap<u32, File>,
|
||
thp: bool,
|
||
) -> Result<(Vec<Arc<GuestRegionMmap>>, MemoryZones), Error> {
|
||
let mut memory_regions = Vec::new();
|
||
let mut memory_zones = HashMap::new();
|
||
|
||
for zone_config in zones_config {
|
||
let zone_page_size = memory_zone_get_align_size(zone_config)?;
|
||
memory_zones.insert(
|
||
zone_config.id.clone(),
|
||
MemoryZone::new(
|
||
zone_config.shared,
|
||
zone_config.hugepages,
|
||
zone_page_size,
|
||
zone_config.mergeable,
|
||
),
|
||
);
|
||
}
|
||
|
||
for guest_ram_mapping in guest_ram_mappings {
|
||
for zone_config in zones_config {
|
||
if guest_ram_mapping.zone_id == zone_config.id {
|
||
let region = MemoryManager::create_ram_region(
|
||
if guest_ram_mapping.virtio_mem {
|
||
&None
|
||
} else {
|
||
&zone_config.file
|
||
},
|
||
guest_ram_mapping.file_offset,
|
||
GuestAddress(guest_ram_mapping.gpa),
|
||
guest_ram_mapping.size as usize,
|
||
prefault.unwrap_or(zone_config.prefault),
|
||
zone_config.reserve,
|
||
zone_config.shared,
|
||
zone_config.hugepages,
|
||
zone_config.hugepage_size,
|
||
zone_config.host_numa_node,
|
||
existing_memory_files.remove(&guest_ram_mapping.slot),
|
||
thp,
|
||
)?;
|
||
memory_regions.push(Arc::clone(®ion));
|
||
if let Some(memory_zone) = memory_zones.get_mut(&guest_ram_mapping.zone_id) {
|
||
if guest_ram_mapping.virtio_mem {
|
||
let hotplugged_size = zone_config.hotplugged_size.unwrap_or(0);
|
||
let region_size = region.len();
|
||
memory_zone.virtio_mem_zone = Some(VirtioMemZone {
|
||
region,
|
||
virtio_device: None,
|
||
hotplugged_size,
|
||
hugepages: zone_config.hugepages,
|
||
blocks_state: Arc::new(Mutex::new(BlocksState::new(region_size))),
|
||
});
|
||
} else {
|
||
memory_zone.regions.push(region);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
memory_regions.sort_by_key(|x| x.start_addr());
|
||
|
||
Ok((memory_regions, memory_zones))
|
||
}
|
||
|
||
fn fill_saved_regions(
|
||
&mut self,
|
||
file_path: PathBuf,
|
||
saved_regions: &MemoryRangeTable,
|
||
) -> Result<(), Error> {
|
||
if saved_regions.is_empty() {
|
||
return Ok(());
|
||
}
|
||
|
||
// Open (read only) the snapshot file.
|
||
let mut memory_file = OpenOptions::new()
|
||
.read(true)
|
||
.open(file_path)
|
||
.map_err(Error::SnapshotOpen)?;
|
||
|
||
let guest_memory = self.guest_memory.memory();
|
||
let mut file_cursor: u64 = 0;
|
||
|
||
for range in saved_regions.regions() {
|
||
let end = file_cursor + range.length;
|
||
|
||
// First call doubles as a SEEK_HOLE-support probe. On error,
|
||
// take the dense path which seeks-and-streams sequentially.
|
||
match next_data_extent(memory_file.as_fd(), file_cursor, end) {
|
||
Ok(mut next) => {
|
||
while let Some((data_off, ext_len)) = next {
|
||
debug_assert!(data_off >= file_cursor);
|
||
let in_region = data_off
|
||
.checked_sub(file_cursor)
|
||
.expect("extent precedes file_cursor");
|
||
memory_file
|
||
.seek(SeekFrom::Start(data_off))
|
||
.map_err(Error::SnapshotRead)?;
|
||
let mut done: u64 = 0;
|
||
while done < ext_len {
|
||
let n = guest_memory
|
||
.read_volatile_from(
|
||
GuestAddress(range.gpa + in_region + done),
|
||
&mut memory_file,
|
||
(ext_len - done) as usize,
|
||
)
|
||
.map_err(Error::SnapshotCopy)?;
|
||
if n == 0 {
|
||
return Err(Error::SnapshotRead(io::Error::new(
|
||
io::ErrorKind::UnexpectedEof,
|
||
"read_volatile_from returned 0 inside data extent",
|
||
)));
|
||
}
|
||
done += n as u64;
|
||
}
|
||
next = next_data_extent(memory_file.as_fd(), data_off + ext_len, end)
|
||
.map_err(Error::SnapshotRead)?;
|
||
}
|
||
}
|
||
Err(_) => {
|
||
memory_file
|
||
.seek(SeekFrom::Start(file_cursor))
|
||
.map_err(Error::SnapshotRead)?;
|
||
let mut offset: u64 = 0;
|
||
// Manual partial-read loop preserves the workaround for
|
||
// https://github.com/rust-vmm/vm-memory/issues/174
|
||
loop {
|
||
let bytes_read = guest_memory
|
||
.read_volatile_from(
|
||
GuestAddress(range.gpa + offset),
|
||
&mut memory_file,
|
||
(range.length - offset) as usize,
|
||
)
|
||
.map_err(Error::SnapshotCopy)?;
|
||
offset += bytes_read as u64;
|
||
if offset == range.length {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
file_cursor = end;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// Restore guest memory using userfaultfd for lazy demand paging.
|
||
///
|
||
/// Instead of reading the entire snapshot into guest RAM upfront (which
|
||
/// blocks restore for hundreds of milliseconds at multi-GB sizes), this
|
||
/// registers the guest memory regions with a userfaultfd. A background
|
||
/// thread handles page faults by reading the corresponding page from the
|
||
/// snapshot file and copying it into guest memory via `UFFDIO_COPY`.
|
||
///
|
||
/// This preserves the original memory mapping type (anonymous or shared),
|
||
/// making it compatible with VFIO device passthrough and shared-memory
|
||
/// guest RAM.
|
||
///
|
||
/// Fails the restore if UFFD setup cannot be completed successfully.
|
||
///
|
||
/// The handler thread keeps the snapshot file open while lazy restore
|
||
/// is active. The file must remain available until the VM is shut down or
|
||
/// all faulted pages have been served.
|
||
fn restore_by_uffd(
|
||
&mut self,
|
||
file_path: &Path,
|
||
saved_regions: &MemoryRangeTable,
|
||
exit_evt: &EventFd,
|
||
) -> Result<(), Error> {
|
||
let mut file_offset: u64 = 0;
|
||
let Some((uffd_fd, ranges)) = self.prepare_uffd(saved_regions, |r| {
|
||
let o = file_offset;
|
||
file_offset += r.length;
|
||
o
|
||
})?
|
||
else {
|
||
return Ok(());
|
||
};
|
||
let snapshot_file = File::open(file_path).map_err(Error::SnapshotOpen)?;
|
||
let source: Box<dyn UffdMemorySource> = Box::new(FileUffdMemorySource::new(snapshot_file));
|
||
self.spawn_uffd_handler(uffd_fd, None, ranges, source, exit_evt)?;
|
||
info!("UFFD restore: demand-paged restore enabled");
|
||
Ok(())
|
||
}
|
||
|
||
/// Register UFFD and spawn the handler that serves faults over `socket`.
|
||
/// No-op when there are no regions.
|
||
pub(crate) fn start_postcopy_serving(
|
||
&mut self,
|
||
saved_regions: &MemoryRangeTable,
|
||
shared_backing: bool,
|
||
socket: SocketStream,
|
||
exit_evt: &EventFd,
|
||
) -> Result<(), Error> {
|
||
// PageFault uses the GPA as the page identifier on the wire.
|
||
let Some((uffd_fd, ranges)) = self.prepare_uffd(saved_regions, |r| r.gpa)? else {
|
||
return Ok(());
|
||
};
|
||
// Make every fault a small request/response round-trip.
|
||
socket.set_nodelay(true).map_err(UffdError::SetSocket)?;
|
||
let socket_fd = socket
|
||
.as_fd()
|
||
.try_clone_to_owned()
|
||
.map_err(UffdError::SetSocket)?;
|
||
let source: Box<dyn UffdMemorySource> =
|
||
Box::new(SocketUffdMemorySource::new(socket, shared_backing));
|
||
self.spawn_uffd_handler(uffd_fd, Some(socket_fd), ranges, source, exit_evt)
|
||
}
|
||
|
||
/// Create a UFFD fd and register every range.
|
||
fn prepare_uffd<F>(
|
||
&mut self,
|
||
saved_regions: &MemoryRangeTable,
|
||
mut source_offset_for: F,
|
||
) -> Result<Option<(OwnedFd, Vec<UffdRange>)>, Error>
|
||
where
|
||
F: FnMut(&MemoryRange) -> u64,
|
||
{
|
||
if saved_regions.is_empty() {
|
||
return Ok(None);
|
||
}
|
||
|
||
let guest_memory = self.guest_memory.memory();
|
||
let required_uffd_features = self.required_uffd_features();
|
||
|
||
// SAFETY: FFI call. Trivially safe.
|
||
let base_page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64;
|
||
|
||
info!(
|
||
"UFFD: registering {} region(s) for demand paging",
|
||
saved_regions.regions().len()
|
||
);
|
||
|
||
if saved_regions
|
||
.regions()
|
||
.iter()
|
||
.any(|range| range.gpa % base_page_size != 0 || range.length % base_page_size != 0)
|
||
{
|
||
return Err(UffdError::UnalignedRanges.into());
|
||
}
|
||
|
||
let uffd_fd = uffd::create(required_uffd_features).map_err(UffdError::Create)?;
|
||
|
||
let mut handler_ranges: Vec<UffdRange> = Vec::new();
|
||
|
||
for range in saved_regions.regions() {
|
||
let host_addr = guest_memory
|
||
.get_host_address(GuestAddress(range.gpa))
|
||
.map_err(|e| UffdError::GpaTranslation {
|
||
gpa: range.gpa,
|
||
source: e,
|
||
})? as u64;
|
||
|
||
let ioctls = uffd::register(uffd_fd.as_fd(), host_addr, range.length).map_err(|e| {
|
||
UffdError::Register {
|
||
addr: host_addr,
|
||
len: range.length,
|
||
source: e,
|
||
}
|
||
})?;
|
||
|
||
if ioctls & userfaultfd::UFFD_API_RANGE_IOCTLS_BASIC
|
||
!= userfaultfd::UFFD_API_RANGE_IOCTLS_BASIC
|
||
{
|
||
return Err(UffdError::MissingIoctlSupport {
|
||
addr: host_addr,
|
||
len: range.length,
|
||
}
|
||
.into());
|
||
}
|
||
|
||
let range_page_size = self
|
||
.memory_zones
|
||
.values()
|
||
.find_map(|zone| zone.backing_page_size_for_gpa(range.gpa))
|
||
.unwrap_or(base_page_size);
|
||
|
||
handler_ranges.push(UffdRange {
|
||
host_addr,
|
||
length: range.length,
|
||
source_offset: source_offset_for(range),
|
||
page_size: range_page_size,
|
||
});
|
||
}
|
||
|
||
Ok(Some((uffd_fd, handler_ranges)))
|
||
}
|
||
|
||
/// Spawn the UFFD handler thread that resolves faults through `source`.
|
||
fn spawn_uffd_handler(
|
||
&mut self,
|
||
uffd_fd: OwnedFd,
|
||
fault_socket_fd: Option<OwnedFd>,
|
||
handler_ranges: Vec<UffdRange>,
|
||
source: Box<dyn UffdMemorySource>,
|
||
exit_evt: &EventFd,
|
||
) -> Result<(), Error> {
|
||
info!(
|
||
"UFFD: spawning handler for {} region(s)",
|
||
handler_ranges.len()
|
||
);
|
||
|
||
let stop_event = EventFd::new(libc::EFD_NONBLOCK).map_err(Error::EventFdFail)?;
|
||
let thread_stop_event = stop_event.try_clone().map_err(Error::EventFdFail)?;
|
||
let thread_exit_evt = exit_evt.try_clone().map_err(Error::EventFdFail)?;
|
||
let panic_exit_evt = exit_evt.try_clone().map_err(Error::EventFdFail)?;
|
||
let (ready_tx, ready_rx) = mpsc::sync_channel(1);
|
||
let (result_tx, result_rx) = mpsc::sync_channel(1);
|
||
let prefault_complete = Arc::new(AtomicBool::new(false));
|
||
let thread_prefault_complete = Arc::clone(&prefault_complete);
|
||
let handle = thread::Builder::new()
|
||
.name("uffd-handler".to_string())
|
||
.spawn(move || {
|
||
panic::catch_unwind(panic::AssertUnwindSafe(move || {
|
||
let result = Self::uffd_handler_loop(
|
||
uffd_fd,
|
||
thread_stop_event,
|
||
source,
|
||
&handler_ranges,
|
||
&ready_tx,
|
||
&thread_prefault_complete,
|
||
);
|
||
|
||
if let Err(e) = &result {
|
||
error!("UFFD handler exited with error: {e}");
|
||
thread_exit_evt.write(1).ok();
|
||
}
|
||
|
||
result_tx.send(result).ok();
|
||
}))
|
||
.map_err(|_| {
|
||
error!("uffd-handler thread panicked");
|
||
panic_exit_evt.write(1).ok();
|
||
})
|
||
.ok();
|
||
})
|
||
.map_err(UffdError::SpawnThread)?;
|
||
|
||
if ready_rx.recv().is_err() {
|
||
handle.join().ok();
|
||
return Err(UffdError::HandlerStartup.into());
|
||
}
|
||
|
||
if let Ok(Err(e)) = result_rx.try_recv() {
|
||
handle.join().ok();
|
||
return Err(UffdError::HandlerFailed(e).into());
|
||
}
|
||
|
||
self.uffd_handler = Some(UffdHandler {
|
||
stop_event,
|
||
result_rx,
|
||
handle,
|
||
fault_socket_fd,
|
||
prefault_complete,
|
||
});
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn required_uffd_features(&self) -> u64 {
|
||
let mut features = 0u64;
|
||
if self.memory_zones.values().any(|z| z.shared || !z.hugepages) {
|
||
features |= userfaultfd::UFFD_FEATURE_MISSING_SHMEM;
|
||
}
|
||
if self.memory_zones.values().any(|z| z.hugepages) {
|
||
features |= userfaultfd::UFFD_FEATURE_MISSING_HUGETLBFS;
|
||
}
|
||
features
|
||
}
|
||
|
||
fn stop_uffd_handler(&mut self) {
|
||
if let Some(uffd_handler) = self.uffd_handler.take() {
|
||
uffd_handler.stop_event.write(1).ok();
|
||
if let Some(fd) = &uffd_handler.fault_socket_fd {
|
||
// SAFETY: fd is a valid owned fd for the duration of this call.
|
||
unsafe { libc::shutdown(fd.as_raw_fd(), libc::SHUT_RDWR) };
|
||
}
|
||
uffd_handler.handle.join().ok();
|
||
|
||
match uffd_handler.result_rx.try_recv() {
|
||
Ok(Err(e)) => error!("UFFD handler terminated with error: {e}"),
|
||
Err(mpsc::TryRecvError::Disconnected) => {
|
||
warn!("UFFD handler terminated unexpectedly (possible panic)");
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// True while an on-demand (UFFD) restore is still faulting in guest RAM.
|
||
pub fn restoring(&self) -> bool {
|
||
self.uffd_handler
|
||
.as_ref()
|
||
.is_some_and(|h| !h.prefault_complete.load(Ordering::Acquire))
|
||
}
|
||
|
||
/// Serve UFFD faults via `source`, prefaulting one page per idle
|
||
/// iteration.
|
||
#[expect(clippy::needless_pass_by_value)]
|
||
fn uffd_handler_loop(
|
||
uffd_fd: OwnedFd,
|
||
stop_event: EventFd,
|
||
mut source: Box<dyn UffdMemorySource>,
|
||
ranges: &[UffdRange],
|
||
ready_tx: &SyncSender<()>,
|
||
prefault_complete: &AtomicBool,
|
||
) -> Result<(), io::Error> {
|
||
let uffd_raw_fd = uffd_fd.as_raw_fd();
|
||
|
||
let total_pages: u64 = ranges.iter().map(UffdRange::num_pages).sum();
|
||
let mut pages_served: u64 = 0;
|
||
let mut pages_prefaulted: u64 = 0;
|
||
|
||
// Per-range bitmap tracking which pages have been populated by the
|
||
// on-demand fault handler. Lets the prefault cursor skip them without
|
||
// doing a wasted file read + UFFDIO_COPY.
|
||
let served_bitmap: Vec<AtomicBitmap> = ranges
|
||
.iter()
|
||
.map(|r| {
|
||
AtomicBitmap::new(
|
||
r.length as usize,
|
||
NonZeroUsize::new(r.page_size as usize).unwrap(),
|
||
)
|
||
})
|
||
.collect();
|
||
|
||
// Prefault cursor: (range index, page index within range). `None`
|
||
// means prefault was given up due to an error (natural completion
|
||
// returns from the function instead).
|
||
let mut prefault_cursor: Option<(usize, u64)> = (!ranges.is_empty()).then_some((0, 0));
|
||
let prefault_start = time::Instant::now();
|
||
|
||
const EVENT_STOP: u64 = 0;
|
||
const EVENT_UFFD: u64 = 1;
|
||
|
||
let epoll_fd = epoll::create(true).map_err(io::Error::other)?;
|
||
// SAFETY: epoll_fd is valid and owned by this scope.
|
||
let _epoll_file = unsafe { File::from_raw_fd(epoll_fd) };
|
||
|
||
epoll::ctl(
|
||
epoll_fd,
|
||
epoll::ControlOptions::EPOLL_CTL_ADD,
|
||
stop_event.as_raw_fd(),
|
||
epoll::Event::new(epoll::Events::EPOLLIN, EVENT_STOP),
|
||
)
|
||
.map_err(io::Error::other)?;
|
||
|
||
epoll::ctl(
|
||
epoll_fd,
|
||
epoll::ControlOptions::EPOLL_CTL_ADD,
|
||
uffd_raw_fd,
|
||
epoll::Event::new(epoll::Events::EPOLLIN | epoll::Events::EPOLLHUP, EVENT_UFFD),
|
||
)
|
||
.map_err(io::Error::other)?;
|
||
|
||
ready_tx.send(()).ok();
|
||
|
||
let mut events = vec![epoll::Event::new(epoll::Events::empty(), 0); 2];
|
||
loop {
|
||
// Block only when prefault is done; otherwise poll non-blocking
|
||
// so we can advance prefault between faults.
|
||
let timeout = if prefault_cursor.is_some() { 0 } else { -1 };
|
||
let num_events = match epoll::wait(epoll_fd, timeout, &mut events) {
|
||
Ok(n) => n,
|
||
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
|
||
Err(e) => return Err(e),
|
||
};
|
||
|
||
let mut got_uffd_data = false;
|
||
for event in events.iter().take(num_events) {
|
||
let token = event.data;
|
||
let evt_flags = event.events;
|
||
|
||
if token == EVENT_STOP {
|
||
stop_event.read().ok();
|
||
info!("UFFD handler: received stop event, exiting");
|
||
return Ok(());
|
||
}
|
||
|
||
if token == EVENT_UFFD
|
||
&& (evt_flags & epoll::Events::EPOLLHUP.bits()) != 0
|
||
&& (evt_flags & epoll::Events::EPOLLIN.bits()) == 0
|
||
{
|
||
info!("UFFD handler: fd closed (EPOLLHUP), exiting");
|
||
return Ok(());
|
||
}
|
||
|
||
if token == EVENT_UFFD && (evt_flags & epoll::Events::EPOLLIN.bits()) != 0 {
|
||
got_uffd_data = true;
|
||
}
|
||
}
|
||
|
||
if got_uffd_data {
|
||
// SAFETY: UffdMsg is a plain repr(C) struct, safe to zero-init.
|
||
let mut msg: uffd::UffdMsg = unsafe { zeroed() };
|
||
// SAFETY: reading a uffd_msg-sized struct from the valid uffd fd.
|
||
let n = unsafe {
|
||
libc::read(
|
||
uffd_raw_fd,
|
||
(&raw mut msg).cast(),
|
||
size_of::<uffd::UffdMsg>(),
|
||
)
|
||
};
|
||
if n < 0 {
|
||
let err = io::Error::last_os_error();
|
||
if err.kind() == io::ErrorKind::WouldBlock {
|
||
continue;
|
||
}
|
||
return Err(err);
|
||
}
|
||
if n == 0 {
|
||
info!("UFFD handler: EOF on fd, exiting");
|
||
return Ok(());
|
||
}
|
||
if n as usize != size_of::<uffd::UffdMsg>() {
|
||
return Err(io::Error::new(
|
||
io::ErrorKind::UnexpectedEof,
|
||
"Short read from userfaultfd",
|
||
));
|
||
}
|
||
|
||
if msg.event != userfaultfd::UFFD_EVENT_PAGEFAULT {
|
||
continue;
|
||
}
|
||
|
||
let fault_addr = msg.pf_address;
|
||
|
||
let mut served = false;
|
||
for (range_idx, range) in ranges.iter().enumerate() {
|
||
let Some(page_idx) = range.page_index_of(fault_addr) else {
|
||
continue;
|
||
};
|
||
|
||
loop {
|
||
match source.resolve(uffd_fd.as_fd(), range, page_idx)? {
|
||
FaultResolution::Served => {
|
||
pages_served += 1;
|
||
served_bitmap[range_idx].set_bit(page_idx as usize);
|
||
break;
|
||
}
|
||
FaultResolution::Retry => {
|
||
// The kernel reported a transient state while the fault
|
||
// is being resolved; yield and retry instead of aborting.
|
||
thread::yield_now();
|
||
}
|
||
}
|
||
}
|
||
served = true;
|
||
break;
|
||
}
|
||
|
||
if !served {
|
||
return Err(io::Error::other(format!(
|
||
"UFFD handler: fault at {fault_addr:#x} does not belong to any registered range",
|
||
)));
|
||
}
|
||
|
||
continue;
|
||
}
|
||
|
||
// No fault pending — advance the prefault cursor past served and
|
||
// end-of-range pages, then prefault one fresh page below.
|
||
let cursor = loop {
|
||
let Some((range_idx, page_idx)) = prefault_cursor else {
|
||
break None;
|
||
};
|
||
if page_idx >= ranges[range_idx].num_pages() {
|
||
if range_idx + 1 < ranges.len() {
|
||
prefault_cursor = Some((range_idx + 1, 0));
|
||
continue;
|
||
}
|
||
// Reached the end of the last range — every page is
|
||
// mapped, so no future faults can occur. Exit.
|
||
let elapsed = prefault_start.elapsed();
|
||
info!(
|
||
"UFFD handler: prefault done in {elapsed:.3?} — \
|
||
prefaulted={pages_prefaulted} served={pages_served} \
|
||
total={total_pages}"
|
||
);
|
||
prefault_complete.store(true, Ordering::Release);
|
||
return Ok(());
|
||
}
|
||
if served_bitmap[range_idx].is_bit_set(page_idx as usize) {
|
||
prefault_cursor = Some((range_idx, page_idx + 1));
|
||
continue;
|
||
}
|
||
break Some((range_idx, page_idx));
|
||
};
|
||
|
||
let Some((range_idx, page_idx)) = cursor else {
|
||
// Prefault was given up earlier (or the range list was
|
||
// empty). Keep serving on-demand faults.
|
||
continue;
|
||
};
|
||
|
||
let range = &ranges[range_idx];
|
||
|
||
let advance = match source.resolve(uffd_fd.as_fd(), range, page_idx) {
|
||
Ok(FaultResolution::Served) => {
|
||
pages_prefaulted += 1;
|
||
served_bitmap[range_idx].set_bit(page_idx as usize);
|
||
true
|
||
}
|
||
Ok(FaultResolution::Retry) => {
|
||
// Unlike the on demand handler (which must retry to wake
|
||
// the faulting thread), prefault can safely skip: any
|
||
// future guest access will simply page-fault and be
|
||
// served by the on demand path.
|
||
true
|
||
}
|
||
Err(e) => {
|
||
let page_addr = range.page_addr(page_idx);
|
||
warn!("UFFD prefault: source error at {page_addr:#x}: {e}");
|
||
false
|
||
}
|
||
};
|
||
|
||
if !advance {
|
||
// Prefault hit an unrecoverable error; give up but keep
|
||
// serving on-demand faults.
|
||
warn!("UFFD prefault: abandoning background prefault after error");
|
||
prefault_cursor = None;
|
||
continue;
|
||
}
|
||
|
||
prefault_cursor = Some((range_idx, page_idx + 1));
|
||
}
|
||
}
|
||
|
||
fn validate_memory_config(
|
||
config: &MemoryConfig,
|
||
user_provided_zones: bool,
|
||
) -> Result<(u64, Vec<MemoryZoneConfig>, bool), Error> {
|
||
let mut allow_mem_hotplug = false;
|
||
|
||
if user_provided_zones {
|
||
if config.zones.is_none() {
|
||
error!(
|
||
"User defined memory regions must be provided if the \
|
||
memory size is 0"
|
||
);
|
||
return Err(Error::MissingMemoryZones);
|
||
}
|
||
|
||
// Safe to unwrap as we checked right above there were some
|
||
// regions.
|
||
let zones = config.zones.clone().unwrap();
|
||
if zones.is_empty() {
|
||
return Err(Error::MissingMemoryZones);
|
||
}
|
||
|
||
let mut total_ram_size: u64 = 0;
|
||
for zone in zones.iter() {
|
||
total_ram_size += zone.size;
|
||
|
||
if zone.shared && zone.file.is_some() && zone.host_numa_node.is_some() {
|
||
error!(
|
||
"Invalid to set host NUMA policy for a memory zone \
|
||
backed by a regular file and mapped as 'shared'"
|
||
);
|
||
return Err(Error::InvalidSharedMemoryZoneWithHostNuma);
|
||
}
|
||
|
||
if zone.hotplug_size.is_some() && config.hotplug_method == HotplugMethod::Acpi {
|
||
error!("Invalid to set ACPI hotplug method for memory zones");
|
||
return Err(Error::InvalidHotplugMethodWithMemoryZones);
|
||
}
|
||
|
||
if let Some(hotplugged_size) = zone.hotplugged_size {
|
||
if let Some(hotplug_size) = zone.hotplug_size {
|
||
if hotplugged_size > hotplug_size {
|
||
error!(
|
||
"'hotplugged_size' {hotplugged_size} can't be bigger than \
|
||
'hotplug_size' {hotplug_size}",
|
||
);
|
||
return Err(Error::InvalidMemoryParameters);
|
||
}
|
||
} else {
|
||
error!(
|
||
"Invalid to define 'hotplugged_size' when there is\
|
||
no 'hotplug_size' for a memory zone"
|
||
);
|
||
return Err(Error::InvalidMemoryParameters);
|
||
}
|
||
if config.hotplug_method == HotplugMethod::Acpi {
|
||
error!(
|
||
"Invalid to define 'hotplugged_size' with hotplug \
|
||
method 'acpi'"
|
||
);
|
||
return Err(Error::InvalidMemoryParameters);
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok((total_ram_size, zones, allow_mem_hotplug))
|
||
} else {
|
||
if config.zones.is_some() {
|
||
error!(
|
||
"User defined memory regions can't be provided if the \
|
||
memory size is not 0"
|
||
);
|
||
return Err(Error::InvalidMemoryParameters);
|
||
}
|
||
|
||
if config.hotplug_size.is_some() {
|
||
allow_mem_hotplug = true;
|
||
}
|
||
|
||
if let Some(hotplugged_size) = config.hotplugged_size {
|
||
if let Some(hotplug_size) = config.hotplug_size {
|
||
if hotplugged_size > hotplug_size {
|
||
error!(
|
||
"'hotplugged_size' {hotplugged_size} can't be bigger than \
|
||
'hotplug_size' {hotplug_size}",
|
||
);
|
||
return Err(Error::InvalidMemoryParameters);
|
||
}
|
||
} else {
|
||
error!(
|
||
"Invalid to define 'hotplugged_size' when there is\
|
||
no 'hotplug_size'"
|
||
);
|
||
return Err(Error::InvalidMemoryParameters);
|
||
}
|
||
if config.hotplug_method == HotplugMethod::Acpi {
|
||
error!(
|
||
"Invalid to define 'hotplugged_size' with hotplug \
|
||
method 'acpi'"
|
||
);
|
||
return Err(Error::InvalidMemoryParameters);
|
||
}
|
||
}
|
||
|
||
// Create a single zone from the global memory config. This lets
|
||
// us reuse the codepath for user defined memory zones.
|
||
let zones = vec![MemoryZoneConfig {
|
||
id: String::from(DEFAULT_MEMORY_ZONE),
|
||
size: config.size,
|
||
file: None,
|
||
shared: config.shared,
|
||
hugepages: config.hugepages,
|
||
hugepage_size: config.hugepage_size,
|
||
host_numa_node: None,
|
||
hotplug_size: config.hotplug_size,
|
||
hotplugged_size: config.hotplugged_size,
|
||
prefault: config.prefault,
|
||
reserve: config.reserve,
|
||
mergeable: config.mergeable,
|
||
}];
|
||
|
||
Ok((config.size, zones, allow_mem_hotplug))
|
||
}
|
||
}
|
||
|
||
pub fn allocate_address_space(&mut self) -> Result<(), Error> {
|
||
let mut list = Vec::new();
|
||
|
||
for (zone_id, memory_zone) in self.memory_zones.iter() {
|
||
let mut regions: Vec<(Arc<vm_memory::GuestRegionMmap<AtomicBitmap>>, bool)> =
|
||
memory_zone
|
||
.regions()
|
||
.iter()
|
||
.map(|r| (r.clone(), false))
|
||
.collect();
|
||
|
||
if let Some(virtio_mem_zone) = memory_zone.virtio_mem_zone() {
|
||
regions.push((virtio_mem_zone.region().clone(), true));
|
||
}
|
||
|
||
list.push((zone_id.clone(), regions, memory_zone.mergeable));
|
||
}
|
||
|
||
for (zone_id, regions, zone_mergeable) in list {
|
||
for (region, virtio_mem) in regions {
|
||
// SAFETY: guaranteed by GuestRegionMmap invariants
|
||
let slot = unsafe {
|
||
self.create_userspace_mapping(
|
||
region.start_addr().raw_value(),
|
||
region.len().try_into().unwrap(),
|
||
region.as_ptr(),
|
||
zone_mergeable,
|
||
false,
|
||
self.log_dirty,
|
||
)
|
||
}?;
|
||
|
||
let file_offset = if let Some(file_offset) = region.file_offset() {
|
||
file_offset.start()
|
||
} else {
|
||
0
|
||
};
|
||
|
||
self.guest_ram_mappings.push(GuestRamMapping {
|
||
gpa: region.start_addr().raw_value(),
|
||
size: region.len(),
|
||
slot,
|
||
zone_id: zone_id.clone(),
|
||
virtio_mem,
|
||
file_offset,
|
||
});
|
||
self.ram_allocator
|
||
.allocate(Some(region.start_addr()), region.len(), None)
|
||
.ok_or(Error::MemoryRangeAllocation)?;
|
||
}
|
||
}
|
||
|
||
// Allocate SubRegion and Reserved address ranges.
|
||
for region in self.arch_mem_regions.iter() {
|
||
if region.r_type == RegionType::Ram {
|
||
// Ignore the RAM type since ranges have already been allocated
|
||
// based on the GuestMemoryBackend regions.
|
||
continue;
|
||
}
|
||
self.ram_allocator
|
||
.allocate(
|
||
Some(GuestAddress(region.base)),
|
||
region.size as GuestUsize,
|
||
None,
|
||
)
|
||
.ok_or(Error::MemoryRangeAllocation)?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
|
||
pub fn add_uefi_flash(&mut self) -> Result<(), Error> {
|
||
// The UEFI binary requires a flash device at address 0.
|
||
// 4 MiB memory is mapped to simulate the flash.
|
||
let uefi_mem_slot = self.allocate_memory_slot();
|
||
let uefi_region = GuestRegionMmap::new(
|
||
MmapRegion::new(layout::UEFI_SIZE as usize).unwrap(),
|
||
layout::UEFI_START,
|
||
)
|
||
.unwrap();
|
||
const _: () = assert!(size_of::<usize>() == size_of::<u64>());
|
||
|
||
// SAFETY: guaranteed by GuestRegionMmap
|
||
unsafe {
|
||
self.vm
|
||
.create_user_memory_region(
|
||
uefi_mem_slot,
|
||
uefi_region.start_addr().raw_value(),
|
||
uefi_region.len() as usize,
|
||
uefi_region.as_ptr(),
|
||
false,
|
||
false,
|
||
)
|
||
.map_err(Error::CreateUefiFlash)?;
|
||
}
|
||
let uefi_flash =
|
||
GuestMemoryAtomic::new(GuestMemoryMmap::from_regions(vec![uefi_region]).unwrap());
|
||
|
||
self.uefi_flash = Some(uefi_flash);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub fn new(
|
||
vm: Arc<dyn hypervisor::Vm>,
|
||
config: &MemoryConfig,
|
||
prefault: Option<bool>,
|
||
phys_bits: u8,
|
||
#[cfg(feature = "tdx")] tdx_enabled: bool,
|
||
restore_data: Option<&MemoryManagerSnapshotData>,
|
||
existing_memory_files: HashMap<u32, File>,
|
||
) -> Result<Arc<Mutex<MemoryManager>>, Error> {
|
||
trace_scoped!("MemoryManager::new");
|
||
|
||
let user_provided_zones = config.size == 0;
|
||
|
||
let mmio_address_space_size = mmio_address_space_size(phys_bits);
|
||
debug_assert_eq!(
|
||
(((mmio_address_space_size) >> 16) << 16),
|
||
mmio_address_space_size
|
||
);
|
||
let start_of_platform_device_area =
|
||
GuestAddress(mmio_address_space_size - PLATFORM_DEVICE_AREA_SIZE);
|
||
let end_of_device_area = start_of_platform_device_area.unchecked_sub(1);
|
||
|
||
let (ram_size, zones, allow_mem_hotplug) =
|
||
Self::validate_memory_config(config, user_provided_zones)?;
|
||
|
||
let (
|
||
start_of_device_area,
|
||
boot_ram,
|
||
current_ram,
|
||
arch_mem_regions,
|
||
memory_zones,
|
||
guest_memory,
|
||
boot_guest_memory,
|
||
hotplug_slots,
|
||
next_memory_slot,
|
||
selected_slot,
|
||
next_hotplug_slot,
|
||
) = if let Some(data) = restore_data {
|
||
let (regions, memory_zones) = Self::restore_memory_regions_and_zones(
|
||
&data.guest_ram_mappings,
|
||
&zones,
|
||
prefault,
|
||
existing_memory_files,
|
||
config.thp,
|
||
)?;
|
||
let guest_memory =
|
||
GuestMemoryMmap::from_arc_regions(regions).map_err(Error::GuestRegionCollection)?;
|
||
let boot_guest_memory = guest_memory.clone();
|
||
(
|
||
GuestAddress(data.start_of_device_area),
|
||
data.boot_ram,
|
||
data.current_ram,
|
||
data.arch_mem_regions.clone(),
|
||
memory_zones,
|
||
guest_memory,
|
||
boot_guest_memory,
|
||
data.hotplug_slots.clone(),
|
||
data.next_memory_slot,
|
||
data.selected_slot,
|
||
data.next_hotplug_slot,
|
||
)
|
||
} else {
|
||
// Init guest memory
|
||
let arch_mem_regions = arch::arch_memory_regions();
|
||
|
||
let ram_regions: Vec<(GuestAddress, usize)> = arch_mem_regions
|
||
.iter()
|
||
.filter(|r| r.2 == RegionType::Ram)
|
||
.map(|r| (r.0, r.1))
|
||
.collect();
|
||
|
||
let arch_mem_regions: Vec<ArchMemRegion> = arch_mem_regions
|
||
.iter()
|
||
.map(|(a, b, c)| ArchMemRegion {
|
||
base: a.0,
|
||
size: *b,
|
||
r_type: *c,
|
||
})
|
||
.collect();
|
||
|
||
let (mem_regions, mut memory_zones) =
|
||
Self::create_memory_regions_from_zones(&ram_regions, &zones, prefault, config.thp)?;
|
||
|
||
let mut guest_memory = GuestMemoryMmap::from_arc_regions(mem_regions)
|
||
.map_err(Error::GuestRegionCollection)?;
|
||
|
||
let boot_guest_memory = guest_memory.clone();
|
||
|
||
let mut start_of_device_area =
|
||
MemoryManager::start_addr(guest_memory.last_addr(), allow_mem_hotplug)?;
|
||
|
||
// Update list of memory zones for resize.
|
||
for zone in zones.iter() {
|
||
if let Some(memory_zone) = memory_zones.get_mut(&zone.id) {
|
||
if let Some(hotplug_size) = zone.hotplug_size {
|
||
if hotplug_size == 0 {
|
||
error!("'hotplug_size' can't be 0");
|
||
return Err(Error::InvalidHotplugSize);
|
||
}
|
||
|
||
if !user_provided_zones && config.hotplug_method == HotplugMethod::Acpi {
|
||
start_of_device_area = start_of_device_area
|
||
.checked_add(hotplug_size)
|
||
.ok_or(Error::GuestAddressOverFlow)?;
|
||
} else {
|
||
// Alignment must be "natural" i.e. same as size of block
|
||
let start_addr = GuestAddress(
|
||
start_of_device_area
|
||
.0
|
||
.div_ceil(virtio_devices::VIRTIO_MEM_ALIGN_SIZE)
|
||
* virtio_devices::VIRTIO_MEM_ALIGN_SIZE,
|
||
);
|
||
|
||
// When `prefault` is set by vm_restore, memory manager
|
||
// will create ram region with `prefault` option in
|
||
// restore config rather than same option in zone
|
||
let region = MemoryManager::create_ram_region(
|
||
&None,
|
||
0,
|
||
start_addr,
|
||
hotplug_size as usize,
|
||
prefault.unwrap_or(zone.prefault),
|
||
zone.reserve,
|
||
zone.shared,
|
||
zone.hugepages,
|
||
zone.hugepage_size,
|
||
zone.host_numa_node,
|
||
None,
|
||
config.thp,
|
||
)?;
|
||
|
||
guest_memory = guest_memory
|
||
.insert_region(Arc::clone(®ion))
|
||
.map_err(Error::GuestRegionCollection)?;
|
||
|
||
let hotplugged_size = zone.hotplugged_size.unwrap_or(0);
|
||
let region_size = region.len();
|
||
memory_zone.virtio_mem_zone = Some(VirtioMemZone {
|
||
region,
|
||
virtio_device: None,
|
||
hotplugged_size,
|
||
hugepages: zone.hugepages,
|
||
blocks_state: Arc::new(Mutex::new(BlocksState::new(region_size))),
|
||
});
|
||
|
||
start_of_device_area = start_addr
|
||
.checked_add(hotplug_size)
|
||
.ok_or(Error::GuestAddressOverFlow)?;
|
||
}
|
||
}
|
||
} else {
|
||
return Err(Error::MissingZoneIdentifier);
|
||
}
|
||
}
|
||
|
||
let mut hotplug_slots = Vec::with_capacity(HOTPLUG_COUNT);
|
||
hotplug_slots.resize_with(HOTPLUG_COUNT, HotPlugState::default);
|
||
|
||
(
|
||
start_of_device_area,
|
||
ram_size,
|
||
ram_size,
|
||
arch_mem_regions,
|
||
memory_zones,
|
||
guest_memory,
|
||
boot_guest_memory,
|
||
hotplug_slots,
|
||
0,
|
||
0,
|
||
0,
|
||
)
|
||
};
|
||
|
||
let guest_memory = GuestMemoryAtomic::new(guest_memory);
|
||
|
||
let allocator = Arc::new(Mutex::new(
|
||
SystemAllocator::new(
|
||
GuestAddress(0),
|
||
1 << 16,
|
||
start_of_platform_device_area,
|
||
PLATFORM_DEVICE_AREA_SIZE,
|
||
#[cfg(target_arch = "x86_64")]
|
||
&[GsiApic::new(
|
||
X86_64_IRQ_BASE,
|
||
ioapic::NUM_IOAPIC_PINS as u32 - X86_64_IRQ_BASE,
|
||
)],
|
||
)
|
||
.ok_or(Error::CreateSystemAllocator)?,
|
||
));
|
||
|
||
#[cfg(not(feature = "tdx"))]
|
||
let dynamic = true;
|
||
#[cfg(feature = "tdx")]
|
||
let dynamic = !tdx_enabled;
|
||
|
||
let acpi_address = if dynamic
|
||
&& config.hotplug_method == HotplugMethod::Acpi
|
||
&& (config.hotplug_size.unwrap_or_default() > 0)
|
||
{
|
||
Some(
|
||
allocator
|
||
.lock()
|
||
.unwrap()
|
||
.allocate_platform_mmio_addresses(None, MEMORY_MANAGER_ACPI_SIZE as u64, None)
|
||
.ok_or(Error::AllocateMmioAddress)?,
|
||
)
|
||
} else {
|
||
None
|
||
};
|
||
|
||
// The start of device area and RAM area are placed next to each other.
|
||
let end_of_ram_area = start_of_device_area.unchecked_sub(1);
|
||
let ram_allocator = AddressAllocator::new(GuestAddress(0), start_of_device_area.0).unwrap();
|
||
|
||
#[allow(unused_mut)]
|
||
let mut memory_manager = MemoryManager {
|
||
boot_guest_memory,
|
||
guest_memory,
|
||
next_memory_slot: Arc::new(AtomicU32::new(next_memory_slot)),
|
||
memory_slot_free_list: Arc::new(Mutex::new(Vec::new())),
|
||
start_of_device_area,
|
||
end_of_device_area,
|
||
end_of_ram_area,
|
||
vm,
|
||
hotplug_slots,
|
||
selected_slot,
|
||
mergeable: config.mergeable,
|
||
allocator,
|
||
hotplug_method: config.hotplug_method,
|
||
boot_ram,
|
||
current_ram,
|
||
next_hotplug_slot,
|
||
shared: config.shared,
|
||
hugepages: config.hugepages,
|
||
hugepage_size: config.hugepage_size,
|
||
prefault: config.prefault,
|
||
reserve: config.reserve,
|
||
user_provided_zones,
|
||
snapshot_memory_ranges: MemoryRangeTable::default(),
|
||
memory_zones,
|
||
guest_ram_mappings: Vec::new(),
|
||
uffd_handler: None,
|
||
acpi_address,
|
||
log_dirty: dynamic, // Cannot log dirty pages on a TD
|
||
arch_mem_regions,
|
||
ram_allocator,
|
||
dynamic,
|
||
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
|
||
uefi_flash: None,
|
||
thp: config.thp,
|
||
};
|
||
|
||
Ok(Arc::new(Mutex::new(memory_manager)))
|
||
}
|
||
|
||
#[expect(clippy::too_many_arguments)]
|
||
pub fn new_from_snapshot(
|
||
snapshot: &Snapshot,
|
||
vm: Arc<dyn hypervisor::Vm>,
|
||
config: &MemoryConfig,
|
||
source_url: Option<&str>,
|
||
prefault: bool,
|
||
memory_restore_mode: MemoryRestoreMode,
|
||
phys_bits: u8,
|
||
exit_evt: &EventFd,
|
||
) -> Result<Arc<Mutex<MemoryManager>>, Error> {
|
||
if let Some(source_url) = source_url {
|
||
let mut memory_file_path = url_to_path(source_url).map_err(Error::Restore)?;
|
||
memory_file_path.push(String::from(SNAPSHOT_FILENAME));
|
||
|
||
let mem_snapshot: MemoryManagerSnapshotData =
|
||
snapshot.to_state().map_err(Error::Restore)?;
|
||
|
||
let mm = MemoryManager::new(
|
||
vm,
|
||
config,
|
||
Some(prefault),
|
||
phys_bits,
|
||
#[cfg(feature = "tdx")]
|
||
false,
|
||
Some(&mem_snapshot),
|
||
Default::default(),
|
||
)?;
|
||
|
||
if memory_restore_mode == MemoryRestoreMode::OnDemand {
|
||
mm.lock().unwrap().restore_by_uffd(
|
||
&memory_file_path,
|
||
&mem_snapshot.memory_ranges,
|
||
exit_evt,
|
||
)?;
|
||
} else {
|
||
mm.lock()
|
||
.unwrap()
|
||
.fill_saved_regions(memory_file_path, &mem_snapshot.memory_ranges)?;
|
||
}
|
||
|
||
Ok(mm)
|
||
} else {
|
||
Err(Error::RestoreMissingSourceUrl)
|
||
}
|
||
}
|
||
|
||
fn memfd_create(name: &ffi::CStr, flags: u32) -> Result<RawFd, io::Error> {
|
||
// SAFETY: FFI call with correct arguments
|
||
let res = unsafe { libc::syscall(libc::SYS_memfd_create, name.as_ptr(), flags) };
|
||
|
||
if res < 0 {
|
||
Err(io::Error::last_os_error())
|
||
} else {
|
||
Ok(res as RawFd)
|
||
}
|
||
}
|
||
|
||
fn mbind(
|
||
addr: *mut u8,
|
||
len: u64,
|
||
mode: u32,
|
||
nodemask: &[u64],
|
||
maxnode: u64,
|
||
flags: u32,
|
||
) -> Result<(), io::Error> {
|
||
// SAFETY: FFI call with correct arguments
|
||
let res = unsafe {
|
||
libc::syscall(
|
||
libc::SYS_mbind,
|
||
addr.cast::<libc::c_void>(),
|
||
len,
|
||
mode,
|
||
nodemask.as_ptr(),
|
||
maxnode,
|
||
flags,
|
||
)
|
||
};
|
||
|
||
if res < 0 {
|
||
Err(io::Error::last_os_error())
|
||
} else {
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
fn create_anonymous_file(
|
||
size: usize,
|
||
hugepages: bool,
|
||
hugepage_size: Option<u64>,
|
||
) -> Result<FileOffset, Error> {
|
||
let fd = Self::memfd_create(
|
||
&ffi::CString::new("ch_ram").unwrap(),
|
||
libc::MFD_CLOEXEC
|
||
| if hugepages {
|
||
libc::MFD_HUGETLB
|
||
| if let Some(hugepage_size) = hugepage_size {
|
||
/*
|
||
* From the Linux kernel:
|
||
* Several system calls take a flag to request "hugetlb" huge pages.
|
||
* Without further specification, these system calls will use the
|
||
* system's default huge page size. If a system supports multiple
|
||
* huge page sizes, the desired huge page size can be specified in
|
||
* bits [26:31] of the flag arguments. The value in these 6 bits
|
||
* will encode the log2 of the huge page size.
|
||
*/
|
||
|
||
hugepage_size.trailing_zeros() << 26
|
||
} else {
|
||
// Use the system default huge page size
|
||
0
|
||
}
|
||
} else {
|
||
0
|
||
},
|
||
)
|
||
.map_err(Error::SharedFileCreate)?;
|
||
|
||
// SAFETY: fd is valid
|
||
let f = unsafe { File::from_raw_fd(fd) };
|
||
f.set_len(size as u64).map_err(Error::SharedFileSetLen)?;
|
||
|
||
Ok(FileOffset::new(f, 0))
|
||
}
|
||
|
||
fn open_backing_file(
|
||
backing_file: &PathBuf,
|
||
file_offset: u64,
|
||
shared: bool,
|
||
) -> Result<FileOffset, Error> {
|
||
if backing_file.is_dir() {
|
||
Err(Error::DirectoryAsBackingFileForMemory)
|
||
} else {
|
||
let f = OpenOptions::new()
|
||
.read(true)
|
||
.write(shared)
|
||
.open(backing_file)
|
||
.map_err(Error::SharedFileCreate)?;
|
||
|
||
Ok(FileOffset::new(f, file_offset))
|
||
}
|
||
}
|
||
|
||
#[expect(clippy::too_many_arguments)]
|
||
pub fn create_ram_region_raw(
|
||
backing_file: &Option<PathBuf>,
|
||
file_offset: u64,
|
||
size: usize,
|
||
prefault: bool,
|
||
reserve: bool,
|
||
shared: bool,
|
||
hugepages: bool,
|
||
hugepage_size: Option<u64>,
|
||
host_numa_node: Option<u32>,
|
||
existing_memory_file: Option<File>,
|
||
thp: bool,
|
||
) -> Result<MmapRegion<AtomicBitmap>, Error> {
|
||
let mut mmap_flags = if reserve { 0 } else { libc::MAP_NORESERVE };
|
||
|
||
// The duplication of mmap_flags ORing here is unfortunate but it also makes
|
||
// the complexity of the handling clear.
|
||
let fo = if let Some(f) = existing_memory_file {
|
||
// It must be MAP_SHARED as we wouldn't already have an FD
|
||
mmap_flags |= libc::MAP_SHARED;
|
||
Some(FileOffset::new(f, file_offset))
|
||
} else if let Some(backing_file) = backing_file {
|
||
if shared {
|
||
mmap_flags |= libc::MAP_SHARED;
|
||
} else {
|
||
mmap_flags |= libc::MAP_PRIVATE;
|
||
}
|
||
Some(Self::open_backing_file(backing_file, file_offset, shared)?)
|
||
} else if shared || hugepages {
|
||
// For hugepages we must also MAP_SHARED otherwise we will trigger #4805
|
||
// because the MAP_PRIVATE will trigger CoW against the backing file with
|
||
// the VFIO pinning
|
||
mmap_flags |= libc::MAP_SHARED;
|
||
Some(Self::create_anonymous_file(size, hugepages, hugepage_size)?)
|
||
} else {
|
||
mmap_flags |= libc::MAP_PRIVATE | libc::MAP_ANONYMOUS;
|
||
None
|
||
};
|
||
|
||
let region = MmapRegion::build(fo, size, libc::PROT_READ | libc::PROT_WRITE, mmap_flags)
|
||
.map_err(Error::GuestMemoryRegion)?;
|
||
|
||
// Apply NUMA policy if needed.
|
||
if let Some(node) = host_numa_node {
|
||
let addr = region.as_ptr();
|
||
let len = region.size() as u64;
|
||
let mode = MPOL_BIND;
|
||
let mut nodemask: Vec<u64> = Vec::new();
|
||
let flags = MPOL_MF_STRICT | MPOL_MF_MOVE;
|
||
|
||
// Linux is kind of buggy in the way it interprets maxnode as it
|
||
// will cut off the last node. That's why we have to add 1 to what
|
||
// we would consider as the proper maxnode value.
|
||
let maxnode = node as u64 + 1 + 1;
|
||
|
||
// Allocate the right size for the vector.
|
||
nodemask.resize((node as usize / 64) + 1, 0);
|
||
|
||
// Fill the global bitmask through the nodemask vector.
|
||
let idx = (node / 64) as usize;
|
||
let shift = node % 64;
|
||
nodemask[idx] |= 1u64 << shift;
|
||
|
||
// Policies are enforced by using MPOL_MF_MOVE flag as it will
|
||
// force the kernel to move all pages that might have been already
|
||
// allocated to the proper set of NUMA nodes. MPOL_MF_STRICT is
|
||
// used to throw an error if MPOL_MF_MOVE didn't succeed.
|
||
// MPOL_BIND is the selected mode as it specifies a strict policy
|
||
// that restricts memory allocation to the nodes specified in the
|
||
// nodemask.
|
||
Self::mbind(addr, len, mode, &nodemask, maxnode, flags)
|
||
.map_err(Error::ApplyNumaPolicy)?;
|
||
}
|
||
|
||
// Prefault the region if needed, in parallel.
|
||
if prefault {
|
||
let page_size =
|
||
Self::get_prefault_align_size(backing_file, hugepages, hugepage_size)? as usize;
|
||
|
||
if !is_aligned(size, page_size) {
|
||
warn!("Prefaulting memory size {size} misaligned with page size {page_size}");
|
||
}
|
||
|
||
let num_pages = size / page_size;
|
||
|
||
let num_threads = Self::get_prefault_num_threads(page_size, num_pages);
|
||
|
||
let pages_per_thread = num_pages / num_threads;
|
||
let remainder = num_pages % num_threads;
|
||
|
||
let barrier = Arc::new(Barrier::new(num_threads));
|
||
thread::scope(|s| -> Result<(), Error> {
|
||
let r = ®ion;
|
||
let mut handles = Vec::new();
|
||
for i in 0..num_threads {
|
||
let barrier = Arc::clone(&barrier);
|
||
let handle = s.spawn(move || {
|
||
// Wait until all threads have been spawned to avoid contention
|
||
// over mmap_sem between thread stack allocation and page faulting.
|
||
barrier.wait();
|
||
let pages = pages_per_thread + if i < remainder { 1 } else { 0 };
|
||
let offset = page_size * ((i * pages_per_thread) + cmp::min(i, remainder));
|
||
// SAFETY: FFI call with correct arguments
|
||
let ret = unsafe {
|
||
let addr = r.as_ptr().add(offset);
|
||
libc::madvise(addr.cast(), pages * page_size, libc::MADV_POPULATE_WRITE)
|
||
};
|
||
if ret != 0 {
|
||
let e = io::Error::last_os_error();
|
||
return Err(e);
|
||
}
|
||
Ok(())
|
||
});
|
||
handles.push(handle);
|
||
}
|
||
|
||
for handle in handles {
|
||
handle
|
||
.join()
|
||
.map_err(|e| {
|
||
Error::PrefaultMemory(io::Error::other(format!(
|
||
"Prefault thread panicked: {e:?}"
|
||
)))
|
||
})?
|
||
.map_err(Error::PrefaultMemory)?;
|
||
}
|
||
|
||
Ok(())
|
||
})?;
|
||
}
|
||
|
||
info!(
|
||
"RAM region mapping at 0x{:x} (size = 0x{:x})",
|
||
region.as_ptr() as u64,
|
||
size
|
||
);
|
||
|
||
if thp && !hugepages {
|
||
// SAFETY: FFI call with correct arguments
|
||
let ret = unsafe { libc::madvise(region.as_ptr().cast(), size, libc::MADV_HUGEPAGE) };
|
||
if ret != 0 {
|
||
let e = io::Error::last_os_error();
|
||
warn!("Failed to mark pages as THP eligible: {e}");
|
||
} else {
|
||
debug!("Successfully marked pages as THP eligible");
|
||
}
|
||
}
|
||
|
||
Ok(region)
|
||
}
|
||
|
||
#[expect(clippy::too_many_arguments)]
|
||
pub fn create_ram_region(
|
||
backing_file: &Option<PathBuf>,
|
||
file_offset: u64,
|
||
start_addr: GuestAddress,
|
||
size: usize,
|
||
prefault: bool,
|
||
reserve: bool,
|
||
shared: bool,
|
||
hugepages: bool,
|
||
hugepage_size: Option<u64>,
|
||
host_numa_node: Option<u32>,
|
||
existing_memory_file: Option<File>,
|
||
thp: bool,
|
||
) -> Result<Arc<GuestRegionMmap>, Error> {
|
||
let r = Self::create_ram_region_raw(
|
||
backing_file,
|
||
file_offset,
|
||
size,
|
||
prefault,
|
||
reserve,
|
||
shared,
|
||
hugepages,
|
||
hugepage_size,
|
||
host_numa_node,
|
||
existing_memory_file,
|
||
thp,
|
||
)?;
|
||
|
||
Ok(Arc::new(GuestRegionMmap::new(r, start_addr).ok_or(
|
||
Error::GuestMemory(MmapError::InvalidGuestAddress(start_addr)),
|
||
)?))
|
||
}
|
||
|
||
// Duplicate of `memory_zone_get_align_size` that does not require a `zone`
|
||
fn get_prefault_align_size(
|
||
backing_file: &Option<PathBuf>,
|
||
hugepages: bool,
|
||
hugepage_size: Option<u64>,
|
||
) -> Result<u64, Error> {
|
||
// SAFETY: FFI call. Trivially safe.
|
||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as u64 };
|
||
match (hugepages, hugepage_size, backing_file) {
|
||
(false, _, _) => Ok(page_size),
|
||
(true, Some(hugepage_size), _) => Ok(hugepage_size),
|
||
(true, None, _) => {
|
||
// There are two scenarios here:
|
||
// - `hugepages` is enabled but `hugepage_size` is not specified:
|
||
// Call `statfs` for `/dev/hugepages` for getting the default size of hugepage
|
||
// - The backing file is specified:
|
||
// Call `statfs` for the file and get its `f_bsize`. If the value is larger than the page
|
||
// size of normal page, just use the `f_bsize` because the file is in a hugetlbfs. If the
|
||
// value is less than or equal to the page size, just use the page size.
|
||
let path = backing_file
|
||
.as_ref()
|
||
.map_or(Ok("/dev/hugepages"), |pathbuf| {
|
||
pathbuf.to_str().ok_or(Error::InvalidMemoryParameters)
|
||
})?;
|
||
let align_size = cmp::max(page_size, statfs_get_bsize(path)?);
|
||
Ok(align_size)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn get_prefault_num_threads(page_size: usize, num_pages: usize) -> usize {
|
||
// Do not create more threads than processors available.
|
||
let mut n = thread::available_parallelism()
|
||
.map_or(1, |val| val.get())
|
||
.min(MAX_PREFAULT_THREAD_COUNT);
|
||
|
||
// Do not create more threads than pages being allocated.
|
||
n = cmp::min(n, num_pages);
|
||
|
||
// Do not create threads to allocate less than 64 MiB of memory.
|
||
n = cmp::min(n, cmp::max(1, page_size * num_pages / (64 * (1 << 26))));
|
||
|
||
n
|
||
}
|
||
|
||
// Update the GuestMemoryMmap with the new range
|
||
fn add_region(&mut self, region: Arc<GuestRegionMmap>) -> Result<(), Error> {
|
||
let guest_memory = self
|
||
.guest_memory
|
||
.memory()
|
||
.insert_region(region)
|
||
.map_err(Error::GuestRegionCollection)?;
|
||
self.guest_memory.lock().unwrap().replace(guest_memory);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
//
|
||
// Calculate the start address of an area next to RAM.
|
||
//
|
||
// If memory hotplug is allowed, the start address needs to be aligned
|
||
// (rounded-up) to 128MiB boundary.
|
||
// If memory hotplug is not allowed, there is no alignment required.
|
||
// And it must also start at the 64bit start.
|
||
fn start_addr(mem_end: GuestAddress, allow_mem_hotplug: bool) -> Result<GuestAddress, Error> {
|
||
let mut start_addr = if allow_mem_hotplug {
|
||
GuestAddress(mem_end.0 | ((128 << 20) - 1))
|
||
} else {
|
||
mem_end
|
||
};
|
||
|
||
start_addr = start_addr
|
||
.checked_add(1)
|
||
.ok_or(Error::GuestAddressOverFlow)?;
|
||
|
||
#[cfg(not(target_arch = "riscv64"))]
|
||
if mem_end < layout::MEM_32BIT_RESERVED_START {
|
||
return Ok(layout::RAM_64BIT_START);
|
||
}
|
||
|
||
Ok(start_addr)
|
||
}
|
||
|
||
pub fn add_ram_region(
|
||
&mut self,
|
||
start_addr: GuestAddress,
|
||
size: usize,
|
||
) -> Result<Arc<GuestRegionMmap>, Error> {
|
||
// Allocate memory for the region
|
||
let region = MemoryManager::create_ram_region(
|
||
&None,
|
||
0,
|
||
start_addr,
|
||
size,
|
||
self.prefault,
|
||
self.reserve,
|
||
self.shared,
|
||
self.hugepages,
|
||
self.hugepage_size,
|
||
None,
|
||
None,
|
||
self.thp,
|
||
)?;
|
||
|
||
// Map it into the guest
|
||
// SAFETY: guaranteed by GuestMmapRegion invariants
|
||
let slot = unsafe {
|
||
self.create_userspace_mapping(
|
||
region.start_addr().0,
|
||
region.len().try_into().unwrap(),
|
||
region.as_ptr(),
|
||
self.memory_zones
|
||
.get(DEFAULT_MEMORY_ZONE)
|
||
.map_or(self.mergeable, |z| z.mergeable),
|
||
false,
|
||
self.log_dirty,
|
||
)
|
||
}?;
|
||
self.guest_ram_mappings.push(GuestRamMapping {
|
||
gpa: region.start_addr().raw_value(),
|
||
size: region.len(),
|
||
slot,
|
||
zone_id: DEFAULT_MEMORY_ZONE.to_string(),
|
||
virtio_mem: false,
|
||
file_offset: 0,
|
||
});
|
||
|
||
self.add_region(Arc::clone(®ion))?;
|
||
|
||
Ok(region)
|
||
}
|
||
|
||
fn hotplug_ram_region(&mut self, size: usize) -> Result<Arc<GuestRegionMmap>, Error> {
|
||
info!("Hotplugging new RAM: {size}");
|
||
|
||
// Check that there is a free slot
|
||
if self.next_hotplug_slot >= HOTPLUG_COUNT {
|
||
return Err(Error::NoSlotAvailable);
|
||
}
|
||
|
||
// "Inserted" DIMM must have a size that is a multiple of 128MiB
|
||
if !size.is_multiple_of(128 << 20) {
|
||
return Err(Error::InvalidSize);
|
||
}
|
||
|
||
let start_addr = MemoryManager::start_addr(self.guest_memory.memory().last_addr(), true)?;
|
||
|
||
if start_addr
|
||
.checked_add((size - 1).try_into().unwrap())
|
||
.unwrap()
|
||
> self.end_of_ram_area
|
||
{
|
||
return Err(Error::InsufficientHotplugRam);
|
||
}
|
||
|
||
let region = self.add_ram_region(start_addr, size)?;
|
||
|
||
// Add region to the list of regions associated with the default
|
||
// memory zone.
|
||
if let Some(memory_zone) = self.memory_zones.get_mut(DEFAULT_MEMORY_ZONE) {
|
||
memory_zone.regions.push(Arc::clone(®ion));
|
||
}
|
||
|
||
// Tell the allocator
|
||
self.ram_allocator
|
||
.allocate(Some(start_addr), size as GuestUsize, None)
|
||
.ok_or(Error::MemoryRangeAllocation)?;
|
||
|
||
// Update the slot so that it can be queried via the I/O port
|
||
let slot = &mut self.hotplug_slots[self.next_hotplug_slot];
|
||
slot.active = true;
|
||
slot.inserting = true;
|
||
slot.base = region.start_addr().0;
|
||
slot.length = region.len();
|
||
|
||
self.next_hotplug_slot += 1;
|
||
|
||
Ok(region)
|
||
}
|
||
|
||
pub fn guest_memory(&self) -> GuestMemoryAtomic<GuestMemoryMmap> {
|
||
self.guest_memory.clone()
|
||
}
|
||
|
||
pub fn boot_guest_memory(&self) -> GuestMemoryMmap {
|
||
self.boot_guest_memory.clone()
|
||
}
|
||
|
||
pub fn allocator(&self) -> Arc<Mutex<SystemAllocator>> {
|
||
self.allocator.clone()
|
||
}
|
||
|
||
pub fn start_of_device_area(&self) -> GuestAddress {
|
||
self.start_of_device_area
|
||
}
|
||
|
||
pub fn end_of_device_area(&self) -> GuestAddress {
|
||
self.end_of_device_area
|
||
}
|
||
|
||
pub fn memory_slot_allocator(&mut self) -> MemorySlotAllocator {
|
||
let memory_slot_free_list = Arc::clone(&self.memory_slot_free_list);
|
||
let next_memory_slot = Arc::clone(&self.next_memory_slot);
|
||
MemorySlotAllocator::new(next_memory_slot, memory_slot_free_list)
|
||
}
|
||
|
||
pub fn allocate_memory_slot(&mut self) -> u32 {
|
||
self.memory_slot_allocator().next_memory_slot()
|
||
}
|
||
|
||
/// # Safety
|
||
///
|
||
/// `userspace_addr` and `memory_size` must be and remain valid
|
||
/// until `remove_userspace_mapping` is called.
|
||
pub unsafe fn create_userspace_mapping(
|
||
&mut self,
|
||
guest_phys_addr: u64,
|
||
memory_size: usize,
|
||
userspace_addr: *mut u8,
|
||
mergeable: bool,
|
||
readonly: bool,
|
||
log_dirty: bool,
|
||
) -> Result<u32, Error> {
|
||
let slot = self.allocate_memory_slot();
|
||
|
||
info!(
|
||
"Creating userspace mapping: {guest_phys_addr:x} -> {userspace_addr_:x} {memory_size:x}, slot {slot}",
|
||
userspace_addr_ = userspace_addr as u64
|
||
);
|
||
|
||
// SAFETY: caller promises parameters are correct.
|
||
unsafe {
|
||
self.vm
|
||
.create_user_memory_region(
|
||
slot,
|
||
guest_phys_addr,
|
||
memory_size,
|
||
userspace_addr,
|
||
readonly,
|
||
log_dirty,
|
||
)
|
||
.map_err(Error::CreateUserMemoryRegion)?;
|
||
}
|
||
|
||
// SAFETY: the address and size are valid since the
|
||
// mmap succeeded.
|
||
let ret = unsafe {
|
||
libc::madvise(
|
||
userspace_addr.cast(),
|
||
memory_size as libc::size_t,
|
||
libc::MADV_DONTDUMP,
|
||
)
|
||
};
|
||
if ret != 0 {
|
||
let e = io::Error::last_os_error();
|
||
warn!("Failed to mark mapping as MADV_DONTDUMP: {e}");
|
||
}
|
||
|
||
// Mark the pages as mergeable if explicitly asked for.
|
||
if mergeable {
|
||
// SAFETY: the address and size are valid since the
|
||
// mmap succeeded.
|
||
let ret = unsafe {
|
||
libc::madvise(
|
||
userspace_addr.cast(),
|
||
memory_size as libc::size_t,
|
||
libc::MADV_MERGEABLE,
|
||
)
|
||
};
|
||
if ret != 0 {
|
||
let err = io::Error::last_os_error();
|
||
// Safe to unwrap because the error is constructed with
|
||
// last_os_error(), which ensures the output will be Some().
|
||
let errno = err.raw_os_error().unwrap();
|
||
if errno == libc::EINVAL {
|
||
warn!("kernel not configured with CONFIG_KSM");
|
||
} else {
|
||
warn!("madvise error: {err}");
|
||
}
|
||
warn!("failed to mark pages as mergeable");
|
||
}
|
||
}
|
||
|
||
info!(
|
||
"Created userspace mapping: {guest_phys_addr:x} -> {userspace_addr_:x} {memory_size:x}",
|
||
userspace_addr_ = userspace_addr as u64
|
||
);
|
||
|
||
Ok(slot)
|
||
}
|
||
|
||
/// # Safety
|
||
///
|
||
/// `userspace_addr` and `memory_size` must have previously been passed
|
||
/// to `create_userspace_mapping`.
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// If this function fails there is no way to clean up resources and you
|
||
/// should probably crash the process.
|
||
pub unsafe fn remove_userspace_mapping(
|
||
&mut self,
|
||
guest_phys_addr: u64,
|
||
memory_size: usize,
|
||
userspace_addr: *mut u8,
|
||
mergeable: bool,
|
||
slot: u32,
|
||
) -> Result<(), Error> {
|
||
// SAFETY: Caller promises parameters are correct.
|
||
unsafe {
|
||
self.vm
|
||
.remove_user_memory_region(
|
||
slot,
|
||
guest_phys_addr,
|
||
memory_size,
|
||
userspace_addr,
|
||
false, /* readonly -- don't care */
|
||
false, /* log dirty */
|
||
)
|
||
.map_err(Error::RemoveUserMemoryRegion)?;
|
||
}
|
||
|
||
// Mark the pages as unmergeable if there were previously marked as
|
||
// mergeable.
|
||
if mergeable {
|
||
// SAFETY: the address and size are valid as the region was
|
||
// previously advised.
|
||
let ret = unsafe {
|
||
libc::madvise(
|
||
userspace_addr.cast(),
|
||
memory_size as libc::size_t,
|
||
libc::MADV_UNMERGEABLE,
|
||
)
|
||
};
|
||
if ret != 0 {
|
||
let err = io::Error::last_os_error();
|
||
// Safe to unwrap because the error is constructed with
|
||
// last_os_error(), which ensures the output will be Some().
|
||
let errno = err.raw_os_error().unwrap();
|
||
if errno == libc::EINVAL {
|
||
warn!("kernel not configured with CONFIG_KSM");
|
||
} else {
|
||
warn!("madvise error: {err}");
|
||
}
|
||
warn!("failed to mark pages as unmergeable");
|
||
}
|
||
}
|
||
|
||
info!(
|
||
"Removed userspace mapping: {guest_phys_addr:x} -> {userspace_addr_:x} {memory_size:x}",
|
||
userspace_addr_ = userspace_addr as u64
|
||
);
|
||
|
||
self.memory_slot_allocator().free_memory_slot(slot);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub fn virtio_mem_resize(&mut self, id: &str, size: u64) -> Result<(), Error> {
|
||
if let Some(memory_zone) = self.memory_zones.get_mut(id) {
|
||
if let Some(virtio_mem_zone) = &mut memory_zone.virtio_mem_zone {
|
||
if let Some(virtio_mem_device) = virtio_mem_zone.virtio_device.as_ref() {
|
||
virtio_mem_device
|
||
.lock()
|
||
.unwrap()
|
||
.resize(size)
|
||
.map_err(Error::VirtioMemResizeFail)?;
|
||
}
|
||
|
||
// Keep the hotplugged_size up to date.
|
||
virtio_mem_zone.hotplugged_size = size;
|
||
} else {
|
||
error!("Failed resizing virtio-mem region: No virtio-mem handler");
|
||
return Err(Error::MissingVirtioMemHandler);
|
||
}
|
||
|
||
return Ok(());
|
||
}
|
||
|
||
error!("Failed resizing virtio-mem region: Unknown memory zone");
|
||
Err(Error::UnknownMemoryZone)
|
||
}
|
||
|
||
/// In case this function resulted in adding a new memory region to the
|
||
/// guest memory, the new region is returned to the caller. The virtio-mem
|
||
/// use case never adds a new region as the whole hotpluggable memory has
|
||
/// already been allocated at boot time.
|
||
pub fn resize(&mut self, desired_ram: u64) -> Result<Option<Arc<GuestRegionMmap>>, Error> {
|
||
if self.user_provided_zones {
|
||
error!(
|
||
"Not allowed to resize guest memory when backed with user \
|
||
defined memory zones."
|
||
);
|
||
return Err(Error::InvalidResizeWithMemoryZones);
|
||
}
|
||
|
||
let mut region: Option<Arc<GuestRegionMmap>> = None;
|
||
match self.hotplug_method {
|
||
HotplugMethod::VirtioMem => {
|
||
if desired_ram < self.boot_ram {
|
||
return Err(Error::InvalidResizeBelowBootSize(
|
||
desired_ram,
|
||
self.boot_ram,
|
||
));
|
||
}
|
||
|
||
if !self.dynamic {
|
||
return Ok(region);
|
||
}
|
||
|
||
self.virtio_mem_resize(DEFAULT_MEMORY_ZONE, desired_ram - self.boot_ram)?;
|
||
self.current_ram = desired_ram;
|
||
}
|
||
HotplugMethod::Acpi => {
|
||
if desired_ram > self.current_ram {
|
||
if !self.dynamic {
|
||
return Ok(region);
|
||
}
|
||
|
||
region =
|
||
Some(self.hotplug_ram_region((desired_ram - self.current_ram) as usize)?);
|
||
self.current_ram = desired_ram;
|
||
}
|
||
}
|
||
}
|
||
Ok(region)
|
||
}
|
||
|
||
pub fn resize_zone(&mut self, zone: &MemoryZoneConfig, desired_ram: u64) -> Result<(), Error> {
|
||
if !self.user_provided_zones {
|
||
error!(
|
||
"Not allowed to resize guest memory zone when no zone is \
|
||
defined."
|
||
);
|
||
return Err(Error::ResizeZone);
|
||
}
|
||
|
||
if desired_ram < zone.size {
|
||
return Err(Error::InvalidResizeBelowBootSize(desired_ram, zone.size));
|
||
}
|
||
|
||
self.virtio_mem_resize(&zone.id, desired_ram - zone.size)
|
||
}
|
||
|
||
pub fn is_hardlink(f: &File) -> bool {
|
||
let mut stat = MaybeUninit::<libc::stat>::uninit();
|
||
// SAFETY: FFI call with correct arguments
|
||
let ret = unsafe { libc::fstat(f.as_raw_fd(), stat.as_mut_ptr()) };
|
||
if ret != 0 {
|
||
error!("Couldn't fstat the backing file");
|
||
return false;
|
||
}
|
||
|
||
// SAFETY: stat is valid
|
||
unsafe { (*stat.as_ptr()).st_nlink as usize > 0 }
|
||
}
|
||
|
||
pub fn virtio_mem_plugged_size(&self) -> u64 {
|
||
self.memory_zones
|
||
.values()
|
||
.filter_map(|zone| {
|
||
zone.virtio_mem_zone
|
||
.as_ref()?
|
||
.virtio_device
|
||
.as_ref()
|
||
.map(|dev| dev.lock().unwrap().plugged_size())
|
||
})
|
||
.sum()
|
||
}
|
||
|
||
pub fn memory_zones(&self) -> &MemoryZones {
|
||
&self.memory_zones
|
||
}
|
||
|
||
pub fn memory_zones_mut(&mut self) -> &mut MemoryZones {
|
||
&mut self.memory_zones
|
||
}
|
||
|
||
pub fn memory_range_table(
|
||
&self,
|
||
mode: MemoryRangePolicy,
|
||
) -> result::Result<MemoryRangeTable, MigratableError> {
|
||
let mut table = MemoryRangeTable::default();
|
||
|
||
for memory_zone in self.memory_zones.values() {
|
||
if let Some(virtio_mem_zone) = memory_zone.virtio_mem_zone() {
|
||
table.extend(virtio_mem_zone.plugged_ranges());
|
||
}
|
||
|
||
for region in memory_zone.regions() {
|
||
let gpa = region.start_addr().raw_value();
|
||
let len = region.len();
|
||
|
||
let shared_file_offset = region
|
||
.file_offset()
|
||
.filter(|_| region.flags() & libc::MAP_SHARED == libc::MAP_SHARED);
|
||
|
||
match mode {
|
||
MemoryRangePolicy::Full => {}
|
||
MemoryRangePolicy::SkipPersisted => {
|
||
// Content already persisted through the backing file.
|
||
if let Some(file_offset) = shared_file_offset
|
||
&& Self::is_hardlink(file_offset.file())
|
||
{
|
||
continue;
|
||
}
|
||
}
|
||
MemoryRangePolicy::Sparse => {
|
||
if let Some(file_offset) = shared_file_offset
|
||
&& let Some(ranges) = Self::sparse_extents(file_offset, gpa, len)
|
||
{
|
||
for range in ranges {
|
||
table.push(range);
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
table.push(MemoryRange { gpa, length: len });
|
||
}
|
||
}
|
||
|
||
Ok(table)
|
||
}
|
||
|
||
/// Return the populated extents of a memory range, or `None` if the
|
||
/// caller should fall back onto sending the entire region.
|
||
fn sparse_extents(file_offset: &FileOffset, gpa: u64, len: u64) -> Option<Vec<MemoryRange>> {
|
||
let offset = file_offset.start();
|
||
let end = offset + len;
|
||
let mut cursor = offset;
|
||
let mut ranges = Vec::new();
|
||
loop {
|
||
match next_data_extent(file_offset.file().as_fd(), cursor, end) {
|
||
Ok(Some((data_offset, ext_len))) => {
|
||
let range_gpa = data_offset
|
||
.checked_sub(offset)
|
||
.and_then(|delta| gpa.checked_add(delta))?;
|
||
ranges.push(MemoryRange {
|
||
gpa: range_gpa,
|
||
length: ext_len,
|
||
});
|
||
cursor = data_offset + ext_len;
|
||
}
|
||
Ok(None) => break,
|
||
Err(_) => return None,
|
||
}
|
||
}
|
||
Some(ranges)
|
||
}
|
||
|
||
pub fn snapshot_data(&self) -> MemoryManagerSnapshotData {
|
||
MemoryManagerSnapshotData {
|
||
memory_ranges: self.snapshot_memory_ranges.clone(),
|
||
guest_ram_mappings: self.guest_ram_mappings.clone(),
|
||
start_of_device_area: self.start_of_device_area.0,
|
||
boot_ram: self.boot_ram,
|
||
current_ram: self.current_ram,
|
||
arch_mem_regions: self.arch_mem_regions.clone(),
|
||
hotplug_slots: self.hotplug_slots.clone(),
|
||
next_memory_slot: self.next_memory_slot.load(Ordering::SeqCst),
|
||
selected_slot: self.selected_slot,
|
||
next_hotplug_slot: self.next_hotplug_slot,
|
||
}
|
||
}
|
||
|
||
pub fn memory_slot_fds(&self) -> HashMap<u32, RawFd> {
|
||
let mut memory_slot_fds = HashMap::new();
|
||
for guest_ram_mapping in &self.guest_ram_mappings {
|
||
let slot = guest_ram_mapping.slot;
|
||
let guest_memory = self.guest_memory.memory();
|
||
let file = guest_memory
|
||
.find_region(GuestAddress(guest_ram_mapping.gpa))
|
||
.unwrap()
|
||
.file_offset()
|
||
.unwrap()
|
||
.file();
|
||
memory_slot_fds.insert(slot, file.as_raw_fd());
|
||
}
|
||
memory_slot_fds
|
||
}
|
||
|
||
pub fn acpi_address(&self) -> Option<GuestAddress> {
|
||
self.acpi_address
|
||
}
|
||
|
||
pub fn num_guest_ram_mappings(&self) -> u32 {
|
||
self.guest_ram_mappings.len() as u32
|
||
}
|
||
|
||
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
|
||
pub fn uefi_flash(&self) -> GuestMemoryAtomic<GuestMemoryMmap> {
|
||
self.uefi_flash.as_ref().unwrap().clone()
|
||
}
|
||
|
||
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
||
pub fn coredump_memory_regions(&self, mem_offset: u64) -> CoredumpMemoryRegions {
|
||
let mut mapping_sorted_by_gpa = self.guest_ram_mappings.clone();
|
||
mapping_sorted_by_gpa.sort_by_key(|m| m.gpa);
|
||
|
||
let mut mem_offset_in_elf = mem_offset;
|
||
let mut ram_maps = BTreeMap::new();
|
||
for mapping in mapping_sorted_by_gpa.iter() {
|
||
ram_maps.insert(
|
||
mapping.gpa,
|
||
CoredumpMemoryRegion {
|
||
mem_offset_in_elf,
|
||
mem_size: mapping.size,
|
||
},
|
||
);
|
||
mem_offset_in_elf += mapping.size;
|
||
}
|
||
|
||
CoredumpMemoryRegions { ram_maps }
|
||
}
|
||
|
||
#[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]
|
||
pub fn coredump_iterate_save_mem(
|
||
&mut self,
|
||
dump_state: &DumpState,
|
||
) -> result::Result<(), GuestDebuggableError> {
|
||
let snapshot_memory_ranges = self
|
||
.memory_range_table(MemoryRangePolicy::Full)
|
||
.map_err(|e| GuestDebuggableError::Coredump(e.into()))?;
|
||
|
||
if snapshot_memory_ranges.is_empty() {
|
||
return Ok(());
|
||
}
|
||
|
||
let coredump_file = dump_state.file.as_ref().unwrap();
|
||
|
||
let guest_memory = self.guest_memory.memory();
|
||
let mut total_bytes: u64 = 0;
|
||
|
||
for range in snapshot_memory_ranges.regions() {
|
||
let mut offset: u64 = 0;
|
||
loop {
|
||
let bytes_written = guest_memory
|
||
.write_volatile_to(
|
||
GuestAddress(range.gpa + offset),
|
||
&mut coredump_file.as_fd(),
|
||
(range.length - offset) as usize,
|
||
)
|
||
.map_err(|e| GuestDebuggableError::Coredump(e.into()))?;
|
||
offset += bytes_written as u64;
|
||
total_bytes += bytes_written as u64;
|
||
|
||
if offset == range.length {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
debug!("coredump total bytes {total_bytes}");
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
struct MemoryNotify {
|
||
slot_id: usize,
|
||
}
|
||
|
||
impl Aml for MemoryNotify {
|
||
fn to_aml_bytes(&self, sink: &mut dyn acpi_tables::AmlSink) {
|
||
let object = aml::Path::new(&format!("M{:03}", self.slot_id));
|
||
aml::If::new(
|
||
&aml::Equal::new(&aml::Arg(0), &self.slot_id),
|
||
vec![&aml::Notify::new(&object, &aml::Arg(1))],
|
||
)
|
||
.to_aml_bytes(sink);
|
||
}
|
||
}
|
||
|
||
struct MemorySlot {
|
||
slot_id: usize,
|
||
}
|
||
|
||
impl Aml for MemorySlot {
|
||
fn to_aml_bytes(&self, sink: &mut dyn acpi_tables::AmlSink) {
|
||
aml::Device::new(
|
||
format!("M{:03}", self.slot_id).as_str().into(),
|
||
vec![
|
||
&aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0C80")),
|
||
&aml::Name::new("_UID".into(), &self.slot_id),
|
||
/*
|
||
_STA return value:
|
||
Bit [0] – Set if the device is present.
|
||
Bit [1] – Set if the device is enabled and decoding its resources.
|
||
Bit [2] – Set if the device should be shown in the UI.
|
||
Bit [3] – Set if the device is functioning properly (cleared if device failed its diagnostics).
|
||
Bit [4] – Set if the battery is present.
|
||
Bits [31:5] – Reserved (must be cleared).
|
||
*/
|
||
&aml::Method::new(
|
||
"_STA".into(),
|
||
0,
|
||
false,
|
||
// Call into MSTA method which will interrogate device
|
||
vec![&aml::Return::new(&aml::MethodCall::new(
|
||
"MSTA".into(),
|
||
vec![&self.slot_id],
|
||
))],
|
||
),
|
||
// Get details of memory
|
||
&aml::Method::new(
|
||
"_CRS".into(),
|
||
0,
|
||
false,
|
||
// Call into MCRS which provides actual memory details
|
||
vec![&aml::Return::new(&aml::MethodCall::new(
|
||
"MCRS".into(),
|
||
vec![&self.slot_id],
|
||
))],
|
||
),
|
||
],
|
||
)
|
||
.to_aml_bytes(sink);
|
||
}
|
||
}
|
||
|
||
struct MemorySlots {
|
||
slots: usize,
|
||
}
|
||
|
||
impl Aml for MemorySlots {
|
||
fn to_aml_bytes(&self, sink: &mut dyn acpi_tables::AmlSink) {
|
||
for slot_id in 0..self.slots {
|
||
MemorySlot { slot_id }.to_aml_bytes(sink);
|
||
}
|
||
}
|
||
}
|
||
|
||
struct MemoryMethods {
|
||
slots: usize,
|
||
}
|
||
|
||
impl Aml for MemoryMethods {
|
||
fn to_aml_bytes(&self, sink: &mut dyn acpi_tables::AmlSink) {
|
||
// Add "MTFY" notification method
|
||
let mut memory_notifies = Vec::new();
|
||
for slot_id in 0..self.slots {
|
||
memory_notifies.push(MemoryNotify { slot_id });
|
||
}
|
||
|
||
let mut memory_notifies_refs: Vec<&dyn Aml> = Vec::new();
|
||
for memory_notifier in memory_notifies.iter() {
|
||
memory_notifies_refs.push(memory_notifier);
|
||
}
|
||
|
||
aml::Method::new("MTFY".into(), 2, true, memory_notifies_refs).to_aml_bytes(sink);
|
||
|
||
// MSCN method
|
||
aml::Method::new(
|
||
"MSCN".into(),
|
||
0,
|
||
true,
|
||
vec![
|
||
// Take lock defined above
|
||
&aml::Acquire::new("MLCK".into(), 0xffff),
|
||
&aml::Store::new(&aml::Local(0), &aml::ZERO),
|
||
&aml::While::new(
|
||
&aml::LessThan::new(&aml::Local(0), &self.slots),
|
||
vec![
|
||
// Write slot number (in first argument) to I/O port via field
|
||
&aml::Store::new(&aml::Path::new("\\_SB_.MHPC.MSEL"), &aml::Local(0)),
|
||
// Check if MINS bit is set (inserting)
|
||
&aml::If::new(
|
||
&aml::Equal::new(&aml::Path::new("\\_SB_.MHPC.MINS"), &aml::ONE),
|
||
// Notify device if it is
|
||
vec![
|
||
&aml::MethodCall::new(
|
||
"MTFY".into(),
|
||
vec![&aml::Local(0), &aml::ONE],
|
||
),
|
||
// Reset MINS bit
|
||
&aml::Store::new(&aml::Path::new("\\_SB_.MHPC.MINS"), &aml::ONE),
|
||
],
|
||
),
|
||
// Check if MRMV bit is set
|
||
&aml::If::new(
|
||
&aml::Equal::new(&aml::Path::new("\\_SB_.MHPC.MRMV"), &aml::ONE),
|
||
// Notify device if it is (with the eject constant 0x3)
|
||
vec![
|
||
&aml::MethodCall::new("MTFY".into(), vec![&aml::Local(0), &3u8]),
|
||
// Reset MRMV bit
|
||
&aml::Store::new(&aml::Path::new("\\_SB_.MHPC.MRMV"), &aml::ONE),
|
||
],
|
||
),
|
||
&aml::Add::new(&aml::Local(0), &aml::Local(0), &aml::ONE),
|
||
],
|
||
),
|
||
// Release lock
|
||
&aml::Release::new("MLCK".into()),
|
||
],
|
||
)
|
||
.to_aml_bytes(sink);
|
||
|
||
// Memory status method
|
||
aml::Method::new(
|
||
"MSTA".into(),
|
||
1,
|
||
true,
|
||
vec![
|
||
// Take lock defined above
|
||
&aml::Acquire::new("MLCK".into(), 0xffff),
|
||
// Write slot number (in first argument) to I/O port via field
|
||
&aml::Store::new(&aml::Path::new("\\_SB_.MHPC.MSEL"), &aml::Arg(0)),
|
||
&aml::Store::new(&aml::Local(0), &aml::ZERO),
|
||
// Check if MEN_ bit is set, if so make the local variable 0xf (see _STA for details of meaning)
|
||
&aml::If::new(
|
||
&aml::Equal::new(&aml::Path::new("\\_SB_.MHPC.MEN_"), &aml::ONE),
|
||
vec![&aml::Store::new(&aml::Local(0), &0xfu8)],
|
||
),
|
||
// Release lock
|
||
&aml::Release::new("MLCK".into()),
|
||
// Return 0 or 0xf
|
||
&aml::Return::new(&aml::Local(0)),
|
||
],
|
||
)
|
||
.to_aml_bytes(sink);
|
||
|
||
// Memory range method
|
||
aml::Method::new(
|
||
"MCRS".into(),
|
||
1,
|
||
true,
|
||
vec![
|
||
// Take lock defined above
|
||
&aml::Acquire::new("MLCK".into(), 0xffff),
|
||
// Write slot number (in first argument) to I/O port via field
|
||
&aml::Store::new(&aml::Path::new("\\_SB_.MHPC.MSEL"), &aml::Arg(0)),
|
||
&aml::Name::new(
|
||
"MR64".into(),
|
||
&aml::ResourceTemplate::new(vec![&aml::AddressSpace::new_memory(
|
||
aml::AddressSpaceCacheable::Cacheable,
|
||
true,
|
||
0x0000_0000_0000_0000u64,
|
||
0xFFFF_FFFF_FFFF_FFFEu64,
|
||
None,
|
||
)]),
|
||
),
|
||
&aml::CreateQWordField::new(
|
||
&aml::Path::new("MINL"),
|
||
&aml::Path::new("MR64"),
|
||
&14usize,
|
||
),
|
||
&aml::CreateDWordField::new(
|
||
&aml::Path::new("MINH"),
|
||
&aml::Path::new("MR64"),
|
||
&18usize,
|
||
),
|
||
&aml::CreateQWordField::new(
|
||
&aml::Path::new("MAXL"),
|
||
&aml::Path::new("MR64"),
|
||
&22usize,
|
||
),
|
||
&aml::CreateDWordField::new(
|
||
&aml::Path::new("MAXH"),
|
||
&aml::Path::new("MR64"),
|
||
&26usize,
|
||
),
|
||
&aml::CreateQWordField::new(
|
||
&aml::Path::new("LENL"),
|
||
&aml::Path::new("MR64"),
|
||
&38usize,
|
||
),
|
||
&aml::CreateDWordField::new(
|
||
&aml::Path::new("LENH"),
|
||
&aml::Path::new("MR64"),
|
||
&42usize,
|
||
),
|
||
&aml::Store::new(&aml::Path::new("MINL"), &aml::Path::new("\\_SB_.MHPC.MHBL")),
|
||
&aml::Store::new(&aml::Path::new("MINH"), &aml::Path::new("\\_SB_.MHPC.MHBH")),
|
||
&aml::Store::new(&aml::Path::new("LENL"), &aml::Path::new("\\_SB_.MHPC.MHLL")),
|
||
&aml::Store::new(&aml::Path::new("LENH"), &aml::Path::new("\\_SB_.MHPC.MHLH")),
|
||
&aml::Add::new(
|
||
&aml::Path::new("MAXL"),
|
||
&aml::Path::new("MINL"),
|
||
&aml::Path::new("LENL"),
|
||
),
|
||
&aml::Add::new(
|
||
&aml::Path::new("MAXH"),
|
||
&aml::Path::new("MINH"),
|
||
&aml::Path::new("LENH"),
|
||
),
|
||
&aml::If::new(
|
||
&aml::LessThan::new(&aml::Path::new("MAXL"), &aml::Path::new("MINL")),
|
||
vec![&aml::Add::new(
|
||
&aml::Path::new("MAXH"),
|
||
&aml::ONE,
|
||
&aml::Path::new("MAXH"),
|
||
)],
|
||
),
|
||
&aml::Subtract::new(&aml::Path::new("MAXL"), &aml::Path::new("MAXL"), &aml::ONE),
|
||
// Release lock
|
||
&aml::Release::new("MLCK".into()),
|
||
&aml::Return::new(&aml::Path::new("MR64")),
|
||
],
|
||
)
|
||
.to_aml_bytes(sink);
|
||
}
|
||
}
|
||
|
||
impl Aml for MemoryManager {
|
||
fn to_aml_bytes(&self, sink: &mut dyn acpi_tables::AmlSink) {
|
||
if let Some(acpi_address) = self.acpi_address {
|
||
// Memory Hotplug Controller
|
||
aml::Device::new(
|
||
"_SB_.MHPC".into(),
|
||
vec![
|
||
&aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0A06")),
|
||
&aml::Name::new("_UID".into(), &"Memory Hotplug Controller"),
|
||
// Mutex to protect concurrent access as we write to choose slot and then read back status
|
||
&aml::Mutex::new("MLCK".into(), 0),
|
||
&aml::Name::new(
|
||
"_CRS".into(),
|
||
&aml::ResourceTemplate::new(vec![&aml::AddressSpace::new_memory(
|
||
aml::AddressSpaceCacheable::NotCacheable,
|
||
true,
|
||
acpi_address.0,
|
||
acpi_address.0 + MEMORY_MANAGER_ACPI_SIZE as u64 - 1,
|
||
None,
|
||
)]),
|
||
),
|
||
// OpRegion and Fields map MMIO range into individual field values
|
||
&aml::OpRegion::new(
|
||
"MHPR".into(),
|
||
aml::OpRegionSpace::SystemMemory,
|
||
&(acpi_address.0 as usize),
|
||
&MEMORY_MANAGER_ACPI_SIZE,
|
||
),
|
||
&aml::Field::new(
|
||
"MHPR".into(),
|
||
aml::FieldAccessType::DWord,
|
||
aml::FieldLockRule::NoLock,
|
||
aml::FieldUpdateRule::Preserve,
|
||
vec![
|
||
aml::FieldEntry::Named(*b"MHBL", 32), // Base (low 4 bytes)
|
||
aml::FieldEntry::Named(*b"MHBH", 32), // Base (high 4 bytes)
|
||
aml::FieldEntry::Named(*b"MHLL", 32), // Length (low 4 bytes)
|
||
aml::FieldEntry::Named(*b"MHLH", 32), // Length (high 4 bytes)
|
||
],
|
||
),
|
||
&aml::Field::new(
|
||
"MHPR".into(),
|
||
aml::FieldAccessType::DWord,
|
||
aml::FieldLockRule::NoLock,
|
||
aml::FieldUpdateRule::Preserve,
|
||
vec![
|
||
aml::FieldEntry::Reserved(128),
|
||
aml::FieldEntry::Named(*b"MHPX", 32), // PXM
|
||
],
|
||
),
|
||
&aml::Field::new(
|
||
"MHPR".into(),
|
||
aml::FieldAccessType::Byte,
|
||
aml::FieldLockRule::NoLock,
|
||
aml::FieldUpdateRule::WriteAsZeroes,
|
||
vec![
|
||
aml::FieldEntry::Reserved(160),
|
||
aml::FieldEntry::Named(*b"MEN_", 1), // Enabled
|
||
aml::FieldEntry::Named(*b"MINS", 1), // Inserting
|
||
aml::FieldEntry::Named(*b"MRMV", 1), // Removing
|
||
aml::FieldEntry::Named(*b"MEJ0", 1), // Ejecting
|
||
],
|
||
),
|
||
&aml::Field::new(
|
||
"MHPR".into(),
|
||
aml::FieldAccessType::DWord,
|
||
aml::FieldLockRule::NoLock,
|
||
aml::FieldUpdateRule::Preserve,
|
||
vec![
|
||
aml::FieldEntry::Named(*b"MSEL", 32), // Selector
|
||
aml::FieldEntry::Named(*b"MOEV", 32), // Event
|
||
aml::FieldEntry::Named(*b"MOSC", 32), // OSC
|
||
],
|
||
),
|
||
&MemoryMethods {
|
||
slots: self.hotplug_slots.len(),
|
||
},
|
||
&MemorySlots {
|
||
slots: self.hotplug_slots.len(),
|
||
},
|
||
],
|
||
)
|
||
.to_aml_bytes(sink);
|
||
} else {
|
||
aml::Device::new(
|
||
"_SB_.MHPC".into(),
|
||
vec![
|
||
&aml::Name::new("_HID".into(), &aml::EISAName::new("PNP0A06")),
|
||
&aml::Name::new("_UID".into(), &"Memory Hotplug Controller"),
|
||
// Empty MSCN for GED
|
||
&aml::Method::new("MSCN".into(), 0, true, vec![]),
|
||
],
|
||
)
|
||
.to_aml_bytes(sink);
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Pausable for MemoryManager {}
|
||
|
||
impl Drop for MemoryManager {
|
||
fn drop(&mut self) {
|
||
self.stop_uffd_handler();
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Serialize, Deserialize)]
|
||
pub struct MemoryManagerSnapshotData {
|
||
memory_ranges: MemoryRangeTable,
|
||
guest_ram_mappings: Vec<GuestRamMapping>,
|
||
start_of_device_area: u64,
|
||
boot_ram: u64,
|
||
current_ram: u64,
|
||
arch_mem_regions: Vec<ArchMemRegion>,
|
||
hotplug_slots: Vec<HotPlugState>,
|
||
next_memory_slot: u32,
|
||
selected_slot: usize,
|
||
next_hotplug_slot: usize,
|
||
}
|
||
|
||
impl Snapshottable for MemoryManager {
|
||
fn id(&self) -> String {
|
||
MEMORY_MANAGER_SNAPSHOT_ID.to_string()
|
||
}
|
||
|
||
fn snapshot(&mut self) -> result::Result<Snapshot, MigratableError> {
|
||
let memory_ranges = self.memory_range_table(MemoryRangePolicy::SkipPersisted)?;
|
||
|
||
// Store locally this list of ranges as it will be used through the
|
||
// Transportable::send() implementation. The point is to avoid the
|
||
// duplication of code regarding the creation of the path for each
|
||
// region. The 'snapshot' step creates the list of memory regions,
|
||
// including information about the need to copy a memory region or
|
||
// not. This saves the 'send' step having to go through the same
|
||
// process, and instead it can directly proceed with storing the
|
||
// memory range content for the ranges requiring it.
|
||
self.snapshot_memory_ranges = memory_ranges;
|
||
|
||
Ok(Snapshot::from_data(SnapshotData::new_from_state(
|
||
&self.snapshot_data(),
|
||
)?))
|
||
}
|
||
}
|
||
|
||
impl Transportable for MemoryManager {
|
||
fn send(
|
||
&self,
|
||
_snapshot: &Snapshot,
|
||
destination_url: &str,
|
||
) -> result::Result<(), MigratableError> {
|
||
if self.snapshot_memory_ranges.is_empty() {
|
||
return Ok(());
|
||
}
|
||
|
||
let mut memory_file_path = url_to_path(destination_url)?;
|
||
memory_file_path.push(String::from(SNAPSHOT_FILENAME));
|
||
|
||
let mut memory_file = OpenOptions::new()
|
||
.read(true)
|
||
.write(true)
|
||
.create_new(true)
|
||
.open(&memory_file_path)
|
||
.with_context(|| format!("Error creating memory snapshot file {memory_file_path:?}"))
|
||
.map_err(MigratableError::MigrateSend)?;
|
||
|
||
let total_len: u64 = self
|
||
.snapshot_memory_ranges
|
||
.regions()
|
||
.iter()
|
||
.map(|r| r.length)
|
||
.sum();
|
||
|
||
// Pre-size the file so per-region write_at lands at the dense-layout
|
||
// offset. On filesystems that support sparse files unwritten bytes
|
||
// become real holes; on others the kernel zero-fills the allocation,
|
||
// which is still byte-correct. If extending the file is not
|
||
// supported by the destination filesystem (some FUSE backends
|
||
// reject ftruncate-extend with EOPNOTSUPP), fall back to the dense
|
||
// write path which never writes past the growing EOF.
|
||
let sparse_layout = memory_file.set_len(total_len).is_ok();
|
||
|
||
let guest_memory = self.guest_memory.memory();
|
||
let mut file_cursor: u64 = 0;
|
||
|
||
for range in self.snapshot_memory_ranges.regions() {
|
||
let mut wrote_sparse = false;
|
||
if sparse_layout
|
||
&& let Some(region) = guest_memory.find_region(GuestAddress(range.gpa))
|
||
&& (region.flags() & libc::MAP_SHARED) == libc::MAP_SHARED
|
||
&& let Some(file_offset) = region.file_offset()
|
||
{
|
||
let region_base = region.start_addr().raw_value();
|
||
let in_fd_off = file_offset.start()
|
||
+ range
|
||
.gpa
|
||
.checked_sub(region_base)
|
||
.expect("range outside its region");
|
||
wrote_sparse = write_region_sparse(
|
||
file_offset.file(),
|
||
in_fd_off,
|
||
&memory_file,
|
||
file_cursor,
|
||
range.length,
|
||
)
|
||
.context("Error writing sparse memory snapshot region")
|
||
.map_err(MigratableError::MigrateSend)?;
|
||
}
|
||
|
||
if !wrote_sparse {
|
||
// Dense fallback: anonymous mmap or hugetlbfs (no
|
||
// SEEK_HOLE). Match the dense layout by seeking to
|
||
// file_cursor and streaming bytes via the existing
|
||
// volatile copy.
|
||
memory_file
|
||
.seek(SeekFrom::Start(file_cursor))
|
||
.context("Error seeking memory snapshot file")
|
||
.map_err(MigratableError::MigrateSend)?;
|
||
let mut offset: u64 = 0;
|
||
// Manual partial-write loop preserves the workaround for
|
||
// https://github.com/rust-vmm/vm-memory/issues/174
|
||
loop {
|
||
let bytes_written = guest_memory
|
||
.write_volatile_to(
|
||
GuestAddress(range.gpa + offset),
|
||
&mut memory_file,
|
||
(range.length - offset) as usize,
|
||
)
|
||
.context("Error writing dense memory snapshot region")
|
||
.map_err(MigratableError::MigrateSend)?;
|
||
offset += bytes_written as u64;
|
||
if offset == range.length {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
file_cursor += range.length;
|
||
}
|
||
|
||
debug_assert_eq!(file_cursor, total_len);
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Migratable for MemoryManager {
|
||
// Start the dirty log in the hypervisor (kvm/mshv).
|
||
// Also, reset the dirty bitmap logged by the vmm.
|
||
// Just before we do a bulk copy we want to start/clear the dirty log so that
|
||
// pages touched during our bulk copy are tracked.
|
||
fn start_dirty_log(&mut self) -> result::Result<(), MigratableError> {
|
||
self.vm
|
||
.start_dirty_log()
|
||
.context("Error starting VM dirty log")
|
||
.map_err(MigratableError::MigrateSend)?;
|
||
|
||
for r in self.guest_memory.memory().iter() {
|
||
(**r).bitmap().reset();
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn stop_dirty_log(&mut self) -> result::Result<(), MigratableError> {
|
||
self.vm
|
||
.stop_dirty_log()
|
||
.context("Error stopping VM dirty log")
|
||
.map_err(MigratableError::MigrateSend)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
// Generate a table for the pages that are dirty. The dirty pages are collapsed
|
||
// together in the table if they are contiguous.
|
||
fn dirty_log(&mut self) -> result::Result<MemoryRangeTable, MigratableError> {
|
||
let mut table = MemoryRangeTable::default();
|
||
for r in &self.guest_ram_mappings {
|
||
let vm_dirty_bitmap = self
|
||
.vm
|
||
.get_dirty_log(r.slot, r.gpa, r.size)
|
||
.context("Error getting VM dirty log")
|
||
.map_err(MigratableError::MigrateSend)?;
|
||
let vmm_dirty_bitmap = match self.guest_memory.memory().find_region(GuestAddress(r.gpa))
|
||
{
|
||
Some(region) => {
|
||
assert!(region.start_addr().raw_value() == r.gpa);
|
||
assert!(region.len() == r.size);
|
||
(**region).bitmap().get_and_reset()
|
||
}
|
||
None => {
|
||
return Err(MigratableError::MigrateSend(anyhow!(
|
||
"Error finding 'guest memory region' with address {:x}",
|
||
r.gpa
|
||
)));
|
||
}
|
||
};
|
||
|
||
let dirty_bitmap = vm_dirty_bitmap
|
||
.iter()
|
||
.zip(vmm_dirty_bitmap.iter())
|
||
.map(|(x, y)| x | y);
|
||
|
||
let sub_table = MemoryRangeTable::from_dirty_bitmap(dirty_bitmap, r.gpa, 4096);
|
||
|
||
if sub_table.regions().is_empty() {
|
||
debug!("Dirty Memory Range Table is empty");
|
||
} else {
|
||
debug!("Dirty Memory Range Table:");
|
||
for range in sub_table.regions() {
|
||
debug!("GPA: {:x} size: {} (KiB)", range.gpa, range.length / 1024);
|
||
}
|
||
}
|
||
|
||
table.extend(sub_table);
|
||
}
|
||
Ok(table)
|
||
}
|
||
}
|