背景:x86/linux 中基于内存保护域的进程内隔离,使用内存保护密钥 (MPK) 和保护密钥寄存器 PKRU。
设置:程序首先执行分配新保护密钥和关联内存的管理员代码,并将用户堆栈指针移动到该内存。这表示切换到用户代码,因为它只能在该内存中操作。如果用户代码导致异常,我希望信号处理程序将执行传递回管理员——我必须这样做:
- (1)分配保护键1
pku=pkey_alloc()
以及相关内存来创建用户堆栈ustack=mmap()
+pkey_mprotect(ustack, ..., pku)
。安装init_handler()
SEGV 和 FPE 的异常处理程序 - (2)将rsp切换到用户栈
mov ustack, %%rsp
- (3a)我们现在正在执行用户代码,这会导致异常,例如。这将通过=> =>
*(int*)0=0
触发信号处理程序。注意,handler_asm() 是必需的,以允许 handler() 在用户堆栈上操作handler_asm()
handler()
unblock_signal()
- (4) 信号处理程序
handler()
返回给管理员,管理员切换rsp
回原来的堆栈
下面展示了管理员/用户切换和信号处理的最小可编译源代码,作为要点描述的步骤要么发生在 main 中,要么发生在由 指示的函数中()
。到目前为止,一切都按预期进行(Ubuntu 23.04)
假设用户代码只需要访问预先分配的资源(即不需要 malloc 或调用任何 C 库函数)。现在,为了限制用户可能造成的损害,我想禁用不属于该用户的所有内存的写入,特别是禁用 pkey 0 的页面上的写入(设置 PKRU.WD0=true 即 PKRU=0x55555552)。因此,将 3a 替换为 3b 或 3c
- (3b) set WD0=true via
wrpkru
// 用户不会导致异常 // set WD0=false =>工作正常 - (3c) WD0=true // 用户导致 FPE 或 SEGV // WD0=false =>与信号处理崩溃相关的系统调用,请参阅下面/或代码中的详细信息
我怎样才能避免这次崩溃,我犯了一个错误吗?或者,如果 WD0=true,通常不可能成功处理异常——在某种程度上违背了 MPK 的目的?
感谢您的任何帮助!!
最小实现:写禁用 pkey0 和用户引发的异常分别由两个开关 PROTECT_WD0 和 INVOKE_SEGV 控制。管理员/用户之间的切换发生在 main() 中
PROTECT_WD0=0 用户代码的 PKRU 为 0x55555550
- INVOKE_SEGV=0:用户导致FPE
- INVOKE_SEGV=1:用户引发SEGV
PROTECT_WD0=1 用户代码的 PKRU 为 0x55555552
- INVOKE_SEGV=0:用户导致 FPE => 在进入信号处理程序时崩溃
handler_asm()
- INVOKE_SEGV=1:用户导致 SEGV => 崩溃后
unblock_signal() => sigprocmask() => __GI___sigprocmask => __GI___pthread_sigmask => syscall 14
这里,“崩溃”是指当内核切换到我的信号处理程序时,它会在指定位置导致另一个SEGV
#include <stdexcept>
#include <signal.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/mman.h>
// Save as "main.cpp" and compile via
// gcc -O0 -fexceptions -fnon-call-exceptions -g main.cpp
// gdb ./a.out
// after exception continue to handler via "signal SIGSEGV" or "signal SIGFPE"
// PROTECT_WD0=1 will write disable pkey 0
#define PROTECT_WD0 1
// INVOKE_SEGV=1: user code causes SEGV, =0: user code causes FPE
#define INVOKE_SEGV 1
uint8_t* ustack, *ostack;
void *ripret;
/////////////////////////////////////////////////////////////////////////////////////
// init_handler installs signal handler "handler_asm"
// handler_asm resets pkru before calling handler
// handler modify RIP to return to after the error
// unblock_signal ensure signal can be resent
//
// modified from https://github.com/Plaristote/segvcatch
// except handler_asm, modified from https://github.com/IAIK/Donky
struct kernel_sigaction {
void (*k_sa_sigaction)(int,siginfo_t *,void *);
unsigned long k_sa_flags;
void (*k_sa_restorer) (void);
sigset_t k_sa_mask;
};
# define RESTORE(name, syscall) RESTORE2 (name, syscall)
# define RESTORE2(name, syscall) \
asm ( \
".text\n" \
".byte 0 # Yes, this really is necessary\n" \
".align 16\n" \
"__" #name ":\n" \
" movq $" #syscall ", %rax\n" \
" syscall\n" \
);
/* The return code for realtime-signals. */
RESTORE (restore_rt, __NR_rt_sigreturn)
void restore_rt (void) asm ("__restore_rt")
__attribute__ ((visibility ("hidden")));
static void unblock_signal(int signum __attribute__((__unused__))) {
sigset_t sigs;
sigemptyset(&sigs);
sigaddset(&sigs, signum);
// SIGSEGV crashes at sigprocmask => __GI___sigprocmask => __GI___pthread_sigmask => syscall 14
sigprocmask(SIG_UNBLOCK, &sigs, NULL);
}
// Exception handler
void handler(int s, siginfo_t *, void *_p __attribute__ ((__unused__))) {
ucontext_t *_uc = (ucontext_t *)_p;
gregset_t &_gregs = _uc->uc_mcontext.gregs;
unblock_signal(s);
_gregs[REG_RIP] = (greg_t)ripret;
}
// kernel resets pkru to 0x55555554: give full access before handling
void __attribute__((naked)) handler_asm(int, siginfo_t*, void *) {
// SIGFPE crashes here
__asm__ volatile(
"mov %%rdx, %%r14\n" // save ucontext
"xorl %%eax, %%eax; xorl %%ecx, %%ecx; xorl %%edx, %%edx; wrpkru;" // full access
"mov %%r14, %%rdx\n" // restore ucontext
"jmp %P0\n" :: "i"(handler));
}
// install signal handlers
void init_handler(int signal) {
struct kernel_sigaction act;
act.k_sa_sigaction = handler_asm;
sigemptyset (&act.k_sa_mask);
act.k_sa_flags = SA_SIGINFO|0x4000000;
act.k_sa_restorer = restore_rt;
syscall (SYS_rt_sigaction, signal, &act, NULL, _NSIG / 8);
}
/////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[]) {
// allocate pkey (assumed:1) and associated stack ustack
int pku=pkey_alloc(0,0);
ustack = (uint8_t*)mmap(NULL, 0x10000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS ,-1, 0);
pkey_mprotect(ustack, 0x10000, PROT_READ | PROT_WRITE, pku);
ustack += 0xFFF0;
// initialize handlers for SEGV and FPE
// ripret is the address for the return from signal handler
init_handler(SIGSEGV);
init_handler(SIGFPE);
ripret = &&ret;
// ADMINISTRATOR: switch to user stack and write-disable pkey 0
asm("mov %%rsp, %0; mov %1, %%rsp" : "=g" (ostack) : "g" (ustack));
#if PROTECT_WD0
asm("xorl %%ecx, %%ecx; rdpkru; xorl $2, %%eax; wrpkru" :::);
#endif
// USER: causes SEGV or FPE
#if INVOKE_SEGV
*(int*) 0 = 0;
#else
ustack[0] = 0;
ustack[0] = 10/ustack[0];
#endif
ret:
// ADMINISTRATOR: write-enable pkey 0 and switch back to original stack
#if PROTECT_WD0
asm("xorl %%ecx, %%ecx; rdpkru; xorl $2, %%eax; wrpkru" :::);
#endif
asm("mov %0, %%rsp" : : "g" (ostack));
printf("done\n");
return 0;
}