christophercrouzet / rexo

Neat single-file cross-platform unit testing framework for C/C++.
The Unlicense
28 stars 6 forks source link

Global buffer overflow on macOS #17

Closed markand closed 2 years ago

markand commented 2 years ago

Hi, when running without any test content, the library seems to do a buffer overflow (no code outside of rexo is involved)

test-board(24232,0x10d806600) malloc: nano zone abandoned due to inability to preallocate reserved vm space.
=================================================================
==24232==ERROR: AddressSanitizer: global-buffer-overflow on address 0x000107974148 at pc 0x000107967a38 bp 0x7ff7b859ed70 sp 0x7ff7b859ed68
READ of size 8 at 0x000107974148 thread T0
    #0 0x107967a37 in rx_enumerate_test_cases rexo.h:6596
    #1 0x10796270f in rx__run_registered_test_cases rexo.h:5349
    #2 0x107961934 in rx_run rexo.h:6682
    #3 0x1079618e3 in rx_main rexo.h:6694
    #4 0x1079618a8 in main test-board.c:28
    #5 0x10d78b52d in start+0x1cd (dyld:x86_64+0x552d)

0x000107974148 is located 24 bytes to the left of global variable 'rx__test_case_desc_ptr_basics_clear' defined in 'tests/test-board.c:21:1' (0x107974160) of size 8
0x000107974148 is located 0 bytes to the right of global variable 'rx__dummy_case' defined in 'extern/librexo/rexo.h:4791:52' (0x107974140) of size 8
SUMMARY: AddressSanitizer: global-buffer-overflow rexo.h:6596 in rx_enumerate_test_cases
Shadow bytes around the buggy address:
  0x100020f2e7d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e7e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e7f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e810: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x100020f2e820: 00 00 00 00 00 f9 f9 f9 00[f9]f9 f9 00 f9 f9 f9
  0x100020f2e830: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e840: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e850: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e860: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100020f2e870: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==24232==ABORTING

The content of the test file is:

#include <rexo.h>

RX_TEST_CASE(basics, clear)
{
}

int
main(int argc, char **argv)
{
    return rx_main(0, NULL, argc, (const char **)argv) == RX_SUCCESS ? 0 : 1;
}

Compiled on macOS using -fsanitize=address,undefined.

christophercrouzet commented 2 years ago

Hi @markand,

I unfortunately don't have access to macOS to test this but it looks like you should be able to reproduce this issue even without Rexo at all. It seems to be caused by the usage of -fsanitize=address and can be alleviated using MallocNanoZone=0 as shown here: https://stackoverflow.com/questions/64126942/malloc-nano-zone-abandoned-due-to-inability-to-preallocate-reserved-vm-space

Does that help?

markand commented 2 years ago

The warning that you have seen about vm space is completely irrelevant and always there even on an empty main program. The global-buffer-overflow happens in rexo code.

I've tried to debug the problem and it seems that this loop goes beyond the data because test_case_count is incremented twice (which is also what the sanitizer tells).

* thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x000000010000862e a.out`rx_enumerate_test_cases(test_case_count=0x00007ff7bfeff080, test_cases=0x0000000000000000) at rexo.h:3123:44
   3120         for (c_it = RXP_TEST_CASE_SECTION_BEGIN;                                                              
   3121              c_it != RXP_TEST_CASE_SECTION_END;
   3122              ++c_it) {   
-> 3123             *test_case_count += (rx_size)(*c_it != NULL);
   3124         } 
christophercrouzet commented 2 years ago

Thanks for looking into this @markand!

At first, I've tried stepping into your test file with a debugger but couldn't find any issue with my config (Ubuntu + Clang 16)—the test_case_count variable is only incremented once, as expected.

I've added the -fsanitize=undefined,address flag to the CMakeLists.txt file and pushed that into a branch, which triggered GitHub actions and... they're failing on Ubuntu + Clang 11, so that's a start!

I've then managed to reproduce the issue on my machine using Clang 10, with the following repro:

However, if I change the compiler to be clang-12, clang-14, or clang-16, then the error is gone... could this issue be related to a bug in how fsanitize is implemented in older versions of Clang?

Which version of Clang are you using? Are you able to reproduce the issue with newer versions?

It's also possible that they've changed how custom data sections are meant to be used, in which case we might need to provide a different implementation for the code below depending on Clang's version.

https://github.com/christophercrouzet/rexo/blob/2a21037f5ee296b44c2425464df77f9074a3496f/include/rexo.h#L1292-L1320

christophercrouzet commented 2 years ago

Update: here's a stripped down repro.

#include <stdio.h>

struct foo
{
    int value;
};

/* Implementation Details                                                     */
/* -------------------------------------------------------------------------- */

#if defined(_MSC_VER)
    __pragma(section("bar$a", read))
    __pragma(section("bar$b", read))
    __pragma(section("bar$c", read))

    __declspec(allocate("bar$a"))
    const struct foo * const bar_begin = NULL;

    __declspec(allocate("bar$c"))
    const struct foo * const bar_end = NULL;

    #define DEFINE_SECTION                                                     \
        __declspec(allocate("bar$b"))
#elif defined(__APPLE__)
    extern const struct foo * const
    __start_bar __asm("section$start$__DATA$bar");
    extern const struct foo * const
    __stop_bar __asm("section$end$__DATA$bar");

    #define DEFINE_SECTION                                                     \
        __attribute__((used,section("__DATA,bar")))

    DEFINE_SECTION
    static const struct foo * const dummy = NULL;
#elif defined(__unix__)
    extern const struct foo * const __start_bar;
    extern const struct foo * const __stop_bar;

    #define DEFINE_SECTION                                                     \
        __attribute__((used,section("bar")))

    DEFINE_SECTION
    static const struct foo * const dummy = NULL;
#endif

/* Public API                                                                 */
/* -------------------------------------------------------------------------- */

#if defined(_MSC_VER)
    #define SECTION_BEGIN                                                      \
        (&bar_begin + 1)
    #define SECTION_END                                                        \
        (&bar_end)
#elif defined(__unix__) || defined(__APPLE__)
    #define SECTION_BEGIN                                                      \
        (&__start_bar)
    #define SECTION_END                                                        \
        (&__stop_bar)
#endif

#define REGISTER_FOO(id, value)                                                \
    static const struct foo id = { value };                                    \
    DEFINE_SECTION                                                             \
    const struct foo * const id##_ptr = &id

/* Usage                                                                      */
/* -------------------------------------------------------------------------- */

REGISTER_FOO(a, 123);

int
main(
    void
)
{
    const struct foo * const *it;

    for (it = SECTION_BEGIN; it < SECTION_END; ++it)
    {
        if (*it != NULL)
        {
            printf("%d\n", (*it)->value);
        }
    }

    return 0;
}
christophercrouzet commented 2 years ago

It looks like we're not the first ones to have run into this issue, see for example https://github.com/google/sanitizers/issues/1028.

The workaround is to... disable the address sanitizer for user data sections, which I've just done in the commit https://github.com/christophercrouzet/rexo/commit/31fa8b114c963e311f078e0f663095b4a254d498 :heavy_check_mark:

Thanks again for your help, @markand!