Signal Handling in DBT

Table of Contents

1. Signal Handling in DBT

1.1. testing code

int main(int argc, char *argv[]) {
     __asm__(
         "mov r0, #100\n\t"                    \
         "mov r1, #100\n\t"                    \
         :);
     int x = *((int *)0);
     return 0;
}

1.2. tombstone info

tombstone generated by debuggerd32 when running the previous testing code with arm_translator:

01-19 22:25:39.278 E/DEBUG   ( 2381): ABI: arm
01-19 22:25:39.279 F/DEBUG   ( 2381): pid: 3867, tid: 3867, name: a.out  >>> ./arm_translator <<<
01-19 22:25:39.281 E/DEBUG   ( 2381): AM write failed: Broken pipe
01-19 22:25:39.282 F/DEBUG   ( 2381): signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x0
01-19 22:25:39.288 F/DEBUG   ( 2381):     r0 00000064  r1 00000064  r2 bffddb2c  r3 00000000
01-19 22:25:39.289 F/DEBUG   ( 2381):     r4 bffddb2c  r5 bffddb24  r6 00000001  r7 80000334
01-19 22:25:39.289 F/DEBUG   ( 2381):     r8 00000000  r9 00000000  sl 00000000  fp bffddaec
01-19 22:25:39.290 F/DEBUG   ( 2381):     ip bf7a55e0  sp bffddad8  lr bf74635f  pc 7f4239f0  cpsr 40000000
01-19 22:25:39.292 F/DEBUG   ( 2381):
01-19 22:25:39.292 F/DEBUG   ( 2381): backtrace:
01-19 22:25:39.293 F/DEBUG   ( 2381):     #00 pc 7f4239f0  <unknown>
01-19 22:25:39.294 F/DEBUG   ( 2381):     #01 pc 0001735d  /system/lib/libc.so (__libc_init+44)
01-19 22:25:39.294 F/DEBUG   ( 2381):     #02 pc 007ffb18  <unknown>
01-19 22:25:39.331 F/DEBUG   ( 2381):
01-19 22:25:39.331 F/DEBUG   ( 2381): Tombstone written to: /data/tombstones/tombstone_03

1.3. before SIGSEGV

before crash, it should be running in the code cache:

3867| DEBUG(translate): emit: 0x7f7f42088c: movz w0, #0x64
3867| DEBUG(translate): emit: 0x7f7f420890: movz w1, #0x64
3867| DEBUG(translate): emit: 0x7f7f420894: movz w3, #0
3867| DEBUG(translate): emit: 0x7f7f420898: ldr w3, [x3]

which is translated from:

3867| DEBUG(translate): 80000348: mov r0, #0x64
3867| DEBUG(translate): 8000034c: mov r1, #0x64
3867| DEBUG(translate): 80000350: mov r3, #0
3867| DEBUG(translate): 80000354: ldr r3, [r3]

when executing the translated instruction

0x7f7f420898: ldr w3,[x3]

a page fault will be triggerd

1.4. page fault

current user space context (gp[0..15], pc, sp, …) will be stored in the kernel stack by hardware and kernel.

ptrace(PTRACE_GETREGS…) will fetch register from the kernel stack, which is shown in the tombstone

1.5. prepare to invoke the master_signal_handler

page fault will deliver SIGSEGV to the process itself. before return to userspace, the master_signal_handler registered by the translator need to be invoked first.

kernel will setup a sigframe on user stack. the sigframe contains:

  1. retcode, which is sigreturn in vdso
  2. ucontext, which is filled with the kernel stack content (or the user space context before the page fault)
  3. siginfo
  4. signo

then, the pc stored in kernel stack is changed to the master_signal_handler, so that ret_from_intr will pop the kernel stack then jump to the master_signal_handler

1.6. master_signal_handler

master_signal_handler(signo, siginfo, uc):
  dbt_handle_signal(siginfo, uc)
    /* uc->uc_mcontext.pc is the pc that caused the SIGSEGV in
     * translated code, which is saved on the kernel stack and copied
     * to uc*/
    fragment_lookup_result_t result = fragment_lookup_by_addr(frag_abs_to_offset((void*)uc->uc_mcontext.pc));
    saved_desc = result.frag->desc;
    /* saved_desc.pc is the entry address in the guest code */
    recover_fault_info(uc, &saved_desc, fault_offset, metadata)

    // uc->uc_mcontext.pc is chaged to the dispatcher_trampoline
    uc->uc_mcontext.pc = (uintptr_t)frag_offset_to_abs(dispatcher_trampoline);
    uc->uc_mcontext.regs[16] = DISPATCH_DATA_ABORT;

1.7. sigreturn

when the master_signal_handler returns, it will call sigreturn, which is setup by the kernel when setting up sigframe.

sigreturn will fetch the sigframe on the user stack and recover the `origin` kernel stack, but since we have changed the uc_mcontext.pc to dispatcher_trampoline, when sigreturn returns to user space, pc will point to `dispatcher_trampoline`

1.8. dispatcher_trampoline

the dispatcher_trampoline is in the begining of code cache, it generally works like this:

/* save all 15 gp registers to dbt_context_t */
for (unsigned i = 0; i < 15; i++)
  emit(trans, aarch64_encode_LDR_STR_unsigned_immed(2, 0, 0, (offsetof(dbt_context_t, gp) >> 2) + i, 31, i)); // STR Wi, [SP, #gp[i]]

/* load dispatcher to x4 and branch to it */
emit_load_immediate64(trans, (uint64_t)dispatcher, 4); // MOV X4, #dispatcher
emit(trans, aarch64_encode_BLR(4)); // BLR X4
emit(trans, aarch64_encode_logical_reg(1, 1, 0, 0, 0, 0, 31, 16)); // MOV X16, X0

/* restore gp registers from dbt_context_t */
for (unsigned i = 0; i < 15; i++)
  emit(trans, aarch64_encode_LDR_STR_unsigned_immed(2, 0, 1, (offsetof(dbt_context_t, gp) >> 2) + i, 31, i)); // LDR Wi, [SP, #gp[i]]

/* branch to the PC value returned by the dispatcher */
emit(trans, aarch64_encode_BR(16)); // BR X16

1.9. dispatcher

1.9.1. dispatcher

dispatcher(dbt_context_t):
  if (reason == DISPATCH_DATA_ABORT || reason == DISPATCH_PENDING_SIGNAL):
    /* saved_desc.pc point to the guest code that cause the abort */
    desc = saved_desc;

  switch (reason):
  case DISPATCH_DATA_ABORT:
    target_regs_t regs;
    ctx_to_target_regs(&regs, ctx, &desc);
      for (unsigned i = 0; i < 15; i++):
        regs->gp[i] = ctx->gp[i];
      regs->pc = desc->pc & ~1;

    raise_sync_signal(&regs, &abort_siginfo, &abort_extra_siginfo)
    /* raise_sync_signal will put the real sa_handler (in guest code)
     * in regs->pc */

    target_regs_to_ctx(ctx, &desc, &regs);
      for (unsigned i = 0; i < 15; i++)
        ctx->gp[i] = regs->gp[i];
      if (regs->pstate & BIT(5))
        desc->pc = regs->pc | 1;
      else
       desc->pc = regs->pc & ~3;

  // desc->pc now points to the sa_handler in guest code
  fragment_lookup_result_t result = fragment_lookup_by_desc(&desc, flexible_bindings);
  if (!result.frag && !result.persist_frag):
    result.frag = translate_basic_block(&desc);
  frag_offset_t next_pc = result.frag->start

  // return the sa_handler in code cache, the dispatcher_trampoline
  // will branch to it
  return frag_offset_to_abs(next_pc);

1.9.2. raise_sync_signal

raise_sync_signal
  do_signal(regs, si, ...)
    /*setup a sigframe in the user stack as the kernel will do */
    target_setup_sigframe()
      sp -= sizeof(target_rt_sigframe_t);
      rt_frame = (target_rt_sigframe_t*)sp;
      /* remember that regs->pc points to guest code, so sig handler
       * could get the `real` pc (pc in guest code) from
       * uc_mcontext.arm_pc */
      safe_write(&frame->uc.uc_mcontext.arm_pc, regs->pc)
      regs->gp[0] = si->si_signo;
      regs->gp[1] = (uintptr_t)&rt_frame->info;
      regs->gp[2] = (uintptr_t)&frame->uc;
      regs->gp[14] = (sa->sa_flags & SA_SIGINFO) ? ARM_RT_SIGRETURN_TRAMPOLINE : ARM_SIGRETURN_TRAMPOLINE;
      /* regs->pc now points to the sig handler in guest code */
      regs->pc = sa->sa_handler_;

1.10. translated sa_handler

the translated sal_handler will notify debuggerd32 to ptrace it and debuggerd will wait on waitpid for another signal, then sal_handler will branch to something (maybe the ret_r14_trampoline?) to make a branch_return to ARM_RT_SIGRETURN_TRAMPOLINE (setup by target_setup_sigframe)

1.11. ARM_RT_SIGRETURN_TRAMPOLINE

ARM_RT_SIGRETURN_TRAMPOLINE is a trampoline in the guest memory, which is:

MOV R7, #TARGET_NR_rt_sigreturn
SVC #0

somthing (maybe the ret_r14_trampoline?) will translate the ARM_RT_SIGRETURN_TRAMPOLINE and branch to the translated code, which will be a svc_trampoline

1.12. svc_trampoline

svc_trampoline will invoke:

dispatcher_trampoline():
  dispatcher()
    do_syscall()
      do_sigreturn()
        target_restore_sigframe()
          safe_read(&frame->uc.uc_mcontext.arm_pc, regs->pc)

the last `target_restore_sigframe` will restore target_regs with the sigframe setup by target_setup_sigframe.

note that target_regs.pc is same as the arm32 pc that cause the SIGSEGV

as shown in dispatcher, dispatcher will find the corresponding arm64 pc of arm32 pc by fragment_lookup_by_desc or translate_basic_block. then branch to the arm64 pc

1.13. SIGSEGV again

the arm64 pc will cause SIGSEGV again

1.14. back to debuggerd32

the second SIGSEGV will wake up debuggerd32 from waitpid, then debuggerd32 will use ptrace(PTRACE_GETREGS,…) to dump registers, which is shown in the tombstone info

1.15. To summaries

  1. master_signal_handler found the arm32 pc that cause the SIGSEGV by fragment_lookup_by_addr
  2. master_signal_handler branch to the dispatcher_trampoline by modifying the uc_mcontext->pc
  3. dispatcher_trampoline will:
    1. set up the sigframe in user stack
    2. find the arm64 pc of the translated sa_handler by fragment_lookup_by_desc or translate_basic_block
    3. branch to translated sa_handler
  4. return address of the translated sa_handler will branch to the svc_trampoline corresponding to rt_sigreturn_trampoline
  5. translated rt_sigreturn_trampoline will call do_sigreturn, which will call target_restore_sigframe to restore the context of translated code
  6. dispatcher will find fragment corresponding to arm32 pc that cause the SIGSEGV and branch to it, which will cause another SIGSEGV
  7. the second SIGSEGV will wake up debuggerd32 and debuggerd32 will dump the tombstone

Backlinks

RISU (RISU > Implementation details > risu.c): 1. master 和 apprentice 通信 2. 通过 sighandler 触发通信和检查, 并且用 sighandler 的 ucontext 读写寄存器 (参 考 kernel signal, signal handling in DBT) 3. 检查寄存器, 内存是否一致

Author: [email protected]
Date: 2017-01-20 Fri 00:00
Last updated: 2023-01-10 Tue 17:48

知识共享许可协议