Skip to content

Commit 54a1c1a

Browse files
committed
api: implement API for dirty memory
Implement API /memory/dirty which returns a bitmap tracking dirty guest memory. The bitmap is structured as a vector of u64, so its length is: total_number_of_pages.div_ceil(64). Pages are ordered in the order of pages as reported by /memory/mappings. Signed-off-by: Babis Chalios <babis.chalios@e2b.dev>
1 parent 717921c commit 54a1c1a

9 files changed

Lines changed: 286 additions & 3 deletions

File tree

resources/seccomp/x86_64-unknown-linux-musl.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@
216216
"syscall": "mincore",
217217
"comment": "Used by get_memory_dirty_bitmap to check if memory pages are resident"
218218
},
219+
{
220+
"syscall": "pread64",
221+
"comment": "Used by get_dirty_memory to read /proc/self/pagemap entries"
222+
},
219223
{
220224
"syscall": "mmap",
221225
"comment": "Used by the VirtIO balloon device",

src/firecracker/src/api_server/parsed_request.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use super::request::logger::parse_put_logger;
2121
use super::request::machine_configuration::{
2222
parse_get_machine_config, parse_patch_machine_config, parse_put_machine_config,
2323
};
24-
use super::request::memory::{parse_get_memory, parse_get_memory_mappings};
24+
use super::request::memory::{parse_get_memory, parse_get_memory_dirty, parse_get_memory_mappings};
2525
use super::request::metrics::parse_put_metrics;
2626
use super::request::mmds::{parse_get_mmds, parse_patch_mmds, parse_put_mmds};
2727
use super::request::net::{parse_patch_net, parse_put_net};
@@ -85,6 +85,7 @@ impl TryFrom<&Request> for ParsedRequest {
8585
(Method::Get, "machine-config", None) => parse_get_machine_config(),
8686
(Method::Get, "memory", None) => match path_tokens.next() {
8787
Some("mappings") => parse_get_memory_mappings(),
88+
Some("dirty") => parse_get_memory_dirty(),
8889
None => parse_get_memory(),
8990
_ => Err(RequestError::InvalidPathMethod(
9091
request_uri.to_string(),
@@ -183,6 +184,7 @@ impl ParsedRequest {
183184
VmmData::InstanceInformation(info) => Self::success_response_with_data(info),
184185
VmmData::MemoryMappings(mappings) => Self::success_response_with_data(mappings),
185186
VmmData::Memory(memory) => Self::success_response_with_data(memory),
187+
VmmData::MemoryDirty(dirty) => Self::success_response_with_data(dirty),
186188
VmmData::VmmVersion(version) => Self::success_response_with_data(
187189
&serde_json::json!({ "firecracker_version": version.as_str() }),
188190
),
@@ -585,6 +587,9 @@ pub mod tests {
585587
VmmData::Memory(memory) => {
586588
http_response(&serde_json::to_string(memory).unwrap(), 200)
587589
}
590+
VmmData::MemoryDirty(dirty) => {
591+
http_response(&serde_json::to_string(dirty).unwrap(), 200)
592+
}
588593
VmmData::VmmVersion(version) => http_response(
589594
&serde_json::json!({ "firecracker_version": version.as_str() }).to_string(),
590595
200,
@@ -615,6 +620,9 @@ pub mod tests {
615620
empty: vec![],
616621
},
617622
));
623+
verify_ok_response_with(VmmData::MemoryDirty(
624+
vmm::vmm_config::instance_info::MemoryDirty { bitmap: vec![] },
625+
));
618626
verify_ok_response_with(VmmData::VmmVersion(String::default()));
619627

620628
// Error.
@@ -712,6 +720,18 @@ pub mod tests {
712720
ParsedRequest::try_from(&req).unwrap();
713721
}
714722

723+
#[test]
724+
fn test_try_from_get_memory_dirty() {
725+
let (mut sender, receiver) = UnixStream::pair().unwrap();
726+
let mut connection = HttpConnection::new(receiver);
727+
sender
728+
.write_all(http_request("GET", "/memory/dirty", None).as_bytes())
729+
.unwrap();
730+
connection.try_read().unwrap();
731+
let req = connection.pop_parsed_request().unwrap();
732+
ParsedRequest::try_from(&req).unwrap();
733+
}
734+
715735
#[test]
716736
fn test_try_from_get_version() {
717737
let (mut sender, receiver) = UnixStream::pair().unwrap();

src/firecracker/src/api_server/request/memory.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ pub(crate) fn parse_get_memory() -> Result<ParsedRequest, RequestError> {
1616
Ok(ParsedRequest::new_sync(VmmAction::GetMemory))
1717
}
1818

19+
pub(crate) fn parse_get_memory_dirty() -> Result<ParsedRequest, RequestError> {
20+
METRICS.get_api_requests.instance_info_count.inc();
21+
Ok(ParsedRequest::new_sync(VmmAction::GetMemoryDirty))
22+
}
23+
1924
#[cfg(test)]
2025
mod tests {
2126
use super::*;
@@ -36,4 +41,12 @@ mod tests {
3641
_ => panic!("Test failed."),
3742
}
3843
}
44+
45+
#[test]
46+
fn test_parse_get_memory_dirty_request() {
47+
match parse_get_memory_dirty().unwrap().into_parts() {
48+
(RequestAction::Sync(action), _) if *action == VmmAction::GetMemoryDirty => {}
49+
_ => panic!("Test failed."),
50+
}
51+
}
3952
}

src/vmm/src/lib.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ pub enum VmmError {
249249
VcpuMessage,
250250
/// Cannot spawn Vcpu thread: {0}
251251
VcpuSpawn(io::Error),
252+
/// Pagemap error: {0}
253+
Pagemap(#[from] utils::pagemap::PagemapError),
252254
/// Vm error: {0}
253255
Vm(#[from] vstate::vm::VmError),
254256
/// Kvm error: {0}
@@ -758,6 +760,44 @@ impl Vmm {
758760
}
759761
}
760762

763+
/// Get dirty pages bitmap for guest memory.
764+
///
765+
/// Returns a bitmap (Vec<u64>) where each bit corresponds to a guest page of `page_size`.
766+
/// A set bit indicates the page is present in RAM and has been written to (not write-protected).
767+
/// Pages are ordered in the same order as reported by `/memory/mappings`.
768+
pub fn get_dirty_memory(&self, page_size: usize) -> Result<Vec<u64>, VmmError> {
769+
let pagemap = utils::pagemap::PagemapReader::new(page_size)?;
770+
let mut dirty_bitmap = vec![];
771+
772+
for region in self.vm.guest_memory().iter() {
773+
let base_addr = region.as_ptr() as usize;
774+
let len = region.size() as usize;
775+
let nr_pages = len / page_size;
776+
777+
// Use mincore to get resident pages bitmap at guest page size granularity
778+
let resident_bitmap =
779+
vstate::vm::mincore_bitmap(base_addr as *mut u8, len, page_size)?;
780+
781+
// Build dirty bitmap: check pagemap only for pages that mincore reports resident.
782+
// This reduces the number of /proc/self/pagemap reads.
783+
let mut slot_bitmap = vec![0u64; nr_pages.div_ceil(64)];
784+
for page_idx in 0..nr_pages {
785+
let is_resident =
786+
(resident_bitmap[page_idx / 64] & (1u64 << (page_idx % 64))) != 0;
787+
if is_resident {
788+
let virt_addr = base_addr + (page_idx * page_size);
789+
if pagemap.is_page_dirty(virt_addr)? {
790+
slot_bitmap[page_idx / 64] |= 1u64 << (page_idx % 64);
791+
}
792+
}
793+
}
794+
795+
dirty_bitmap.extend_from_slice(&slot_bitmap);
796+
}
797+
798+
Ok(dirty_bitmap)
799+
}
800+
761801
/// Signals Vmm to stop and exit.
762802
pub fn stop(&mut self, exit_code: FcExitCode) {
763803
// To avoid cycles, all teardown paths take the following route:

src/vmm/src/rpc_interface.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ use crate::vmm_config::balloon::{
2626
use crate::vmm_config::boot_source::{BootSourceConfig, BootSourceConfigError};
2727
use crate::vmm_config::drive::{BlockDeviceConfig, BlockDeviceUpdateConfig, DriveError};
2828
use crate::vmm_config::entropy::{EntropyDeviceConfig, EntropyDeviceError};
29-
use crate::vmm_config::instance_info::{InstanceInfo, MemoryMappingsResponse, MemoryResponse};
29+
use crate::vmm_config::instance_info::{
30+
InstanceInfo, MemoryDirty, MemoryMappingsResponse, MemoryResponse, VmState,
31+
};
3032
use crate::vmm_config::machine_config::{MachineConfig, MachineConfigError, MachineConfigUpdate};
3133
use crate::vmm_config::metrics::{MetricsConfig, MetricsConfigError};
3234
use crate::vmm_config::mmds::{MmdsConfig, MmdsConfigError};
@@ -69,6 +71,8 @@ pub enum VmmAction {
6971
GetMemoryMappings,
7072
/// Get memory info (resident and empty pages).
7173
GetMemory,
74+
/// Get guest memory dirty pages information.
75+
GetMemoryDirty,
7276
/// Get microVM version.
7377
GetVmmVersion,
7478
/// Flush the metrics. This action can only be called after the logger has been configured.
@@ -168,6 +172,8 @@ pub enum VmmActionError {
168172
OperationNotSupportedPostBoot,
169173
/// The requested operation is not supported before starting the microVM.
170174
OperationNotSupportedPreBoot,
175+
/// The requested operation is not supported while the microVM is running.
176+
OperationNotSupportedWhileRunning,
171177
/// Start microvm error: {0}
172178
StartMicrovm(#[from] StartMicrovmError),
173179
/// Vsock config error: {0}
@@ -197,6 +203,8 @@ pub enum VmmData {
197203
MemoryMappings(MemoryMappingsResponse),
198204
/// Memory info (resident and empty pages).
199205
Memory(MemoryResponse),
206+
/// The guest memory dirty pages information.
207+
MemoryDirty(MemoryDirty),
200208
/// The microVM version.
201209
VmmVersion(String),
202210
}
@@ -427,7 +435,9 @@ impl<'a> PrebootApiController<'a> {
427435
self.vm_resources.machine_config.clone(),
428436
)),
429437
GetVmInstanceInfo => Ok(VmmData::InstanceInformation(self.instance_info.clone())),
430-
GetMemoryMappings | GetMemory => Err(VmmActionError::OperationNotSupportedPreBoot),
438+
GetMemoryMappings | GetMemory | GetMemoryDirty => {
439+
Err(VmmActionError::OperationNotSupportedPreBoot)
440+
}
431441
GetVmmVersion => Ok(VmmData::VmmVersion(self.instance_info.vmm_version.clone())),
432442
InsertBlockDevice(config) => self.insert_block_device(config),
433443
InsertNetworkDevice(config) => self.insert_net_device(config),
@@ -680,6 +690,7 @@ impl RuntimeApiController {
680690
empty: empty_bitmap,
681691
}))
682692
}
693+
GetMemoryDirty => self.get_dirty_memory_info(),
683694
GetVmmVersion => Ok(VmmData::VmmVersion(
684695
self.vmm.lock().expect("Poisoned lock").version(),
685696
)),
@@ -825,6 +836,27 @@ impl RuntimeApiController {
825836
Ok(VmmData::Empty)
826837
}
827838

839+
/// Get dirty pages information for guest memory.
840+
fn get_dirty_memory_info(&self) -> Result<VmmData, VmmActionError> {
841+
let start_us = get_time_us(ClockType::Monotonic);
842+
let vmm = self.vmm.lock().expect("Poisoned lock");
843+
844+
// Dirty memory can only be queried while the VM is paused
845+
if vmm.instance_info.state != VmState::Paused {
846+
return Err(VmmActionError::OperationNotSupportedWhileRunning);
847+
}
848+
849+
let page_size = self.vm_resources.machine_config.huge_pages.page_size();
850+
let bitmap = vmm
851+
.get_dirty_memory(page_size)
852+
.map_err(VmmActionError::InternalVmm)?;
853+
854+
let elapsed_time_us = get_time_us(ClockType::Monotonic) - start_us;
855+
info!("'get dirty memory' VMM action took {elapsed_time_us} us.");
856+
857+
Ok(VmmData::MemoryDirty(MemoryDirty { bitmap }))
858+
}
859+
828860
/// Updates block device properties:
829861
/// - path of the host file backing the emulated block device, update the disk image on the
830862
/// device and its virtio configuration

src/vmm/src/utils/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub mod net;
99
pub mod signal;
1010
/// Module with state machine
1111
pub mod sm;
12+
/// Module with pagemap utilities
13+
pub mod pagemap;
1214

1315
use std::num::Wrapping;
1416
use std::result::Result;

src/vmm/src/utils/pagemap.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//! Utilities for reading /proc/self/pagemap to track dirty pages.
2+
3+
#![allow(clippy::cast_possible_wrap)]
4+
5+
use std::fs::File;
6+
use std::os::unix::io::AsRawFd;
7+
8+
use crate::arch::host_page_size;
9+
10+
const PAGEMAP_ENTRY_SIZE: usize = 8;
11+
12+
/// Errors related to pagemap operations
13+
#[derive(Debug, thiserror::Error, displaydoc::Display)]
14+
pub enum PagemapError {
15+
/// Failed to open /proc/self/pagemap: {0}
16+
OpenPagemap(#[source] std::io::Error),
17+
/// Failed to read pagemap entry: {0}
18+
ReadEntry(#[source] std::io::Error),
19+
/// Failed to open /proc/self/clear_refs: {0}
20+
OpenClearRefs(#[source] std::io::Error),
21+
/// Failed to clear soft-dirty bits: {0}
22+
ClearSoftDirty(#[source] std::io::Error),
23+
}
24+
25+
/// Represents a single entry in /proc/pid/pagemap.
26+
///
27+
/// Each virtual page has an 8-byte entry with the following layout:
28+
/// - Bits 0-54: Page frame number (PFN) if present
29+
/// - Bit 55: Page is soft-dirty (written to since last clear)
30+
/// - Bit 56: Page is exclusively mapped
31+
/// - Bit 57: Page is write-protected via userfaultfd
32+
/// - Bit 58: Unused
33+
/// - Bit 59-60: Unused
34+
/// - Bit 61: Page is file-page or shared-anon
35+
/// - Bit 62: Page is swapped
36+
/// - Bit 63: Page is present in RAM
37+
#[derive(Debug, Clone, Copy)]
38+
pub struct PagemapEntry {
39+
raw: u64,
40+
}
41+
42+
impl PagemapEntry {
43+
/// Create a PagemapEntry from bytes (little-endian)
44+
pub fn from_bytes(bytes: [u8; 8]) -> Self {
45+
Self {
46+
raw: u64::from_ne_bytes(bytes),
47+
}
48+
}
49+
50+
/// Check if page is write-protected via userfaultfd
51+
pub fn is_write_protected(&self) -> bool {
52+
(self.raw & (1u64 << 57)) != 0
53+
}
54+
55+
/// Check if page is present in RAM (bit 63)
56+
pub fn is_present(&self) -> bool {
57+
(self.raw & (1u64 << 63)) != 0
58+
}
59+
}
60+
61+
/// Reader for /proc/self/pagemap
62+
#[derive(Debug)]
63+
pub struct PagemapReader {
64+
pagemap_fd: File,
65+
}
66+
67+
impl PagemapReader {
68+
/// Create a new PagemapReader
69+
pub fn new(_page_size: usize) -> Result<Self, PagemapError> {
70+
let pagemap_fd = File::open("/proc/self/pagemap").map_err(PagemapError::OpenPagemap)?;
71+
72+
Ok(Self { pagemap_fd })
73+
}
74+
75+
/// Check if a single page is dirty (write-protected bit cleared).
76+
///
77+
/// Checks the first host page (4K) of the guest page at the given address.
78+
/// For huge pages, all host pages within the huge page typically have the same
79+
/// dirty status, so sampling the first is sufficient.
80+
///
81+
/// # Arguments
82+
/// * `virt_addr` - Virtual address of the page to check
83+
///
84+
/// # Returns
85+
/// True if the page is present and write-protected bit is cleared (dirty).
86+
pub fn is_page_dirty(&self, virt_addr: usize) -> Result<bool, PagemapError> {
87+
// Pagemap always uses host (4K) page size
88+
let host_page_size = host_page_size();
89+
90+
// Calculate offset for this virtual page (using host page size)
91+
let host_vpn = virt_addr / host_page_size;
92+
let offset = (host_vpn * PAGEMAP_ENTRY_SIZE) as i64;
93+
94+
let mut entry_bytes = [0u8; 8];
95+
96+
// SAFETY: pread is safe as long as the fd is valid and the buffer is properly sized
97+
let ret = unsafe {
98+
libc::pread(
99+
self.pagemap_fd.as_raw_fd(),
100+
entry_bytes.as_mut_ptr().cast(),
101+
PAGEMAP_ENTRY_SIZE,
102+
offset,
103+
)
104+
};
105+
106+
if ret != PAGEMAP_ENTRY_SIZE as isize {
107+
return Err(PagemapError::ReadEntry(std::io::Error::last_os_error()));
108+
}
109+
110+
let entry = PagemapEntry::from_bytes(entry_bytes);
111+
112+
// Page must be present and the write_protected bit cleared (indicating it was written to)
113+
Ok(entry.is_present() && !entry.is_write_protected())
114+
}
115+
}

src/vmm/src/vmm_config/instance_info.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,11 @@ pub struct MemoryResponse {
6767
/// This is a subset of the resident pages.
6868
pub empty: Vec<u64>,
6969
}
70+
71+
/// Information about dirty guest memory pages
72+
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
73+
pub struct MemoryDirty {
74+
/// Bitmap for dirty pages. The bitmap is encoded as a vector of u64 values.
75+
/// Each bit represents whether a page has been written since the last snapshot.
76+
pub bitmap: Vec<u64>,
77+
}

0 commit comments

Comments
 (0)