virt_support_aarch64emu/
translate.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! aarch64 page table walking.
5
6#![warn(missing_docs)]
7
8use crate::emulate::EmuTranslateError;
9use crate::emulate::EmuTranslateResult;
10use crate::emulate::TranslateGvaSupport;
11use crate::emulate::TranslateMode;
12use aarch64defs::Cpsr64;
13use aarch64defs::EsrEl2;
14use aarch64defs::FaultStatusCode;
15use aarch64defs::IntermPhysAddrSize;
16use aarch64defs::IssDataAbort;
17use aarch64defs::Pte;
18use aarch64defs::SctlrEl1;
19use aarch64defs::TranslationControlEl1;
20use aarch64defs::TranslationGranule0;
21use aarch64defs::TranslationGranule1;
22use guestmem::GuestMemory;
23use hvdef::HV_PAGE_SHIFT;
24use hvdef::hypercall::TranslateGvaControlFlagsArm64;
25use hvdef::hypercall::TranslateGvaResultCode;
26use thiserror::Error;
27
28/// Registers needed to walk the page table.
29#[derive(Debug, Clone)]
30pub struct TranslationRegisters {
31    /// SPSR_EL2
32    pub cpsr: Cpsr64,
33    /// SCTLR_ELx
34    pub sctlr: SctlrEl1,
35    /// TCR_ELx
36    pub tcr: TranslationControlEl1,
37    /// TTBR0_ELx
38    pub ttbr0: u64,
39    /// TTBR1_ELx
40    pub ttbr1: u64,
41    /// EsrEl2
42    pub syndrome: u64,
43
44    /// The way the processor uses to determine if an access is to encrypted
45    /// memory. This is used to enforce that page tables and executable code are
46    /// in encrypted memory.
47    pub encryption_mode: EncryptionMode,
48}
49
50/// The way the processor uses to determine if an access is to encrypted memory.
51#[derive(Debug, Copy, Clone)]
52pub enum EncryptionMode {
53    /// Memory accesses below the virtual top of memory address are encrypted.
54    Vtom(u64),
55    /// No memory is encrypted.
56    None,
57}
58
59/// Flags to control the page table walk.
60#[derive(Debug, Clone)]
61pub struct TranslateFlags {
62    /// Validate a VP in the current state can execute from this GVA.
63    pub validate_execute: bool,
64    /// Validate a VP in the current state can read from this GVA.
65    pub validate_read: bool,
66    /// Validate a VP in the current state can write to this GVA.
67    pub validate_write: bool,
68    /// The type of privilege check to perform.
69    pub privilege_check: TranslatePrivilegeCheck,
70    /// Update the page table entries' access and dirty bits as appropriate.
71    pub set_page_table_bits: bool,
72}
73
74/// The type of privilege check to perform.
75#[derive(Debug, Copy, Clone)]
76pub enum TranslatePrivilegeCheck {
77    /// No privilege checks.
78    None,
79    /// Validate user-mode access.
80    User,
81    /// Validate supervisor access.
82    Supervisor,
83    /// Validate both supervisor and user-mode access.
84    Both,
85    /// Validate according to the current privilege level.
86    CurrentPrivilegeLevel,
87}
88
89impl TranslateFlags {
90    /// Return flags based on the `HvTranslateVirtualAddress` hypercall input
91    /// flags.
92    ///
93    /// Note that not all flags are considered.
94    pub fn from_hv_flags(flags: TranslateGvaControlFlagsArm64) -> Self {
95        Self {
96            validate_execute: flags.validate_execute(),
97            validate_read: flags.validate_read(),
98            validate_write: flags.validate_write(),
99            privilege_check: if flags.pan_clear() {
100                TranslatePrivilegeCheck::None
101            } else if flags.user_access() {
102                if flags.supervisor_access() {
103                    TranslatePrivilegeCheck::Both
104                } else {
105                    TranslatePrivilegeCheck::User
106                }
107            } else if flags.supervisor_access() {
108                TranslatePrivilegeCheck::Supervisor
109            } else {
110                TranslatePrivilegeCheck::CurrentPrivilegeLevel
111            },
112            set_page_table_bits: flags.set_page_table_bits(),
113        }
114    }
115}
116
117/// Translation error.
118#[derive(Debug, Error)]
119pub enum Error {
120    /// Invalid address size
121    #[error("invalid address size at level")]
122    InvalidAddressSize(u8),
123    /// The page table flags were invalid.
124    #[error("invalid page table flags at level")]
125    InvalidPageTableFlags(u8),
126    /// A page table GPA was not mapped.
127    #[error("gpa unmapped at level")]
128    GpaUnmapped(u8),
129    /// The page was not present in the page table.
130    #[error("page not present at level")]
131    PageNotPresent(u8),
132    /// Accessing the GVA would create a privilege violation.
133    #[error("privilege violation at level")]
134    PrivilegeViolation(u8),
135}
136
137impl From<&Error> for TranslateGvaResultCode {
138    fn from(err: &Error) -> TranslateGvaResultCode {
139        match err {
140            Error::InvalidAddressSize(_) => TranslateGvaResultCode::INVALID_PAGE_TABLE_FLAGS,
141            Error::InvalidPageTableFlags(_) => TranslateGvaResultCode::INVALID_PAGE_TABLE_FLAGS,
142            Error::GpaUnmapped(_) => TranslateGvaResultCode::GPA_UNMAPPED,
143            Error::PageNotPresent(_) => TranslateGvaResultCode::PAGE_NOT_PRESENT,
144            Error::PrivilegeViolation(_) => TranslateGvaResultCode::PRIVILEGE_VIOLATION,
145        }
146    }
147}
148
149impl From<Error> for TranslateGvaResultCode {
150    fn from(err: Error) -> TranslateGvaResultCode {
151        (&err).into()
152    }
153}
154
155impl From<&Error> for EsrEl2 {
156    fn from(err: &Error) -> EsrEl2 {
157        let dfsc = match err {
158            Error::InvalidAddressSize(i) => FaultStatusCode::ADDRESS_SIZE_FAULT_LEVEL0.0 + i,
159            Error::InvalidPageTableFlags(i) => FaultStatusCode::TRANSLATION_FAULT_LEVEL0.0 + i,
160            Error::GpaUnmapped(i) => FaultStatusCode::ACCESS_FLAG_FAULT_LEVEL0.0 + i,
161            Error::PageNotPresent(i) => FaultStatusCode::ACCESS_FLAG_FAULT_LEVEL0.0 + i,
162            Error::PrivilegeViolation(i) => FaultStatusCode::PERMISSION_FAULT_LEVEL0.0 + i,
163        };
164        let data_abort = IssDataAbort::new().with_dfsc(FaultStatusCode(dfsc));
165        data_abort.into()
166    }
167}
168
169impl From<Error> for EsrEl2 {
170    fn from(err: Error) -> EsrEl2 {
171        (&err).into()
172    }
173}
174
175/// Emulates a page table walk.
176///
177/// This is suitable for implementing [`crate::emulate::EmulatorSupport::translate_gva`].
178pub fn emulate_translate_gva<T: TranslateGvaSupport>(
179    support: &mut T,
180    gva: u64,
181    mode: TranslateMode,
182) -> Result<Result<EmuTranslateResult, EmuTranslateError>, T::Error> {
183    // Always acquire the TLB lock for this path.
184    support.acquire_tlb_lock();
185
186    let registers = support.registers()?;
187    let flags = TranslateFlags {
188        validate_execute: matches!(mode, TranslateMode::Execute),
189        validate_read: matches!(mode, TranslateMode::Execute | TranslateMode::Read),
190        validate_write: matches!(mode, TranslateMode::Write),
191        privilege_check: TranslatePrivilegeCheck::CurrentPrivilegeLevel,
192        set_page_table_bits: true,
193    };
194
195    let r = match translate_gva_to_gpa(support.guest_memory(), gva, &registers, flags) {
196        Ok(gpa) => Ok(EmuTranslateResult {
197            gpa,
198            overlay_page: None,
199        }),
200        Err(err) => {
201            let mut syndrome: EsrEl2 = (&err).into();
202            let cur_syndrome: EsrEl2 = registers.syndrome.into();
203            syndrome.set_il(cur_syndrome.il());
204            Err(EmuTranslateError {
205                code: err.into(),
206                event_info: Some(syndrome),
207            })
208        }
209    };
210    Ok(r)
211}
212
213struct Aarch64PageTable {
214    pub table_address_gpa: u64,
215    pub page_shift: u64,
216    pub span_shift: u64,
217    pub level: u64,
218    pub level_width: u64,
219    pub is_hierarchical_permissions: bool,
220}
221
222fn get_root_page_table(
223    gva: u64,
224    registers: &TranslationRegisters,
225    flags: &TranslateFlags,
226) -> Result<(u64, Aarch64PageTable), Error> {
227    let use_ttbr1 = (gva & 0x00400000_00000000) != 0;
228    let (
229        root_address,
230        address_width,
231        granule_width,
232        ignore_top_byte,
233        ignore_top_byte_instruction,
234        is_hierarchical_permissions,
235    ) = if use_ttbr1 {
236        let granule_width = match registers.tcr.tg1() {
237            TranslationGranule1::TG_INVALID => return Err(Error::InvalidPageTableFlags(0)),
238            TranslationGranule1::TG_16KB => 14,
239            TranslationGranule1::TG_4KB => 12,
240            TranslationGranule1::TG_64KB => 16,
241            _ => return Err(Error::InvalidPageTableFlags(0)),
242        };
243        (
244            registers.ttbr1 & ((1 << 48) - 2),
245            registers.tcr.ttbr1_valid_address_bits(),
246            granule_width,
247            registers.tcr.tbi1() != 0,
248            registers.tcr.tbid1() != 0,
249            !registers.tcr.hpd1() != 0, // || processor does not support HPDS
250        )
251    } else {
252        let granule_width = match registers.tcr.tg0() {
253            TranslationGranule0::TG_4KB => 12,
254            TranslationGranule0::TG_64KB => 16,
255            TranslationGranule0::TG_16KB => 14,
256            _ => return Err(Error::InvalidPageTableFlags(0)),
257        };
258        (
259            registers.ttbr0 & ((1 << 48) - 2),
260            registers.tcr.ttbr0_valid_address_bits(),
261            granule_width,
262            registers.tcr.tbi0() != 0,
263            registers.tcr.tbid0() != 0,
264            !registers.tcr.hpd0() != 0, // || processor does not support HPDS
265        )
266    };
267    if !(25..=48).contains(&address_width) {
268        tracing::trace!(address_width, "Invalid TCR value");
269        return Err(Error::InvalidAddressSize(0));
270    }
271    let num_levels = (address_width - 1) / granule_width;
272    if num_levels == 0 || num_levels > 4 {
273        tracing::trace!(address_width, granule_width, "Invalid page hierarchy");
274        return Err(Error::InvalidPageTableFlags(0));
275    }
276    let ignore_top_byte =
277        ignore_top_byte && (!flags.validate_execute || ignore_top_byte_instruction);
278    let high_mask = !((1 << address_width) - 1);
279    let verify_high_bits = if use_ttbr1 {
280        // TTBR1 addresses should have all the high bits set.
281        let masked_address = gva
282            | if ignore_top_byte {
283                0xff000000_00000000
284            } else {
285                0
286            };
287        (masked_address & high_mask) == high_mask
288    } else {
289        // TTBR0 addresses should have all the high bits clear.
290        let masked_address = gva
291            & if ignore_top_byte {
292                0x00ffffff_ffffffff
293            } else {
294                0xffffffff_ffffffff
295            };
296        (masked_address & high_mask) == 0
297    };
298    if !verify_high_bits {
299        tracing::trace!(gva, address_width, "Invalid high bits");
300        return Err(Error::InvalidAddressSize(0));
301    }
302    let span_shift = granule_width + (granule_width - 3) * (num_levels - 1);
303    let level_width = address_width - span_shift;
304    Ok((
305        gva & !high_mask,
306        Aarch64PageTable {
307            table_address_gpa: root_address & !((1 << (level_width + 3)) - 1),
308            page_shift: granule_width,
309            span_shift,
310            level: num_levels - 1,
311            level_width,
312            is_hierarchical_permissions,
313        },
314    ))
315}
316
317struct PageTableWalkContext<'a> {
318    guest_memory: &'a GuestMemory,
319    flags: TranslateFlags,
320    check_user_access: bool,
321    check_supervisor_access: bool,
322    write_no_execute: bool,
323    output_size_mask: u64,
324}
325
326enum PageTableWalkResult {
327    Table(Aarch64PageTable),
328    BaseGpa(u64, u64),
329}
330
331fn get_next_page_table(
332    level: u8,
333    address: u64,
334    page_table: &Aarch64PageTable,
335    context: &PageTableWalkContext<'_>,
336    is_user_address: &mut bool,
337    is_writeable_address: &mut bool,
338    is_executable_address: &mut bool,
339) -> Result<PageTableWalkResult, Error> {
340    if page_table.table_address_gpa & context.output_size_mask != page_table.table_address_gpa {
341        tracing::trace!(
342            address,
343            level = page_table.level,
344            page_table_address = page_table.table_address_gpa,
345            "Invalid page table address"
346        );
347        return Err(Error::InvalidAddressSize(level));
348    }
349    let index_mask = (1 << page_table.level_width) - 1;
350    let pte_index = (address >> page_table.span_shift) & index_mask;
351    let pte_gpa = page_table.table_address_gpa + (pte_index << 3);
352    let mut pte_access = context
353        .guest_memory
354        .read_plain::<u64>(pte_gpa)
355        .map(Pte::from);
356    let mut pte;
357    loop {
358        pte = pte_access.map_err(|_| Error::GpaUnmapped(level))?;
359        let large_page_supported = match page_table.level {
360            3 => false,
361            2 => page_table.page_shift > 12,
362            _ => true,
363        };
364        if !pte.valid() || (!pte.not_large_page() && !large_page_supported) {
365            return Err(Error::PageNotPresent(level));
366        }
367        let next_address = pte.pfn() << HV_PAGE_SHIFT;
368        if pte.reserved_must_be_zero() != 0
369            || (next_address & context.output_size_mask) != next_address
370        {
371            return Err(Error::InvalidPageTableFlags(level));
372        }
373        if page_table.level > 0 && pte.not_large_page() {
374            if page_table.is_hierarchical_permissions {
375                *is_user_address = *is_user_address && !pte.ap_table_privileged_only();
376                *is_writeable_address = *is_writeable_address && !pte.ap_table_read_only();
377                *is_executable_address = *is_executable_address
378                    && !(if context.check_user_access {
379                        pte.uxn_table()
380                    } else {
381                        pte.pxn_table()
382                    });
383            }
384        } else {
385            // check permissions
386            *is_user_address = *is_user_address && pte.ap_unprivileged();
387            *is_writeable_address = *is_writeable_address && !pte.ap_read_only();
388            *is_executable_address = *is_executable_address
389                && !(if context.check_user_access {
390                    pte.user_no_execute()
391                } else {
392                    pte.privilege_no_execute()
393                });
394            if context.check_user_access {
395                if context.flags.validate_read && !*is_user_address {
396                    return Err(Error::PrivilegeViolation(level));
397                }
398                if context.write_no_execute && *is_writeable_address && *is_user_address {
399                    *is_executable_address = false;
400                }
401            } else {
402                if context.check_supervisor_access && *is_user_address {
403                    return Err(Error::PrivilegeViolation(level));
404                }
405                if *is_writeable_address && (*is_user_address || context.write_no_execute) {
406                    *is_executable_address = false;
407                }
408            }
409            if context.flags.validate_write && !*is_writeable_address
410                || context.flags.validate_execute && !*is_executable_address
411            {
412                return Err(Error::PrivilegeViolation(level));
413            }
414        }
415
416        // Update access and dirty bits.
417        let mut new_pte = pte;
418        if context.flags.set_page_table_bits {
419            new_pte.set_access_flag(true);
420            if context.flags.validate_write && new_pte.dbm() {
421                new_pte.set_ap_read_only(false);
422            }
423        }
424
425        // Access bits already set.
426        if new_pte == pte {
427            break;
428        }
429
430        let r = if !pte.not_large_page() {
431            context.guest_memory.compare_exchange(address, pte, new_pte)
432        } else {
433            context
434                .guest_memory
435                .compare_exchange(address, u64::from(pte) as u32, u64::from(new_pte) as u32)
436                .map(|r| {
437                    r.map(|n| Pte::from(n as u64))
438                        .map_err(|n| Pte::from(n as u64))
439                })
440        };
441
442        match r {
443            Ok(Ok(_)) => {
444                // Compare exchange succeeded, so continue.
445                break;
446            }
447            Ok(Err(pte)) => {
448                // Compare exchange failed. Loop around again.
449                pte_access = Ok(pte);
450                continue;
451            }
452            Err(err) => {
453                // Memory access failed. Loop around again to handle the
454                // failure consistently.
455                pte_access = Err(err);
456                continue;
457            }
458        }
459    }
460    let pfn_mask = !(1_u64 << (page_table.page_shift - HV_PAGE_SHIFT)).wrapping_sub(1);
461    let next_address = (pte.pfn() & pfn_mask) << HV_PAGE_SHIFT;
462    if page_table.level == 0 || !pte.not_large_page() {
463        Ok(PageTableWalkResult::BaseGpa(
464            next_address,
465            (1 << page_table.span_shift) - 1,
466        ))
467    } else {
468        Ok(PageTableWalkResult::Table(Aarch64PageTable {
469            table_address_gpa: next_address,
470            page_shift: page_table.page_shift,
471            span_shift: page_table.span_shift - (page_table.page_shift - 3),
472            level: page_table.level - 1,
473            level_width: page_table.page_shift - 3,
474            is_hierarchical_permissions: page_table.is_hierarchical_permissions,
475        }))
476    }
477}
478
479/// Translate a GVA by walking the processor's page tables.
480pub fn translate_gva_to_gpa(
481    guest_memory: &GuestMemory,
482    gva: u64,
483    registers: &TranslationRegisters,
484    flags: TranslateFlags,
485) -> Result<u64, Error> {
486    tracing::trace!(gva, ?registers, ?flags, "translating gva");
487
488    // If paging is disabled, just return the GVA as the GPA.
489    if !registers.sctlr.m() {
490        return Ok(gva);
491    }
492
493    // FEAT_LPA2 - Larger physical address for 4KB and 16KB translation granules
494
495    let (check_user_access, check_supervisor_access) = match flags.privilege_check {
496        TranslatePrivilegeCheck::None => (false, false),
497        TranslatePrivilegeCheck::User => (true, false),
498        TranslatePrivilegeCheck::Both => (true, true),
499        TranslatePrivilegeCheck::CurrentPrivilegeLevel if registers.cpsr.el() == 0 => (true, false),
500        TranslatePrivilegeCheck::Supervisor | TranslatePrivilegeCheck::CurrentPrivilegeLevel => {
501            (false, true)
502        }
503    };
504    let output_size = match registers.tcr.ips() {
505        IntermPhysAddrSize::IPA_32_BITS_4_GB => 32,
506        IntermPhysAddrSize::IPA_36_BITS_64_GB => 36,
507        IntermPhysAddrSize::IPA_40_BITS_1_TB => 40,
508        IntermPhysAddrSize::IPA_42_BITS_4_TB => 42,
509        IntermPhysAddrSize::IPA_44_BITS_16_TB => 44,
510        IntermPhysAddrSize::IPA_48_BITS_256_TB => 48,
511        IntermPhysAddrSize::IPA_52_BITS_4_PB => 52,
512        IntermPhysAddrSize::IPA_56_BITS_64_PB => 56,
513        _ => return Err(Error::InvalidPageTableFlags(0)),
514    };
515    let write_no_execute = registers.sctlr.wxn();
516    let walk_context = PageTableWalkContext {
517        guest_memory,
518        flags,
519        check_user_access,
520        check_supervisor_access,
521        write_no_execute,
522        output_size_mask: (1 << output_size) - 1,
523    };
524    let mut is_user_address = true;
525    let mut is_writeable_address = true;
526    let mut is_executable_address = true;
527    let (address, mut page_table) = get_root_page_table(gva, registers, &walk_context.flags)?;
528    let mut level = 1;
529    loop {
530        page_table = match get_next_page_table(
531            level,
532            address,
533            &page_table,
534            &walk_context,
535            &mut is_user_address,
536            &mut is_writeable_address,
537            &mut is_executable_address,
538        )? {
539            PageTableWalkResult::BaseGpa(base_address, mask) => {
540                break Ok(base_address + (gva & mask));
541            }
542            PageTableWalkResult::Table(next_table) => next_table,
543        };
544        level += 1;
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use aarch64defs::Cpsr64;
552    use aarch64defs::IntermPhysAddrSize;
553    use aarch64defs::Pte;
554    use aarch64defs::SctlrEl1;
555    use aarch64defs::TranslationControlEl1;
556    use guestmem::GuestMemory;
557
558    /// Helper: set up a 2-level page table walk for a 4KB granule.
559    ///
560    /// Memory layout (all within first 16 KB of guest memory):
561    ///   GPA 0x0000: Level 1 table (root)
562    ///   GPA 0x1000: Level 0 table
563    ///   Output maps to GPA 0x2000
564    ///
565    /// TCR: t0sz = 34 → address_width = 30 → 2 levels (L1 → L0)
566    fn setup_page_tables(
567        gm: &GuestMemory,
568        l1_table_pte: Pte, // table descriptor at L1[0]
569        l0_leaf_pte: Pte,  // leaf PTE at L0[0]
570    ) {
571        gm.write_plain::<u64>(0x0000, &l1_table_pte.into()).unwrap();
572        gm.write_plain::<u64>(0x1000, &l0_leaf_pte.into()).unwrap();
573    }
574
575    fn make_registers() -> TranslationRegisters {
576        TranslationRegisters {
577            cpsr: Cpsr64::new(),                 // EL0
578            sctlr: SctlrEl1::new().with_m(true), // paging enabled
579            tcr: TranslationControlEl1::new()
580                .with_t0sz(34) // address_width = 30 → 2 levels
581                .with_ips(IntermPhysAddrSize::IPA_48_BITS_256_TB),
582            // tg0 = 0 → TranslationGranule0::TG_4KB
583            ttbr0: 0x0000, // L1 table at GPA 0
584            ttbr1: 0,
585            syndrome: 0,
586            encryption_mode: EncryptionMode::None,
587        }
588    }
589
590    #[test]
591    fn test_table_level_uxn_should_deny_execution() {
592        let gm = GuestMemory::allocate(0x4000);
593
594        // L1 table descriptor: valid, points to L0 table at 0x1000,
595        // with UXN_TABLE = 1 (user execute never).
596        let l1_pte = Pte::new()
597            .with_valid(true)
598            .with_not_large_page(true) // table descriptor
599            .with_pfn(0x1000 >> 12)
600            .with_uxn_table(true); // USER EXECUTE NEVER
601
602        // L0 leaf PTE: valid page at 0x2000, user accessible, executable
603        // at the leaf level (user_no_execute = false).
604        let l0_pte = Pte::new()
605            .with_valid(true)
606            .with_pfn(0x2000 >> 12)
607            .with_access_flag(true)
608            .with_ap_unprivileged(true); // user accessible
609        // user_no_execute defaults to false → leaf says executable
610
611        setup_page_tables(&gm, l1_pte, l0_pte);
612
613        let registers = make_registers();
614        // HPD=0 means hierarchical permissions ARE enabled.
615        assert_eq!(registers.tcr.hpd0(), 0);
616
617        let flags = TranslateFlags {
618            validate_execute: true,
619            validate_read: true,
620            validate_write: false,
621            privilege_check: TranslatePrivilegeCheck::User,
622            set_page_table_bits: false,
623        };
624
625        let result = translate_gva_to_gpa(&gm, 0, &registers, flags);
626
627        // The table descriptor says UXN=1 (user execute never).
628        // Translation with validate_execute should FAIL.
629        assert!(matches!(result, Err(Error::PrivilegeViolation(_))));
630    }
631
632    #[test]
633    fn test_user_read_succeeds_on_user_accessible_page() {
634        let gm = GuestMemory::allocate(0x4000);
635
636        // L1 table descriptor: plain table, no restrictions.
637        let l1_pte = Pte::new()
638            .with_valid(true)
639            .with_not_large_page(true)
640            .with_pfn(0x1000 >> 12);
641
642        // L0 leaf PTE: user-accessible page at GPA 0x2000.
643        let l0_pte = Pte::new()
644            .with_valid(true)
645            .with_pfn(0x2000 >> 12)
646            .with_access_flag(true)
647            .with_ap_unprivileged(true); // USER ACCESSIBLE
648
649        setup_page_tables(&gm, l1_pte, l0_pte);
650
651        let registers = make_registers();
652
653        // User-mode read validation.
654        let flags = TranslateFlags {
655            validate_execute: false,
656            validate_read: true,
657            validate_write: false,
658            privilege_check: TranslatePrivilegeCheck::User,
659            set_page_table_bits: false,
660        };
661
662        let result = translate_gva_to_gpa(&gm, 0, &registers, flags);
663
664        // The page has ap_unprivileged=true, so a user-mode read should
665        // succeed and resolve to GPA 0x2000.
666        assert_eq!(result.unwrap(), 0x2000);
667    }
668
669    #[test]
670    fn test_table_level_pxn_should_deny_execution() {
671        let gm = GuestMemory::allocate(0x4000);
672
673        // L1 table descriptor with PXN_TABLE = 1 (privileged execute never).
674        let l1_pte = Pte::new()
675            .with_valid(true)
676            .with_not_large_page(true)
677            .with_pfn(0x1000 >> 12)
678            .with_pxn_table(true); // PRIVILEGED EXECUTE NEVER
679
680        // L0 leaf PTE: valid, executable at leaf level.
681        let l0_pte = Pte::new()
682            .with_valid(true)
683            .with_pfn(0x2000 >> 12)
684            .with_access_flag(true);
685        // privilege_no_execute defaults to false → leaf says executable
686
687        setup_page_tables(&gm, l1_pte, l0_pte);
688
689        let registers = make_registers();
690
691        let flags = TranslateFlags {
692            validate_execute: true,
693            validate_read: true,
694            validate_write: false,
695            privilege_check: TranslatePrivilegeCheck::Supervisor,
696            set_page_table_bits: false,
697        };
698
699        let result = translate_gva_to_gpa(&gm, 0, &registers, flags);
700
701        // The table descriptor says PXN=1 (privileged execute never).
702        // Translation with validate_execute should FAIL.
703        assert!(matches!(result, Err(Error::PrivilegeViolation(_))));
704    }
705}