From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from [87.239.111.99] (localhost [127.0.0.1]) by dev.tarantool.org (Postfix) with ESMTP id 32161D49AFC; Thu, 22 Aug 2024 21:16:18 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 32161D49AFC DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=tarantool.org; s=dev; t=1724350578; bh=R++66PKVMxNZ+MYAgBfNdPCahIJ8cRgAaojFbpYZNxM=; h=To:Cc:Date:In-Reply-To:References:Subject:List-Id: List-Unsubscribe:List-Archive:List-Post:List-Help:List-Subscribe: From:Reply-To:From; b=TVNNePs/JrkSS8crOQGa6hIA84CosD8Z7vyEPN4LXWVkSm1gk8De0WWQMow/O/vQ5 p6IkdwTQubJ2JiDwKgkLGozXiiYExKjpEopESO4vtagWT7mNw1R/iRNjeqKFTQuljY mrdzfFCRuCfRPP+9UGEFpcQbdFx1YX1CxWxs0VLo= Received: from mail-lf1-f45.google.com (mail-lf1-f45.google.com [209.85.167.45]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by dev.tarantool.org (Postfix) with ESMTPS id 5CF0B4AA35A for ; Thu, 22 Aug 2024 21:15:47 +0300 (MSK) DKIM-Filter: OpenDKIM Filter v2.11.0 dev.tarantool.org 5CF0B4AA35A Received: by mail-lf1-f45.google.com with SMTP id 2adb3069b0e04-5334a8a1af7so1171264e87.2 for ; Thu, 22 Aug 2024 11:15:47 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1724350546; x=1724955346; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=TzUFUTfSBLxSPfVEmfcMxWMu9GW4kzPUndRW32/d25A=; b=uCQ1H3inmbbxB4FfZ2Ok2Ta1pb/2SqS5mWvz9zJKRLOikCrHqDn4zxh0uL9oXAfkbp 4GpsXLD/m+D9NVZtll/0Wf0THXbYNo/8PPUva631L4s7nMXTZiHm+RMLdN7O9eEjYcsU gq4V0pEP3jP0mkiY6PiWFNusau4Jf+QRgHncXIWSOsjXryKgc5Ow4z1wVlmRuHzkHpqc 7pSvyBHqYWtMDHcboKhjYkfIIrwc5gwmgqZtSQirO2FKwK4FWKgQXBv8uDdxR8lw2SrQ F6++f0T/ZYrVFZ7TyEB1UJ5FYp3bGakkBbbwHwSkru7DP2NbawCxBAedJ4btDMejNSbR ACnA== X-Gm-Message-State: AOJu0Yy3Qtq9z2sYaXB/+9epNdTVtYoWA1japXYVSfHPuG1Vl/ZqZ2TG lXawVhHZeuDWI1nLOg8Pex64nY8zru9VR7gLlLEUAhD42FEeGBGkFgE+SRCnTco= X-Google-Smtp-Source: AGHT+IEi/imfeyWJQe0X8xU6klo6cKe+GliDk4w+fzLHLfmFiHwvZrbZmm0fQsywgPx51ZoKPAstFw== X-Received: by 2002:a05:6512:3c85:b0:533:4591:fbf1 with SMTP id 2adb3069b0e04-53348589d18mr4151870e87.15.1724350545509; Thu, 22 Aug 2024 11:15:45 -0700 (PDT) Received: from mandesero.. ([95.153.162.123]) by smtp.gmail.com with ESMTPSA id 2adb3069b0e04-5334ea88e53sm321988e87.269.2024.08.22.11.15.44 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 22 Aug 2024 11:15:45 -0700 (PDT) To: tarantool-patches@dev.tarantool.org, skaplun@tarantool.org, m.kokryashkin@tarantool.org Cc: Maksim Tiushev Date: Thu, 22 Aug 2024 18:15:42 +0000 Message-Id: <20240822181542.482869-2-mandesero@gmail.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20240822181542.482869-1-mandesero@gmail.com> References: <20240822181542.482869-1-mandesero@gmail.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Subject: [Tarantool-patches] [PATCH luajit 1/1] asan: instrumented LuaJIT memory allocator X-BeenThere: tarantool-patches@dev.tarantool.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: Tarantool development patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: mandesero--- via Tarantool-patches Reply-To: mandesero@gmail.com Errors-To: tarantool-patches-bounces@dev.tarantool.org Sender: "Tarantool-patches" From: Maksim Tiushev This patch adds instrumentation of the internal LuaJIT memory allocator. It now enables the detection of memory-related errors when using FFI and within LuaJIT itself. This enhancement improves reliability and debugging capabilities. This patch introduces two scenarios for using ASAN with LuaJIT: - LuaJIT using sysmalloc: `-DLUAJIT_USE_ASAN=ON` - LuaJIT using internal memory allocator: `-DLUAJIT_USE_ASAN_HARDENING=ON` If you want to skip tests when LuaJIT uses the internal memory allocator, you can check the `LJ_ASAN_HARDENING` environment variable. The test `test/tarantool-tests/lj-1034-tabov-error-frame.test.lua` has been disabled under ASAN & LuaJIT's internal allocator due to consistently failing with a timeout. Part of #10231 --- .github/workflows/sanitizers-testing.yml | 20 +- CMakeLists.txt | 29 +- src/lj_alloc.c | 267 +++++++++++++++++- .../asan-alloc-instrumentation.test.c | 141 +++++++++ .../asan-mmap-instrumentation.test.c | 128 +++++++++ .../ASAN-left-memory-miss.test.lua | 12 + .../ASAN-left-memory-miss/script.lua | 19 ++ .../ASAN-right-memory-miss.test.lua | 12 + .../ASAN-right-memory-miss/script.lua | 19 ++ .../ASAN-use-after-free.test.lua | 12 + .../ASAN-use-after-free/script.lua | 24 ++ test/tarantool-tests/CMakeLists.txt | 2 +- .../lj-1034-tabov-error-frame.test.lua | 2 + 13 files changed, 669 insertions(+), 18 deletions(-) create mode 100644 test/tarantool-c-tests/asan-alloc-instrumentation.test.c create mode 100644 test/tarantool-c-tests/asan-mmap-instrumentation.test.c create mode 100644 test/tarantool-tests/ASAN-left-memory-miss.test.lua create mode 100644 test/tarantool-tests/ASAN-left-memory-miss/script.lua create mode 100644 test/tarantool-tests/ASAN-right-memory-miss.test.lua create mode 100644 test/tarantool-tests/ASAN-right-memory-miss/script.lua create mode 100644 test/tarantool-tests/ASAN-use-after-free.test.lua create mode 100644 test/tarantool-tests/ASAN-use-after-free/script.lua diff --git a/.github/workflows/sanitizers-testing.yml b/.github/workflows/sanitizers-testing.yml index 4bf7d023..5699f843 100644 --- a/.github/workflows/sanitizers-testing.yml +++ b/.github/workflows/sanitizers-testing.yml @@ -34,17 +34,23 @@ jobs: # XXX: Let's start with only Linux/x86_64 BUILDTYPE: [Debug, Release] CC: [gcc-10, clang-11] + ALLOCATOR: [sysmalloc, dlmalloc] include: - BUILDTYPE: Debug CMAKEFLAGS: -DCMAKE_BUILD_TYPE=Debug -DLUA_USE_ASSERT=ON -DLUA_USE_APICHECK=ON - BUILDTYPE: Release CMAKEFLAGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo + - ALLOCATOR: sysmalloc + ASANFLAGS: -DLUAJIT_USE_ASAN=ON -DLUAJIT_USE_SYSMALLOC=ON + - ALLOCATOR: dlmalloc + ASANFLAGS: -DLUAJIT_USE_ASAN_HARDENING=ON 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 + ALLOC=${{ matrix.ALLOCATOR }} steps: - uses: actions/checkout@v4 with: @@ -55,24 +61,18 @@ jobs: with: cc_name: ${{ matrix.CC }} - name: configure - # XXX: LuaJIT configuration requires a couple of tweaks: - # LUAJIT_USE_SYSMALLOC=ON: 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). - # For more info, see root CMakeLists.txt. # LUAJIT_ENABLE_GC64=ON: LUAJIT_USE_SYSMALLOC cannot be # enabled on x64 without GC64, since realloc usually # doesn't return addresses in the right address range. + # Additionally, the ASAN instrumentation for LuaJIT's + # internal memory allocator is only available for GC64. # For more info, see root CMakeLists.txt. run: > cmake -S . -B ${{ env.BUILDDIR }} -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 aa2103b3..619babad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -284,17 +284,34 @@ if(LUA_USE_ASSERT) AppendFlags(TARGET_C_FLAGS -DLUA_USE_ASSERT) endif() +option(LUAJIT_USE_ASAN "Build LuaJIT with AddressSanitizer" OFF) +# 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) + set(LUAJIT_USE_ASAN ON) + if(NOT LUAJIT_ENABLE_GC64) + message(FATAL_ERROR + "ASAN only with GC64." + ) + endif() + AppendFlags(CMAKE_C_FLAGS + # Enable ASAN instrumentation of internal LuaJIT memory allocator + # see (src/lj_alloc.c) + -DLUAJIT_USE_ASAN_HARDENING + ) +endif() + # Turn on AddressSanitizer support. As a result, all artefacts # (i.e. buildvm, LuaJIT, testing infrastructure) are built with # ASan enabled. -option(LUAJIT_USE_ASAN "Build LuaJIT with AddressSanitizer" OFF) if(LUAJIT_USE_ASAN) - if(NOT LUAJIT_USE_SYSMALLOC) + if(NOT (LUAJIT_USE_SYSMALLOC OR LUAJIT_USE_ASAN_HARDENING)) 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: diff --git a/src/lj_alloc.c b/src/lj_alloc.c index f82c9854..097a078d 100644 --- a/src/lj_alloc.c +++ b/src/lj_alloc.c @@ -226,6 +226,108 @@ static int CALL_MUNMAP(void *ptr, size_t size) #define LJ_ALLOC_MMAP_PROBE_LOWER ((uintptr_t)0x4000) +#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))) + +#define alloc2mem(p) ((void *)((char *)(p) + REDZONE_SIZE)) +#define mem2alloc(mem) ((void *)((char *)(mem) - REDZONE_SIZE)) + +/* 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 = alloc2mem(ptr); + 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 +354,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 +454,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 +488,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 = mem2alloc(ptr); +#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 +509,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 = CALL_MMAP(nsz); + if (new_ptr != MFAIL) { + size_t oms = asan_get_size(ptr, MEM_SIZE); + memcpy(new_ptr, ptr, oms > nsz ? nsz : oms); + CALL_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 = mem2alloc(ptr); +#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; } @@ -432,9 +594,15 @@ typedef unsigned int flag_t; /* The type of various bit flag sets */ #define MIN_CHUNK_SIZE\ ((MCHUNK_SIZE + CHUNK_ALIGN_MASK) & ~CHUNK_ALIGN_MASK) +#if LUAJIT_USE_ASAN_HARDENING +/* conversion from malloc headers to user pointers, and back */ +#define chunk2mem(p) ((void *)((char *)(p) + TWO_SIZE_T_SIZES + REDZONE_SIZE)) +#define mem2chunk(mem) ((mchunkptr)((char *)(mem) - TWO_SIZE_T_SIZES - REDZONE_SIZE)) +#else /* conversion from malloc headers to user pointers, and back */ #define chunk2mem(p) ((void *)((char *)(p) + TWO_SIZE_T_SIZES)) #define mem2chunk(mem) ((mchunkptr)((char *)(mem) - TWO_SIZE_T_SIZES)) +#endif /* chunk associated with aligned address A */ #define align_as_chunk(A) (mchunkptr)((A) + align_offset(chunk2mem(A))) @@ -837,7 +1005,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) { @@ -887,7 +1061,12 @@ static mchunkptr direct_resize(mchunkptr oldp, size_t nb) static void init_top(mstate m, mchunkptr p, size_t psize) { /* Ensure alignment */ - size_t offset = align_offset(chunk2mem(p)); + void *t = chunk2mem(p); +#if LUAJIT_USE_ASAN_HARDENING + t = mem2alloc(t); +#endif + size_t offset = align_offset(t); + p = (mchunkptr)((char *)p + offset); psize -= offset; @@ -949,6 +1128,9 @@ static void add_segment(mstate m, char *tbase, size_t tsize) /* Determine locations and sizes of segment, fenceposts, old top */ char *old_top = (char *)m->top; msegmentptr oldsp = segment_holding(m, old_top); +#if LUAJIT_USE_ASAN_HARDENING + ASAN_UNPOISON_MEMORY_REGION(oldsp, sizeof(struct malloc_segment)); +#endif char *old_end = oldsp->base + oldsp->size; size_t ssize = pad_request(sizeof(struct malloc_segment)); char *rawsp = old_end - (ssize + FOUR_SIZE_T_SIZES + CHUNK_ALIGN_MASK); @@ -957,6 +1139,9 @@ static void add_segment(mstate m, char *tbase, size_t tsize) char *csp = (asp < (old_top + MIN_CHUNK_SIZE))? old_top : asp; mchunkptr sp = (mchunkptr)csp; msegmentptr ss = (msegmentptr)(chunk2mem(sp)); +#if LUAJIT_USE_ASAN_HARDENING + ss = (msegmentptr)(mem2alloc(ss)); +#endif mchunkptr tnext = chunk_plus_offset(sp, ssize); mchunkptr p = tnext; @@ -1006,7 +1191,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,21 +1429,33 @@ 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)); if (tbase != CMFAIL) { size_t msize = pad_request(sizeof(struct malloc_state)); mchunkptr mn; +#if LUAJIT_USE_ASAN_HARDENING + mchunkptr msp = (mchunkptr)(tbase + align_offset(mem2alloc(chunk2mem(tbase)))); + mstate m = (mstate)(mem2alloc(chunk2mem(msp))); +#else mchunkptr msp = align_as_chunk(tbase); mstate m = (mstate)(chunk2mem(msp)); +#endif memset(m, 0, msize); msp->head = (msize|PINUSE_BIT|CINUSE_BIT); m->seg.base = tbase; m->seg.size = tsize; m->release_checks = MAX_RELEASE_CHECK_RATE; init_bins(m); +#if LUAJIT_USE_ASAN_HARDENING + mn = next_chunk((mchunkptr)((char *)(m) - TWO_SIZE_T_SIZES)); +#else mn = next_chunk(mem2chunk(m)); +#endif init_top(m, mn, (size_t)((tbase + tsize) - (char *)mn) - TOP_FOOT_SIZE); return m; } @@ -1267,12 +1470,22 @@ void lj_alloc_destroy(void *msp) char *base = sp->base; size_t size = sp->size; sp = sp->next; +#if LUAJIT_USE_ASAN_HARDENING + ASAN_UNPOISON_MEMORY_REGION(base, size); +#endif CALL_MUNMAP(base, size); } } static LJ_NOINLINE void *lj_alloc_malloc(void *msp, size_t nsize) { +#if LUAJIT_USE_ASAN_HARDENING + if (nsize == 0) + nsize = MIN_CHUNK_SIZE; + size_t mem_size = nsize; + size_t poison_size = ALIGN_SIZE(nsize, SIZE_ALIGNMENT) + TOTAL_REDZONE_SIZE; + nsize = poison_size; +#endif mstate ms = (mstate)msp; void *mem; size_t nb; @@ -1291,6 +1504,9 @@ static LJ_NOINLINE void *lj_alloc_malloc(void *msp, size_t nsize) unlink_first_small_chunk(ms, b, p, idx); set_inuse_and_pinuse(ms, p, small_index2size(idx)); mem = chunk2mem(p); +#if LUAJIT_USE_ASAN_HARDENING + mem = mark_memory_region(mem2alloc(mem), mem_size, poison_size); +#endif return mem; } else if (nb > ms->dvsize) { if (smallbits != 0) { /* Use chunk in next nonempty smallbin */ @@ -1312,8 +1528,14 @@ static LJ_NOINLINE void *lj_alloc_malloc(void *msp, size_t nsize) replace_dv(ms, r, rsize); } mem = chunk2mem(p); +#if LUAJIT_USE_ASAN_HARDENING + mem = mark_memory_region(mem2alloc(mem), mem_size, poison_size); +#endif return mem; } else if (ms->treemap != 0 && (mem = tmalloc_small(ms, nb)) != 0) { +#if LUAJIT_USE_ASAN_HARDENING + mem = mark_memory_region(mem2alloc(mem), mem_size, poison_size); +#endif return mem; } } @@ -1322,6 +1544,9 @@ static LJ_NOINLINE void *lj_alloc_malloc(void *msp, size_t nsize) } else { nb = pad_request(nsize); if (ms->treemap != 0 && (mem = tmalloc_large(ms, nb)) != 0) { +#if LUAJIT_USE_ASAN_HARDENING + mem = mark_memory_region(mem2alloc(mem), mem_size, poison_size); +#endif return mem; } } @@ -1341,6 +1566,9 @@ static LJ_NOINLINE void *lj_alloc_malloc(void *msp, size_t nsize) set_inuse_and_pinuse(ms, p, dvs); } mem = chunk2mem(p); +#if LUAJIT_USE_ASAN_HARDENING + mem = mark_memory_region(mem2alloc(mem), mem_size, poison_size); +#endif return mem; } else if (nb < ms->topsize) { /* Split top */ size_t rsize = ms->topsize -= nb; @@ -1349,13 +1577,30 @@ static LJ_NOINLINE void *lj_alloc_malloc(void *msp, size_t nsize) r->head = rsize | PINUSE_BIT; set_size_and_pinuse_of_inuse_chunk(ms, p, nb); mem = chunk2mem(p); +#if LUAJIT_USE_ASAN_HARDENING + mem = mark_memory_region(mem2alloc(mem), mem_size, poison_size); +#endif return mem; } +#if LUAJIT_USE_ASAN_HARDENING + return mark_memory_region(mem2alloc(alloc_sys(ms, nb)), mem_size, poison_size); +#else return alloc_sys(ms, nb); +#endif } static LJ_NOINLINE void *lj_alloc_free(void *msp, void *ptr) { +#if LUAJIT_USE_ASAN_HARDENING + if (ptr != 0) { + size_t mem_size = asan_get_size(ptr, MEM_SIZE); + size_t poison_size = asan_get_size(ptr, POISON_SIZE); + + memmove(ptr, ptr, mem_size); + ASAN_POISON_MEMORY_REGION(mem2alloc(ptr), poison_size); + } + return NULL; +#else if (ptr != 0) { mchunkptr p = mem2chunk(ptr); mstate fm = (mstate)msp; @@ -1423,10 +1668,29 @@ static LJ_NOINLINE void *lj_alloc_free(void *msp, void *ptr) } } return NULL; +#endif } static LJ_NOINLINE void *lj_alloc_realloc(void *msp, void *ptr, size_t nsize) { +#if LUAJIT_USE_ASAN_HARDENING + if (nsize >= MAX_REQUEST) + return NULL; + + mstate m = (mstate)msp; + + size_t mem_size = asan_get_size(ptr, MEM_SIZE); + size_t poison_size = asan_get_size(ptr, POISON_SIZE); + + void *newmem = lj_alloc_malloc(m, nsize); + + if (newmem == NULL) + return NULL; + + memcpy(newmem, ptr, nsize > mem_size ? mem_size : nsize); + ASAN_POISON_MEMORY_REGION(mem2alloc(ptr), poison_size); + return newmem; +#else if (nsize >= MAX_REQUEST) { return NULL; } else { @@ -1473,6 +1737,7 @@ static LJ_NOINLINE void *lj_alloc_realloc(void *msp, void *ptr, size_t nsize) return newmem; } } +#endif } void *lj_alloc_f(void *msp, void *ptr, size_t osize, size_t nsize) diff --git a/test/tarantool-c-tests/asan-alloc-instrumentation.test.c b/test/tarantool-c-tests/asan-alloc-instrumentation.test.c new file mode 100644 index 00000000..fbbd387b --- /dev/null +++ b/test/tarantool-c-tests/asan-alloc-instrumentation.test.c @@ -0,0 +1,141 @@ +#include "lua.h" +#include "test.h" +#include "utils.h" +#include "lj_alloc.c" +#include "lj_gc.h" + +#if LUAJIT_USE_ASAN_HARDENING +#include + +#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 global_State *main_GS = NULL; + +static int small_malloc_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 = 30; + void *p = lj_mem_new(main_LS, size); + size_t algn = (MALLOC_ALIGNMENT - size % MALLOC_ALIGNMENT) % MALLOC_ALIGNMENT; + + 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; + + lj_mem_free(main_GS, p, size); + return res == TEST_EXIT_SUCCESS ? TEST_EXIT_SUCCESS : TEST_EXIT_FAILURE; +#endif +} + +static int large_malloc_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 = 1234; + void *p = lj_mem_new(main_LS, size); + size_t algn = (MALLOC_ALIGNMENT - size % MALLOC_ALIGNMENT) % MALLOC_ALIGNMENT; + + 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; + + lj_mem_free(main_GS, p, size); + return res == TEST_EXIT_SUCCESS ? TEST_EXIT_SUCCESS : TEST_EXIT_FAILURE; +#endif +} + +static int free_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 = 1234; + void *p = lj_mem_new(main_LS, size); + size_t algn = (MALLOC_ALIGNMENT - size % MALLOC_ALIGNMENT) % MALLOC_ALIGNMENT; + lj_mem_free(main_GS, p, size); + + if (IS_POISONED_REGION(p - REDZONE_SIZE, TOTAL_REDZONE_SIZE + size + algn)) + { + return TEST_EXIT_SUCCESS; + } + return TEST_EXIT_FAILURE; +#endif +} + +static int realloc_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 = 150; + size_t new_size = size * 2; + void *p = lj_mem_new(main_LS, size); + uint8_t *ptr = (uint8_t *)p; + size_t algn = (MALLOC_ALIGNMENT - size % MALLOC_ALIGNMENT) % MALLOC_ALIGNMENT; + size_t new_algn = (MALLOC_ALIGNMENT - new_size % MALLOC_ALIGNMENT) % MALLOC_ALIGNMENT; + + for (size_t i = 0; i < size; ++i) + { + ptr[i] = i; + } + + void *newptr = lj_mem_realloc(main_LS, p, size, new_size); + + if (IS_POISONED_REGION(ptr - REDZONE_SIZE, TOTAL_REDZONE_SIZE + size + algn)) + { + ASAN_UNPOISON_MEMORY_REGION(ptr, size); + if (memcmp(ptr, newptr, size) != 0) + res = TEST_EXIT_FAILURE; + + if (IS_POISONED_REGION(newptr - REDZONE_SIZE, REDZONE_SIZE) && + !IS_POISONED_REGION(newptr, new_size) && + IS_POISONED_REGION(newptr + new_size, new_algn + REDZONE_SIZE)) + res = TEST_EXIT_SUCCESS; + } + lj_mem_free(main_GS, newptr, new_size); + return res == TEST_EXIT_SUCCESS ? TEST_EXIT_SUCCESS : TEST_EXIT_FAILURE; +#endif +} + +int main(void) +{ + lua_State *L = utils_lua_init(); + global_State *g = G(L); + main_LS = L; + main_GS = g; + + const struct test_unit tgroup[] = { + test_unit_def(small_malloc_test), + test_unit_def(large_malloc_test), + test_unit_def(free_test), + test_unit_def(realloc_test), + }; + + const int test_result = test_run_group(tgroup, L); + utils_lua_close(L); + return test_result; +} \ No newline at end of file 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 00000000..9eabadfb --- /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" + +#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; +} diff --git a/test/tarantool-tests/ASAN-left-memory-miss.test.lua b/test/tarantool-tests/ASAN-left-memory-miss.test.lua new file mode 100644 index 00000000..94673a88 --- /dev/null +++ b/test/tarantool-tests/ASAN-left-memory-miss.test.lua @@ -0,0 +1,12 @@ +local tap = require('tap') +local asan_hardening = os.getenv("LJ_ASAN_HARDENING") +local test = tap.test('asan-left-memory-miss'):skipcond({ + ['Test requires ASAN-HARDENING enabled'] = asan_hardening == 'OFF', +}) +test:plan(1) + +local script = require('utils').exec.makecmd(arg, { redirect = '2>&1' }) +local output, status = script() + +test:ok(status == 1, output) +test:done(true) \ No newline at end of file diff --git a/test/tarantool-tests/ASAN-left-memory-miss/script.lua b/test/tarantool-tests/ASAN-left-memory-miss/script.lua new file mode 100644 index 00000000..45cfad6e --- /dev/null +++ b/test/tarantool-tests/ASAN-left-memory-miss/script.lua @@ -0,0 +1,19 @@ +local ffi = require('ffi') + +ffi.cdef[[ +typedef struct { + uint8_t a; + uint8_t b; +} TestStruct; +]] + +local obj = ffi.new("TestStruct") +local intPtr = ffi.cast("uint64_t *", obj) + +-- mem dump: ... RZ 00 00 02 RZ ... \\ RZ - redzone +-- ^ ^ +-- | | +-- ptr[-3] ptr + +-- error (left RZ) +print(intPtr[-3]) \ No newline at end of file diff --git a/test/tarantool-tests/ASAN-right-memory-miss.test.lua b/test/tarantool-tests/ASAN-right-memory-miss.test.lua new file mode 100644 index 00000000..965a576b --- /dev/null +++ b/test/tarantool-tests/ASAN-right-memory-miss.test.lua @@ -0,0 +1,12 @@ +local tap = require('tap') +local asan_hardening = os.getenv("LJ_ASAN_HARDENING") +local test = tap.test('asan-right-memory-miss'):skipcond({ + ['Test requires ASAN-HARDENING enabled'] = asan_hardening == 'OFF', +}) +test:plan(1) + +local script = require('utils').exec.makecmd(arg, { redirect = '2>&1' }) +local output, status = script() + +test:ok(status == 1, output) +test:done(true) \ No newline at end of file diff --git a/test/tarantool-tests/ASAN-right-memory-miss/script.lua b/test/tarantool-tests/ASAN-right-memory-miss/script.lua new file mode 100644 index 00000000..a0133524 --- /dev/null +++ b/test/tarantool-tests/ASAN-right-memory-miss/script.lua @@ -0,0 +1,19 @@ +local ffi = require('ffi') + +ffi.cdef[[ +typedef struct { + uint8_t a; + uint8_t b; +} TestStruct; +]] + +local obj = ffi.new("TestStruct") +local intPtr = ffi.cast("uint64_t *", obj) + +-- mem dump: ... RZ 00 00 02 RZ ... \\ RZ - redzone +-- ^ +-- | +-- ptr + +-- error (right RZ) +print(intPtr[2]) \ No newline at end of file diff --git a/test/tarantool-tests/ASAN-use-after-free.test.lua b/test/tarantool-tests/ASAN-use-after-free.test.lua new file mode 100644 index 00000000..413e4a57 --- /dev/null +++ b/test/tarantool-tests/ASAN-use-after-free.test.lua @@ -0,0 +1,12 @@ +local tap = require('tap') +local asan_hardening = os.getenv("LJ_ASAN_HARDENING") +local test = tap.test('asan-use-after-free'):skipcond({ + ['Test requires ASAN-HARDENING enabled'] = asan_hardening == 'OFF', +}) +test:plan(1) + +local script = require('utils').exec.makecmd(arg, { redirect = '2>&1' }) +local output, status = script() + +test:ok(status == 1, output) +test:done(true) \ No newline at end of file diff --git a/test/tarantool-tests/ASAN-use-after-free/script.lua b/test/tarantool-tests/ASAN-use-after-free/script.lua new file mode 100644 index 00000000..ffbdaf79 --- /dev/null +++ b/test/tarantool-tests/ASAN-use-after-free/script.lua @@ -0,0 +1,24 @@ +local ffi = require('ffi') + +ffi.cdef[[ +typedef struct { + uint8_t a; + uint8_t b; +} TestStruct; +]] + +local obj = ffi.new("TestStruct") +obj = ffi.gc(obj, nil) +local intPtr = ffi.cast("uint8_t *", obj) +obj = nil +collectgarbage("collect") +print(obj) + +-- after free +-- mem dump: ... RZ RZ RZ RZ RZ ... \\ RZ - redzone +-- ^ +-- | +-- ptr + +-- error +print(intPtr[0]) diff --git a/test/tarantool-tests/CMakeLists.txt b/test/tarantool-tests/CMakeLists.txt index e3750bf3..a1159ec2 100644 --- a/test/tarantool-tests/CMakeLists.txt +++ b/test/tarantool-tests/CMakeLists.txt @@ -130,7 +130,7 @@ foreach(test_path ${tests}) # LUA_CPATH and LD_LIBRARY_PATH variables and also # dependencies list with libraries are set in scope of # BuildTestLib macro. - ENVIRONMENT "LUA_PATH=${LUA_PATH};LUA_CPATH=${LUA_CPATH};${LUA_TEST_ENV_MORE}" + ENVIRONMENT "LUA_PATH=${LUA_PATH};LUA_CPATH=${LUA_CPATH};${LUA_TEST_ENV_MORE};LJ_ASAN_HARDENING=${LUAJIT_USE_ASAN_HARDENING}" LABELS ${TEST_SUITE_NAME} DEPENDS tarantool-tests-deps ) diff --git a/test/tarantool-tests/lj-1034-tabov-error-frame.test.lua b/test/tarantool-tests/lj-1034-tabov-error-frame.test.lua index 0e23fdb2..6925f0ab 100644 --- a/test/tarantool-tests/lj-1034-tabov-error-frame.test.lua +++ b/test/tarantool-tests/lj-1034-tabov-error-frame.test.lua @@ -1,9 +1,11 @@ local tap = require('tap') local ffi = require('ffi') +local asan_hardening = os.getenv("LJ_ASAN_HARDENING") local test = tap.test('lj-1034-tabov-error-frame'):skipcond({ ['Test requires JIT enabled'] = not jit.status(), ['Test requires GC64 mode enabled'] = not ffi.abi('gc64'), ['Disabled on MacOS due to #8652'] = jit.os == 'OSX', + ['Test requires ASAN-HARDENING disabled'] = asan_hardening == 'ON', }) -- XXX: The test for the problem uses the table of GC -- 2.34.1