2

I am debugging a kernel oops (vmcore) using the crash utility on CentOS 7.9 and I have a function foo, which calls a callback, but when I disassemble foo I don't see a callq instruction that references the callback, nor do I see the assembly for the callback in the caller (suggesting it isn't inlined there).

However, the kernel stack shows that the RIP was on offset 33 of the callback function. What gives?

The last instruction for foo shows:

callq  0xffffffff91c9af10 <__stack_chk_fail>

Does this perhaps means that the callback smashed the stack and glibc replaced it with this stack_chk thingamajig?

// signature for foo
foo(some_t *some, size_t off, size_t size,
    my_callback_t *func, void *private)

// callback gets called in foo like:
ret = func(args)

Update I do also see a callq to: __x86_indirect_thunk_rax Which I have no idea about.. Perhaps that is somehow the call? Looking into it, it has something to do with a return trampoline, which sounds fun! XD

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847

1 Answers1

2

Normally you should see call *%rcx or something (the function pointer could be in a different register, or stack memory, but some kind of indirect call). Or with an optimized tailcall like jmp *%rcx if your code can be optimized into return func(args). Again, possibly after moving the function pointer to a different register, and after cleaning up the stack.

(The 4th integer/pointer function arg arrives in RCX, but it could be in any other register when eventually used).

call _stack_chk_fail is just part of the -fstack-protector=strong machinery.


Kernel code uses gcc -mindirect-branch=thunk to enable GCC's Spectre mitigation for indirect calls, so yes, indirect calls will go through __x86_indirect_thunk_rax. (Or a different one taking the pointer in any other register.)

User-space code can of course be compiled with this option, too, although I don't think most distros enable it by default.

typedef int (*my_callback_t)(int);

int foo(int a, int b, int c, my_callback_t func)
{
    int ret = func(a);
    return ret;
}

compiles like this, with -O3 -Wall -mindirect-branch=thunk (GCC10.2 on Godbolt, with -mcmodel=kernel as well for good measure)

# your function
foo:
        jmp     __x86_indirect_thunk_rcx  # like jmp *%rcx  tailcall 

# extra code that will be present once in the whole kernel, deduplicated by the linker
.section        .text.__x86_indirect_thunk_rcx,"axG",@progbits,__x86_indirect_thunk_rcx,comdat
__x86_indirect_thunk_rcx:
        call    .LIND1
.LIND0:
        pause
        lfence            # block speculation along this never-executed return path that return prediction will jump to.
        jmp     .LIND0    # this seems unnecessary after lfence in this unreachable code.
.LIND1:
        mov     %rcx, (%rsp)       # overwrite the return address with your func ptr
        ret                        # and pop it into RIP

This is a retpoline.

Without -mindirect-branch=thunk, you of course get the expected jmp *%rcx.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • Is it a given that I would see %rdi populated with the first parameter of the callback (by the disassembled caller) before the callq? Or is it possible that, if the function params are local in the caller, that the registers not need follow conventions for param-passing? – Gregg Leventhal Mar 28 '21 at 19:09
  • 1
    The only other callq I see that I cannot explain is to __x86_indirect_thunk_rax I am going to guess that the register optimization you reference is related to this? – Gregg Leventhal Mar 28 '21 at 19:15
  • 1
    @GreggLeventhal: Oh right, I forgot kernel code would be compiled with Spectre mitigation enabled. Updated. – Peter Cordes Mar 28 '21 at 19:36
  • Thanks, this is a great answer. If you're interested in a job in the NYC area working on Linux systems (programming or administration), shoot me a message! – Gregg Leventhal Mar 29 '21 at 13:14