1#[cfg(unix)]
28use std::os::unix::fs::FileExt;
29use std::{
30 collections::BTreeMap,
31 fs::File,
32 io::{Read, Write},
33 os::unix::net::UnixStream,
34 path::{Path, PathBuf},
35 sync::{
36 Arc,
37 atomic::{AtomicBool, AtomicU64, Ordering},
38 },
39 time::Duration,
40};
41
42use bytes::{Bytes, BytesMut};
43use parking_lot::Mutex;
44use thiserror::Error;
45
46#[derive(Debug, Clone)]
48pub struct PagerConfig {
49 pub poll_interval: Duration,
52 pub prewarm: PrewarmList,
54}
55
56impl Default for PagerConfig {
57 fn default() -> Self {
58 Self {
59 poll_interval: Duration::from_secs(1),
60 prewarm: PrewarmList::default(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default)]
68pub struct PrewarmList {
69 pub pages: Vec<u64>,
71}
72
73impl PrewarmList {
74 #[must_use]
80 pub fn from_boot_critical(kernel_text_ipa: u64, vcpu0_stack_ipa: u64, fdt_ipa: u64) -> Self {
81 Self {
82 pages: vec![kernel_text_ipa, vcpu0_stack_ipa, fdt_ipa],
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct PageRequest {
90 pub ipa: u64,
92 pub page_size: u64,
94}
95
96#[derive(Debug, Error)]
98pub enum PageSourceError {
99 #[error("page source returned a short read: {got} bytes, expected {expected}")]
101 Short {
102 got: u64,
104 expected: u64,
106 },
107 #[error("page source open: {0}")]
109 Open(String),
110 #[error("page source I/O: {0}")]
112 Io(#[source] std::io::Error),
113}
114
115impl From<std::io::Error> for PageSourceError {
116 fn from(err: std::io::Error) -> Self {
117 Self::Io(err)
118 }
119}
120
121pub trait PageSource: Send + Sync + std::fmt::Debug {
125 fn fetch(&self, req: &PageRequest) -> Result<Bytes, PageSourceError>;
130}
131
132#[derive(Debug)]
134pub struct FilePageSource {
135 file: File,
136 ram_start: u64,
137}
138
139impl FilePageSource {
140 pub fn open(path: &Path, ram_start: u64) -> Result<Self, PageSourceError> {
148 let file = File::open(path)
149 .map_err(|e| PageSourceError::Open(format!("{}: {e}", path.display())))?;
150 Ok(Self { file, ram_start })
151 }
152}
153
154impl PageSource for FilePageSource {
155 fn fetch(&self, req: &PageRequest) -> Result<Bytes, PageSourceError> {
156 if req.ipa < self.ram_start {
157 return Err(PageSourceError::Open(format!(
158 "ipa {:#x} below ram_start {:#x}",
159 req.ipa, self.ram_start
160 )));
161 }
162 let offset = req.ipa - self.ram_start;
163 let aligned = offset & !(req.page_size - 1);
164 let mut buf = vec![
165 0u8;
166 usize::try_from(req.page_size).map_err(|_| {
167 PageSourceError::Open("page_size > usize::MAX".into())
168 })?
169 ];
170 #[cfg(unix)]
173 let n = self.file.read_at(&mut buf, aligned)?;
174 #[cfg(not(unix))]
175 let n = {
176 let _ = aligned;
178 buf.fill(0);
179 buf.len()
180 };
181 let n_u64 = u64::try_from(n).unwrap_or(u64::MAX);
185 if n_u64 < req.page_size {
186 return Err(PageSourceError::Short {
187 got: n_u64,
188 expected: req.page_size,
189 });
190 }
191 Ok(Bytes::from(buf))
192 }
193}
194
195#[derive(Debug)]
211pub struct UffdPageSource {
212 socket: Mutex<UnixStream>,
213 uds_path: PathBuf,
214}
215
216impl UffdPageSource {
217 pub fn connect(path: &Path) -> Result<Self, PageSourceError> {
222 let sock = UnixStream::connect(path)
223 .map_err(|e| PageSourceError::Open(format!("connect({}): {e}", path.display())))?;
224 Ok(Self {
225 socket: Mutex::new(sock),
226 uds_path: path.to_path_buf(),
227 })
228 }
229
230 #[must_use]
232 pub fn path(&self) -> &Path {
233 &self.uds_path
234 }
235}
236
237impl PageSource for UffdPageSource {
238 fn fetch(&self, req: &PageRequest) -> Result<Bytes, PageSourceError> {
239 let mut sock = self.socket.lock();
240 sock.write_all(&req.ipa.to_le_bytes())?;
242 sock.write_all(&req.page_size.to_le_bytes())?;
243 let mut hdr = [0u8; 16];
245 sock.read_exact(&mut hdr)?;
246 let echo_ipa = u64::from_le_bytes(hdr[0..8].try_into().unwrap_or([0; 8]));
247 let echo_size = u64::from_le_bytes(hdr[8..16].try_into().unwrap_or([0; 8]));
248 if echo_ipa != req.ipa || echo_size != req.page_size {
249 return Err(PageSourceError::Open(format!(
250 "Uffd protocol violation: expected ipa={:#x} size={} got ipa={:#x} size={}",
251 req.ipa, req.page_size, echo_ipa, echo_size
252 )));
253 }
254 let want = usize::try_from(req.page_size)
255 .map_err(|_| PageSourceError::Open("page_size > usize::MAX".into()))?;
256 let mut buf = BytesMut::zeroed(want);
257 sock.read_exact(&mut buf)?;
258 Ok(buf.freeze())
259 }
260}
261
262#[derive(Debug, Default)]
265pub struct PagerStats {
266 pub faults: AtomicU64,
268 pub prewarmed: AtomicU64,
270 pub port_reinstalls: AtomicU64,
273 pub forwarded_exceptions: AtomicU64,
275}
276
277impl PagerStats {
278 pub fn snapshot(&self) -> PagerStatsSnapshot {
280 PagerStatsSnapshot {
281 faults: self.faults.load(Ordering::Relaxed),
282 prewarmed: self.prewarmed.load(Ordering::Relaxed),
283 port_reinstalls: self.port_reinstalls.load(Ordering::Relaxed),
284 forwarded_exceptions: self.forwarded_exceptions.load(Ordering::Relaxed),
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub struct PagerStatsSnapshot {
292 pub faults: u64,
294 pub prewarmed: u64,
296 pub port_reinstalls: u64,
298 pub forwarded_exceptions: u64,
300}
301
302#[derive(Debug, Error)]
304pub enum PagerError {
305 #[error("postcopy region missing: ipa={ipa:#x}")]
307 OutOfRegion {
308 ipa: u64,
310 },
311 #[error("page source: {0}")]
313 Source(#[from] PageSourceError),
314 #[error("mach error: {0}")]
316 Mach(String),
317 #[error("pager server thread spawn: {0}")]
320 Spawn(#[source] std::io::Error),
321}
322
323#[derive(Debug, Clone, Copy)]
325struct Region {
326 base: u64,
327 size: u64,
328}
329
330impl Region {
331 fn contains(&self, ipa: u64) -> bool {
332 ipa >= self.base && ipa < self.base.saturating_add(self.size)
333 }
334}
335
336#[derive(Debug)]
344pub struct Pager {
345 inner: Arc<PagerInner>,
346}
347
348#[derive(Debug)]
349struct PagerInner {
350 source: Box<dyn PageSource>,
351 config: PagerConfig,
352 regions: Mutex<BTreeMap<u64, Region>>, stats: PagerStats,
354 shutdown: AtomicBool,
357}
358
359impl Pager {
360 #[must_use]
365 pub fn new(source: Box<dyn PageSource>, config: PagerConfig) -> Self {
366 Self {
367 inner: Arc::new(PagerInner {
368 source,
369 config,
370 regions: Mutex::new(BTreeMap::new()),
371 stats: PagerStats::default(),
372 shutdown: AtomicBool::new(false),
373 }),
374 }
375 }
376
377 pub fn register_region(&self, base: u64, size: u64) {
380 let mut regs = self.inner.regions.lock();
381 regs.insert(base, Region { base, size });
382 }
383
384 pub fn prewarm(&self) -> Result<(), PagerError> {
389 let pages = self.inner.config.prewarm.pages.clone();
390 for ipa in pages {
391 self.serve_fault(ipa, host_page_size())?;
392 self.inner.stats.prewarmed.fetch_add(1, Ordering::Relaxed);
393 }
394 Ok(())
395 }
396
397 pub fn serve_fault(&self, ipa: u64, page_size: u64) -> Result<Bytes, PagerError> {
406 if !self.contains_ipa(ipa) {
407 return Err(PagerError::OutOfRegion { ipa });
408 }
409 let aligned = ipa & !(page_size - 1);
410 let req = PageRequest {
411 ipa: aligned,
412 page_size,
413 };
414 let bytes = self.inner.source.fetch(&req)?;
415 self.inner.stats.faults.fetch_add(1, Ordering::Relaxed);
416 Ok(bytes)
417 }
418
419 #[must_use]
421 pub fn contains_ipa(&self, ipa: u64) -> bool {
422 let regs = self.inner.regions.lock();
423 regs.values().any(|r| r.contains(ipa))
424 }
425
426 #[must_use]
428 pub fn stats(&self) -> PagerStatsSnapshot {
429 self.inner.stats.snapshot()
430 }
431
432 pub fn request_shutdown(&self) {
434 self.inner.shutdown.store(true, Ordering::SeqCst);
435 }
436
437 #[must_use]
440 pub fn handle(&self) -> PagerHandle {
441 PagerHandle(Arc::clone(&self.inner))
442 }
443
444 #[doc(hidden)]
447 pub fn record_port_reinstall(&self) {
448 self.inner
449 .stats
450 .port_reinstalls
451 .fetch_add(1, Ordering::Relaxed);
452 }
453
454 #[doc(hidden)]
456 pub fn record_forwarded(&self) {
457 self.inner
458 .stats
459 .forwarded_exceptions
460 .fetch_add(1, Ordering::Relaxed);
461 }
462}
463
464#[derive(Debug, Clone)]
466pub struct PagerHandle(Arc<PagerInner>);
467
468impl PagerHandle {
469 pub fn is_shutting_down(&self) -> bool {
471 self.0.shutdown.load(Ordering::SeqCst)
472 }
473
474 #[must_use]
476 pub fn poll_interval(&self) -> Duration {
477 self.0.config.poll_interval
478 }
479}
480
481#[must_use]
483pub fn host_page_size() -> u64 {
484 16 * 1024
485}
486
487#[cfg(target_os = "macos")]
492mod mach_imp {
493 use std::thread::{self, JoinHandle};
510 #[cfg(not(feature = "pager-live-mach"))]
511 use std::time::Instant;
512
513 #[cfg(not(feature = "pager-live-mach"))]
514 use tracing::{debug, info};
515
516 #[cfg(not(feature = "pager-live-mach"))]
517 use super::PagerHandle;
518 use super::{Pager, PagerError};
519
520 pub fn spawn_server(pager: &Pager) -> Result<JoinHandle<Result<(), PagerError>>, PagerError> {
533 let handle = pager.handle();
534 thread::Builder::new()
535 .name("squib-pager".into())
536 .spawn(move || -> Result<(), PagerError> {
537 #[cfg(feature = "pager-live-mach")]
538 {
539 live::run_live_server(&handle)
540 }
541 #[cfg(not(feature = "pager-live-mach"))]
542 {
543 run_skeleton_loop(&handle);
544 Ok(())
545 }
546 })
547 .map_err(PagerError::Spawn)
548 }
549
550 #[cfg(not(feature = "pager-live-mach"))]
551 fn run_skeleton_loop(handle: &PagerHandle) {
552 info!(
553 poll_interval = ?handle.poll_interval(),
554 "squib-pager server starting (drift-poll skeleton; build with `--features pager-live-mach` to install a real exception port)"
555 );
556 let mut last_drift_check = Instant::now();
557 while !handle.is_shutting_down() {
558 thread::park_timeout(handle.poll_interval());
559 if last_drift_check.elapsed() >= handle.poll_interval() {
560 debug!("squib-pager drift check (no-op skeleton)");
561 last_drift_check = Instant::now();
562 }
563 }
564 debug!("squib-pager server exiting (shutdown requested)");
565 }
566
567 #[cfg(feature = "pager-live-mach")]
568 mod live {
569 use std::time::Instant;
588
589 use mach2::{
590 exception_types::{
591 EXC_MASK_BAD_ACCESS, EXCEPTION_DEFAULT, exception_behavior_array_t,
592 exception_flavor_array_t, exception_mask_array_t, exception_mask_t,
593 },
594 kern_return::KERN_SUCCESS,
595 mach_port::{mach_port_allocate, mach_port_deallocate},
596 mach_types::exception_handler_array_t,
597 message::{
598 MACH_MSG_TIMEOUT_NONE, MACH_RCV_MSG, MACH_RCV_TIMEOUT, MACH_RCV_TOO_LARGE,
599 mach_msg, mach_msg_header_t,
600 },
601 port::{MACH_PORT_RIGHT_RECEIVE, mach_port_t},
602 task::{task_get_exception_ports, task_swap_exception_ports},
603 thread_status::THREAD_STATE_NONE,
604 traps::mach_task_self,
605 };
606 use tracing::{debug, info, warn};
607
608 use super::super::{PagerError, PagerHandle};
609
610 const SQUIB_EXC_MASK: exception_mask_t = EXC_MASK_BAD_ACCESS as exception_mask_t;
615
616 const MAX_EXCEPTION_PORTS: usize = 32;
620
621 pub(super) fn run_live_server(handle: &PagerHandle) -> Result<(), PagerError> {
624 let our_port = allocate_receive_port()
625 .map_err(|kr| PagerError::Mach(format!("mach_port_allocate: kr={kr}")))?;
626 install_exception_port(our_port).map_err(|kr| {
627 PagerError::Mach(format!("task_swap_exception_ports install: kr={kr}"))
628 })?;
629 info!(
630 port = our_port,
631 "squib-pager live: exception port installed"
632 );
633
634 let mut last_drift_check = Instant::now();
635 while !handle.is_shutting_down() {
636 let timeout_ms =
637 u32::try_from(handle.poll_interval().as_millis()).unwrap_or(u32::MAX);
638 let kr = recv_one(our_port, timeout_ms);
639 match kr {
640 KERN_SUCCESS => {
641 debug!("squib-pager live: received exception (no-reply forwarder)");
649 }
650 rc if rc == MACH_RCV_TIMEOUT => {
651 }
654 rc if rc == MACH_RCV_TOO_LARGE => {
655 warn!("squib-pager live: oversize exception message dropped");
656 }
657 rc => {
658 warn!(kr = rc, "squib-pager live: unexpected mach_msg return");
659 }
660 }
661
662 if last_drift_check.elapsed() >= handle.poll_interval() {
663 if let Err(e) = drift_check_live(our_port) {
664 warn!(error = ?e, "squib-pager live: drift check failed");
665 }
666 last_drift_check = Instant::now();
667 }
668 }
669
670 let dealloc = unsafe { mach_port_deallocate(mach_task_self(), our_port) };
675 if dealloc != KERN_SUCCESS {
676 warn!(
677 kr = dealloc,
678 "squib-pager live: mach_port_deallocate failed (best-effort)"
679 );
680 }
681 debug!("squib-pager live: server exiting (shutdown requested)");
682 Ok(())
683 }
684
685 fn allocate_receive_port() -> Result<mach_port_t, i32> {
686 let mut port: mach_port_t = 0;
687 let kr = unsafe {
690 mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &raw mut port)
691 };
692 if kr == KERN_SUCCESS {
693 Ok(port)
694 } else {
695 Err(kr)
696 }
697 }
698
699 fn install_exception_port(port: mach_port_t) -> Result<(), i32> {
700 let mut masks: [exception_mask_t; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
701 let mut handlers: [mach_port_t; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
702 let mut behaviors: [u32; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
703 let mut flavors: [i32; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
704 let mut count: u32 = MAX_EXCEPTION_PORTS as u32;
705
706 let kr = unsafe {
711 task_swap_exception_ports(
712 mach_task_self(),
713 SQUIB_EXC_MASK,
714 port,
715 EXCEPTION_DEFAULT.cast_signed(),
716 THREAD_STATE_NONE,
717 masks.as_mut_ptr() as exception_mask_array_t,
718 &raw mut count,
719 handlers.as_mut_ptr() as exception_handler_array_t,
720 behaviors.as_mut_ptr() as exception_behavior_array_t,
721 flavors.as_mut_ptr() as exception_flavor_array_t,
722 )
723 };
724 if kr == KERN_SUCCESS { Ok(()) } else { Err(kr) }
725 }
726
727 fn drift_check_live(our_port: mach_port_t) -> Result<(), i32> {
728 let mut masks: [exception_mask_t; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
732 let mut handlers: [mach_port_t; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
733 let mut behaviors: [u32; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
734 let mut flavors: [i32; MAX_EXCEPTION_PORTS] = [0; MAX_EXCEPTION_PORTS];
735 let mut count: u32 = MAX_EXCEPTION_PORTS as u32;
736 let kr = unsafe {
740 task_get_exception_ports(
741 mach_task_self(),
742 SQUIB_EXC_MASK,
743 masks.as_mut_ptr() as exception_mask_array_t,
744 &raw mut count,
745 handlers.as_mut_ptr() as exception_handler_array_t,
746 behaviors.as_mut_ptr() as exception_behavior_array_t,
747 flavors.as_mut_ptr() as exception_flavor_array_t,
748 )
749 };
750 if kr != KERN_SUCCESS {
751 return Err(kr);
752 }
753 let drifted = (0..count as usize)
756 .any(|i| (masks[i] & SQUIB_EXC_MASK) != 0 && handlers[i] != our_port);
757 if drifted {
758 debug!("squib-pager live: drift detected, re-installing");
759 install_exception_port(our_port)?;
760 }
761 Ok(())
762 }
763
764 fn recv_one(port: mach_port_t, timeout_ms: u32) -> i32 {
765 let mut header = mach_msg_header_t::default();
770 let option = if timeout_ms == 0 {
771 MACH_RCV_MSG
772 } else {
773 MACH_RCV_MSG | MACH_RCV_TIMEOUT
774 };
775 let timeout = if timeout_ms == 0 {
776 MACH_MSG_TIMEOUT_NONE
777 } else {
778 timeout_ms
779 };
780 unsafe {
785 mach_msg(
786 (&raw mut header).cast(),
787 option,
788 0,
789 size_of::<mach_msg_header_t>() as u32,
790 port,
791 timeout,
792 0,
793 )
794 }
795 }
796 }
797}
798
799#[cfg(target_os = "macos")]
800pub use mach_imp::spawn_server as spawn_mach_server;
801
802#[cfg(not(target_os = "macos"))]
808pub fn spawn_mach_server(
809 _pager: &Pager,
810) -> Result<std::thread::JoinHandle<Result<(), PagerError>>, PagerError> {
811 std::thread::Builder::new()
812 .name("squib-pager-stub".into())
813 .spawn(|| Ok(()))
814 .map_err(PagerError::Spawn)
815}
816
817#[cfg(test)]
818mod tests {
819 use std::io::Write as _;
820
821 use bytes::Bytes;
822 use tempfile::TempDir;
823
824 use super::*;
825
826 #[derive(Debug)]
827 struct FakeSource {
828 bytes: Bytes,
829 }
830
831 impl PageSource for FakeSource {
832 fn fetch(&self, _req: &PageRequest) -> Result<Bytes, PageSourceError> {
833 Ok(self.bytes.clone())
834 }
835 }
836
837 #[test]
838 fn test_should_register_and_match_a_postcopy_region() {
839 let pager = Pager::new(
840 Box::new(FakeSource {
841 bytes: Bytes::from(vec![0u8; 16 * 1024]),
842 }),
843 PagerConfig::default(),
844 );
845 pager.register_region(0x8000_0000, 0x1000_0000);
846 assert!(pager.contains_ipa(0x8000_1234));
847 assert!(!pager.contains_ipa(0x9000_0000));
848 }
849
850 #[test]
851 fn test_should_serve_fault_inside_registered_region() {
852 let bytes = Bytes::from(vec![0xAB; 16 * 1024]);
853 let pager = Pager::new(
854 Box::new(FakeSource {
855 bytes: bytes.clone(),
856 }),
857 PagerConfig::default(),
858 );
859 pager.register_region(0x8000_0000, 0x1000_0000);
860 let got = pager.serve_fault(0x8000_1234, 16 * 1024).unwrap();
861 assert_eq!(got, bytes);
862 assert_eq!(pager.stats().faults, 1);
863 }
864
865 #[test]
866 fn test_should_reject_fault_outside_region() {
867 let pager = Pager::new(
868 Box::new(FakeSource {
869 bytes: Bytes::from(vec![0u8; 16 * 1024]),
870 }),
871 PagerConfig::default(),
872 );
873 pager.register_region(0x8000_0000, 0x1000_0000);
874 let err = pager.serve_fault(0xC000_0000, 16 * 1024).unwrap_err();
875 assert!(matches!(err, PagerError::OutOfRegion { ipa: 0xC000_0000 }));
876 }
877
878 #[derive(Debug, Default)]
879 struct CaptureSource {
880 seen: Mutex<Vec<u64>>,
881 }
882
883 impl PageSource for CaptureSource {
884 fn fetch(&self, req: &PageRequest) -> Result<Bytes, PageSourceError> {
885 self.seen.lock().push(req.ipa);
886 Ok(Bytes::from(vec![0u8; req.page_size as usize]))
887 }
888 }
889
890 #[derive(Debug)]
891 struct WrapperSource {
892 inner: Arc<CaptureSource>,
893 }
894
895 impl PageSource for WrapperSource {
896 fn fetch(&self, req: &PageRequest) -> Result<Bytes, PageSourceError> {
897 self.inner.fetch(req)
898 }
899 }
900
901 #[test]
902 fn test_should_align_request_to_page_boundary() {
903 let src = Arc::new(CaptureSource::default());
904 let pager = Pager::new(
905 Box::new(WrapperSource { inner: src.clone() }),
906 PagerConfig::default(),
907 );
908 pager.register_region(0x8000_0000, 0x1000_0000);
909 pager.serve_fault(0x8000_1234, 16 * 1024).unwrap();
910 let seen = src.seen.lock().clone();
911 assert_eq!(seen, vec![0x8000_0000]);
912 }
913
914 #[test]
915 fn test_should_prewarm_each_page_on_demand() {
916 let bytes = Bytes::from(vec![0xCD; 16 * 1024]);
917 let cfg = PagerConfig {
918 prewarm: PrewarmList::from_boot_critical(
919 0x8000_2000, 0x9000_0000, 0x9F00_0000, ),
923 ..Default::default()
924 };
925 let pager = Pager::new(Box::new(FakeSource { bytes }), cfg);
926 pager.register_region(0x8000_0000, 0x2000_0000);
927 pager.prewarm().unwrap();
928 let stats = pager.stats();
929 assert_eq!(stats.prewarmed, 3);
930 assert_eq!(stats.faults, 3);
931 }
932
933 #[test]
934 fn test_file_page_source_reads_at_offset_from_ram_start() {
935 let dir = TempDir::new().unwrap();
936 let path = dir.path().join("x.mem");
937 let mut bytes = vec![0u8; 64 * 1024];
938 for byte in &mut bytes[16 * 1024..32 * 1024] {
940 *byte = 0x77;
941 }
942 std::fs::write(&path, &bytes).unwrap();
943 let src = FilePageSource::open(&path, 0x8000_0000).unwrap();
944 let got = src
945 .fetch(&PageRequest {
946 ipa: 0x8000_4000, page_size: 16 * 1024,
948 })
949 .unwrap();
950 assert!(got.iter().all(|&b| b == 0x77));
951 }
952
953 #[test]
954 fn test_file_page_source_rejects_below_ram_start() {
955 let dir = TempDir::new().unwrap();
956 let path = dir.path().join("x.mem");
957 std::fs::write(&path, vec![0u8; 16 * 1024]).unwrap();
958 let src = FilePageSource::open(&path, 0x8000_0000).unwrap();
959 let err = src
960 .fetch(&PageRequest {
961 ipa: 0x4000_0000,
962 page_size: 16 * 1024,
963 })
964 .unwrap_err();
965 assert!(matches!(err, PageSourceError::Open(_)));
966 }
967
968 #[test]
969 fn test_uffd_page_source_round_trips_protocol() {
970 let dir = TempDir::new().unwrap();
972 let sock_path = dir.path().join("pager.sock");
973 let listener = std::os::unix::net::UnixListener::bind(&sock_path).unwrap();
974 let server = std::thread::spawn(move || {
975 let (mut sock, _) = listener.accept().unwrap();
976 let mut hdr = [0u8; 16];
977 sock.read_exact(&mut hdr).unwrap();
978 let ipa = u64::from_le_bytes(hdr[0..8].try_into().unwrap());
979 let size = u64::from_le_bytes(hdr[8..16].try_into().unwrap());
980 sock.write_all(&hdr).unwrap();
981 let payload = vec![0xEEu8; size as usize];
982 sock.write_all(&payload).unwrap();
983 (ipa, size)
984 });
985 let src = UffdPageSource::connect(&sock_path).unwrap();
986 let got = src
987 .fetch(&PageRequest {
988 ipa: 0x8000_0000,
989 page_size: 16 * 1024,
990 })
991 .unwrap();
992 assert!(got.iter().all(|&b| b == 0xEE));
993 let _ = server.join().unwrap();
994 }
995
996 #[test]
997 fn test_should_request_shutdown_via_handle() {
998 let pager = Pager::new(
999 Box::new(FakeSource {
1000 bytes: Bytes::from(vec![0u8; 16 * 1024]),
1001 }),
1002 PagerConfig::default(),
1003 );
1004 let h = pager.handle();
1005 assert!(!h.is_shutting_down());
1006 pager.request_shutdown();
1007 assert!(h.is_shutting_down());
1008 }
1009
1010 #[test]
1011 fn test_stats_record_port_reinstall_and_forwarded() {
1012 let pager = Pager::new(
1013 Box::new(FakeSource {
1014 bytes: Bytes::from(vec![0u8; 16 * 1024]),
1015 }),
1016 PagerConfig::default(),
1017 );
1018 pager.record_port_reinstall();
1019 pager.record_port_reinstall();
1020 pager.record_forwarded();
1021 let s = pager.stats();
1022 assert_eq!(s.port_reinstalls, 2);
1023 assert_eq!(s.forwarded_exceptions, 1);
1024 }
1025}