use core::{
ptr,
sync::atomic::{AtomicBool, Ordering},
};
use patina_internal_cpu::paging::PatinaPageTable;
use patina_paging::MemoryAttributes;
use crate::arch::DebuggerArch;
const PAGE_SIZE: u64 = 0x1000;
const PAGE_MASK: u64 = !(PAGE_SIZE - 1);
static ALLOW_UNCACHED_ACCESS: AtomicBool = AtomicBool::new(false);
pub fn toggle_uncached_access() -> bool {
!ALLOW_UNCACHED_ACCESS.fetch_xor(true, Ordering::Relaxed)
}
pub fn read_memory<Arch: DebuggerArch>(address: u64, buffer: &mut [u8], unsafe_read: bool) -> Result<usize, ()> {
let page_table = Arch::get_page_table()?;
let len = if unsafe_read { buffer.len() } else { check_range_access::<Arch>(&page_table, address, buffer.len())? };
if len == 0 {
return Err(());
}
let ptr = address as *const u8;
unsafe {
ptr::copy(ptr, buffer.as_mut_ptr(), len);
}
Ok(len)
}
pub fn write_memory<Arch: DebuggerArch>(address: u64, buffer: &[u8]) -> Result<(), ()> {
let end_address = address + buffer.len() as u64;
let mut page_table = Arch::get_page_table()?;
let valid_bytes = check_range_access::<Arch>(&page_table, address, buffer.len())?;
if valid_bytes != buffer.len() {
return Err(());
}
let mut current = address;
while current < end_address {
let page = current & PAGE_MASK;
let end = (page + PAGE_SIZE).min(end_address);
let len = (end - current) as usize;
let offset = (current - address) as usize;
let attributes = page_table
.query_memory_region(page, PAGE_SIZE)
.map_err(|_| ())
.expect("Unexpected failure on address that was already checked.");
if attributes.contains(MemoryAttributes::ReadOnly) {
page_table.map_memory_region(page, PAGE_SIZE, attributes & !MemoryAttributes::ReadOnly).map_err(|_| ())?;
}
let ptr = current as *mut u8;
unsafe {
ptr::copy_nonoverlapping(buffer.as_ptr().add(offset), ptr, len);
}
if attributes.contains(MemoryAttributes::ReadOnly) {
page_table
.map_memory_region(page, PAGE_SIZE, attributes)
.expect("Failed to restore page table attributes!");
}
current += len as u64;
}
Ok(())
}
fn check_range_access<Arch: DebuggerArch>(
page_table: &impl PatinaPageTable,
address: u64,
length: usize,
) -> Result<usize, ()> {
let len = check_paging_range(page_table, address, length)?;
Arch::memory_poke_test(address)?;
Ok(len)
}
fn check_paging_range<P: PatinaPageTable>(page_table: &P, start_address: u64, length: usize) -> Result<usize, ()> {
let allow_uncached = ALLOW_UNCACHED_ACCESS.load(Ordering::Relaxed);
let mut page = start_address & PAGE_MASK;
while page <= start_address + (length - 1) as u64 {
let res = page_table.query_memory_region(page, PAGE_SIZE);
let valid = match res {
Ok(attributes) => {
!attributes.contains(MemoryAttributes::ReadProtect)
&& (allow_uncached || !attributes.contains(MemoryAttributes::Uncached))
}
Err(_) => false,
};
if !valid {
if page > start_address {
return Ok((page - start_address) as usize);
}
return Err(());
}
match page.checked_add(PAGE_SIZE) {
Some(next) => page = next,
None => break,
}
}
Ok(length)
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod tests {
use super::*;
use crate::*;
use gdbstub::target::ext::breakpoints;
use mockall::{predicate::*, *};
use patina_internal_cpu::paging::{CacheAttributeSource, PagingError};
use patina_paging::MemoryAttributes;
mock! {
pub MemPageTable {}
impl PatinaPageTable for MemPageTable {
fn map_memory_region(&mut self, address: u64, size: u64, attributes: MemoryAttributes) -> Result<(), PagingError>;
fn map_aliased_memory_region(
&mut self,
virtual_address: u64,
physical_address: u64,
size: u64,
attributes: MemoryAttributes,
) -> Result<(), PagingError>;
fn unmap_memory_region(&mut self, address: u64, size: u64) -> Result<(), PagingError>;
fn install_page_table(&mut self) -> Result<(), PagingError>;
fn query_memory_region(&self, address: u64, size: u64) -> Result<MemoryAttributes, (PagingError, Option<MemoryAttributes>)>;
fn cache_attribute_source(&self) -> CacheAttributeSource;
fn dump_page_tables(&self, address: u64, size: u64) -> Result<(), PagingError>;
fn handle_cacheability_change(
&self,
address: u64,
size: u64,
old_cache_attributes: MemoryAttributes,
new_cache_attributes: MemoryAttributes,
);
}
}
mock! {
pub MemDebuggerArch {}
#[allow(refining_impl_trait_internal)]
impl DebuggerArch for MemDebuggerArch {
const DEFAULT_EXCEPTION_TYPES: &'static [usize] = &[];
const BREAKPOINT_INSTRUCTION: &'static [u8] = &[];
const GDB_TARGET_XML: &'static str = "";
const GDB_REGISTERS_XML: &'static str = "";
fn breakpoint();
fn process_entry(exception_type: u64, context: &mut ExceptionContext) -> ExceptionInfo;
fn process_exit(exception_info: &mut ExceptionInfo);
fn set_single_step(exception_info: &mut ExceptionInfo);
fn initialize();
fn add_watchpoint(address: u64, length: u64, access_type: breakpoints::WatchKind) -> bool;
fn remove_watchpoint(address: u64, length: u64, access_type: breakpoints::WatchKind) -> bool;
fn get_page_table() -> Result<MockMemPageTable, ()>;
fn reboot();
fn memory_poke_test(address: u64) -> Result<(), ()>;
fn check_memory_poke_test(context: &mut ExceptionContext) -> bool;
}
}
#[test]
fn test_access_check_valid_page() {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().once().returning(|_, _| Ok(MemoryAttributes::empty()));
let result = check_paging_range(&mock_page_table, 0, 0x1000);
assert_eq!(result.expect("Failed to check range access."), 0x1000);
}
#[test]
fn test_access_check_invalid_page() {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table
.expect_query_memory_region()
.times(2)
.returning(|_, _| Err((PagingError::InvalidMemoryRange, None)));
let result = check_paging_range(&mock_page_table, 0, 0x1000);
result.expect_err("Should have return a failure.");
let result = check_paging_range(&mock_page_table, 0x800, 0x1000);
result.expect_err("Should have return a failure.");
}
#[test]
fn test_access_check_valid_range() {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().times(4).returning(|_, _| Ok(MemoryAttributes::empty()));
let result = check_paging_range(&mock_page_table, 0x800, 0x3000);
assert_eq!(result.expect("Failed to check range access."), 0x3000);
}
#[test]
fn test_access_check_partially_valid_range() {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().times(2).returning(|_, _| Ok(MemoryAttributes::empty()));
mock_page_table
.expect_query_memory_region()
.times(1)
.returning(|_, _| Err((PagingError::InvalidMemoryRange, None)));
let result = check_paging_range(&mock_page_table, 0x800, 0x3000);
assert_eq!(result.expect("Failed to check range access."), 0x1800);
}
static PAGE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn test_read_memory_valid() {
let data = [0xCF_u8];
let mut buffer = [0_u8];
let _lock = PAGE_LOCK.lock().unwrap();
let poke_ctx = MockMemDebuggerArch::memory_poke_test_context();
poke_ctx.expect().returning(|_| Ok(()));
let ctx = MockMemDebuggerArch::get_page_table_context();
ctx.expect().returning(|| {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().returning(|_, _| Ok(MemoryAttributes::ReadOnly));
Ok(mock_page_table)
});
let address = &raw const data as u64;
let result = read_memory::<MockMemDebuggerArch>(address, &mut buffer, false);
assert_eq!(result.expect("Failed to read memory."), buffer.len());
assert_eq!(buffer, data);
}
#[test]
fn test_read_memory_invalid() {
let mut buffer = [0_u8; 1];
let _lock = PAGE_LOCK.lock().unwrap();
let ctx = MockMemDebuggerArch::get_page_table_context();
ctx.expect().returning(|| {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().returning(|_, _| Err((PagingError::InvalidMemoryRange, None)));
Ok(mock_page_table)
});
let result = read_memory::<MockMemDebuggerArch>(0, &mut buffer, false);
assert!(result.is_err());
}
#[test]
fn test_write_memory_valid() {
let data = [0_u8];
let buffer = [0xCF_u8; 1];
let _lock = PAGE_LOCK.lock().unwrap();
let poke_ctx = MockMemDebuggerArch::memory_poke_test_context();
poke_ctx.expect().returning(|_| Ok(()));
let ctx = MockMemDebuggerArch::get_page_table_context();
ctx.expect().returning(|| {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().returning(|_, _| Ok(MemoryAttributes::empty()));
Ok(mock_page_table)
});
let address = &raw const data as u64;
let result = write_memory::<MockMemDebuggerArch>(address, &buffer);
assert!(result.is_ok());
assert_eq!(buffer, data);
}
#[test]
fn test_write_memory_cross_page_boundary() {
const SIZE: usize = PAGE_SIZE as usize * 2;
let layout = std::alloc::Layout::from_size_align(SIZE, PAGE_SIZE as usize).unwrap();
let dest = unsafe { std::alloc::alloc_zeroed(layout) };
assert!(!dest.is_null());
let offset = (PAGE_SIZE / 2) as usize;
let write_len = PAGE_SIZE as usize; let address = dest as u64 + offset as u64;
let src: Vec<u8> = (0..write_len).map(|i| (i & 0xFF) as u8).collect();
let _lock = PAGE_LOCK.lock().unwrap();
let poke_ctx = MockMemDebuggerArch::memory_poke_test_context();
poke_ctx.expect().returning(|_| Ok(()));
let ctx = MockMemDebuggerArch::get_page_table_context();
ctx.expect().returning(|| {
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().returning(|_, _| Ok(MemoryAttributes::empty()));
Ok(mock_page_table)
});
let result = write_memory::<MockMemDebuggerArch>(address, &src);
assert!(result.is_ok());
for (i, &expected) in src.iter().enumerate() {
let actual = unsafe { *dest.add(offset + i) };
assert_eq!(actual, expected, "Mismatch at byte offset {i}: expected {expected}, got {actual}");
}
unsafe { std::alloc::dealloc(dest, layout) };
}
#[test]
fn test_access_check_uncached_page_skipped_by_default() {
let _lock = PAGE_LOCK.lock().unwrap();
ALLOW_UNCACHED_ACCESS.store(false, Ordering::Relaxed);
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().once().returning(|_, _| Ok(MemoryAttributes::Uncached));
let result = check_paging_range(&mock_page_table, 0, 0x1000);
result.expect_err("Uncached page should be treated as invalid by default.");
}
#[test]
fn test_access_check_uncached_page_allowed_when_toggled() {
let _lock = PAGE_LOCK.lock().unwrap();
ALLOW_UNCACHED_ACCESS.store(true, Ordering::Relaxed);
let mut mock_page_table = MockMemPageTable::new();
mock_page_table.expect_query_memory_region().once().returning(|_, _| Ok(MemoryAttributes::Uncached));
let result = check_paging_range(&mock_page_table, 0, 0x1000);
assert_eq!(result.expect("Uncached page should be allowed when toggled."), 0x1000);
ALLOW_UNCACHED_ACCESS.store(false, Ordering::Relaxed);
}
#[test]
fn test_toggle_uncached_access() {
let _lock = PAGE_LOCK.lock().unwrap();
ALLOW_UNCACHED_ACCESS.store(false, Ordering::Relaxed);
assert!(!ALLOW_UNCACHED_ACCESS.load(Ordering::Relaxed));
let enabled = toggle_uncached_access();
assert!(enabled);
assert!(ALLOW_UNCACHED_ACCESS.load(Ordering::Relaxed));
let disabled = toggle_uncached_access();
assert!(!disabled);
assert!(!ALLOW_UNCACHED_ACCESS.load(Ordering::Relaxed));
}
}