patina_debugger 23.1.0

Debugger implementation for a Patina Core.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! Implements memory operations for the Patina debugger.
//!
//! ## License
//!
//! Copyright (C) Microsoft Corporation.
//!
//! SPDX-License-Identifier: Apache-2.0
//!

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);

/// Controls whether the debugger will read/write uncached memory regions.
/// By default (`false`), uncached regions are skipped to avoid side-effects
/// on memory-mapped I/O. A monitor command can toggle this to `true`.
static ALLOW_UNCACHED_ACCESS: AtomicBool = AtomicBool::new(false);

/// Toggles uncached memory access and returns the new state.
pub fn toggle_uncached_access() -> bool {
    !ALLOW_UNCACHED_ACCESS.fetch_xor(true, Ordering::Relaxed)
}

/// Reads memory from the specified address into the buffer.
///
/// This will ensure that the address is valid in the page tables before reading.
/// If the base address is not valid, this will return an error. If the range is partially
/// valid then the valid portion will be read and the size of the valid region will
/// be returned.
///
pub fn read_memory<Arch: DebuggerArch>(address: u64, buffer: &mut [u8], unsafe_read: bool) -> Result<usize, ()> {
    let page_table = Arch::get_page_table()?;

    // Check that all of the pages are mapped before accessing the memory.
    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;
    // SAFETY: We have ensured these pages are readable before accessing them.
    unsafe {
        ptr::copy(ptr, buffer.as_mut_ptr(), len);
    }

    Ok(len)
}

/// Writes the buffer to the specified address.
///
/// This will ensure that the address is valid in the page tables before writing.
/// If the mapping is valid, but is not writable, this routine will temporarily
/// edit the page tables to allow the write to occur. If the mapping is not valid,
/// this will return an error.
///
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()?;

    // Check that all of the pages are mapped before accessing the memory.
    let valid_bytes = check_range_access::<Arch>(&page_table, address, buffer.len())?;
    if valid_bytes != buffer.len() {
        return Err(());
    }

    // all pages are mapped. Edit one at a time, adding write permissions as needed.
    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 isize;

        // Check that this page is writable before writing. If it is not, then temporarily
        // modify the page table to allow writing.
        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;
        // SAFETY: We have ensured these pages are writable before accessing them.
        unsafe {
            ptr::copy_nonoverlapping(buffer.as_ptr().offset(offset), ptr, len);
        }

        if attributes.contains(MemoryAttributes::ReadOnly) {
            // Restore the original page attributes. Panic if this fails as the
            // debugger should not allow the system to continue if it's state cannot be restored.
            page_table
                .map_memory_region(page, PAGE_SIZE, attributes)
                .expect("Failed to restore page table attributes!");
        }

        current += len as u64;
    }

    Ok(())
}

/// Checks if the range is valid for access. This will check the page tables and
/// attempt to read the memory at the address to ensure that it is accessible.
fn check_range_access<Arch: DebuggerArch>(
    page_table: &impl PatinaPageTable,
    address: u64,
    length: usize,
) -> Result<usize, ()> {
    // Check the page tables first.
    let len = check_paging_range(page_table, address, length)?;

    // Poke it with a stick. This will just check the first address
    // to try to catch bogus memory ranges that are still mapped, which is common
    // on the debugger's initial breakpoint on the inherited page tables.
    Arch::memory_poke_test(address)?;

    Ok(len)
}

/// Checks that the range of memory is valid in the page tables. This ensures that
/// reads to this region will not fault. On success returns the number of bytes valid
/// to read from. If the region is uncached and uncached access is not allowed, it will
/// be treated as invalid.
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);

    // This is done page-by-page because it is unknown if the memory region has
    // consistent attributes across the entire range.
    // The length takes us to the start of the next memory range, so we go until the end of the range, e.g
    // start_address + length - 1. This avoids overflow in the self map case
    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 {
            // Only return a valid number if this isn't the first page, otherwise
            // return an error.
            if page > start_address {
                return Ok((page - start_address) as usize);
            }
            return Err(());
        }

        // if this is the last page, return the full length
        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::CacheAttributeValue;
    use patina_paging::{MemoryAttributes, PtError};

    mock! {
        pub MemPageTable {}

        impl PatinaPageTable for MemPageTable {
            fn map_memory_region(&mut self, address: u64, size: u64, attributes: MemoryAttributes) -> Result<(), PtError>;
            fn unmap_memory_region(&mut self, address: u64, size: u64) -> Result<(), PtError>;
            fn install_page_table(&mut self) -> Result<(), PtError>;
            fn query_memory_region(&self, address: u64, size: u64) -> Result<MemoryAttributes, (PtError, CacheAttributeValue)>;
            fn dump_page_tables(&self, address: u64, size: u64) -> Result<(), PtError>;
            fn handle_cacheability_change(
                &self,
                address: u64,
                size: u64,
                old_cache_attributes: MemoryAttributes,
                new_cache_attributes: MemoryAttributes,
            );
        }
    }

    mock! {
        pub MemDebuggerArch {}

        // mockall doesn't support mocking associated types, so we have to use a concrete type for the page table.
        #[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!(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((PtError::InvalidMemoryRange, CacheAttributeValue::NotSupported)));

        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!(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((
                patina_paging::PtError::InvalidMemoryRange,
                patina_internal_cpu::paging::CacheAttributeValue::NotSupported,
            ))
        });

        let result = check_paging_range(&mock_page_table, 0x800, 0x3000);
        assert!(result.expect("Failed to check range access.") == 0x1800);
    }

    // This is an artifact of having to mock something that relies on static
    // architectural state.
    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!(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((
                    patina_paging::PtError::InvalidMemoryRange,
                    patina_internal_cpu::paging::CacheAttributeValue::NotSupported,
                ))
            });
            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() {
        // Allocate a page-aligned buffer large enough to span two pages.
        // Write a pattern across the page boundary and verify all bytes land
        // at the correct addresses.
        const SIZE: usize = PAGE_SIZE as usize * 2;
        let layout = std::alloc::Layout::from_size_align(SIZE, PAGE_SIZE as usize).unwrap();
        // SAFETY: layout has non-zero size and power-of-two alignment.
        let dest = unsafe { std::alloc::alloc_zeroed(layout) };
        assert!(!dest.is_null());

        // Start the write halfway into the first page so it crosses the boundary.
        let offset = (PAGE_SIZE / 2) as usize;
        let write_len = PAGE_SIZE as usize; // spans from page-mid to page-mid
        let address = dest as u64 + offset as u64;

        // Fill the source buffer with a recognizable pattern.
        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());

        // Verify every byte was written to the correct location.
        for (i, &expected) in src.iter().enumerate() {
            // SAFETY: dest is valid for SIZE bytes and we read within bounds.
            let actual = unsafe { *dest.add(offset + i) };
            assert_eq!(actual, expected, "Mismatch at byte offset {i}: expected {expected}, got {actual}");
        }

        // SAFETY: dest was allocated with this layout.
        unsafe { std::alloc::dealloc(dest, layout) };
    }

    #[test]
    fn test_access_check_uncached_page_skipped_by_default() {
        let _lock = PAGE_LOCK.lock().unwrap();
        // Reset the toggle to the default (disabled).
        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();
        // Enable uncached access.
        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);

        // Reset the toggle back to default.
        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));
    }
}