setjmp, longjmpで使われるjmp_bufには何がどのように保存されているのか

setjmp, longjmpはC言語で実行コンテキストを保存し、保存したコンテキストに復帰するために使われる関数(orマクロ)である。g++の例外実装の一つはこのsetjmp, longjmpを用いて実現されている。

この記事では 実行コンテキスト とは何を指すのか? また、その保存方法の実装について調べる。

jmp_buf

setjmpの実装はglibcsetjmp/setjmp.h にある。

typedef struct __jmp_buf_tag jmp_buf[1];

/* Store the calling environment in ENV, also saving the signal mask.
   Return 0.  */
extern int setjmp (jmp_buf __env) __THROWNL;

このjmp_bufsetjmp/bits/types/struct___jmp_buf_tag.h で定義されている。

/* Calling environment, plus possibly a saved signal mask.  */
struct __jmp_buf_tag
  {
    /* NOTE: The machine-dependent definitions of `__sigsetjmp'
       assume that a `jmp_buf' begins with a `__jmp_buf' and that
       `__mask_was_saved' follows it.  Do not move these members
       or add others before it.  */
    __jmp_buf __jmpbuf;     /* Calling environment.  */
    int __mask_was_saved;  /* Saved the signal mask?  */
    __sigset_t __saved_mask;    /* Saved signal mask.  */
  };

x86-64の場合、__jmp_bufsysdeps/x86/bits/setjmp.hで定義されている。__jmp_bufは8個のint型整数からなり、更に64bitかどうかで場合分けがなされていることがわかる。

# if __WORDSIZE == 64
typedef long int __jmp_buf[8];
# elif defined  __x86_64__
__extension__ typedef long long int __jmp_buf[8];
# else
typedef int __jmp_buf[6];
# endif

setjmp

setjmp自体の実装はsysdeps/x86_64/setjmp.Sにある。Linux x86-64では第一引数を%rdiで渡すので、第一引数として渡された構造体にレジスタの値を保存していることがわかる。保存されているのは、%rbx, %r12, %r13, %r14, %r15, %rsp, PCである。しかし、%rsp, %rbp, PCはPTR_MANGLEで処理されてから保存されている。PTR_MANGLEとは何だろうか? また、なぜ必要なのだろう?

ENTRY (__sigsetjmp)
    /* Save registers.  */
    movq %rbx, (JB_RBX*8)(%rdi)
#ifdef PTR_MANGLE
# ifdef __ILP32__
    /* Save the high bits of %rbp first, since PTR_MANGLE will
       only handle the low bits but we cannot presume %rbp is
       being used as a pointer and truncate it.  Here we write all
       of %rbp, but the low bits will be overwritten below.  */
    movq %rbp, (JB_RBP*8)(%rdi)
# endif
    mov %RBP_LP, %RAX_LP
    PTR_MANGLE (%RAX_LP)
    mov %RAX_LP, (JB_RBP*8)(%rdi)
#else
    movq %rbp, (JB_RBP*8)(%rdi)
#endif
    movq %r12, (JB_R12*8)(%rdi)
    movq %r13, (JB_R13*8)(%rdi)
    movq %r14, (JB_R14*8)(%rdi)
    movq %r15, (JB_R15*8)(%rdi)
    lea 8(%rsp), %RDX_LP    /* Save SP as it will be after we return.  */
#ifdef PTR_MANGLE
    PTR_MANGLE (%RDX_LP)
#endif
    movq %rdx, (JB_RSP*8)(%rdi)
    mov (%rsp), %RAX_LP /* Save PC we are returning to now.  */
    LIBC_PROBE (setjmp, 3, LP_SIZE@%RDI_LP, -4@%esi, LP_SIZE@%RAX_LP)
#ifdef PTR_MANGLE
    PTR_MANGLE (%RAX_LP)
#endif
    movq %rax, (JB_PC*8)(%rdi)

#ifdef SHADOW_STACK_POINTER_OFFSET
# if IS_IN (libc) && defined SHARED && defined FEATURE_1_OFFSET
    /* Check if Shadow Stack is enabled.  */
    testl $X86_FEATURE_1_SHSTK, %fs:FEATURE_1_OFFSET
    jz L(skip_ssp)
# else
    xorl %eax, %eax
# endif
    /* Get the current Shadow-Stack-Pointer and save it.  */
    rdsspq %rax
    movq %rax, SHADOW_STACK_POINTER_OFFSET(%rdi)
# if IS_IN (libc) && defined SHARED && defined FEATURE_1_OFFSET
L(skip_ssp):
# endif
#endif
#if IS_IN (rtld)
    /* In ld.so we never save the signal mask.  */
    xorl %eax, %eax
    retq
#else
    /* Make a tail call to __sigjmp_save; it takes the same args.  */
    jmp __sigjmp_save
#endif
END (__sigsetjmp)

PTR_MANGLEは sysdeps/unix/sysv/linux/x86_64/sysdep.h に定義がある。

#  define PTR_MANGLE(reg)    xor %fs:POINTER_GUARD, reg;           \
                rol $2*LP_SIZE+1, reg
#  define PTR_DEMANGLE(reg) ror $2*LP_SIZE+1, reg;                \
                xor %fs:POINTER_GUARD, reg

Linux x86-64では%fsレジスタがThread Local Storageのベースポインタとして使われることを思い出すと、このコードは

  • スレッド固有の値(%fs:POINTER_GUARD)とのxorを取り
  • $2*LP_SIZE+1だけレジスタのビットを回転させる

ことがわかる。PTR_MAGNLEの詳細は Pointer Encryptionにある。

なぜsetjmp, longjmpに際してポインタを暗号化するのだろうか? 最近のglibcではatexit関数やjmp_bufを狙った攻撃は効かない (PTR_MANGLE)Pointer Subterfugeによれば、書き込み可能な領域に生のEIPを保存すると攻撃対象になるからだそうだ。

結論

  • setjmpは%rbx, %r12, %r13, %r14, %r15, %rsp, PCをjmp_bufに保存する
  • このとき、生のEIPを保存するとそのデータ自体が攻撃目標になるため、PTR_MANGLEで暗号化する