Skip to content

Commit 70dd008

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 bd0f10a commit 70dd008

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
@@ -80,6 +80,28 @@ endif
8080
ENABLE_ARCH_TEST ?= 0
8181
$(call set-feature, ARCH_TEST)
8282

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

357+
# ThreadSanitizer flags (ENABLE_TSAN is set earlier to override SDL/FULL4G)
358+
ifeq ("$(ENABLE_TSAN)", "1")
359+
CFLAGS += -fsanitize=thread -g
360+
LDFLAGS += -fsanitize=thread
361+
endif
362+
335363
$(OUT)/emulate.o: CFLAGS += -foptimize-sibling-calls -fomit-frame-pointer -fno-stack-check -fno-stack-protector
336364

337365
# .DEFAULT_GOAL should be set to all since the very first target is not all
@@ -445,7 +473,7 @@ define check-test
445473
$(Q)true; \
446474
$(PRINTF) "Running $(3) ... "; \
447475
OUTPUT_FILE="$$(mktemp)"; \
448-
if (LC_ALL=C $(BIN) $(1) $(2) > "$$OUTPUT_FILE") && \
476+
if (LC_ALL=C $(BIN_WRAPPER) $(BIN) $(1) $(2) > "$$OUTPUT_FILE") && \
449477
[ "$$(cat "$$OUTPUT_FILE" | $(LOG_FILTER) | $(4))" = "$(5)" ]; then \
450478
$(call notice, [OK]); \
451479
else \

src/emulate.c

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ static block_t *block_alloc(riscv_t *rv)
304304
block->hot2 = false;
305305
block->has_loops = false;
306306
block->n_invoke = 0;
307+
block->func = NULL;
307308
INIT_LIST_HEAD(&block->list);
308309
#if RV32_HAS(T2C)
309310
block->compiled = false;
@@ -1176,22 +1177,32 @@ void rv_step(void *arg)
11761177
#if RV32_HAS(JIT)
11771178
#if RV32_HAS(T2C)
11781179
/* executed through the tier-2 JIT compiler */
1179-
if (block->hot2) {
1180+
/* Use acquire semantics to ensure we see func write before using it */
1181+
if (__atomic_load_n(&block->hot2, __ATOMIC_ACQUIRE)) {
11801182
((exec_t2c_func_t) block->func)(rv);
11811183
prev = NULL;
11821184
continue;
11831185
} /* check if invoking times of t1 generated code exceed threshold */
1184-
else if (!block->compiled && block->n_invoke >= THRESHOLD) {
1185-
block->compiled = true;
1186+
else if (!__atomic_load_n(&block->compiled, __ATOMIC_RELAXED) &&
1187+
__atomic_load_n(&block->n_invoke, __ATOMIC_RELAXED) >=
1188+
THRESHOLD) {
1189+
__atomic_store_n(&block->compiled, true, __ATOMIC_RELAXED);
11861190
queue_entry_t *entry = malloc(sizeof(queue_entry_t));
11871191
if (unlikely(!entry)) {
11881192
/* Malloc failed - reset compiled flag to allow retry later */
1189-
block->compiled = false;
1193+
__atomic_store_n(&block->compiled, false, __ATOMIC_RELAXED);
11901194
continue;
11911195
}
1192-
entry->block = block;
1196+
/* Store cache key instead of pointer to prevent use-after-free */
1197+
#if RV32_HAS(SYSTEM)
1198+
entry->key =
1199+
(uint64_t) block->pc_start | ((uint64_t) block->satp << 32);
1200+
#else
1201+
entry->key = (uint64_t) block->pc_start;
1202+
#endif
11931203
pthread_mutex_lock(&rv->wait_queue_lock);
11941204
list_add(&entry->list, &rv->wait_queue);
1205+
pthread_cond_signal(&rv->wait_queue_cond);
11951206
pthread_mutex_unlock(&rv->wait_queue_lock);
11961207
}
11971208
#endif
@@ -1203,7 +1214,11 @@ void rv_step(void *arg)
12031214
* entry in compiled binary buffer.
12041215
*/
12051216
if (block->hot) {
1217+
#if RV32_HAS(T2C)
1218+
__atomic_fetch_add(&block->n_invoke, 1, __ATOMIC_RELAXED);
1219+
#else
12061220
block->n_invoke++;
1221+
#endif
12071222
((exec_block_func_t) state->buf)(
12081223
rv, (uintptr_t) (state->buf + block->offset));
12091224
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
@@ -777,6 +799,7 @@ riscv_t *rv_create(riscv_user_t rv_attr)
777799
/* prepare wait queue. */
778800
pthread_mutex_init(&rv->wait_queue_lock, NULL);
779801
pthread_mutex_init(&rv->cache_lock, NULL);
802+
pthread_cond_init(&rv->wait_queue_cond, NULL);
780803
INIT_LIST_HEAD(&rv->wait_queue);
781804
/* activate the background compilation thread. */
782805
pthread_create(&t2c_thread, NULL, t2c_runloop, rv);
@@ -910,10 +933,24 @@ void rv_delete(riscv_t *rv)
910933
block_map_destroy(rv);
911934
#else
912935
#if RV32_HAS(T2C)
936+
/* Signal the thread to quit */
937+
pthread_mutex_lock(&rv->wait_queue_lock);
913938
rv->quit = true;
939+
pthread_cond_signal(&rv->wait_queue_cond);
940+
pthread_mutex_unlock(&rv->wait_queue_lock);
941+
914942
pthread_join(t2c_thread, NULL);
943+
944+
/* Clean up any remaining entries in wait queue */
945+
queue_entry_t *entry, *safe;
946+
list_for_each_entry_safe (entry, safe, &rv->wait_queue, list) {
947+
list_del(&entry->list);
948+
free(entry);
949+
}
950+
915951
pthread_mutex_destroy(&rv->wait_queue_lock);
916952
pthread_mutex_destroy(&rv->cache_lock);
953+
pthread_cond_destroy(&rv->wait_queue_cond);
917954
jit_cache_exit(rv->jit_cache);
918955
#endif
919956
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)