Skip to content

Commit 3bdcec5

Browse files
committed
Enable TSAN with FULL4G and T2C support
ThreadSanitizer (TSAN) can now detect race conditions across the entire multi-threaded JIT pipeline with full 4GB address space emulation. This enables testing of the tier-2 LLVM compilation thread while maintaining production memory layout. Memory Layout (TSAN-compatible): - Main memory: MAP_FIXED at 0x7d0000000000 (4GB) - JIT buffer: MAP_FIXED at 0x7d1000000000 - Both allocations within TSAN app range (0x7cf-0x7ff trillion) - Prevents conflicts with TSAN shadow memory (0x02a-0x7ce trillion) ASLR Mitigation: - Added setarch -R wrapper for TSAN test execution - Disables ASLR to prevent random allocations in shadow memory - Only affects test runs, not production builds SDL Conflict Resolution: - SDL (uninstrumented system library) creates threads TSAN cannot track - Disabled SDL when TSAN enabled to focus on built-in race detection - Production builds still fully support SDL
1 parent 65a1ec6 commit 3bdcec5

File tree

8 files changed

+165
-21
lines changed

8 files changed

+165
-21
lines changed

Makefile

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ endif
7373
ENABLE_ARCH_TEST ?= 0
7474
$(call set-feature, ARCH_TEST)
7575

76+
# ThreadSanitizer support
77+
# TSAN on x86-64 memory layout:
78+
# Shadow: 0x02a000000000 - 0x7cefffffffff (reserved by TSAN)
79+
# App: 0x7cf000000000 - 0x7ffffffff000 (usable by application)
80+
#
81+
# We use MAP_FIXED to allocate FULL4G's 4GB memory at a fixed address
82+
# (0x7d0000000000) within TSAN's app range, ensuring compatibility.
83+
#
84+
# IMPORTANT: TSAN requires ASLR (Address Space Layout Randomization) to be
85+
# disabled to prevent system allocations from landing in TSAN's shadow memory.
86+
# Tests are run with 'setarch $(uname -m) -R' to disable ASLR.
87+
ENABLE_TSAN ?= 0
88+
ifeq ("$(ENABLE_TSAN)", "1")
89+
override ENABLE_SDL := 0 # SDL (uninstrumented system lib) creates threads TSAN cannot track
90+
override ENABLE_LTO := 0 # LTO interferes with TSAN instrumentation
91+
CFLAGS += -DTSAN_ENABLED # Signal code to use TSAN-compatible allocations
92+
# Disable ASLR for TSAN tests to prevent allocations in TSAN shadow memory
93+
BIN_WRAPPER = setarch $(shell uname -m) -R
94+
else
95+
BIN_WRAPPER =
96+
endif
97+
7698
# Enable link-time optimization (LTO)
7799
ENABLE_LTO ?= 1
78100
ifeq ($(call has, LTO), 1)
@@ -281,6 +303,12 @@ CFLAGS += -fsanitize=undefined -fno-sanitize=alignment -fno-sanitize-recover=all
281303
LDFLAGS += -fsanitize=undefined -fno-sanitize=alignment -fno-sanitize-recover=all
282304
endif
283305

306+
# ThreadSanitizer flags (ENABLE_TSAN is set earlier to override SDL/FULL4G)
307+
ifeq ("$(ENABLE_TSAN)", "1")
308+
CFLAGS += -fsanitize=thread -g
309+
LDFLAGS += -fsanitize=thread
310+
endif
311+
284312
$(OUT)/emulate.o: CFLAGS += -foptimize-sibling-calls -fomit-frame-pointer -fno-stack-check -fno-stack-protector
285313

286314
# .DEFAULT_GOAL should be set to all since the very first target is not all
@@ -375,7 +403,7 @@ define check-test
375403
$(Q)true; \
376404
$(PRINTF) "Running $(3) ... "; \
377405
OUTPUT_FILE="$$(mktemp)"; \
378-
if (LC_ALL=C $(BIN) $(1) $(2) > "$$OUTPUT_FILE") && \
406+
if (LC_ALL=C $(BIN_WRAPPER) $(BIN) $(1) $(2) > "$$OUTPUT_FILE") && \
379407
[ "$$(cat "$$OUTPUT_FILE" | $(LOG_FILTER) | $(4))" = "$(5)" ]; then \
380408
$(call notice, [OK]); \
381409
else \

src/emulate.c

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ static block_t *block_alloc(riscv_t *rv)
283283
block->hot2 = false;
284284
block->has_loops = false;
285285
block->n_invoke = 0;
286+
block->func = NULL;
286287
INIT_LIST_HEAD(&block->list);
287288
#if RV32_HAS(T2C)
288289
block->compiled = false;
@@ -1151,22 +1152,32 @@ void rv_step(void *arg)
11511152
#if RV32_HAS(JIT)
11521153
#if RV32_HAS(T2C)
11531154
/* executed through the tier-2 JIT compiler */
1154-
if (block->hot2) {
1155+
/* Use acquire semantics to ensure we see func write before using it */
1156+
if (__atomic_load_n(&block->hot2, __ATOMIC_ACQUIRE)) {
11551157
((exec_t2c_func_t) block->func)(rv);
11561158
prev = NULL;
11571159
continue;
11581160
} /* check if invoking times of t1 generated code exceed threshold */
1159-
else if (!block->compiled && block->n_invoke >= THRESHOLD) {
1160-
block->compiled = true;
1161+
else if (!__atomic_load_n(&block->compiled, __ATOMIC_RELAXED) &&
1162+
__atomic_load_n(&block->n_invoke, __ATOMIC_RELAXED) >=
1163+
THRESHOLD) {
1164+
__atomic_store_n(&block->compiled, true, __ATOMIC_RELAXED);
11611165
queue_entry_t *entry = malloc(sizeof(queue_entry_t));
11621166
if (unlikely(!entry)) {
11631167
/* Malloc failed - reset compiled flag to allow retry later */
1164-
block->compiled = false;
1168+
__atomic_store_n(&block->compiled, false, __ATOMIC_RELAXED);
11651169
continue;
11661170
}
1167-
entry->block = block;
1171+
/* Store cache key instead of pointer to prevent use-after-free */
1172+
#if RV32_HAS(SYSTEM)
1173+
entry->key =
1174+
(uint64_t) block->pc_start | ((uint64_t) block->satp << 32);
1175+
#else
1176+
entry->key = (uint64_t) block->pc_start;
1177+
#endif
11681178
pthread_mutex_lock(&rv->wait_queue_lock);
11691179
list_add(&entry->list, &rv->wait_queue);
1180+
pthread_cond_signal(&rv->wait_queue_cond);
11701181
pthread_mutex_unlock(&rv->wait_queue_lock);
11711182
}
11721183
#endif
@@ -1178,7 +1189,11 @@ void rv_step(void *arg)
11781189
* entry in compiled binary buffer.
11791190
*/
11801191
if (block->hot) {
1192+
#if RV32_HAS(T2C)
1193+
__atomic_fetch_add(&block->n_invoke, 1, __ATOMIC_RELAXED);
1194+
#else
11811195
block->n_invoke++;
1196+
#endif
11821197
((exec_block_func_t) state->buf)(
11831198
rv, (uintptr_t) (state->buf + block->offset));
11841199
prev = NULL;

src/io.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,33 @@ memory_t *memory_new(uint32_t size)
2727
return NULL;
2828
assert(mem);
2929
#if HAVE_MMAP
30+
#if defined(TSAN_ENABLED) && defined(__x86_64__)
31+
/* ThreadSanitizer compatibility: Use MAP_FIXED to allocate at a specific
32+
* address within TSAN's app range (0x7cf000000000 - 0x7ffffffff000).
33+
*
34+
* Fixed address: 0x7d0000000000
35+
* Size: up to 4GB (0x100000000)
36+
* End: 0x7d0100000000 (well within app range)
37+
*
38+
* This guarantees the allocation won't land in TSAN's shadow memory,
39+
* preventing "unexpected memory mapping" errors.
40+
*/
41+
void *fixed_addr = (void *) 0x7d0000000000UL;
42+
data_memory_base = mmap(fixed_addr, size, PROT_READ | PROT_WRITE,
43+
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
44+
if (data_memory_base == MAP_FAILED) {
45+
free(mem);
46+
return NULL;
47+
}
48+
#else
49+
/* Standard allocation without TSAN */
3050
data_memory_base = mmap(NULL, size, PROT_READ | PROT_WRITE,
3151
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
3252
if (data_memory_base == MAP_FAILED) {
3353
free(mem);
3454
return NULL;
3555
}
56+
#endif
3657
#else
3758
data_memory_base = malloc(size);
3859
if (!data_memory_base) {

src/jit.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,6 +2336,25 @@ struct jit_state *jit_state_init(size_t size)
23362336

23372337
state->offset = 0;
23382338
state->size = size;
2339+
#if defined(TSAN_ENABLED) && defined(__x86_64__)
2340+
/* ThreadSanitizer compatibility: Allocate JIT code buffer at a fixed
2341+
* address above the main memory region to avoid conflicts.
2342+
*
2343+
* Main memory: 0x7d0000000000 - 0x7d0100000000 (4GB for FULL4G)
2344+
* JIT buffer: 0x7d1000000000 + size
2345+
*
2346+
* This keeps both allocations in TSAN's app range (0x7cf000000000 -
2347+
* 0x7ffffffff000) and prevents overlap with main memory or TSAN shadow.
2348+
*/
2349+
void *jit_addr = (void *) 0x7d1000000000UL;
2350+
state->buf = mmap(jit_addr, size, PROT_READ | PROT_WRITE | PROT_EXEC,
2351+
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
2352+
if (state->buf == MAP_FAILED) {
2353+
free(state);
2354+
return NULL;
2355+
}
2356+
#else
2357+
/* Standard allocation without TSAN */
23392358
state->buf = mmap(0, size, PROT_READ | PROT_WRITE | PROT_EXEC,
23402359
MAP_PRIVATE | MAP_ANONYMOUS
23412360
#if defined(__APPLE__)
@@ -2347,8 +2366,7 @@ struct jit_state *jit_state_init(size_t size)
23472366
free(state);
23482367
return NULL;
23492368
}
2350-
assert(state->buf != MAP_FAILED);
2351-
2369+
#endif
23522370
state->n_blocks = 0;
23532371
set_reset(&state->set);
23542372
reset_reg();

src/main.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,28 @@
1919
#include "riscv.h"
2020
#include "utils.h"
2121

22+
/* ThreadSanitizer configuration for FULL4G compatibility
23+
*
24+
* We use MAP_FIXED to allocate emulated memory at 0x7d0000000000, which is
25+
* within TSAN's application memory range (0x7cf000000000 - 0x7ffffffff000).
26+
* This avoids conflicts with TSAN's shadow memory and allows race detection
27+
* to work with FULL4G's 4GB address space.
28+
*
29+
* Configuration optimizes for race detection with minimal overhead.
30+
*/
31+
#if defined(__SANITIZE_THREAD__)
32+
const char *__tsan_default_options()
33+
{
34+
return "halt_on_error=0" /* Continue after errors */
35+
":report_bugs=1" /* Report data races */
36+
":second_deadlock_stack=1" /* Full deadlock info */
37+
":verbosity=0" /* Reduce noise */
38+
":memory_limit_mb=0" /* No memory limit */
39+
":history_size=7" /* Larger race detection window */
40+
":io_sync=0"; /* Don't sync on I/O */
41+
}
42+
#endif
43+
2244
/* enable program trace mode */
2345
#if !RV32_HAS(SYSTEM) || (RV32_HAS(SYSTEM) && RV32_HAS(ELF_LOADER))
2446
static bool opt_trace = false;

src/riscv.c

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,41 @@ static pthread_t t2c_thread;
206206
static void *t2c_runloop(void *arg)
207207
{
208208
riscv_t *rv = (riscv_t *) arg;
209+
pthread_mutex_lock(&rv->wait_queue_lock);
209210
while (!rv->quit) {
210-
if (!list_empty(&rv->wait_queue)) {
211-
queue_entry_t *entry =
212-
list_last_entry(&rv->wait_queue, queue_entry_t, list);
213-
pthread_mutex_lock(&rv->wait_queue_lock);
214-
list_del_init(&entry->list);
215-
pthread_mutex_unlock(&rv->wait_queue_lock);
216-
pthread_mutex_lock(&rv->cache_lock);
217-
t2c_compile(rv, entry->block);
218-
pthread_mutex_unlock(&rv->cache_lock);
219-
free(entry);
220-
}
211+
/* Wait for work or quit signal */
212+
while (list_empty(&rv->wait_queue) && !rv->quit)
213+
pthread_cond_wait(&rv->wait_queue_cond, &rv->wait_queue_lock);
214+
215+
if (rv->quit)
216+
break;
217+
218+
/* Extract work item while holding the lock */
219+
queue_entry_t *entry =
220+
list_last_entry(&rv->wait_queue, queue_entry_t, list);
221+
list_del_init(&entry->list);
222+
pthread_mutex_unlock(&rv->wait_queue_lock);
223+
224+
/* Perform compilation with cache lock */
225+
pthread_mutex_lock(&rv->cache_lock);
226+
/* Look up block from cache using the key (might have been evicted) */
227+
uint32_t pc = (uint32_t) entry->key;
228+
block_t *block = (block_t *) cache_get(rv->block_cache, pc, false);
229+
#if RV32_HAS(SYSTEM)
230+
/* Verify SATP matches (for system mode) */
231+
uint32_t satp = (uint32_t) (entry->key >> 32);
232+
if (block && block->satp != satp)
233+
block = NULL;
234+
#endif
235+
/* Compile only if block still exists in cache */
236+
if (block)
237+
t2c_compile(rv, block);
238+
pthread_mutex_unlock(&rv->cache_lock);
239+
free(entry);
240+
241+
pthread_mutex_lock(&rv->wait_queue_lock);
221242
}
243+
pthread_mutex_unlock(&rv->wait_queue_lock);
222244
return NULL;
223245
}
224246
#endif
@@ -745,6 +767,7 @@ riscv_t *rv_create(riscv_user_t rv_attr)
745767
/* prepare wait queue. */
746768
pthread_mutex_init(&rv->wait_queue_lock, NULL);
747769
pthread_mutex_init(&rv->cache_lock, NULL);
770+
pthread_cond_init(&rv->wait_queue_cond, NULL);
748771
INIT_LIST_HEAD(&rv->wait_queue);
749772
/* activate the background compilation thread. */
750773
pthread_create(&t2c_thread, NULL, t2c_runloop, rv);
@@ -866,10 +889,24 @@ void rv_delete(riscv_t *rv)
866889
block_map_destroy(rv);
867890
#else
868891
#if RV32_HAS(T2C)
892+
/* Signal the thread to quit */
893+
pthread_mutex_lock(&rv->wait_queue_lock);
869894
rv->quit = true;
895+
pthread_cond_signal(&rv->wait_queue_cond);
896+
pthread_mutex_unlock(&rv->wait_queue_lock);
897+
870898
pthread_join(t2c_thread, NULL);
899+
900+
/* Clean up any remaining entries in wait queue */
901+
queue_entry_t *entry, *safe;
902+
list_for_each_entry_safe (entry, safe, &rv->wait_queue, list) {
903+
list_del(&entry->list);
904+
free(entry);
905+
}
906+
871907
pthread_mutex_destroy(&rv->wait_queue_lock);
872908
pthread_mutex_destroy(&rv->cache_lock);
909+
pthread_cond_destroy(&rv->wait_queue_cond);
873910
jit_cache_exit(rv->jit_cache);
874911
#endif
875912
jit_state_exit(rv->jit_state);

src/riscv_private.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ typedef struct block {
105105

106106
#if RV32_HAS(JIT) && RV32_HAS(T2C)
107107
typedef struct {
108-
block_t *block;
108+
uint64_t key; /**< cache key (PC or PC|SATP) to look up block */
109109
struct list_head list;
110110
} queue_entry_t;
111111
#endif
@@ -197,6 +197,7 @@ struct riscv_internal {
197197
#if RV32_HAS(T2C)
198198
struct list_head wait_queue;
199199
pthread_mutex_t wait_queue_lock, cache_lock;
200+
pthread_cond_t wait_queue_cond;
200201
volatile bool quit; /**< Determine the main thread is terminated or not */
201202
#endif
202203
void *jit_state;

src/t2c.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,9 @@ void t2c_compile(riscv_t *rv, block_t *block)
346346

347347
jit_cache_update(rv->jit_cache, key, block->func);
348348

349-
block->hot2 = true;
349+
/* Use release semantics to ensure func write is visible before hot2 is set
350+
*/
351+
__atomic_store_n(&block->hot2, true, __ATOMIC_RELEASE);
350352
}
351353

352354
struct jit_cache *jit_cache_init()

0 commit comments

Comments
 (0)