Skip to content

Commit fee2137

Browse files
committed
Track dirty pages from host side
Signed-off-by: Ludvig Liljenberg <[email protected]>
1 parent 6e81fcf commit fee2137

File tree

1 file changed

+133
-38
lines changed

1 file changed

+133
-38
lines changed

src/hyperlight_host/src/mem/shared_mem.rs

Lines changed: 133 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ use std::ffi::c_void;
1919
use std::io::Error;
2020
#[cfg(target_os = "linux")]
2121
use std::ptr::null_mut;
22-
use std::sync::{Arc, RwLock};
22+
use std::sync::{Arc, Mutex, RwLock};
2323

24-
use hyperlight_common::mem::PAGE_SIZE_USIZE;
24+
use hyperlight_common::mem::{PAGE_SIZE_USIZE, PAGES_IN_BLOCK};
2525
use tracing::{Span, instrument};
2626
#[cfg(target_os = "windows")]
2727
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
@@ -78,8 +78,10 @@ macro_rules! generate_writer {
7878
#[allow(dead_code)]
7979
pub(crate) fn $fname(&mut self, offset: usize, value: $ty) -> Result<()> {
8080
let data = self.as_mut_slice();
81-
bounds_check!(offset, std::mem::size_of::<$ty>(), data.len());
82-
data[offset..offset + std::mem::size_of::<$ty>()].copy_from_slice(&value.to_le_bytes());
81+
let size = std::mem::size_of::<$ty>();
82+
bounds_check!(offset, size, data.len());
83+
data[offset..offset + size].copy_from_slice(&value.to_le_bytes());
84+
self.mark_pages_dirty(offset, size)?;
8385
Ok(())
8486
}
8587
};
@@ -133,6 +135,7 @@ impl Drop for HostMapping {
133135
#[derive(Debug)]
134136
pub struct ExclusiveSharedMemory {
135137
region: Arc<HostMapping>,
138+
dirty_page_tracker: Arc<Mutex<Vec<u64>>>,
136139
}
137140
unsafe impl Send for ExclusiveSharedMemory {}
138141

@@ -147,6 +150,8 @@ unsafe impl Send for ExclusiveSharedMemory {}
147150
#[derive(Debug)]
148151
pub struct GuestSharedMemory {
149152
region: Arc<HostMapping>,
153+
dirty_page_tracker: Arc<Mutex<Vec<u64>>>,
154+
150155
/// The lock that indicates this shared memory is being used by non-Rust code
151156
///
152157
/// This lock _must_ be held whenever the guest is executing,
@@ -298,6 +303,8 @@ unsafe impl Send for GuestSharedMemory {}
298303
#[derive(Clone, Debug)]
299304
pub struct HostSharedMemory {
300305
region: Arc<HostMapping>,
306+
dirty_page_tracker: Arc<Mutex<Vec<u64>>>,
307+
301308
lock: Arc<RwLock<()>>,
302309
}
303310
unsafe impl Send for HostSharedMemory {}
@@ -316,6 +323,7 @@ impl ExclusiveSharedMemory {
316323
};
317324

318325
use crate::error::HyperlightError::{MemoryRequestTooBig, MmapFailed, MprotectFailed};
326+
use crate::mem::bitmap::new_page_bitmap;
319327

320328
if min_size_bytes == 0 {
321329
return Err(new_error!("Cannot create shared memory with size 0"));
@@ -370,30 +378,48 @@ impl ExclusiveSharedMemory {
370378
return Err(MprotectFailed(Error::last_os_error().raw_os_error()));
371379
}
372380

381+
// HostMapping is only non-Send/Sync because raw pointers
382+
// are not ("as a lint", as the Rust docs say). We don't
383+
// want to mark HostMapping Send/Sync immediately, because
384+
// that could socially imply that it's "safe" to use
385+
// unsafe accesses from multiple threads at once. Instead, we
386+
// directly impl Send and Sync on this type. Since this
387+
// type does have Send and Sync manually impl'd, the Arc
388+
// is not pointless as the lint suggests.
389+
#[allow(clippy::arc_with_non_send_sync)]
390+
let host_mapping = Arc::new(HostMapping {
391+
ptr: addr as *mut u8,
392+
size: total_size,
393+
});
394+
395+
let dirty_page_tracker = new_page_bitmap(min_size_bytes, false)?;
396+
373397
Ok(Self {
374-
// HostMapping is only non-Send/Sync because raw pointers
375-
// are not ("as a lint", as the Rust docs say). We don't
376-
// want to mark HostMapping Send/Sync immediately, because
377-
// that could socially imply that it's "safe" to use
378-
// unsafe accesses from multiple threads at once. Instead, we
379-
// directly impl Send and Sync on this type. Since this
380-
// type does have Send and Sync manually impl'd, the Arc
381-
// is not pointless as the lint suggests.
382-
#[allow(clippy::arc_with_non_send_sync)]
383-
region: Arc::new(HostMapping {
384-
ptr: addr as *mut u8,
385-
size: total_size,
386-
}),
398+
region: host_mapping,
399+
dirty_page_tracker: Arc::new(Mutex::new(dirty_page_tracker)),
387400
})
388401
}
389402

403+
/// Gets the dirty bitmap and then clears it in self.
404+
pub(crate) fn get_and_clear_dirty_pages(&mut self) -> Result<Vec<u64>> {
405+
let mut guard = self
406+
.dirty_page_tracker
407+
.try_lock()
408+
.map_err(|_| new_error!("Failed to acquire lock on dirty page tracker"))?;
409+
let bitmap = guard.clone();
410+
guard.fill(0);
411+
Ok(bitmap)
412+
}
413+
390414
/// Create a new region of shared memory with the given minimum
391415
/// size in bytes. The region will be surrounded by guard pages.
392416
///
393417
/// Return `Err` if shared memory could not be allocated.
394418
#[cfg(target_os = "windows")]
395419
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
396420
pub fn new(min_size_bytes: usize) -> Result<Self> {
421+
use super::bitmap::new_page_bitmap;
422+
397423
if min_size_bytes == 0 {
398424
return Err(new_error!("Cannot create shared memory with size 0"));
399425
}
@@ -484,21 +510,26 @@ impl ExclusiveSharedMemory {
484510
log_then_return!(WindowsAPIError(e.clone()));
485511
}
486512

513+
// HostMapping is only non-Send/Sync because raw pointers
514+
// are not ("as a lint", as the Rust docs say). We don't
515+
// want to mark HostMapping Send/Sync immediately, because
516+
// that could socially imply that it's "safe" to use
517+
// unsafe accesses from multiple threads at once. Instead, we
518+
// directly impl Send and Sync on this type. Since this
519+
// type does have Send and Sync manually impl'd, the Arc
520+
// is not pointless as the lint suggests.
521+
#[allow(clippy::arc_with_non_send_sync)]
522+
let host_mapping = Arc::new(HostMapping {
523+
ptr: addr.Value as *mut u8,
524+
size: total_size,
525+
handle,
526+
});
527+
528+
let dirty_page_tracker = new_page_bitmap(min_size_bytes, false)?;
529+
487530
Ok(Self {
488-
// HostMapping is only non-Send/Sync because raw pointers
489-
// are not ("as a lint", as the Rust docs say). We don't
490-
// want to mark HostMapping Send/Sync immediately, because
491-
// that could socially imply that it's "safe" to use
492-
// unsafe accesses from multiple threads at once. Instead, we
493-
// directly impl Send and Sync on this type. Since this
494-
// type does have Send and Sync manually impl'd, the Arc
495-
// is not pointless as the lint suggests.
496-
#[allow(clippy::arc_with_non_send_sync)]
497-
region: Arc::new(HostMapping {
498-
ptr: addr.Value as *mut u8,
499-
size: total_size,
500-
handle,
501-
}),
531+
region: host_mapping,
532+
dirty_page_tracker: Arc::new(Mutex::new(dirty_page_tracker)),
502533
})
503534
}
504535

@@ -613,6 +644,16 @@ impl ExclusiveSharedMemory {
613644
let data = self.as_mut_slice();
614645
bounds_check!(offset, src.len(), data.len());
615646
data[offset..offset + src.len()].copy_from_slice(src);
647+
self.mark_pages_dirty(offset, src.len())?;
648+
Ok(())
649+
}
650+
651+
/// Copies bytes from `self` to `dst` starting at offset
652+
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
653+
pub fn copy_to_slice(&self, dst: &mut [u8], offset: usize) -> Result<()> {
654+
let data = self.as_slice();
655+
bounds_check!(offset, dst.len(), data.len());
656+
dst.copy_from_slice(&data[offset..offset + dst.len()]);
616657
Ok(())
617658
}
618659

@@ -624,6 +665,16 @@ impl ExclusiveSharedMemory {
624665
Ok(self.base_addr() + offset)
625666
}
626667

668+
/// Fill the memory in the range `[offset, offset + len)` with `value`
669+
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
670+
pub fn zero_fill(&mut self, offset: usize, len: usize) -> Result<()> {
671+
bounds_check!(offset, len, self.mem_size());
672+
let data = self.as_mut_slice();
673+
data[offset..offset + len].fill(0);
674+
self.mark_pages_dirty(offset, len)?;
675+
Ok(())
676+
}
677+
627678
generate_reader!(read_u8, u8);
628679
generate_reader!(read_i8, i8);
629680
generate_reader!(read_u16, u16);
@@ -657,15 +708,35 @@ impl ExclusiveSharedMemory {
657708
(
658709
HostSharedMemory {
659710
region: self.region.clone(),
711+
dirty_page_tracker: self.dirty_page_tracker.clone(),
660712
lock: lock.clone(),
661713
},
662714
GuestSharedMemory {
663715
region: self.region.clone(),
716+
dirty_page_tracker: self.dirty_page_tracker.clone(),
664717
lock: lock.clone(),
665718
},
666719
)
667720
}
668721

722+
/// Marks pages that cover bytes [offset, offset + size) as dirty
723+
pub(super) fn mark_pages_dirty(&mut self, offset: usize, size: usize) -> Result<()> {
724+
bounds_check!(offset, size, self.mem_size());
725+
let mut bitmap = self
726+
.dirty_page_tracker
727+
.try_lock()
728+
.map_err(|_| new_error!("Failed to lock dirty page tracker"))?;
729+
730+
let start_page = offset / PAGE_SIZE_USIZE;
731+
let end_page = (offset + size - 1) / PAGE_SIZE_USIZE; // offset + size - 1 is the last affected byte.
732+
for page_idx in start_page..=end_page {
733+
let block_idx = page_idx / PAGES_IN_BLOCK;
734+
let bit_idx = page_idx % PAGES_IN_BLOCK;
735+
bitmap[block_idx] |= 1 << bit_idx;
736+
}
737+
Ok(())
738+
}
739+
669740
/// Gets the file handle of the shared memory region for this Sandbox
670741
#[cfg(target_os = "windows")]
671742
pub fn get_mmap_file_handle(&self) -> HANDLE {
@@ -743,6 +814,7 @@ impl SharedMemory for GuestSharedMemory {
743814
fn region(&self) -> &HostMapping {
744815
&self.region
745816
}
817+
746818
fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
747819
&mut self,
748820
f: F,
@@ -753,6 +825,7 @@ impl SharedMemory for GuestSharedMemory {
753825
.map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
754826
let mut excl = ExclusiveSharedMemory {
755827
region: self.region.clone(),
828+
dirty_page_tracker: self.dirty_page_tracker.clone(),
756829
};
757830
let ret = f(&mut excl);
758831
drop(excl);
@@ -803,7 +876,7 @@ impl HostSharedMemory {
803876
/// Write a value of type T, whose representation is the same
804877
/// between the sandbox and the host, and which has no invalid bit
805878
/// patterns
806-
pub fn write<T: AllValid>(&self, offset: usize, data: T) -> Result<()> {
879+
pub fn write<T: AllValid>(&mut self, offset: usize, data: T) -> Result<()> {
807880
bounds_check!(offset, std::mem::size_of::<T>(), self.mem_size());
808881
unsafe {
809882
let slice: &[u8] = core::slice::from_raw_parts(
@@ -812,6 +885,7 @@ impl HostSharedMemory {
812885
);
813886
self.copy_from_slice(slice, offset)?;
814887
}
888+
self.mark_pages_dirty(offset, std::mem::size_of::<T>())?;
815889
Ok(())
816890
}
817891

@@ -834,9 +908,8 @@ impl HostSharedMemory {
834908
Ok(())
835909
}
836910

837-
/// Copy the contents of the sandbox at the specified offset into
838-
/// the slice
839-
pub fn copy_from_slice(&self, slice: &[u8], offset: usize) -> Result<()> {
911+
/// Copy the contents of the given slice into self
912+
pub fn copy_from_slice(&mut self, slice: &[u8], offset: usize) -> Result<()> {
840913
bounds_check!(offset, slice.len(), self.mem_size());
841914
let base = self.base_ptr().wrapping_add(offset);
842915
let guard = self
@@ -850,6 +923,7 @@ impl HostSharedMemory {
850923
}
851924
}
852925
drop(guard);
926+
self.mark_pages_dirty(offset, slice.len())?;
853927
Ok(())
854928
}
855929

@@ -867,6 +941,7 @@ impl HostSharedMemory {
867941
unsafe { base.wrapping_add(i).write_volatile(value) };
868942
}
869943
drop(guard);
944+
self.mark_pages_dirty(offset, len)?;
870945
Ok(())
871946
}
872947

@@ -979,12 +1054,31 @@ impl HostSharedMemory {
9791054

9801055
Ok(to_return)
9811056
}
1057+
1058+
/// Marks pages that cover bytes [offset, offset + size) as dirty
1059+
pub(super) fn mark_pages_dirty(&mut self, offset: usize, size: usize) -> Result<()> {
1060+
bounds_check!(offset, size, self.mem_size());
1061+
let mut bitmap = self
1062+
.dirty_page_tracker
1063+
.try_lock()
1064+
.map_err(|_| new_error!("Failed to lock dirty page tracker"))?;
1065+
1066+
let start_page = offset / PAGE_SIZE_USIZE;
1067+
let end_page = (offset + size - 1) / PAGE_SIZE_USIZE; // offset + size - 1 is the last affected byte.
1068+
for page_idx in start_page..=end_page {
1069+
let block_idx = page_idx / PAGES_IN_BLOCK;
1070+
let bit_idx = page_idx % PAGES_IN_BLOCK;
1071+
bitmap[block_idx] |= 1 << bit_idx;
1072+
}
1073+
Ok(())
1074+
}
9821075
}
9831076

9841077
impl SharedMemory for HostSharedMemory {
9851078
fn region(&self) -> &HostMapping {
9861079
&self.region
9871080
}
1081+
9881082
fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
9891083
&mut self,
9901084
f: F,
@@ -995,6 +1089,7 @@ impl SharedMemory for HostSharedMemory {
9951089
.map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
9961090
let mut excl = ExclusiveSharedMemory {
9971091
region: self.region.clone(),
1092+
dirty_page_tracker: self.dirty_page_tracker.clone(),
9981093
};
9991094
let ret = f(&mut excl);
10001095
drop(excl);
@@ -1048,7 +1143,7 @@ mod tests {
10481143
let mem_size: usize = 4096;
10491144
let vec_len = 10;
10501145
let eshm = ExclusiveSharedMemory::new(mem_size)?;
1051-
let (hshm, _) = eshm.build();
1146+
let (mut hshm, _) = eshm.build();
10521147
let vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
10531148
// write the value to the memory at the beginning.
10541149
hshm.copy_from_slice(&vec, 0)?;
@@ -1135,8 +1230,8 @@ mod tests {
11351230
#[test]
11361231
fn clone() {
11371232
let eshm = ExclusiveSharedMemory::new(PAGE_SIZE_USIZE).unwrap();
1138-
let (hshm1, _) = eshm.build();
1139-
let hshm2 = hshm1.clone();
1233+
let (mut hshm1, _) = eshm.build();
1234+
let mut hshm2 = hshm1.clone();
11401235

11411236
// after hshm1 is cloned, hshm1 and hshm2 should have identical
11421237
// memory sizes and pointers.

0 commit comments

Comments
 (0)