Skip to content

Commit 4ea9dcb

Browse files
author
Dmitrii Kuvaiskii
committed
[PAL/Linux-SGX] Add AEX-Notify flows in exception handling
This commit adds the AEX-Notify flows inside the enclave. The stage-1 signal handler is augmented as follows when AEX-Notify is enabled: manually restore SSA[0] context, invoke the EDECCSSA instruction instead of EEXIT (to go from SSA[1] to SSA[0] without exiting the enclave) and finally jump to SSA[0].GPRSGX.RIP to resume enclave execution (it will resume in stage-2 signal handler). The stage-2 signal handler is augmented as follows: set bit 0 of SSA[0].GPRSGX.AEXNOTIFY (so that AEX-Notify starts working again for this thread), then apply AEX-Notify mitigations and finally restore regular enclave execution. This commit does not add any real AEX-Notify mitigations. Instead, we count the number of AEX events reported inside the SGX enclave and print this number on enclave termination (if log level is at least "warning"). Note that current implementation of AEX-Notify does not use the checkpoint mechanism described in the official AEX-Notify whitepaper. That checkpoint mechanism allows to coalesce multiple AEX events that occur during the execution of mitigations. This saves some CPU cycles and some signal-handling stack space, but we leave implementing this optimization as future work. Signed-off-by: Dmitrii Kuvaiskii <[email protected]>
1 parent 45f12b3 commit 4ea9dcb

File tree

6 files changed

+138
-8
lines changed

6 files changed

+138
-8
lines changed

pal/src/host/linux-sgx/enclave_entry.S

+92-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@
4141
# Its sole purpose is to prepare the stage-2 handler by:
4242
# - copying the interrupted SSA[0] context on the stack ("CPU context")
4343
# - rewiring the SSA[0] context to point to _PalExceptionHandler()
44-
# - invoking EEXIT (so that untrusted runtime can perform ERESUME)
44+
# - handing over control to the SSA[0] context, in one of two ways:
45+
# - AEX-Notify disabled or unavailable (legacy default flows): invoke EEXIT (so that untrusted
46+
# runtime can perform ERESUME which will resume enclave execution in the SSA[0] context)
47+
# - AEX-Notify enabled (explicit opt-in flows): manually restore SSA[0] context, invoke
48+
# EDECCSSA instruction to go from SSA[1] to SSA[0] without exiting the enclave, and jump to
49+
# SSA[0].GPRSGX.RIP to resume enclave execution
4550

4651
#include "sgx_arch.h"
4752
#include "asm-offsets.h"
@@ -544,6 +549,12 @@ enclave_entry:
544549
movq %r10, %rdx
545550

546551
.Lcssa1_exception_eexit:
552+
cmpq $0, %gs:SGX_READY_FOR_AEX_NOTIFY
553+
jne .Lcssa1_exception_eexit_aexnotify
554+
555+
.Lcssa1_exception_eexit_legacy:
556+
# AEX-Notify is disabled/unavailable, stage-1 exception handler follows the normal EEXIT flow
557+
547558
# .Lcssa0_ocall_or_cssa1_exception_eexit has an ABI that uses RSI, RDI, RSP; clear the relevant
548559
# regs (note that stage-1 handler didn't clobber RSP -- which contains an untrusted pointer to
549560
# untrusted-runtime stack -- but this flow doesn't read/write RSP at all so there is no need to
@@ -555,6 +566,70 @@ enclave_entry:
555566
movq %rdx, %rbx
556567
jmp .Lcssa0_ocall_or_cssa1_exception_eexit
557568

569+
.Lcssa1_exception_eexit_aexnotify:
570+
# AEX-Notify is enabled, stage-1 exception handler doesn't invoke EEXIT but instead the new
571+
# EDECCSSA instruction (before this, need to manually restore SSA[0] context)
572+
573+
# After restoring SSA[0] context into GPRs, we'll need to jump to the stage-2 handler, so
574+
# memorize SSA[0].GPRSGX.RIP in an otherwise-unused R11. There is a corner case of the stage-1
575+
# handler's final jmp instruction, see comment at .Lcssa1_exception_eexit_aexnotify_finaljmp.
576+
leaq .Lcssa1_exception_eexit_aexnotify_finaljmp(%rip), %r11
577+
cmpq %r11, SGX_GPR_RIP(%rbx)
578+
je 1f
579+
580+
# not at the stage-1 handler's final jmp instruction, use SSA0's RIP directly
581+
movq SGX_GPR_RIP(%rbx), %r11
582+
jmp 2f
583+
584+
1:
585+
# at the stage-1 handler's final jmp instruction, skip over this jmp instruction by directly
586+
# jumping to the previously-saved SSA0's RIP (which was saved in R11)
587+
movq SGX_GPR_R11(%rbx), %r11
588+
589+
2:
590+
# Clear bit 0 within SSA[0].GPRSGX.AEXNOTIFY (so that ERESUME actually resumes stage-2
591+
# handler if it was interrupted by yet another AEX). The stage-2 handler, _PalExceptionHandler()
592+
# func, will re-instate this bit before applying mitigations.
593+
movb $0, SGX_GPR_AEXNOTIFY(%rbx)
594+
595+
# restore context from SSA[0] (which was already modified above to jump to stage-2 C handler);
596+
# note that XSAVE area (xregs) was already restored above, so only need to restore GPRs;
597+
# note that we don't care about SSA[0].GPRSGX.{URSP,URBP,EXITINFO,RESERVED,GSBASE}
598+
599+
leaq SGX_GPR_RFLAGS(%rbx), %rsp # trick to restore RFLAGS directly from SSA[0].GPRSGX.RFLAGS
600+
popfq
601+
602+
movq SGX_GPR_FSBASE(%rbx), %rdi
603+
.byte 0xf3, 0x48, 0x0f, 0xae, 0xd7 # WRFSBASE %RDI
604+
605+
movq SGX_GPR_RDI(%rbx), %rdi # 1st arg to _PalExceptionHandler()
606+
movq SGX_GPR_RSI(%rbx), %rsi # 2nd arg to _PalExceptionHandler()
607+
movq SGX_GPR_RDX(%rbx), %rdx # 3rd arg to _PalExceptionHandler()
608+
movq SGX_GPR_RCX(%rbx), %rcx # 4th arg to _PalExceptionHandler()
609+
movq SGX_GPR_R8(%rbx), %r8 # 5th arg to _PalExceptionHandler()
610+
movq SGX_GPR_R9(%rbx), %r9 # not strictly needed
611+
movq SGX_GPR_R10(%rbx), %r10 # not strictly needed
612+
movq SGX_GPR_R12(%rbx), %r12 # not strictly needed
613+
movq SGX_GPR_R13(%rbx), %r13 # not strictly needed
614+
movq SGX_GPR_R14(%rbx), %r14 # not strictly needed
615+
movq SGX_GPR_R15(%rbx), %r15 # not strictly needed
616+
movq SGX_GPR_RBP(%rbx), %rbp # not strictly needed
617+
movq SGX_GPR_RSP(%rbx), %rsp
618+
xorq %rbx, %rbx # for sanity
619+
620+
# go from SSA[1] to SSA[0] (more specifically, simply decrement CSSA from 1 to 0);
621+
# must be careful after this ENCLU instruction because may be interrupted by new exceptions
622+
movq $EDECCSSA, %rax
623+
enclu
624+
625+
# Finally jump to the stage-2 C exception handler. An async signal can arrive at this exact jmp
626+
# instruction. At this point, SSA0 has the correct internally-consistent context for the
627+
# "stage-2 exception handler", so in this corner case the stage-1 exception handler can skip
628+
# over this jmp and rewire SSA0's RIP directly to _PalExceptionHandler(), which at this point is
629+
# stored in SSA[0].GPRSGX.R11. See also code at .Lcssa1_exception_eexit_aexnotify.
630+
.Lcssa1_exception_eexit_aexnotify_finaljmp:
631+
jmp *%r11
632+
558633
.cfi_endproc
559634

560635

@@ -590,6 +665,15 @@ sgx_ocall:
590665
pushq %rbx
591666
.cfi_offset %rbx, -24
592667

668+
# disable AEX-Notify during the ocall-exit assembly logic, as (1) there is no security benefit
669+
# in mitigating AEX events during the ocall asm, and (2) AEX-Notify would otherwise clash with
670+
# ocall-exit and return-from-ocall corner case flows (see .Lcssa1_exception_during_ocall_flows)
671+
cmpq $0, %gs:SGX_READY_FOR_AEX_NOTIFY
672+
je 1f
673+
movq %gs:SGX_GPR, %rax
674+
movb $0, SGX_GPR_AEXNOTIFY(%rax)
675+
676+
1:
593677
CHECK_IF_SIGNAL_STACK_IS_USED %rsp, .Lon_signal_stack_ocall, .Lout_of_signal_stack_ocall
594678

595679
.Lout_of_signal_stack_ocall:
@@ -745,6 +829,13 @@ sgx_ocall:
745829
cmpq $PAL_EVENT_NUM_BOUND, %rsi
746830
jb 2f
747831

832+
1:
833+
# re-enable AEX-Notify after return-from-ocall; see .Lcssa0_ocall for details
834+
cmpq $0, %gs:SGX_READY_FOR_AEX_NOTIFY
835+
je 1f
836+
movq %gs:SGX_GPR, %rdi
837+
movb $1, SGX_GPR_AEXNOTIFY(%rdi)
838+
748839
1:
749840
# there was no event, simply call _restore_sgx_context(uc, xsave_area)
750841
movq %rsp, %rdi

pal/src/host/linux-sgx/generated_offsets.c

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const struct generated_offset generated_offsets[] = {
5656
OFFSET_T(SGX_GPR_RIP, sgx_pal_gpr_t, rip),
5757
OFFSET_T(SGX_GPR_EXITINFO, sgx_pal_gpr_t, exitinfo),
5858
OFFSET_T(SGX_GPR_AEXNOTIFY, sgx_pal_gpr_t, aexnotify),
59+
OFFSET_T(SGX_GPR_FSBASE, sgx_pal_gpr_t, fsbase),
5960
DEFINE(SGX_GPR_SIZE, sizeof(sgx_pal_gpr_t)),
6061

6162
/* sgx_cpu_context_t */
@@ -169,6 +170,7 @@ const struct generated_offset generated_offsets[] = {
169170

170171
/* pal.h */
171172
DEFINE(PAL_EVENT_NO_EVENT, PAL_EVENT_NO_EVENT),
173+
DEFINE(PAL_EVENT_INTERRUPTED, PAL_EVENT_INTERRUPTED),
172174
DEFINE(PAL_EVENT_NUM_BOUND, PAL_EVENT_NUM_BOUND),
173175

174176
/* errno */

pal/src/host/linux-sgx/host_entry.S

+10
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ async_exit_pointer:
134134
movb $0, %gs:PAL_HOST_TCB_IN_AEX
135135
.cfi_endproc
136136

137+
# In case of non-AEX-Notify flows, ERESUME never morphs into EENTER, so the value in RDI is
138+
# ignored. Below code snippet becomes a no-op.
139+
# In case of AEX-Notify flows, ERESUME will morph into EENTER. The only way we could arrive to
140+
# this line of code is that there was no pending signal to handle inside the SGX enclave, i.e.,
141+
# maybe_raise_pending_signal() didn't do anything. Since there was no real pending signal (but
142+
# AEX happened, so AEX-Notify must react to this), we put a dummy PAL_EVENT_INTERRUPTED (aka
143+
# SIGCONT). By putting this dummy signal, we survive all checks inside Gramine's in-enclave
144+
# logic, because SIGCONT can always arrive and is benign and side-effect-free.
145+
movq $PAL_EVENT_INTERRUPTED, %rdi
146+
137147
# fall-through to ERESUME
138148

139149
.global eresume_pointer

pal/src/host/linux-sgx/pal_exception.c

+23-7
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,16 @@
2323
#define ADDR_IN_PAL(addr) ((void*)(addr) > TEXT_START && (void*)(addr) < TEXT_END)
2424

2525
bool g_aex_notify_enabled = false;
26+
uint64_t g_aex_notify_counter = 0;
2627

2728
void init_aex_notify_for_thread(void) {
2829
if (!g_aex_notify_enabled)
2930
return;
3031

3132
SET_ENCLAVE_TCB(ready_for_aex_notify, 1UL);
3233
MB();
33-
#if 0
34-
/*
35-
* FIXME: Re-enable in the following commit, when all AEX-Notify flows are added.
36-
* Currently this would fail, as the untrusted runtime expects AEX-Notify flows but
37-
* in-enclave runtime doesn't yet implement AEX-Notify flows.
38-
*/
3934
GET_ENCLAVE_TCB(gpr)->aexnotify = 1U;
4035
MB();
41-
#endif
4236
}
4337

4438
void fini_aex_notify_for_thread(void) {
@@ -51,6 +45,18 @@ void fini_aex_notify_for_thread(void) {
5145
MB();
5246
}
5347

48+
static void apply_aex_notify_mitigations(sgx_cpu_context_t* uc, PAL_XREGS_STATE* xregs_state) {
49+
/*
50+
* TODO: introduce mitigations like atomic prefetching of the working set, see proposed
51+
* mitigations in academic paper "AEX-Notify: Thwarting Precise Single-Stepping
52+
* Attacks through Interrupt Awareness for Intel SGX Enclaves"
53+
*/
54+
__UNUSED(uc);
55+
__UNUSED(xregs_state);
56+
57+
__atomic_fetch_add(&g_aex_notify_counter, 1, __ATOMIC_RELAXED);
58+
}
59+
5460
/* Restore an sgx_cpu_context_t as generated by .Lhandle_exception. Execution will
5561
* continue as specified by the rip in the context. */
5662
__attribute_no_sanitize_address
@@ -65,6 +71,16 @@ noreturn static void restore_sgx_context(sgx_cpu_context_t* uc, PAL_XREGS_STATE*
6571
asan_unpoison_current_stack(sig_stack_low, sig_stack_high - sig_stack_low);
6672
#endif
6773

74+
if (g_aex_notify_enabled && GET_ENCLAVE_TCB(ready_for_aex_notify)) {
75+
/*
76+
* AEX-Notify must be re-enabled for this enclave thread before applying any mitigations
77+
* (and consequently before restoring the regular execution of the enclave thread). For
78+
* details, see e.g. the official whitepaper on AEX-Notify from Intel.
79+
*/
80+
GET_ENCLAVE_TCB(gpr)->aexnotify = 1;
81+
apply_aex_notify_mitigations(uc, xregs_state);
82+
}
83+
6884
_restore_sgx_context(uc, xregs_state);
6985
}
7086

pal/src/host/linux-sgx/pal_linux.h

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ void restore_xregs(const PAL_XREGS_STATE* xsave_area);
9696
noreturn void _restore_sgx_context(sgx_cpu_context_t* uc, PAL_XREGS_STATE* xsave_area);
9797

9898
extern bool g_aex_notify_enabled;
99+
extern uint64_t g_aex_notify_counter;
99100
void init_aex_notify_for_thread(void);
100101
void fini_aex_notify_for_thread(void);
101102

pal/src/host/linux-sgx/pal_process.c

+10
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,16 @@ int init_child_process(int parent_stream_fd, PAL_HANDLE* out_parent_handle,
246246
noreturn void _PalProcessExit(int exitcode) {
247247
if (exitcode)
248248
log_debug("PalProcessExit: Returning exit code %d", exitcode);
249+
250+
/*
251+
* FIXME: remove this when proper AEX-Notify mitigations are implemented;
252+
* see pal_exception.c:apply_aex_notify_mitigations()
253+
*/
254+
if (g_aex_notify_enabled) {
255+
uint64_t aex_notify_counter = __atomic_load_n(&g_aex_notify_counter, __ATOMIC_RELAXED);
256+
log_warning("AEX-Notify counter at process exit: %lu", aex_notify_counter);
257+
}
258+
249259
ocall_exit(exitcode, /*is_exitgroup=*/true);
250260
/* Unreachable. */
251261
}

0 commit comments

Comments
 (0)