use std::{cell::RefCell, collections::HashMap};
use vmi_arch_amd64::{Amd64, PageTableEntry, PageTableLevel};
use vmi_core::{
AddressContext, Architecture as _, Gfn, MemoryAccess, MemoryAccessOptions, Pa, Va, VcpuId,
View, VmiCore, VmiDriver, VmiError, VmiInfo, VmiMappedPage, VmiQueryProtection, VmiRead,
VmiSetProtection,
};
use super::super::{PageTableMonitor, PageTableMonitorEvent};
struct MockPtmDriver {
pages: RefCell<HashMap<Gfn, Vec<u8>>>,
}
impl MockPtmDriver {
fn new() -> Self {
Self {
pages: RefCell::new(HashMap::new()),
}
}
fn insert_page(&self, gfn: Gfn) {
self.pages.borrow_mut().insert(gfn, vec![0u8; 4096]);
}
fn write_pte(&self, pa: Pa, pte: PageTableEntry) {
let gfn = Amd64::gfn_from_pa(pa);
let offset = Amd64::pa_offset(pa) as usize;
let mut pages = self.pages.borrow_mut();
let page = pages
.get_mut(&gfn)
.unwrap_or_else(|| panic!("no page at {:?}", gfn));
page[offset..offset + 8].copy_from_slice(&pte.0.to_le_bytes());
}
}
fn make_pte(gfn: Gfn) -> PageTableEntry {
PageTableEntry((gfn.0 << 12) | 1)
}
fn make_large_pte(gfn: Gfn) -> PageTableEntry {
PageTableEntry((gfn.0 << 12) | (1 << 7) | 1)
}
fn make_not_present_pte() -> PageTableEntry {
PageTableEntry(0)
}
impl VmiDriver for MockPtmDriver {
type Architecture = Amd64;
fn info(&self) -> Result<VmiInfo, VmiError> {
Ok(VmiInfo {
page_size: 4096,
page_shift: 12,
max_gfn: Gfn(0xFFFF),
vcpus: 1,
})
}
}
impl VmiRead for MockPtmDriver {
fn read_page(&self, gfn: Gfn) -> Result<VmiMappedPage, VmiError> {
let pages = self.pages.borrow();
let page = pages.get(&gfn).ok_or(VmiError::Other("page not found"))?;
Ok(VmiMappedPage::new(page.clone()))
}
}
impl VmiQueryProtection for MockPtmDriver {
fn memory_access(&self, _gfn: Gfn, _view: View) -> Result<MemoryAccess, VmiError> {
Ok(MemoryAccess::RW)
}
}
impl VmiSetProtection for MockPtmDriver {
fn set_memory_access(
&self,
_gfn: Gfn,
_view: View,
_access: MemoryAccess,
) -> Result<(), VmiError> {
Ok(())
}
fn set_memory_access_with_options(
&self,
_gfn: Gfn,
_view: View,
_access: MemoryAccess,
_options: MemoryAccessOptions,
) -> Result<(), VmiError> {
Ok(())
}
}
const PML4_GFN: Gfn = Gfn(1);
const PDPT_GFN: Gfn = Gfn(2);
const PD_GFN: Gfn = Gfn(3);
const PT_GFN: Gfn = Gfn(4);
const DATA_GFN: Gfn = Gfn(5);
const VIEW: View = View(0);
const VCPU: VcpuId = VcpuId(0);
const TEST_VA: Va = Va(0x1000);
fn root_pa() -> Pa {
Amd64::pa_from_gfn(PML4_GFN)
}
fn test_ctx() -> AddressContext {
AddressContext::new(TEST_VA, root_pa())
}
fn build_full_hierarchy(driver: &MockPtmDriver) {
driver.insert_page(PML4_GFN);
driver.insert_page(PDPT_GFN);
driver.insert_page(PD_GFN);
driver.insert_page(PT_GFN);
driver.insert_page(DATA_GFN);
let pml4_entry_pa =
Amd64::pa_from_gfn(PML4_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8;
driver.write_pte(pml4_entry_pa, make_pte(PDPT_GFN));
let pdpt_entry_pa =
Amd64::pa_from_gfn(PDPT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8;
driver.write_pte(pdpt_entry_pa, make_pte(PD_GFN));
let pd_entry_pa =
Amd64::pa_from_gfn(PD_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8;
driver.write_pte(pd_entry_pa, make_pte(PT_GFN));
let pt_entry_pa =
Amd64::pa_from_gfn(PT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8;
driver.write_pte(pt_entry_pa, make_pte(DATA_GFN));
}
fn pt_entry_pa() -> Pa {
Amd64::pa_from_gfn(PT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8
}
fn pd_entry_pa() -> Pa {
Amd64::pa_from_gfn(PD_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8
}
fn pdpt_entry_pa() -> Pa {
Amd64::pa_from_gfn(PDPT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8
}
fn pml4_entry_pa() -> Pa {
Amd64::pa_from_gfn(PML4_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8
}
fn expected_data_pa() -> Pa {
Amd64::pa_from_gfn(DATA_GFN) + Amd64::va_offset(TEST_VA)
}
fn make_vmi(driver: MockPtmDriver) -> Result<VmiCore<MockPtmDriver>, VmiError> {
let mut vmi = VmiCore::new(driver)?;
vmi.disable_gfn_cache();
Ok(vmi)
}
#[test]
fn monitor_already_paged_in_address() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 4);
assert_eq!(ptm.monitored_entries(), 4);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn monitor_unmonitor_lifecycle() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 4);
assert_eq!(ptm.paged_in_entries(), 1);
ptm.unmonitor(&vmi, test_ctx(), VIEW)?;
assert_eq!(ptm.monitored_tables(), 0);
assert_eq!(ptm.monitored_entries(), 0);
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn multiple_vas_sharing_page_table_pages() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let va2 = Va(0x2000);
let data2_gfn = Gfn(6);
driver.insert_page(data2_gfn);
let pt_entry_pa2 =
Amd64::pa_from_gfn(PT_GFN) + Amd64::va_index_for(va2, PageTableLevel::Pt) * 8;
driver.write_pte(pt_entry_pa2, make_pte(data2_gfn));
let ctx2 = AddressContext::new(va2, root_pa());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test1")?;
ptm.monitor(&vmi, ctx2, VIEW, "test2")?;
assert_eq!(ptm.monitored_tables(), 4);
assert_eq!(ptm.paged_in_entries(), 2);
ptm.unmonitor(&vmi, test_ctx(), VIEW)?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
ptm.unmonitor(&vmi, ctx2, VIEW)?;
assert_eq!(ptm.monitored_tables(), 0);
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn unmonitor_all_clears_state() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 4);
assert_eq!(ptm.paged_in_entries(), 1);
ptm.unmonitor_all(&vmi);
assert_eq!(ptm.monitored_tables(), 0);
assert_eq!(ptm.monitored_entries(), 0);
Ok(())
}
#[test]
fn monitor_remonitor_same_va() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 4);
assert_eq!(ptm.paged_in_entries(), 1);
ptm.monitor(&vmi, test_ctx(), VIEW, "test2")?;
assert_eq!(ptm.monitored_tables(), 4);
assert_eq!(ptm.paged_in_entries(), 1);
ptm.unmonitor(&vmi, test_ctx(), VIEW)?;
assert_eq!(ptm.monitored_tables(), 0);
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn monitor_not_present_at_every_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
driver.insert_page(PML4_GFN);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 1); assert_eq!(ptm.monitored_entries(), 1); assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn unmonitor_nonexistent_va_is_noop() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.unmonitor(&vmi, test_ctx(), VIEW)?;
assert_eq!(ptm.monitored_tables(), 0);
assert_eq!(ptm.monitored_entries(), 0);
Ok(())
}
#[test]
fn unmonitor_with_not_present_intermediate() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
driver.write_pte(pd_entry_pa(), make_not_present_pte());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 3);
ptm.unmonitor(&vmi, test_ctx(), VIEW)?;
assert_eq!(ptm.monitored_tables(), 0);
assert_eq!(ptm.monitored_entries(), 0);
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn unmonitor_view_only_affects_target_view() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let view0 = View(0);
let view1 = View(1);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), view0, "v0")?;
ptm.monitor(&vmi, test_ctx(), view1, "v1")?;
assert_eq!(ptm.paged_in_entries(), 2);
ptm.unmonitor_view(&vmi, view0);
assert_eq!(ptm.paged_in_entries(), 1);
ptm.unmonitor_view(&vmi, view1);
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 0);
Ok(())
}
#[test]
fn different_roots_are_independent() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let pml4_2_gfn = Gfn(20);
let pdpt_2_gfn = Gfn(21);
let pd_2_gfn = Gfn(22);
let pt_2_gfn = Gfn(23);
let data_2_gfn = Gfn(24);
driver.insert_page(pml4_2_gfn);
driver.insert_page(pdpt_2_gfn);
driver.insert_page(pd_2_gfn);
driver.insert_page(pt_2_gfn);
driver.insert_page(data_2_gfn);
let root2 = Amd64::pa_from_gfn(pml4_2_gfn);
driver.write_pte(
Amd64::pa_from_gfn(pml4_2_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8,
make_pte(pdpt_2_gfn),
);
driver.write_pte(
Amd64::pa_from_gfn(pdpt_2_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8,
make_pte(pd_2_gfn),
);
driver.write_pte(
Amd64::pa_from_gfn(pd_2_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8,
make_pte(pt_2_gfn),
);
driver.write_pte(
Amd64::pa_from_gfn(pt_2_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8,
make_pte(data_2_gfn),
);
let ctx2 = AddressContext::new(TEST_VA, root2);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "root1")?;
ptm.monitor(&vmi, ctx2, VIEW, "root2")?;
assert_eq!(ptm.monitored_tables(), 8);
assert_eq!(ptm.paged_in_entries(), 2);
vmi.driver()
.write_pte(pt_entry_pa(), make_not_present_pte());
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()));
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn page_change_pfn_at_pt_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_data_gfn = Gfn(10);
driver.insert_page(new_data_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pt_entry_pa(), make_pte(new_data_gfn));
let marked = ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == Amd64::pa_from_gfn(new_data_gfn) + Amd64::va_offset(TEST_VA)));
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn page_change_pfn_at_pd_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_pt_gfn = Gfn(10);
let new_data_gfn = Gfn(11);
driver.insert_page(new_pt_gfn);
driver.insert_page(new_data_gfn);
let new_pt_entry_pa =
Amd64::pa_from_gfn(new_pt_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8;
driver.write_pte(new_pt_entry_pa, make_pte(new_data_gfn));
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver().write_pte(pd_entry_pa(), make_pte(new_pt_gfn));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == Amd64::pa_from_gfn(new_data_gfn) + Amd64::va_offset(TEST_VA)));
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn page_change_pfn_at_pdpt_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_pd_gfn = Gfn(10);
let new_pt_gfn = Gfn(11);
let new_data_gfn = Gfn(12);
driver.insert_page(new_pd_gfn);
driver.insert_page(new_pt_gfn);
driver.insert_page(new_data_gfn);
let new_pd_entry_pa =
Amd64::pa_from_gfn(new_pd_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8;
driver.write_pte(new_pd_entry_pa, make_pte(new_pt_gfn));
let new_pt_entry_pa =
Amd64::pa_from_gfn(new_pt_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8;
driver.write_pte(new_pt_entry_pa, make_pte(new_data_gfn));
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver()
.write_pte(pdpt_entry_pa(), make_pte(new_pd_gfn));
let marked = ptm.mark_dirty_entry(pdpt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == Amd64::pa_from_gfn(new_data_gfn) + Amd64::va_offset(TEST_VA)));
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn page_change_pfn_at_pml4_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_pdpt_gfn = Gfn(10);
let new_pd_gfn = Gfn(11);
let new_pt_gfn = Gfn(12);
let new_data_gfn = Gfn(13);
driver.insert_page(new_pdpt_gfn);
driver.insert_page(new_pd_gfn);
driver.insert_page(new_pt_gfn);
driver.insert_page(new_data_gfn);
let new_pdpt_entry_pa =
Amd64::pa_from_gfn(new_pdpt_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8;
driver.write_pte(new_pdpt_entry_pa, make_pte(new_pd_gfn));
let new_pd_entry_pa =
Amd64::pa_from_gfn(new_pd_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8;
driver.write_pte(new_pd_entry_pa, make_pte(new_pt_gfn));
let new_pt_entry_pa =
Amd64::pa_from_gfn(new_pt_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8;
driver.write_pte(new_pt_entry_pa, make_pte(new_data_gfn));
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver()
.write_pte(pml4_entry_pa(), make_pte(new_pdpt_gfn));
let marked = ptm.mark_dirty_entry(pml4_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == Amd64::pa_from_gfn(new_data_gfn) + Amd64::va_offset(TEST_VA)));
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn permission_bit_change_produces_no_events() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
let original_pte = make_pte(DATA_GFN);
let modified_pte = PageTableEntry(original_pte.0 | (1 << 5) | (1 << 6));
assert!(modified_pte.present());
assert_eq!(modified_pte.pfn(), original_pte.pfn());
vmi.driver().write_pte(pt_entry_pa(), modified_pte);
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert!(events.is_empty());
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn page_out_at_pt_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pt_entry_pa(), make_not_present_pte());
let marked = ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()));
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn page_out_at_pd_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver()
.write_pte(pd_entry_pa(), make_not_present_pte());
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()
));
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 3);
Ok(())
}
#[test]
fn page_out_at_pdpt_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver()
.write_pte(pdpt_entry_pa(), make_not_present_pte());
let marked = ptm.mark_dirty_entry(pdpt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()
));
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 2);
Ok(())
}
#[test]
fn page_out_at_pml4_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver()
.write_pte(pml4_entry_pa(), make_not_present_pte());
let marked = ptm.mark_dirty_entry(pml4_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()
));
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 1);
Ok(())
}
#[test]
fn page_out_at_shared_level_affects_all_vas() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let va2 = Va(0x2000);
let data2_gfn = Gfn(6);
driver.insert_page(data2_gfn);
let pt_entry_pa2 =
Amd64::pa_from_gfn(PT_GFN) + Amd64::va_index_for(va2, PageTableLevel::Pt) * 8;
driver.write_pte(pt_entry_pa2, make_pte(data2_gfn));
let ctx2 = AddressContext::new(va2, root_pa());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test1")?;
ptm.monitor(&vmi, ctx2, VIEW, "test2")?;
assert_eq!(ptm.paged_in_entries(), 2);
vmi.driver()
.write_pte(pd_entry_pa(), make_not_present_pte());
ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
let page_outs: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageOut(..)))
.collect();
assert_eq!(page_outs.len(), 2);
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn page_in_at_pt_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
driver.write_pte(pt_entry_pa(), make_not_present_pte());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 0);
vmi.driver().write_pte(pt_entry_pa(), make_pte(DATA_GFN));
let marked = ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_data_pa())
);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn page_in_at_pd_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
driver.write_pte(pd_entry_pa(), make_not_present_pte());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 3);
vmi.driver().write_pte(pd_entry_pa(), make_pte(PT_GFN));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_data_pa())
);
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn page_in_at_pdpt_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
driver.write_pte(pdpt_entry_pa(), make_not_present_pte());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 2);
vmi.driver().write_pte(pdpt_entry_pa(), make_pte(PD_GFN));
let marked = ptm.mark_dirty_entry(pdpt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_data_pa())
);
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn page_in_at_pml4_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
driver.write_pte(pml4_entry_pa(), make_not_present_pte());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 1);
vmi.driver().write_pte(pml4_entry_pa(), make_pte(PDPT_GFN));
let marked = ptm.mark_dirty_entry(pml4_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_data_pa())
);
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn page_out_then_page_in_round_trip() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pt_entry_pa(), make_not_present_pte());
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert_eq!(ptm.paged_in_entries(), 0);
vmi.driver().write_pte(pt_entry_pa(), make_pte(DATA_GFN));
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_data_pa())
);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn page_out_at_pd_then_page_in_restores_subtree() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 4);
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pd_entry_pa(), make_not_present_pte());
ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert_eq!(ptm.monitored_tables(), 3); assert_eq!(ptm.paged_in_entries(), 0);
vmi.driver().write_pte(pd_entry_pa(), make_pte(PT_GFN));
ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(
matches!(events[0], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_data_pa())
);
assert_eq!(ptm.monitored_tables(), 4); assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn hierarchical_dirty_ordering() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pd_entry_pa(), make_not_present_pte());
ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
let page_outs: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageOut(..)))
.collect();
assert!(
!page_outs.is_empty(),
"expected at least one PageOut event from hierarchical dirty"
);
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn no_dirty_entries_returns_empty() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert!(events.is_empty());
Ok(())
}
#[test]
fn mark_dirty_nonexistent_entry_returns_false() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
let bogus_pa = Pa(0xDEAD_0000);
let marked = ptm.mark_dirty_entry(bogus_pa, VIEW, VCPU);
assert!(!marked);
Ok(())
}
#[test]
fn dirty_entry_unchanged_produces_no_events() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
let marked = ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert!(
events.is_empty(),
"unchanged entry should produce no events"
);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn multiple_dirty_marks_same_entry_deduplicates() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_data_gfn = Gfn(10);
driver.insert_page(new_data_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
vmi.driver()
.write_pte(pt_entry_pa(), make_pte(new_data_gfn));
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(..)));
Ok(())
}
#[test]
fn process_dirty_after_unmonitor_is_safe() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU);
ptm.unmonitor(&vmi, test_ctx(), VIEW)?;
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert!(events.is_empty());
Ok(())
}
#[test]
fn shared_higher_level_pfn_change_rebuilds_both_vas() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let va2 = Va(0x2000);
let data2_gfn = Gfn(6);
driver.insert_page(data2_gfn);
let pt_entry_pa2 =
Amd64::pa_from_gfn(PT_GFN) + Amd64::va_index_for(va2, PageTableLevel::Pt) * 8;
driver.write_pte(pt_entry_pa2, make_pte(data2_gfn));
let new_pt_gfn = Gfn(20);
let new_data1_gfn = Gfn(21);
let new_data2_gfn = Gfn(22);
driver.insert_page(new_pt_gfn);
driver.insert_page(new_data1_gfn);
driver.insert_page(new_data2_gfn);
let new_pt_entry1 =
Amd64::pa_from_gfn(new_pt_gfn) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8;
driver.write_pte(new_pt_entry1, make_pte(new_data1_gfn));
let new_pt_entry2 =
Amd64::pa_from_gfn(new_pt_gfn) + Amd64::va_index_for(va2, PageTableLevel::Pt) * 8;
driver.write_pte(new_pt_entry2, make_pte(new_data2_gfn));
let ctx2 = AddressContext::new(va2, root_pa());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test1")?;
ptm.monitor(&vmi, ctx2, VIEW, "test2")?;
assert_eq!(ptm.paged_in_entries(), 2);
vmi.driver().write_pte(pd_entry_pa(), make_pte(new_pt_gfn));
ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
let page_outs: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageOut(..)))
.collect();
let page_ins: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageIn(..)))
.collect();
assert_eq!(page_outs.len(), 2);
assert_eq!(page_ins.len(), 2);
assert_eq!(ptm.paged_in_entries(), 2);
Ok(())
}
#[test]
fn shared_physical_page_at_different_levels_across_roots() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
let shared_gfn = Gfn(3);
let pml4_1 = Gfn(1);
let pdpt_1 = Gfn(2);
let pt_1 = Gfn(4);
let data_1 = Gfn(5);
let va1 = Va(0x200000);
driver.insert_page(pml4_1);
driver.insert_page(pdpt_1);
driver.insert_page(shared_gfn);
driver.insert_page(pt_1);
driver.insert_page(data_1);
driver.write_pte(
Amd64::pa_from_gfn(pml4_1) + Amd64::va_index_for(va1, PageTableLevel::Pml4) * 8,
make_pte(pdpt_1),
);
driver.write_pte(
Amd64::pa_from_gfn(pdpt_1) + Amd64::va_index_for(va1, PageTableLevel::Pdpt) * 8,
make_pte(shared_gfn),
);
let shared_pa =
Amd64::pa_from_gfn(shared_gfn) + Amd64::va_index_for(va1, PageTableLevel::Pd) * 8;
driver.write_pte(shared_pa, make_pte(pt_1));
driver.write_pte(
Amd64::pa_from_gfn(pt_1) + Amd64::va_index_for(va1, PageTableLevel::Pt) * 8,
make_pte(data_1),
);
let pml4_2 = Gfn(10);
let pdpt_2 = Gfn(11);
let pd_2 = Gfn(12);
let va2 = Va(0x1000);
driver.insert_page(pml4_2);
driver.insert_page(pdpt_2);
driver.insert_page(pd_2);
driver.write_pte(
Amd64::pa_from_gfn(pml4_2) + Amd64::va_index_for(va2, PageTableLevel::Pml4) * 8,
make_pte(pdpt_2),
);
driver.write_pte(
Amd64::pa_from_gfn(pdpt_2) + Amd64::va_index_for(va2, PageTableLevel::Pdpt) * 8,
make_pte(pd_2),
);
driver.write_pte(
Amd64::pa_from_gfn(pd_2) + Amd64::va_index_for(va2, PageTableLevel::Pd) * 8,
make_pte(shared_gfn),
);
assert_eq!(
Amd64::va_index_for(va1, PageTableLevel::Pd),
Amd64::va_index_for(va2, PageTableLevel::Pt),
);
let new_gfn = Gfn(20);
let new_data_1 = Gfn(21);
driver.insert_page(new_gfn);
driver.insert_page(new_data_1);
driver.write_pte(
Amd64::pa_from_gfn(new_gfn) + Amd64::va_index_for(va1, PageTableLevel::Pt) * 8,
make_pte(new_data_1),
);
let root1_pa = Amd64::pa_from_gfn(pml4_1);
let root2_pa = Amd64::pa_from_gfn(pml4_2);
let ctx1 = AddressContext::new(va1, root1_pa);
let ctx2 = AddressContext::new(va2, root2_pa);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, ctx1, VIEW, "root1_pd")?;
ptm.monitor(&vmi, ctx2, VIEW, "root2_pt")?;
assert_eq!(ptm.paged_in_entries(), 2);
vmi.driver().write_pte(shared_pa, make_pte(new_gfn));
ptm.mark_dirty_entry(shared_pa, VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
let page_outs: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageOut(..)))
.collect();
let page_ins: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageIn(..)))
.collect();
assert_eq!(page_outs.len(), 2, "both VAs should page out");
assert_eq!(page_ins.len(), 2, "both VAs should page in");
let root1_expected_pa =
Amd64::pa_from_gfn(new_data_1) + Amd64::va_offset_for(va1, PageTableLevel::Pt);
let root1_in = events.iter().find_map(|e| match e {
PageTableMonitorEvent::PageIn(u) if u.ctx == ctx1 => Some(u),
_ => None,
});
assert_eq!(
root1_in.unwrap().pa,
root1_expected_pa,
"root1 should resolve through new PT subtree"
);
let root2_expected_pa =
Amd64::pa_from_gfn(new_gfn) + Amd64::va_offset_for(va2, PageTableLevel::Pt);
let root2_in = events.iter().find_map(|e| match e {
PageTableMonitorEvent::PageIn(u) if u.ctx == ctx2 => Some(u),
_ => None,
});
assert_eq!(
root2_in.unwrap().pa,
root2_expected_pa,
"root2 should resolve as leaf at PT level"
);
assert_eq!(ptm.paged_in_entries(), 2);
Ok(())
}
fn build_large_page_hierarchy(driver: &MockPtmDriver) {
driver.insert_page(PML4_GFN);
driver.insert_page(PDPT_GFN);
driver.insert_page(PD_GFN);
driver.insert_page(DATA_GFN);
let pml4_pa =
Amd64::pa_from_gfn(PML4_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8;
driver.write_pte(pml4_pa, make_pte(PDPT_GFN));
let pdpt_pa =
Amd64::pa_from_gfn(PDPT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8;
driver.write_pte(pdpt_pa, make_pte(PD_GFN));
driver.write_pte(pd_entry_pa(), make_large_pte(DATA_GFN));
}
fn expected_large_page_pa(gfn: Gfn) -> Pa {
Amd64::pa_from_gfn(gfn) + (TEST_VA.0 & 0x1f_ffff)
}
#[test]
fn large_page_initial_monitoring() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
driver.insert_page(PML4_GFN);
driver.insert_page(PDPT_GFN);
driver.insert_page(PD_GFN);
driver.insert_page(DATA_GFN);
let pml4_entry_pa =
Amd64::pa_from_gfn(PML4_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8;
driver.write_pte(pml4_entry_pa, make_pte(PDPT_GFN));
let pdpt_entry_pa =
Amd64::pa_from_gfn(PDPT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8;
driver.write_pte(pdpt_entry_pa, make_pte(PD_GFN));
let pd_entry_pa =
Amd64::pa_from_gfn(PD_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8;
driver.write_pte(pd_entry_pa, make_large_pte(DATA_GFN));
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 3);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn large_page_pfn_change() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_large_page_hierarchy(&driver);
let new_data_gfn = Gfn(10);
driver.insert_page(new_data_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 3);
vmi.driver()
.write_pte(pd_entry_pa(), make_large_pte(new_data_gfn));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == expected_large_page_pa(new_data_gfn)));
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 3);
Ok(())
}
#[test]
fn large_page_page_out() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_large_page_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 3);
vmi.driver()
.write_pte(pd_entry_pa(), make_not_present_pte());
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()));
assert_eq!(ptm.paged_in_entries(), 0);
Ok(())
}
#[test]
fn large_page_page_in() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_large_page_hierarchy(&driver);
driver.write_pte(pd_entry_pa(), make_not_present_pte());
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 3);
vmi.driver()
.write_pte(pd_entry_pa(), make_large_pte(DATA_GFN));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageIn(ref u)
if u.pa == expected_large_page_pa(DATA_GFN)));
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn large_page_1gb_at_pdpt_level() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
driver.insert_page(PML4_GFN);
driver.insert_page(PDPT_GFN);
driver.insert_page(DATA_GFN);
let pml4_pa =
Amd64::pa_from_gfn(PML4_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8;
driver.write_pte(pml4_pa, make_pte(PDPT_GFN));
let pdpt_pa =
Amd64::pa_from_gfn(PDPT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8;
driver.write_pte(pdpt_pa, make_large_pte(DATA_GFN));
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 2);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn large_page_1gb_pfn_change() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
driver.insert_page(PML4_GFN);
driver.insert_page(PDPT_GFN);
driver.insert_page(DATA_GFN);
let pml4_pa =
Amd64::pa_from_gfn(PML4_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8;
driver.write_pte(pml4_pa, make_pte(PDPT_GFN));
let pdpt_pa =
Amd64::pa_from_gfn(PDPT_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8;
driver.write_pte(pdpt_pa, make_large_pte(DATA_GFN));
let new_data_gfn = Gfn(10);
driver.insert_page(new_data_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pdpt_entry_pa(), make_large_pte(new_data_gfn));
let marked = ptm.mark_dirty_entry(pdpt_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
let expected_pa = Amd64::pa_from_gfn(new_data_gfn) + (TEST_VA.0 & 0x3fff_ffff);
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_pa));
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn large_page_transition_regular_to_large_same_pfn() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver()
.write_pte(pd_entry_pa(), make_large_pte(PT_GFN));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == expected_large_page_pa(PT_GFN)));
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 3);
Ok(())
}
#[test]
fn large_page_transition_regular_to_large_different_pfn() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_data_gfn = Gfn(10);
driver.insert_page(new_data_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 4);
vmi.driver()
.write_pte(pd_entry_pa(), make_large_pte(new_data_gfn));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == expected_large_page_pa(new_data_gfn)));
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 3);
Ok(())
}
#[test]
fn large_page_transition_large_to_regular_same_pfn() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_large_page_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 3);
vmi.driver().write_pte(pd_entry_pa(), make_pte(DATA_GFN));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()));
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn large_page_transition_large_to_regular_different_pfn() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_large_page_hierarchy(&driver);
let new_pt_gfn = Gfn(10);
driver.insert_page(new_pt_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
assert_eq!(ptm.monitored_tables(), 3);
vmi.driver().write_pte(pd_entry_pa(), make_pte(new_pt_gfn));
let marked = ptm.mark_dirty_entry(pd_entry_pa(), VIEW, VCPU);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 1);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()));
assert_eq!(ptm.paged_in_entries(), 0);
assert_eq!(ptm.monitored_tables(), 4);
Ok(())
}
#[test]
fn large_page_transition_large_to_large_pfn_change_at_pdpt() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
driver.insert_page(PML4_GFN);
driver.insert_page(PDPT_GFN);
driver.insert_page(DATA_GFN);
let pml4_pa =
Amd64::pa_from_gfn(PML4_GFN) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8;
driver.write_pte(pml4_pa, make_pte(PDPT_GFN));
driver.write_pte(pdpt_entry_pa(), make_large_pte(DATA_GFN));
let new_gfn = Gfn(10);
driver.insert_page(new_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.monitored_tables(), 2);
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pdpt_entry_pa(), make_large_pte(new_gfn));
ptm.mark_dirty_entry(pdpt_entry_pa(), VIEW, VCPU);
let events = ptm.process_dirty_entries(&vmi, VCPU)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
let expected_pa = Amd64::pa_from_gfn(new_gfn) + (TEST_VA.0 & 0x3fff_ffff);
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u) if u.pa == expected_pa));
assert_eq!(ptm.monitored_tables(), 2);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
const VCPU_0: VcpuId = VcpuId(0);
const VCPU_1: VcpuId = VcpuId(1);
#[test]
fn dirty_entry_is_per_vcpu() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_data_gfn = Gfn(10);
driver.insert_page(new_data_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
vmi.driver()
.write_pte(pt_entry_pa(), make_pte(new_data_gfn));
let marked = ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU_0);
assert!(marked);
let events = ptm.process_dirty_entries(&vmi, VCPU_1)?;
assert!(
events.is_empty(),
"vcpu 1 should not see vcpu 0's dirty entries"
);
assert_eq!(ptm.paged_in_entries(), 1);
let events = ptm.process_dirty_entries(&vmi, VCPU_0)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == Amd64::pa_from_gfn(new_data_gfn) + Amd64::va_offset(TEST_VA)));
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn independent_dirty_entries_across_vcpus() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let va2 = Va(0x2000);
let data2_gfn = Gfn(6);
driver.insert_page(data2_gfn);
let pt_entry_pa2 =
Amd64::pa_from_gfn(PT_GFN) + Amd64::va_index_for(va2, PageTableLevel::Pt) * 8;
driver.write_pte(pt_entry_pa2, make_pte(data2_gfn));
let ctx2 = AddressContext::new(va2, root_pa());
let new_data1 = Gfn(10);
let new_data2 = Gfn(11);
driver.insert_page(new_data1);
driver.insert_page(new_data2);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test1")?;
ptm.monitor(&vmi, ctx2, VIEW, "test2")?;
assert_eq!(ptm.paged_in_entries(), 2);
vmi.driver().write_pte(pt_entry_pa(), make_pte(new_data1));
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU_0);
vmi.driver().write_pte(pt_entry_pa2, make_pte(new_data2));
ptm.mark_dirty_entry(pt_entry_pa2, VIEW, VCPU_1);
let events = ptm.process_dirty_entries(&vmi, VCPU_1)?;
assert_eq!(events.len(), 2, "vcpu 1 should see VA2 page-out + page-in");
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(ref u) if u.ctx == ctx2));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == Amd64::pa_from_gfn(new_data2) + Amd64::va_offset(va2)));
let events = ptm.process_dirty_entries(&vmi, VCPU_0)?;
assert_eq!(events.len(), 2, "vcpu 0 should see VA1 page-out + page-in");
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(ref u) if u.ctx == test_ctx()));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(ref u)
if u.pa == Amd64::pa_from_gfn(new_data1) + Amd64::va_offset(TEST_VA)));
assert_eq!(ptm.paged_in_entries(), 2);
Ok(())
}
#[test]
fn process_empty_vcpu_returns_empty() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
let events = ptm.process_dirty_entries(&vmi, VCPU_1)?;
assert!(events.is_empty());
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn same_entry_marked_dirty_by_multiple_vcpus() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
build_full_hierarchy(&driver);
let new_data_gfn = Gfn(10);
driver.insert_page(new_data_gfn);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, test_ctx(), VIEW, "test")?;
assert_eq!(ptm.paged_in_entries(), 1);
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU_0);
ptm.mark_dirty_entry(pt_entry_pa(), VIEW, VCPU_1);
vmi.driver()
.write_pte(pt_entry_pa(), make_pte(new_data_gfn));
let events = ptm.process_dirty_entries(&vmi, VCPU_0)?;
assert_eq!(events.len(), 2);
assert!(matches!(events[0], PageTableMonitorEvent::PageOut(..)));
assert!(matches!(events[1], PageTableMonitorEvent::PageIn(..)));
let events = ptm.process_dirty_entries(&vmi, VCPU_1)?;
assert!(
events.is_empty(),
"vcpu 1 should see no change after vcpu 0 already processed"
);
assert_eq!(ptm.paged_in_entries(), 1);
Ok(())
}
#[test]
fn walk_subtree_does_not_mask_pending_dirty_entry() -> Result<(), VmiError> {
let driver = MockPtmDriver::new();
let pml4_1 = Gfn(30);
let pdpt_1 = Gfn(31);
let pd_1 = Gfn(32);
let pt_old = Gfn(33);
let data_1 = Gfn(34);
for gfn in [pml4_1, pdpt_1, pd_1, pt_old, data_1] {
driver.insert_page(gfn);
}
driver.write_pte(
Amd64::pa_from_gfn(pml4_1) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8,
make_pte(pdpt_1),
);
driver.write_pte(
Amd64::pa_from_gfn(pdpt_1) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8,
make_pte(pd_1),
);
let pd_1_entry_pa =
Amd64::pa_from_gfn(pd_1) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8;
driver.write_pte(pd_1_entry_pa, make_pte(pt_old));
driver.write_pte(
Amd64::pa_from_gfn(pt_old) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8,
make_pte(data_1),
);
let pml4_2 = Gfn(40);
let pdpt_2 = Gfn(41);
let pd_2 = Gfn(42);
let pt_shared = Gfn(50);
let data_2 = Gfn(51);
let new_data_2 = Gfn(52);
for gfn in [pml4_2, pdpt_2, pd_2, pt_shared, new_data_2] {
driver.insert_page(gfn);
}
driver.write_pte(
Amd64::pa_from_gfn(pml4_2) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pml4) * 8,
make_pte(pdpt_2),
);
driver.write_pte(
Amd64::pa_from_gfn(pdpt_2) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pdpt) * 8,
make_pte(pd_2),
);
driver.write_pte(
Amd64::pa_from_gfn(pd_2) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pd) * 8,
make_pte(pt_shared),
);
let shared_pt_entry_pa =
Amd64::pa_from_gfn(pt_shared) + Amd64::va_index_for(TEST_VA, PageTableLevel::Pt) * 8;
driver.write_pte(shared_pt_entry_pa, make_pte(data_2));
let root1 = Amd64::pa_from_gfn(pml4_1);
let root2 = Amd64::pa_from_gfn(pml4_2);
let ctx1 = AddressContext::new(TEST_VA, root1);
let ctx2 = AddressContext::new(TEST_VA, root2);
let vmi = make_vmi(driver)?;
let mut ptm = PageTableMonitor::<MockPtmDriver>::new();
ptm.monitor(&vmi, ctx1, VIEW, "root1")?;
ptm.monitor(&vmi, ctx2, VIEW, "root2")?;
assert_eq!(ptm.paged_in_entries(), 2);
vmi.driver().write_pte(pd_1_entry_pa, make_pte(pt_shared));
vmi.driver()
.write_pte(shared_pt_entry_pa, make_pte(new_data_2));
ptm.mark_dirty_entry(pd_1_entry_pa, VIEW, VCPU_0);
ptm.mark_dirty_entry(shared_pt_entry_pa, VIEW, VCPU_0);
let events = ptm.process_dirty_entries(&vmi, VCPU_0)?;
let root2_page_outs: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageOut(u) if u.ctx == ctx2))
.collect();
assert!(
!root2_page_outs.is_empty(),
"Root2 must get PageOut when its PT entry PFN changes"
);
let root2_page_ins: Vec<_> = events
.iter()
.filter(|e| matches!(e, PageTableMonitorEvent::PageIn(u) if u.ctx == ctx2))
.collect();
assert!(
!root2_page_ins.is_empty(),
"Root2 must get PageIn for new_DATA_2"
);
Ok(())
}