//! Bounded multi-page syscall copies for one currently running process. //! Resolve every segment and permission before touching any backing byte. //! A guard hole is not made readable merely because its neighbours are owned. use crate::user_copy::{EFAULT, EINVAL}; use crate::{ memory::PAGE_SIZE, process::{ProcessId, ProcessTable, State}, }; pub const MAX_PROCESS_COPY: usize = 8192; const MAX_PAGES: usize = 8; const MAX_SEGMENTS: usize = 3; // An unaligned 8 KiB range can span three pages. #[derive(Clone, Copy, Debug)] pub struct OwnedPage { pub virtual_base: u64, pub physical_base: u64, pub writable: bool, } pub struct ProcessPages<'a> { owner: ProcessId, pages: &'a [OwnedPage], } #[derive(Clone, Copy, Default)] struct Segment { physical: u64, length: usize, } struct Plan { segments: [Segment; MAX_SEGMENTS], count: usize, } impl<'a> ProcessPages<'a> { /// Structural validation only: the trusted kernel must derive this list /// from the owner's live page tables, not from user-supplied physical VAs. pub fn new(owner: ProcessId, pages: &'a [OwnedPage]) -> Result { if owner.generation == 0 || pages.is_empty() || pages.len() > MAX_PAGES { return Err(EFAULT); } for (index, page) in pages.iter().enumerate() { if page.virtual_base % PAGE_SIZE != 0 || page.physical_base % PAGE_SIZE != 0 || page.virtual_base.checked_add(PAGE_SIZE).is_none() || page.physical_base.checked_add(PAGE_SIZE).is_none() || pages[..index].iter().any(|prior| { prior.virtual_base == page.virtual_base || prior.physical_base == page.physical_base }) { return Err(EFAULT); } } Ok(Self { owner, pages }) } fn plan( &self, processes: &ProcessTable, caller: ProcessId, address: u64, length: usize, write: bool, ) -> Result { if caller != self.owner || processes.state(caller) != Ok(State::Running) { return Err(EFAULT); } if length > MAX_PROCESS_COPY { return Err(EINVAL); } let mut plan = Plan { segments: [Segment::default(); MAX_SEGMENTS], count: 0, }; // Zero-length calls still check process identity but do not evaluate VA. if length == 0 { return Ok(plan); } address.checked_add(length as u64 - 1).ok_or(EFAULT)?; let mut cursor = address; let mut remaining = length; while remaining != 0 { let base = cursor & !(PAGE_SIZE - 1); let page = self .pages .iter() .find(|p| p.virtual_base == base) .ok_or(EFAULT)?; if (write && !page.writable) || plan.count == MAX_SEGMENTS { return Err(EFAULT); } let offset = cursor - base; let take = remaining.min((PAGE_SIZE - offset) as usize); plan.segments[plan.count] = Segment { physical: page.physical_base + offset, length: take, }; plan.count += 1; remaining -= take; if remaining != 0 { cursor = cursor.checked_add(take as u64).ok_or(EFAULT)?; } } Ok(plan) } /// # Safety /// Each physical frame is live, identity-mapped and exclusively owned by /// `caller` throughout the copy. No concurrent exit/unmap or output alias. pub unsafe fn copy_from( &self, processes: &ProcessTable, caller: ProcessId, address: u64, output: &mut [u8], ) -> Result { let plan = self.plan(processes, caller, address, output.len(), false)?; let mut offset = 0; for segment in &plan.segments[..plan.count] { unsafe { core::ptr::copy_nonoverlapping( segment.physical as *const u8, output.as_mut_ptr().add(offset), segment.length, ) }; offset += segment.length; } Ok(offset) } /// # Safety /// Same live-owner guarantees as `copy_from`; input aliases no owned page. /// Permission/hole/overflow failure cannot write even the first segment. pub unsafe fn copy_to( &self, processes: &ProcessTable, caller: ProcessId, address: u64, input: &[u8], ) -> Result { let plan = self.plan(processes, caller, address, input.len(), true)?; let mut offset = 0; for segment in &plan.segments[..plan.count] { unsafe { core::ptr::copy_nonoverlapping( input.as_ptr().add(offset), segment.physical as *mut u8, segment.length, ) }; offset += segment.length; } Ok(offset) } } #[cfg(test)] mod tests { extern crate std; use super::*; use std::boxed::Box; #[repr(align(4096))] struct Page([u8; 4096]); const VA: u64 = 0x8001_0000; #[test] fn p10_k526_discontiguous_physical_frames_copy_across_two_and_three_pages() { let mut table = ProcessTable::new(); let id = table.create().unwrap(); table.run(id).unwrap(); let mut a = Box::new(Page([0xa1; 4096])); let mut b = Box::new(Page([0xb2; 4096])); let mut c = Box::new(Page([0xc3; 4096])); let pages = [ OwnedPage { virtual_base: VA, physical_base: a.0.as_mut_ptr() as u64, writable: true, }, OwnedPage { virtual_base: VA + PAGE_SIZE, physical_base: b.0.as_mut_ptr() as u64, writable: true, }, OwnedPage { virtual_base: VA + 2 * PAGE_SIZE, physical_base: c.0.as_mut_ptr() as u64, writable: true, }, ]; let view = ProcessPages::new(id, &pages).unwrap(); let mut out = [0; MAX_PROCESS_COPY]; assert_eq!( unsafe { view.copy_from(&table, id, VA + 4094, &mut out) }, Ok(8192) ); assert_eq!(&out[..2], &[0xa1; 2]); assert!(out[2..4098].iter().all(|b| *b == 0xb2)); assert!(out[4098..].iter().all(|b| *b == 0xc3)); assert_eq!( unsafe { view.copy_to(&table, id, VA + 4094, b"ABCD") }, Ok(4) ); assert_eq!(&a.0[4094..], b"AB"); assert_eq!(&b.0[..2], b"CD"); } #[test] fn p10_k526_hole_readonly_overflow_and_length_fail_without_prefix_copy() { let mut table = ProcessTable::new(); let id = table.create().unwrap(); table.run(id).unwrap(); let a = Box::new(Page([0xa5; 4096])); let b = Box::new(Page([0x5a; 4096])); let pages = [ OwnedPage { virtual_base: VA, physical_base: a.0.as_ptr() as u64, writable: true, }, OwnedPage { virtual_base: VA + PAGE_SIZE, physical_base: b.0.as_ptr() as u64, writable: false, }, ]; let view = ProcessPages::new(id, &pages).unwrap(); assert_eq!( unsafe { view.copy_to(&table, id, VA + 4094, b"FAIL") }, Err(EFAULT) ); assert_eq!(&a.0[4094..], &[0xa5; 2]); assert_eq!(&b.0[..2], &[0x5a; 2]); let mut out = [0xcc; 4]; for address in [u64::MAX - 1, VA + 2 * PAGE_SIZE - 2] { assert_eq!( unsafe { view.copy_from(&table, id, address, &mut out) }, Err(EFAULT) ); assert_eq!(out, [0xcc; 4]); } let mut large = [0xcc; MAX_PROCESS_COPY + 1]; assert_eq!( unsafe { view.copy_from(&table, id, VA, &mut large) }, Err(EINVAL) ); assert!(large.iter().all(|b| *b == 0xcc)); assert_eq!( unsafe { view.copy_from(&table, id, u64::MAX, &mut []) }, Ok(0) ); } #[test] fn p10_k526_stale_foreign_waiting_and_exited_owners_cannot_copy() { let mut table = ProcessTable::new(); let id = table.create().unwrap(); let other = table.create().unwrap(); let page = Box::new(Page([0xaa; 4096])); let pages = [OwnedPage { virtual_base: VA, physical_base: page.0.as_ptr() as u64, writable: true, }]; let view = ProcessPages::new(id, &pages).unwrap(); let mut out = [0xcc; 4]; assert_eq!( unsafe { view.copy_from(&table, id, VA, &mut out) }, Err(EFAULT) ); table.run(id).unwrap(); assert_eq!( unsafe { view.copy_from(&table, other, VA, &mut out) }, Err(EFAULT) ); table.wait(id).unwrap(); assert_eq!( unsafe { view.copy_from(&table, id, VA, &mut out) }, Err(EFAULT) ); table.exit(id, 0).unwrap(); table.reap(id).unwrap(); let reused = table.create().unwrap(); table.run(reused).unwrap(); for caller in [id, reused] { assert_eq!( unsafe { view.copy_from(&table, caller, VA, &mut out) }, Err(EFAULT) ); } assert_eq!(out, [0xcc; 4]); } #[test] fn p10_k526_mapping_alias_alignment_and_wrapped_frames_are_rejected() { let id = ProcessId { slot: 0, generation: 1, }; let page = OwnedPage { virtual_base: VA, physical_base: 0x4000_0000, writable: true, }; assert!(ProcessPages::new(id, &[page, page]).is_err()); assert!( ProcessPages::new( id, &[ page, OwnedPage { virtual_base: VA + PAGE_SIZE, ..page } ] ) .is_err() ); for bad in [ OwnedPage { physical_base: 3, ..page }, OwnedPage { virtual_base: VA + 1, ..page }, OwnedPage { physical_base: u64::MAX - PAGE_SIZE + 1, ..page }, ] { assert!(ProcessPages::new(id, &[bad]).is_err()); } } }