Search code examples
clinuxdebuggingmockingabort

Strange behaviour while wrapping abort() system call


I need, to write unitary tests, to wrap the abort() system call.

Here is a snippet of code:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

extern void __real_abort(void);
extern void * __real_malloc(int c);
extern void __real_free(void *);


void __wrap_abort(void)
{
    printf("=== Abort called !=== \n");
}   

void * __wrap_malloc(int s)
{
    void *p = __real_malloc(s);
    printf("allocated %d bytes @%p\n",s, (void *)p);
    return p;
}

void __wrap_free(void *p)
{
    printf("freeing @%p\n",(void *)p);
    return __real_free((void *)p);
}


int main(int ac, char **av)
{
    char *p = NULL;
    printf("pre malloc: p=%p\n",p);
    p = malloc(40);
    printf("post malloc p=%p\n",p);

    printf("pre abort\n");
    //abort();
    printf("post abort\n");

    printf("pre free\n");
    free(p);
    printf("post free\n");
    return -1;
}

Then i compile this using the following command line :

gcc -Wl,--wrap=abort,--wrap=free,--wrap=malloc -ggdb -o test test.c

Running it give the following output:

$ ./test
pre malloc: p=(nil)
allocated 40 bytes @0xd06010
post malloc p=0xd06010
pre abort
post abort
pre free
freeing @0xd06010
post free

So everything is fine. Now let's test the same code but with abort() call uncommented:

$ ./test
pre malloc: p=(nil)
allocated 40 bytes @0x1bf2010
post malloc p=0x1bf2010
pre abort
=== Abort called !=== 
Segmentation fault (core dumped)

I don't really understand why i get a segmentation fault while mocking abort() syscall... Every advice is welcome !

I run Debian GNU/Linux 8.5 on an x86_64 kernel. Machine is a Core i7 based laptop.


Solution

  • In glibc (which is the libc Debian uses) the abort function (it's not a system call, it's a normal function) is declared like this:

    extern void abort (void) __THROW __attribute__ ((__noreturn__));
    

    This bit: __attribute__ ((__noreturn__)) is a gcc extension that tells it that the function can't return. Your wrapper function does return which the compiler didn't expect. Because of that it will crash or do something completely unexpected.

    Your code when compiled will be using the declarations from stdlib.h for the call to abort, the flags you gave to the linker won't change that.

    Noreturn functions are called differently, the compiler doesn't have to preserve registers, it can just jump to the function instead of doing a proper call, it might even just not generate any code after it because that code is by definition not reachable.

    Here's a simple example:

    extern void ret(void);
    extern void noret(void) __attribute__((__noreturn__));
    
    void
    foo(void)
    {
        ret();
        noret();
        ret();
        ret();
    }
    

    Compiled into assembler (even without optimizations):

    $ cc -S foo.c
    $ cat foo.s
    [...]
    foo:
    .LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        call    ret
        call    noret
        .cfi_endproc
    .LFE0:
        .size   foo, .-foo
        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)"
        .section    .note.GNU-stack,"",@progbits
    

    Notice that there is a call to noret, but there isn't any code after this. The two calls to ret were not generated and there is no ret instruction. The function just ends. This means that if the function noret actually returns because of a bug (which your implementation of abort has), anything can happen. In this case we'll just continue executing whatever happens to be in the code segment after us. Maybe another function, or some strings, or just zeroes, or maybe we're lucky and the memory mapping ends just after this.

    In fact, let's do something evil. Never do this in real code. If you ever think that this is a good idea you'll need to hand over the keys to your computer and slowly step away from the keyboard while keeping your hands up:

    $ cat foo.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    void __wrap_abort(void)
    {
        printf("=== Abort called !=== \n");
    }
    
    int
    main(int argc, char **argv)
    {
        abort();
        return 0;
    }
    
    void
    evil(void)
    {
        printf("evil\n");
        _exit(17);
    }
    $ gcc -Wl,--wrap=abort -o foo foo.c && ./foo
    === Abort called !===
    evil
    $ echo $?
    17
    

    As I thought, the code just keeps going after whatever happened to be placed after main and in this simple example the compiler didn't think it would be a good idea to reorganize the functions.