From cc150b967a22f61fc8d4cde8e49fa4f1a8c58558 Mon Sep 17 00:00:00 2001 From: mandesero Date: Fri, 19 Jul 2024 15:30:43 +0000 Subject: [PATCH] c: add asan instrumentation for mmap/munmap/mremap TEST --- .github/workflows/sanitizers-testing.yml | 10 +- CMakeLists.txt | 32 +++- src/lj_alloc.c | 176 ++++++++++++++++++ .../asan-mmap-instrumentation.test.c | 128 +++++++++++++ 4 files changed, 339 insertions(+), 7 deletions(-) create mode 100644 test/tarantool-c-tests/asan-mmap-instrumentation.test.c diff --git a/.github/workflows/sanitizers-testing.yml b/.github/workflows/sanitizers-testing.yml index 4bf7d023aa..60c76049fa 100644 --- a/.github/workflows/sanitizers-testing.yml +++ b/.github/workflows/sanitizers-testing.yml @@ -34,17 +34,22 @@ jobs: # XXX: Let's start with only Linux/x86_64 BUILDTYPE: [Debug, Release] CC: [gcc-10, clang-11] + ASANFLAGS: ["-DLUAJIT_USE_ASAN=ON -DLUAJIT_USE_SYSMALLOC=ON", -LUAJIT_USE_ASAN_HARDENING=ON] include: - BUILDTYPE: Debug CMAKEFLAGS: -DCMAKE_BUILD_TYPE=Debug -DLUA_USE_ASSERT=ON -DLUA_USE_APICHECK=ON - BUILDTYPE: Release CMAKEFLAGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo + - ASANFLAGS: "-DLUAJIT_USE_ASAN=ON -DLUAJIT_USE_SYSMALLOC=ON" + ALLOCATOR: SYSMALLOC + - ASANFLAGS: -LUAJIT_USE_ASAN_HARDENING=ON + ALLOCATOR: LUAJIT ALLOCATOR runs-on: [self-hosted, regular, Linux, x86_64] name: > LuaJIT with ASan and UBSan (Linux/x86_64) ${{ matrix.BUILDTYPE }} CC:${{ matrix.CC }} - GC64:ON SYSMALLOC:ON + GC64:ON ${{ matrix.ALLOCATOR }} steps: - uses: actions/checkout@v4 with: @@ -71,8 +76,7 @@ jobs: -G Ninja ${{ matrix.CMAKEFLAGS }} -DLUAJIT_ENABLE_GC64=ON - -DLUAJIT_USE_ASAN=ON - -DLUAJIT_USE_SYSMALLOC=ON + ${{ matrix.ASANFLAGS }} -DLUAJIT_USE_UBSAN=ON - name: build run: cmake --build . --parallel diff --git a/CMakeLists.txt b/CMakeLists.txt index aa2103b357..d7979e2ad4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -291,10 +291,10 @@ option(LUAJIT_USE_ASAN "Build LuaJIT with AddressSanitizer" OFF) if(LUAJIT_USE_ASAN) if(NOT LUAJIT_USE_SYSMALLOC) message(WARNING - "Unfortunately, internal LuaJIT memory allocator is not instrumented yet," - " so to find any memory errors it's better to build LuaJIT with system" - " provided memory allocator (i.e. run CMake configuration phase with" - " -DLUAJIT_USE_SYSMALLOC=ON)." + "Run CMake configuration phase with -DLUAJIT_USE_SYSMALLOC=ON" + "to use system memory allocator or replace -DLUAJIT_USE_ASAN=ON" + "to -DLUAJIT_USE_ASAN_HARDENING=ON to use internal LuaJIT memory" + "allocator." ) endif() # Use all recommendations described in AddressSanitize docs: @@ -309,6 +309,30 @@ if(LUAJIT_USE_ASAN) ) endif() +# Same as LUAJIT_USE_ASAN, but using the internal LuaJIT memory +# allocator instrumented with ASAN. +option(LUAJIT_USE_ASAN_HARDENING "Build LuaJIT with an internal allocator with integrated AddressSanitizer" OFF) +if(LUAJIT_USE_ASAN_HARDENING) + if(NOT LUAJIT_ENABLE_GC64) + message(FATAL_ERROR + "ASAN only with GC64." + ) + endif() + # Use all recommendations described in AddressSanitize docs: + # https://clang.llvm.org/docs/AddressSanitizer.html. + AppendFlags(CMAKE_C_FLAGS + # Enable hints for AddressSanitizer (see src/lj_str.c). + -DLUAJIT_USE_ASAN + # Enable ASAN instrumentation of internal LuaJIT memory allocator + # see (src/lj_alloc.c) + -DLUAJIT_USE_ASAN_HARDENING + # XXX: To get nicer stack traces in error messages. + -fno-omit-frame-pointer + # Enable AddressSanitizer support. + -fsanitize=address + ) +endif() + option(LUAJIT_USE_UBSAN "Build LuaJIT with UndefinedBehaviorSanitizer" OFF) if(LUAJIT_USE_UBSAN) # Use all needed checks from the UndefinedBehaviorSanitizer diff --git a/src/lj_alloc.c b/src/lj_alloc.c index f82c9854c8..a506656b71 100644 --- a/src/lj_alloc.c +++ b/src/lj_alloc.c @@ -226,6 +226,107 @@ static int CALL_MUNMAP(void *ptr, size_t size) #define LJ_ALLOC_MMAP_PROBE_LOWER ((uintptr_t)0x4000) +#define LUAJIT_USE_ASAN_HARDENING 1 + +#if LUAJIT_USE_ASAN_HARDENING + +/* +** The work of asan (AddressSanitizer) is to detect memory errors during program execution. +** One way to achieve this is by adding redzones around memory allocations. The redzone is a +** specially allocated area of memory before and after the allocated block, which is filled +** with a unique value. If the program tries to access memory outside of the allocation, +** asan detects this attempt and generates an error message, allowing the developer to +** detect and fix the issue early. +** +** - Original paper: https://www.usenix.org/system/files/conference/atc12/atc12-final39.pdf +** +** LuaJIT ASAN instrumentation (mmap and others): +** +** - Memory map around allocation: +** ------------------------------------------------------------------------------------- +** .. .. | [f7] ... [f7] | [00] ... [0(0-7)] | [f7] ... [f7] | .. .. +** | left redzone | data | right redzone | +** | REDZONE_SIZE bytes | N bytes | REDZONE_SIZE bytes | +** ------------------------------------------------------------------------------------- +** +** left redzone: +** The first SIZE_T_SIZE bytes of the redzone contain the data size N, the next SIZE_T_SIZE bytes +** of the redzone contain the full size of the allocation, including the alignment of the size N +** and the size of the redzones themselves. +*/ + +#include + +/** + * + * Memory map for 64-bit (shift = 3) + * The shadow address is calculated by (Mem >> shift) + 0x7fff8000 + * + * [0x10007fff8000, 0x7fffffffffff] HighMem + * [0x02008fff7000, 0x10007fff7fff] HighShadow + * [0x00008fff7000, 0x02008fff6fff] ShadowGap + * [0x00007fff8000, 0x00008fff6fff] LowShadow + * [0x000000000000, 0x00007fff7fff] LowMem + * + */ + +/* Recommended redzone size from 16 to 2048 bytes (must be a a power of two) +** https://github.com/google/sanitizers/wiki/AddressSanitizerFlags +*/ +#define REDZONE_SIZE FOUR_SIZE_T_SIZES + +/* Total redzone size around allocation */ +#define TOTAL_REDZONE_SIZE (REDZONE_SIZE << 1) + +/* Multiple of the allocated memory size */ +#define SIZE_ALIGNMENT MALLOC_ALIGNMENT + +/** + * We can only use the address from HighMem, so we must force the system allocator (mmap) + * to return addresses starting from the lower bound of HighMem. + */ +static inline uintptr_t asan_lower_address() +{ + size_t shadow_scale; + size_t shadow_offset; + __asan_get_shadow_mapping(&shadow_scale, &shadow_offset); + return (uintptr_t)(shadow_offset + (1ULL << (LJ_ALLOC_MBITS - shadow_scale))); +} + +/* Casting to the nearest multiple of SIZE_ALIGNMENT from above */ +#define ALIGN_SIZE(S, ALIGN) ((size_t)((S + (ALIGN) - 1) & ~((ALIGN) - 1))) + +/* Add redzones around allocation and keep the memory size and poison size. */ +void *mark_memory_region(void *ptr, size_t msize, size_t psize) +{ + if (ptr == NULL) + return NULL; + + ASAN_UNPOISON_MEMORY_REGION(ptr, TWO_SIZE_T_SIZES); + *((size_t *)(ptr)) = msize; + *((size_t *)(ptr) + 1) = psize; + ASAN_POISON_MEMORY_REGION(ptr, psize); + ptr = (void *)((char *)(ptr) + REDZONE_SIZE); + ASAN_UNPOISON_MEMORY_REGION(ptr, msize); + return ptr; +} + +typedef enum { + MEM_SIZE, + POISON_SIZE +} SizeType; + +size_t asan_get_size(void *ptr, SizeType type) +{ + size_t offset = (type == MEM_SIZE) ? 0 : SIZE_T_SIZE; + ASAN_UNPOISON_MEMORY_REGION(ptr - REDZONE_SIZE + offset, SIZE_T_SIZE); + size_t size = *((size_t *)(ptr - REDZONE_SIZE + offset)); + ASAN_POISON_MEMORY_REGION(ptr - REDZONE_SIZE + offset, SIZE_T_SIZE); + return size; +} + +#endif + /* No point in a giant ifdef mess. Just try to open /dev/urandom. ** It doesn't really matter if this fails, since we get some ASLR bits from ** every unsuitable allocation, too. And we prefer linear allocation, anyway. @@ -252,14 +353,28 @@ static void *mmap_probe(size_t size) static uintptr_t hint_prng = 0; int olderr = errno; int retry; +#if LUAJIT_USE_ASAN_HARDENING + /* Save the request memory size */ + size_t msize = size; + /* Total allocation size corresponds to the memory size and the size of redzones */ + size = ALIGN_SIZE(size + TOTAL_REDZONE_SIZE, LJ_PAGESIZE); +#endif for (retry = 0; retry < LJ_ALLOC_MMAP_PROBE_MAX; retry++) { void *p = mmap((void *)hint_addr, size, MMAP_PROT, MMAP_FLAGS_PROBE, -1, 0); uintptr_t addr = (uintptr_t)p; +#if LUAJIT_USE_ASAN_HARDENING + if ((addr >> LJ_ALLOC_MBITS) == 0 && addr >= asan_lower_address() && + ((addr + size) >> LJ_ALLOC_MBITS) == 0) { +#else if ((addr >> LJ_ALLOC_MBITS) == 0 && addr >= LJ_ALLOC_MMAP_PROBE_LOWER && ((addr + size) >> LJ_ALLOC_MBITS) == 0) { +#endif /* We got a suitable address. Bump the hint address. */ hint_addr = addr + size; errno = olderr; +#if LUAJIT_USE_ASAN_HARDENING + p = mark_memory_region(p, msize, size); +#endif return p; } if (p != MFAIL) { @@ -338,8 +453,19 @@ static void *mmap_map32(size_t size) static void *CALL_MMAP(size_t size) { int olderr = errno; +#if LUAJIT_USE_ASAN_HARDENING + size_t msize = size; + size = ALIGN_SIZE(size + TOTAL_REDZONE_SIZE, LJ_PAGESIZE); +#endif +#if LUAJIT_USE_ASAN_HARDENING + void *ptr = mmap((void *)asan_lower_address(), size, MMAP_PROT, MMAP_FLAGS, -1, 0); +#else void *ptr = mmap(NULL, size, MMAP_PROT, MMAP_FLAGS, -1, 0); +#endif errno = olderr; +#if LUAJIT_USE_ASAN_HARDENING + ptr = mark_memory_region(ptr, msize, size); +#endif return ptr; } #endif @@ -361,7 +487,18 @@ static void init_mmap(void) static int CALL_MUNMAP(void *ptr, size_t size) { int olderr = errno; +#if LUAJIT_USE_ASAN_HARDENING + /* check that memory is not poisoned */ + memmove(ptr, ptr, size); + size = asan_get_size(ptr, POISON_SIZE); + ptr = (void *)((char *)(ptr) - REDZONE_SIZE); +#endif int ret = munmap(ptr, size); +#if LUAJIT_USE_ASAN_HARDENING + if (ret == 0) { + ASAN_POISON_MEMORY_REGION(ptr, size); + } +#endif errno = olderr; return ret; } @@ -371,7 +508,31 @@ static int CALL_MUNMAP(void *ptr, size_t size) static void *CALL_MREMAP_(void *ptr, size_t osz, size_t nsz, int flags) { int olderr = errno; +#if LUAJIT_USE_ASAN_HARDENING && !(LJ_64 && (!LJ_GC64 || LJ_TARGET_ARM64)) + void *new_ptr = mmap_probe(nsz); + if (new_ptr != MFAIL) { + size_t oms = asan_get_size(ptr, MEM_SIZE); + memcpy(new_ptr, ptr, oms); + munmap(ptr, osz); + ptr = new_ptr; + } +#else + +#if LUAJIT_USE_ASAN_HARDENING + void *old_ptr = ptr; + size_t nms = nsz; + osz = asan_get_size(old_ptr, POISON_SIZE); + nsz = ALIGN_SIZE(nsz + TOTAL_REDZONE_SIZE, LJ_PAGESIZE); + ptr = (void *)((char *)(ptr) - REDZONE_SIZE); +#endif ptr = mremap(ptr, osz, nsz, flags); +#if LUAJIT_USE_ASAN_HARDENING + if (ptr != MFAIL) { + ASAN_POISON_MEMORY_REGION((void *)((char *)(old_ptr) - REDZONE_SIZE), osz); + ptr = mark_memory_region(ptr, nms, nsz); + } +#endif +#endif errno = olderr; return ptr; } @@ -837,7 +998,13 @@ static int has_segment_link(mstate m, msegmentptr ss) static void *direct_alloc(size_t nb) { +#if LUAJIT_USE_ASAN_HARDENING + nb += TOTAL_REDZONE_SIZE; +#endif size_t mmsize = mmap_align(nb + SIX_SIZE_T_SIZES + CHUNK_ALIGN_MASK); +#if LUAJIT_USE_ASAN_HARDENING + mmsize -= TOTAL_REDZONE_SIZE; +#endif if (LJ_LIKELY(mmsize > nb)) { /* Check for wrap around 0 */ char *mm = (char *)(DIRECT_MMAP(mmsize)); if (mm != CMFAIL) { @@ -1006,7 +1173,13 @@ static void *alloc_sys(mstate m, size_t nb) { size_t req = nb + TOP_FOOT_SIZE + SIZE_T_ONE; +#if LUAJIT_USE_ASAN_HARDENING + req += TOTAL_REDZONE_SIZE; +#endif size_t rsize = granularity_align(req); +#if LUAJIT_USE_ASAN_HARDENING + rsize -= TOTAL_REDZONE_SIZE; +#endif if (LJ_LIKELY(rsize > nb)) { /* Fail if wraps around zero */ char *mp = (char *)(CALL_MMAP(rsize)); if (mp != CMFAIL) { @@ -1238,6 +1411,9 @@ static void *tmalloc_small(mstate m, size_t nb) void *lj_alloc_create(void) { size_t tsize = DEFAULT_GRANULARITY; +#if LUAJIT_USE_ASAN_HARDENING + tsize -= TOTAL_REDZONE_SIZE; +#endif char *tbase; INIT_MMAP(); tbase = (char *)(CALL_MMAP(tsize)); diff --git a/test/tarantool-c-tests/asan-mmap-instrumentation.test.c b/test/tarantool-c-tests/asan-mmap-instrumentation.test.c new file mode 100644 index 0000000000..c1aa96229b --- /dev/null +++ b/test/tarantool-c-tests/asan-mmap-instrumentation.test.c @@ -0,0 +1,128 @@ +#include "lua.h" +#include "test.h" +#include "utils.h" +#include "lj_alloc.c" +#include "lj_gc.h" +#define LUAJIT_USE_ASAN_HARDENING 1 +#if LUAJIT_USE_ASAN_HARDENING +#include + +#define MALLOC(size) mmap_probe(size) +#define FREE(ptr, size) CALL_MUNMAP(ptr, size) +#define REALLOC(ptr, osz, nsz) CALL_MREMAP(ptr, osz, nsz, CALL_MREMAP_MV) +#define IS_POISONED(ptr) __asan_address_is_poisoned(ptr) + + +int IS_POISONED_REGION(void *ptr, size_t size) +{ + int res = 1; + int i = 0; + do { + res *= IS_POISONED(ptr + i); + } while (res == 1 && ++i < size); + return res; +} +#endif + +static lua_State *main_LS = NULL; + +static int mmap_probe_test(void *test_state) +{ +#if !LUAJIT_USE_ASAN_HARDENING || LUAJIT_USE_SYSMALLOC + UNUSED(test_state); + return skip("Requires build with ASAN"); +#else + int res = -1; + size_t size = DEFAULT_GRANULARITY - TOTAL_REDZONE_SIZE; + void *p = MALLOC(size); + size_t algn = ALIGN_SIZE(size, SIZE_ALIGNMENT) - size; + + if (p == MFAIL) { + perror("mmap memory allocation error"); + return TEST_EXIT_FAILURE; + } + + if (IS_POISONED_REGION(p - REDZONE_SIZE, REDZONE_SIZE) && + !IS_POISONED_REGION(p, size) && + IS_POISONED_REGION(p + size, algn + REDZONE_SIZE)) + res = TEST_EXIT_SUCCESS; + else + perror("Not correct poison and unpoison areas"); + FREE(p, size); + return res == TEST_EXIT_SUCCESS ? TEST_EXIT_SUCCESS : TEST_EXIT_FAILURE; +#endif +} + +static int munmap_test(void *test_state) +{ +#if !LUAJIT_USE_ASAN_HARDENING || LUAJIT_USE_SYSMALLOC + UNUSED(test_state); + return skip("Requires build with ASAN"); +#else + size_t size = DEFAULT_GRANULARITY - TOTAL_REDZONE_SIZE; + size_t algn = ALIGN_SIZE(size, SIZE_ALIGNMENT) - size; + void *p = MALLOC(size); + + if (p == MFAIL) { + perror("mmap memory allocation error"); + return TEST_EXIT_FAILURE; + } + + FREE(p, size); + if (IS_POISONED_REGION(p - REDZONE_SIZE, TOTAL_REDZONE_SIZE + size + algn)) + return TEST_EXIT_SUCCESS; + perror("Not correct poison and unpoison areas"); + return TEST_EXIT_FAILURE; +#endif +} + +static int mremap_test(void *test_state) +{ +#if !LUAJIT_USE_ASAN_HARDENING || LUAJIT_USE_SYSMALLOC + UNUSED(test_state); + return skip("Requires build with ASAN"); +#else + int res = -1; + size_t size = (DEFAULT_GRANULARITY >> 2) - TOTAL_REDZONE_SIZE; + size_t new_size = (DEFAULT_GRANULARITY >> 1) - TOTAL_REDZONE_SIZE; + void *p = MALLOC(size); + + if (p == MFAIL) { + perror("mmap memory allocation error"); + return TEST_EXIT_FAILURE; + } + + void *newptr = REALLOC(p, size, new_size); + if (newptr == MFAIL) { + perror("mremap return MFAIL"); + FREE(p, size); + return TEST_EXIT_FAILURE; + } + + if (IS_POISONED_REGION(newptr - REDZONE_SIZE, REDZONE_SIZE) && + !IS_POISONED_REGION(newptr, new_size) && + IS_POISONED_REGION(newptr + new_size, REDZONE_SIZE)) + res = TEST_EXIT_SUCCESS; + else + perror("Not correct poison and unpoison areas"); + + FREE(newptr, new_size); + return res == TEST_EXIT_SUCCESS ? TEST_EXIT_SUCCESS : TEST_EXIT_FAILURE; +#endif +} + +int main(void) +{ + lua_State *L = utils_lua_init(); + main_LS = L; + + const struct test_unit tgroup[] = { + test_unit_def(mmap_probe_test), + test_unit_def(munmap_test), + test_unit_def(mremap_test) + }; + + const int test_result = test_run_group(tgroup, L); + utils_lua_close(L); + return test_result; +}