Skip to content

Commit e335f54

Browse files
committed
Add API for getting VM's dirty pages, and add some bitmap utility functions.
Signed-off-by: Ludvig Liljenberg <[email protected]>
1 parent 9e6169a commit e335f54

File tree

8 files changed

+365
-5
lines changed

8 files changed

+365
-5
lines changed

src/hyperlight_common/src/mem.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
pub const PAGE_SHIFT: u64 = 12;
1818
pub const PAGE_SIZE: u64 = 1 << 12;
1919
pub const PAGE_SIZE_USIZE: usize = 1 << 12;
20+
// The number of pages in 1 "block". A single u64 can be used as bitmap to keep track of all dirty pages in a block.
21+
pub const PAGES_IN_BLOCK: usize = 64;
2022

2123
/// A memory region in the guest address space
2224
#[derive(Debug, Clone, Copy)]

src/hyperlight_host/src/hypervisor/hyperv_linux.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ use std::sync::Arc;
2929
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3030

3131
use log::{LevelFilter, error};
32+
#[cfg(mshv3)]
33+
use mshv_bindings::MSHV_GPAP_ACCESS_OP_CLEAR;
3234
#[cfg(mshv2)]
3335
use mshv_bindings::hv_message;
3436
use mshv_bindings::{
@@ -76,6 +78,9 @@ use crate::sandbox::SandboxConfiguration;
7678
use crate::sandbox::uninitialized::SandboxRuntimeConfig;
7779
use crate::{Result, log_then_return, new_error};
7880

81+
#[cfg(mshv2)]
82+
const CLEAR_DIRTY_BIT_FLAG: u64 = 0b100;
83+
7984
#[cfg(gdb)]
8085
mod debug {
8186
use std::sync::{Arc, Mutex};
@@ -302,6 +307,7 @@ pub(crate) struct HypervLinuxDriver {
302307
vcpu_fd: VcpuFd,
303308
entrypoint: u64,
304309
mem_regions: Vec<MemoryRegion>,
310+
n_initial_regions: usize,
305311
orig_rsp: GuestPtr,
306312
interrupt_handle: Arc<LinuxInterruptHandle>,
307313

@@ -351,6 +357,7 @@ impl HypervLinuxDriver {
351357
vm_fd.initialize()?;
352358
vm_fd
353359
};
360+
vm_fd.enable_dirty_page_tracking()?;
354361

355362
let mut vcpu_fd = vm_fd.create_vcpu(0)?;
356363

@@ -391,13 +398,31 @@ impl HypervLinuxDriver {
391398
(None, None)
392399
};
393400

401+
let mut base_pfn = u64::MAX;
402+
let mut total_size: usize = 0;
403+
394404
mem_regions.iter().try_for_each(|region| {
395-
let mshv_region = region.to_owned().into();
405+
let mshv_region: mshv_user_mem_region = region.to_owned().into();
406+
if base_pfn == u64::MAX {
407+
base_pfn = mshv_region.guest_pfn;
408+
}
409+
total_size += mshv_region.size as usize;
396410
vm_fd.map_user_memory(mshv_region)
397411
})?;
398412

399413
Self::setup_initial_sregs(&mut vcpu_fd, pml4_ptr.absolute()?)?;
400414

415+
// get/clear the dirty page bitmap, mshv sets all the bit dirty at initialization
416+
// if we dont clear them then we end up taking a complete snapsot of memory page by page which gets
417+
// progressively slower as the sandbox size increases
418+
// the downside of doing this here is that the call to get_dirty_log will takes longer as the number of pages increase
419+
// but for larger sandboxes its easily cheaper than copying all the pages
420+
421+
#[cfg(mshv2)]
422+
vm_fd.get_dirty_log(base_pfn, total_size, CLEAR_DIRTY_BIT_FLAG)?;
423+
#[cfg(mshv3)]
424+
vm_fd.get_dirty_log(base_pfn, total_size, MSHV_GPAP_ACCESS_OP_CLEAR as u8)?;
425+
401426
let interrupt_handle = Arc::new(LinuxInterruptHandle {
402427
running: AtomicU64::new(0),
403428
cancel_requested: AtomicBool::new(false),
@@ -428,6 +453,7 @@ impl HypervLinuxDriver {
428453
page_size: 0,
429454
vm_fd,
430455
vcpu_fd,
456+
n_initial_regions: mem_regions.len(),
431457
mem_regions,
432458
entrypoint: entrypoint_ptr.absolute()?,
433459
orig_rsp: rsp_ptr,
@@ -885,6 +911,69 @@ impl Hypervisor for HypervLinuxDriver {
885911
self.interrupt_handle.clone()
886912
}
887913

914+
// TODO: Implement getting additional host-mapped dirty pages.
915+
fn get_and_clear_dirty_pages(&mut self) -> Result<Vec<u64>> {
916+
let first_mshv_region: mshv_user_mem_region = self
917+
.mem_regions
918+
.first()
919+
.ok_or(new_error!(
920+
"tried to get dirty page bitmap of 0-sized region"
921+
))?
922+
.to_owned()
923+
.into();
924+
925+
let n_contiguous = self
926+
.mem_regions
927+
.windows(2)
928+
.take_while(|window| window[0].guest_region.end == window[1].guest_region.start)
929+
.count()
930+
+ 1; // +1 because windows(2) gives us n-1 pairs for n regions
931+
932+
if n_contiguous != self.n_initial_regions {
933+
return Err(new_error!(
934+
"get_and_clear_dirty_pages: not all regions are contiguous, expected {} but got {}",
935+
self.n_initial_regions,
936+
n_contiguous
937+
));
938+
}
939+
940+
let sandbox_total_size = self
941+
.mem_regions
942+
.iter()
943+
.take(n_contiguous)
944+
.map(|r| r.guest_region.len())
945+
.sum();
946+
947+
let mut sandbox_dirty_pages = self.vm_fd.get_dirty_log(
948+
first_mshv_region.guest_pfn,
949+
sandbox_total_size,
950+
#[cfg(mshv2)]
951+
CLEAR_DIRTY_BIT_FLAG,
952+
#[cfg(mshv3)]
953+
(MSHV_GPAP_ACCESS_OP_CLEAR as u8),
954+
)?;
955+
956+
// Sanitize bits beyond sandbox
957+
//
958+
// TODO: remove this once bug in mshv is fixed. The bug makes it possible
959+
// for non-mapped memory to incorrectly be marked dirty. To fix this, we just zero out
960+
// any bits that are not within the sandbox size.
961+
let sandbox_pages = sandbox_total_size / self.page_size;
962+
let last_block_idx = sandbox_dirty_pages.len().saturating_sub(1);
963+
if let Some(last_block) = sandbox_dirty_pages.last_mut() {
964+
let last_block_start_page = last_block_idx * 64;
965+
let last_block_end_page = last_block_start_page + 64;
966+
967+
// If the last block extends beyond the sandbox, clear the invalid bits
968+
if last_block_end_page > sandbox_pages {
969+
let valid_bits_in_last_block = sandbox_pages - last_block_start_page;
970+
let mask = (1u64 << valid_bits_in_last_block) - 1;
971+
*last_block &= mask;
972+
}
973+
}
974+
Ok(sandbox_dirty_pages)
975+
}
976+
888977
#[cfg(crashdump)]
889978
fn crashdump_context(&self) -> Result<Option<super::crashdump::CrashDumpContext>> {
890979
if self.rt_cfg.guest_core_dump {

src/hyperlight_host/src/hypervisor/hyperv_windows.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ use super::{
5858
use super::{HyperlightExit, Hypervisor, InterruptHandle, VirtualCPU};
5959
use crate::hypervisor::fpu::FP_CONTROL_WORD_DEFAULT;
6060
use crate::hypervisor::wrappers::WHvGeneralRegisters;
61+
use crate::mem::bitmap::new_page_bitmap;
6162
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
6263
use crate::mem::ptr::{GuestPtr, RawPtr};
6364
#[cfg(crashdump)]
@@ -615,13 +616,21 @@ impl Hypervisor for HypervWindowsDriver {
615616
Ok(())
616617
}
617618

619+
fn get_and_clear_dirty_pages(&mut self) -> Result<Vec<u64>> {
620+
// For now we just mark all pages dirty which is the equivalent of taking a full snapshot
621+
let total_size = self.mem_regions.iter().map(|r| r.guest_region.len()).sum();
622+
new_page_bitmap(total_size, true)
623+
}
624+
618625
#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
619626
unsafe fn map_region(&mut self, _rgn: &MemoryRegion) -> Result<()> {
620-
log_then_return!("Mapping host memory into the guest not yet supported on this platform");
627+
// TODO: when adding support, also update `get_and_clear_dirty_pages`, see kvm/mshv for details
628+
log_then_return!("Mapping host memory into the guest not yet supported on this platform.");
621629
}
622630

623631
#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
624632
unsafe fn unmap_regions(&mut self, n: u64) -> Result<()> {
633+
// TODO: when adding support, also update `get_and_clear_dirty_pages`, see kvm/mshv for details
625634
if n > 0 {
626635
log_then_return!(
627636
"Mapping host memory into the guest not yet supported on this platform"

src/hyperlight_host/src/hypervisor/kvm.rs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::sync::Arc;
2121
use std::sync::Mutex;
2222
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2323

24+
use hyperlight_common::mem::{PAGE_SIZE_USIZE, PAGES_IN_BLOCK};
2425
use kvm_bindings::{kvm_fpu, kvm_regs, kvm_userspace_memory_region};
2526
use kvm_ioctls::Cap::UserMemory;
2627
use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd};
@@ -43,7 +44,8 @@ use super::{
4344
use super::{HyperlightExit, Hypervisor, InterruptHandle, LinuxInterruptHandle, VirtualCPU};
4445
#[cfg(gdb)]
4546
use crate::HyperlightError;
46-
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
47+
use crate::mem::bitmap::{bit_index_iterator, new_page_bitmap};
48+
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
4749
use crate::mem::ptr::{GuestPtr, RawPtr};
4850
use crate::sandbox::SandboxConfiguration;
4951
#[cfg(crashdump)]
@@ -290,6 +292,7 @@ pub(crate) struct KVMDriver {
290292
entrypoint: u64,
291293
orig_rsp: GuestPtr,
292294
mem_regions: Vec<MemoryRegion>,
295+
n_initial_regions: usize,
293296
interrupt_handle: Arc<LinuxInterruptHandle>,
294297

295298
#[cfg(gdb)]
@@ -372,6 +375,7 @@ impl KVMDriver {
372375
vcpu_fd,
373376
entrypoint,
374377
orig_rsp: rsp_gp,
378+
n_initial_regions: mem_regions.len(),
375379
mem_regions,
376380
interrupt_handle: interrupt_handle.clone(),
377381
#[cfg(gdb)]
@@ -750,6 +754,61 @@ impl Hypervisor for KVMDriver {
750754
self.interrupt_handle.clone()
751755
}
752756

757+
// TODO: Implement getting additional host-mapped dirty pages.
758+
fn get_and_clear_dirty_pages(&mut self) -> Result<Vec<u64>> {
759+
let n_contiguous = self
760+
.mem_regions
761+
.windows(2)
762+
.take_while(|window| window[0].guest_region.end == window[1].guest_region.start)
763+
.count()
764+
+ 1; // +1 because windows(2) gives us n-1 pairs for n regions
765+
766+
if n_contiguous != self.n_initial_regions {
767+
return Err(new_error!(
768+
"get_and_clear_dirty_pages: not all regions are contiguous, expected {} but got {}",
769+
self.n_initial_regions,
770+
n_contiguous
771+
));
772+
}
773+
let mut page_indices = vec![];
774+
let mut current_page = 0;
775+
776+
// Iterate over all memory regions and get the dirty pages for each region ignoring guard pages which cannot be dirty
777+
for (i, mem_region) in self.mem_regions.iter().take(n_contiguous).enumerate() {
778+
let num_pages = mem_region.guest_region.len() / PAGE_SIZE_USIZE;
779+
let bitmap = match mem_region.flags {
780+
MemoryRegionFlags::READ => {
781+
// read-only page. It can never be dirty so return zero dirty pages.
782+
new_page_bitmap(mem_region.guest_region.len(), false)?
783+
}
784+
_ => {
785+
if mem_region.region_type == MemoryRegionType::GuardPage {
786+
// Trying to get dirty pages for a guard page region results in a VMMSysError(2)
787+
new_page_bitmap(mem_region.guest_region.len(), false)?
788+
} else {
789+
// Get the dirty bitmap for the memory region
790+
self.vm_fd
791+
.get_dirty_log(i as u32, mem_region.guest_region.len())?
792+
}
793+
}
794+
};
795+
for page_idx in bit_index_iterator(&bitmap) {
796+
page_indices.push(current_page + page_idx);
797+
}
798+
current_page += num_pages;
799+
}
800+
801+
// convert vec of page indices to vec of blocks
802+
let mut sandbox_dirty_pages = new_page_bitmap(current_page * PAGE_SIZE_USIZE, false)?;
803+
for page_idx in page_indices {
804+
let block_idx = page_idx / PAGES_IN_BLOCK;
805+
let bit_idx = page_idx % PAGES_IN_BLOCK;
806+
sandbox_dirty_pages[block_idx] |= 1 << bit_idx;
807+
}
808+
809+
Ok(sandbox_dirty_pages)
810+
}
811+
753812
#[cfg(crashdump)]
754813
fn crashdump_context(&self) -> Result<Option<crashdump::CrashDumpContext>> {
755814
if self.rt_cfg.guest_core_dump {

src/hyperlight_host/src/hypervisor/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,12 @@ pub(crate) trait Hypervisor: Debug + Sync + Send {
196196
None
197197
}
198198

199+
/// Get dirty pages as a bitmap (Vec<u64>).
200+
/// Each bit in a u64 represents a page.
201+
/// This also clears the bitflags, marking the pages as non-dirty.
202+
/// TODO: Implement getting additional host-mapped dirty pages.
203+
fn get_and_clear_dirty_pages(&mut self) -> Result<Vec<u64>>;
204+
199205
/// Get InterruptHandle to underlying VM
200206
fn interrupt_handle(&self) -> Arc<dyn InterruptHandle>;
201207

0 commit comments

Comments
 (0)