1

I noticed that upon an invalid 64bit MacOS syscall

    xor eax,eax
    syscall
;lldb stops here after the syscall

When lldb stops the process while single stepping on:

thread #1, stop reason = EXC_SYSCALL (code=5797, subcode=0x1)

The rcx is equal to rsp. However when lldb is not attached or is not single stepping rcx is equal to expected return address after the syscall (i.e. is exactly of the same value as rip in user space). Is this some kind of a bug / side effect?

I'm observing this on MacOS 10.14.5, lldb-1000.11.38.2 also 10.14.6, lldb-1001.0.13.3

After some investigation relevant XNU code seems to be:

Entry(hndl_syscall)
    TIME_TRAP_UENTRY

    movq    %gs:CPU_ACTIVE_THREAD,%rcx  /* get current thread     */
    movl    $-1, TH_IOTIER_OVERRIDE(%rcx)   /* Reset IO tier override to -1 before handling syscall */
    movq    TH_TASK(%rcx),%rbx      /* point to current task  */

    /* Check for active vtimers in the current task */
    TASK_VTIMER_CHECK(%rbx,%rcx)

    /*
     * We can be here either for a mach, unix machdep or diag syscall,
     * as indicated by the syscall class:
     */
    movl    R64_RAX(%r15), %eax     /* syscall number/class */
    movl    %eax, %edx
    andl    $(SYSCALL_CLASS_MASK), %edx /* syscall class */
    cmpl    $(SYSCALL_CLASS_MACH<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_mach_scall64)
    cmpl    $(SYSCALL_CLASS_UNIX<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_unix_scall64)
    cmpl    $(SYSCALL_CLASS_MDEP<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_mdep_scall64)
    cmpl    $(SYSCALL_CLASS_DIAG<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_diag_scall64)

    /* Syscall class unknown */
    sti
    CCALL3(i386_exception, $(EXC_SYSCALL), %rax, $1)
    /* no return */

I do indeed get the rax value as the (code=xxxx) reported by lldb. And the subcode 1 also matches.

The parts relevant for returning to user space are:

EXT(ret64_iret):
        iretq               /* return from interrupt */
L_sysret:
    /*
     * Here to restore rcx/r11/rsp and perform the sysret back to user-space.
     *  rcx user rip
     *  r11 user rflags
     *  rsp user stack pointer
     */
    pop %rcx
    add $8, %rsp
    pop %r11
    pop %rsp
    sysretq             /* return from system call */

and also

L_fast_exit:
    pop %rdx            /* user return eip */
    pop %rcx            /* pop and toss cs */
    andl    $(~EFL_IF), (%rsp)  /* clear interrupts enable, sti below */
    popf                /* flags - carry denotes failure */
    pop %rcx            /* user return esp */
    sti             /* interrupts enabled after sysexit */
    sysexitl            /* 32-bit sysexit */

So I actually did disassemble the MacOS 10.14.6 kernel binary found at /System/Library/Kernels/kernel.

Interestingly the sysret variant is not there at all. Only the iret and sysexit.

Looking at 64bit relevant part really helps.

                     _ret64_iret:
ffffff800019985a         iretq                                                  ; CODE XREF=_idt64_debug+38, _ks_64bit_return+118, _ret64_iret+25DATA XREF=_idt64_stack_fault+32

                     loc_ffffff800019985c:
ffffff800019985c         cmp        eax, 0x1                                    ; CODE XREF=_ks_64bit_return+170
ffffff800019985f         je         loc_ffffff8000199875

ffffff8000199861         pop        rax
ffffff8000199862         pop        rcx
ffffff8000199863         add        rsp, 0x8
ffffff8000199867         pop        r11
ffffff8000199869         pop        rsp
ffffff800019986a         sysret

                     loc_ffffff800019986d:
ffffff800019986d         pop        rax                                         ; CODE XREF=_ks_64bit_return+179
ffffff800019986e         verw       word [rsp-0x30+arg_50]
ffffff8000199873         jmp        _ret64_iret

                     loc_ffffff8000199875:
ffffff8000199875         pop        rax                                         ; CODE XREF=_ret64_iret+5
ffffff8000199876         pop        rcx
ffffff8000199877         add        rsp, 0x8
ffffff800019987b         pop        r11
ffffff800019987d         verw       word [rsp-0x20+arg_20]
ffffff8000199882         pop        rsp
ffffff8000199883         sysret
Kamil.S
  • 5,205
  • 2
  • 22
  • 51
  • 2
    The kernel might be using a different syscall return-path in a process being single-stepped. Linux does this (using `iret` to work around exploitable CPU bugs in `sysret` if a debugger might have set a non-canonical RIP https://github.com/torvalds/linux/blob/e7d0c41ecc2e372a81741a30894f556afec24315/arch/x86/entry/entry_64.S#L133), but IDK about the XNU/Darwin kernel in MacOS. See also [amd and intel programmer's model compatibility](//stackoverflow.com/q/54379076) where I wrote more detail about CPU bugs with `sysret` in an answer. – Peter Cordes Aug 16 '19 at 11:55
  • `CCALL3` is the failure case when RAX doesn't match any of the 4 classes it checks for. The normal path is `je EXT(hndl_mach_scall64)` or `je EXT(hndl_unix_scall64)` – Peter Cordes Aug 17 '19 at 14:10
  • @PeterCordes yes that's my understanding. I updated my question with "fresh" `XNU` source link and added some remarks. Is the `sysexit` vs `iret` while single stepping a plausible explanation of what I'm observing? – Kamil.S Aug 17 '19 at 15:08
  • Interesting, I didn't realize 64-bit kernels would ever use `sysexitl` to return to 32-bit userspace. It's interesting that the `sysexit` return path does load user-space ESP into RCX, but I'm pretty sure your code can't be using it. The 64-bit `sysret` return path obviously returns to user-space with RCX=RIP because that's how `sysret` works. You'll need to look at the `iret` return path to see what it does. – Peter Cordes Aug 17 '19 at 15:13

0 Answers0