Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ endif
ENABLE_ARCH_TEST ?= 0
$(call set-feature, ARCH_TEST)

# ThreadSanitizer support
# TSAN on x86-64 memory layout:
# Shadow: 0x02a000000000 - 0x7cefffffffff (reserved by TSAN)
# App: 0x7cf000000000 - 0x7ffffffff000 (usable by application)
#
# We use MAP_FIXED to allocate FULL4G's 4GB memory at a fixed address
# (0x7d0000000000) within TSAN's app range, ensuring compatibility.
#
# IMPORTANT: TSAN requires ASLR (Address Space Layout Randomization) to be
# disabled to prevent system allocations from landing in TSAN's shadow memory.
# Tests are run with 'setarch $(uname -m) -R' to disable ASLR.
ENABLE_TSAN ?= 0
ifeq ("$(ENABLE_TSAN)", "1")
override ENABLE_SDL := 0 # SDL (uninstrumented system lib) creates threads TSAN cannot track
override ENABLE_LTO := 0 # LTO interferes with TSAN instrumentation
CFLAGS += -DTSAN_ENABLED # Signal code to use TSAN-compatible allocations
# Disable ASLR for TSAN tests to prevent allocations in TSAN shadow memory
BIN_WRAPPER = setarch $(shell uname -m) -R
else
BIN_WRAPPER =
endif

# Enable link-time optimization (LTO)
ENABLE_LTO ?= 1
ifeq ($(call has, LTO), 1)
Expand Down Expand Up @@ -281,6 +303,12 @@ CFLAGS += -fsanitize=undefined -fno-sanitize=alignment -fno-sanitize-recover=all
LDFLAGS += -fsanitize=undefined -fno-sanitize=alignment -fno-sanitize-recover=all
endif

# ThreadSanitizer flags (ENABLE_TSAN is set earlier to override SDL/FULL4G)
ifeq ("$(ENABLE_TSAN)", "1")
CFLAGS += -fsanitize=thread -g
LDFLAGS += -fsanitize=thread
endif

$(OUT)/emulate.o: CFLAGS += -foptimize-sibling-calls -fomit-frame-pointer -fno-stack-check -fno-stack-protector

# .DEFAULT_GOAL should be set to all since the very first target is not all
Expand Down Expand Up @@ -375,7 +403,7 @@ define check-test
$(Q)true; \
$(PRINTF) "Running $(3) ... "; \
OUTPUT_FILE="$$(mktemp)"; \
if (LC_ALL=C $(BIN) $(1) $(2) > "$$OUTPUT_FILE") && \
if (LC_ALL=C $(BIN_WRAPPER) $(BIN) $(1) $(2) > "$$OUTPUT_FILE") && \
[ "$$(cat "$$OUTPUT_FILE" | $(LOG_FILTER) | $(4))" = "$(5)" ]; then \
$(call notice, [OK]); \
else \
Expand Down
25 changes: 20 additions & 5 deletions src/emulate.c
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ static block_t *block_alloc(riscv_t *rv)
block->hot2 = false;
block->has_loops = false;
block->n_invoke = 0;
block->func = NULL;
INIT_LIST_HEAD(&block->list);
#if RV32_HAS(T2C)
block->compiled = false;
Expand Down Expand Up @@ -1151,22 +1152,32 @@ void rv_step(void *arg)
#if RV32_HAS(JIT)
#if RV32_HAS(T2C)
/* executed through the tier-2 JIT compiler */
if (block->hot2) {
/* Use acquire semantics to ensure we see func write before using it */
if (__atomic_load_n(&block->hot2, __ATOMIC_ACQUIRE)) {
((exec_t2c_func_t) block->func)(rv);
prev = NULL;
continue;
} /* check if invoking times of t1 generated code exceed threshold */
else if (!block->compiled && block->n_invoke >= THRESHOLD) {
block->compiled = true;
else if (!__atomic_load_n(&block->compiled, __ATOMIC_RELAXED) &&
__atomic_load_n(&block->n_invoke, __ATOMIC_RELAXED) >=
THRESHOLD) {
__atomic_store_n(&block->compiled, true, __ATOMIC_RELAXED);
queue_entry_t *entry = malloc(sizeof(queue_entry_t));
if (unlikely(!entry)) {
/* Malloc failed - reset compiled flag to allow retry later */
block->compiled = false;
__atomic_store_n(&block->compiled, false, __ATOMIC_RELAXED);
continue;
}
entry->block = block;
/* Store cache key instead of pointer to prevent use-after-free */
#if RV32_HAS(SYSTEM)
entry->key =
(uint64_t) block->pc_start | ((uint64_t) block->satp << 32);
#else
entry->key = (uint64_t) block->pc_start;
#endif
pthread_mutex_lock(&rv->wait_queue_lock);
list_add(&entry->list, &rv->wait_queue);
pthread_cond_signal(&rv->wait_queue_cond);
pthread_mutex_unlock(&rv->wait_queue_lock);
}
#endif
Expand All @@ -1178,7 +1189,11 @@ void rv_step(void *arg)
* entry in compiled binary buffer.
*/
if (block->hot) {
#if RV32_HAS(T2C)
__atomic_fetch_add(&block->n_invoke, 1, __ATOMIC_RELAXED);
#else
block->n_invoke++;
#endif
((exec_block_func_t) state->buf)(
rv, (uintptr_t) (state->buf + block->offset));
prev = NULL;
Expand Down
21 changes: 21 additions & 0 deletions src/io.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,33 @@ memory_t *memory_new(uint32_t size)
return NULL;
assert(mem);
#if HAVE_MMAP
#if defined(TSAN_ENABLED) && defined(__x86_64__)
/* ThreadSanitizer compatibility: Use MAP_FIXED to allocate at a specific
* address within TSAN's app range (0x7cf000000000 - 0x7ffffffff000).
*
* Fixed address: 0x7d0000000000
* Size: up to 4GB (0x100000000)
* End: 0x7d0100000000 (well within app range)
*
* This guarantees the allocation won't land in TSAN's shadow memory,
* preventing "unexpected memory mapping" errors.
*/
void *fixed_addr = (void *) 0x7d0000000000UL;
data_memory_base = mmap(fixed_addr, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
if (data_memory_base == MAP_FAILED) {
free(mem);
return NULL;
}
#else
/* Standard allocation without TSAN */
data_memory_base = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (data_memory_base == MAP_FAILED) {
free(mem);
return NULL;
}
#endif
#else
data_memory_base = malloc(size);
if (!data_memory_base) {
Expand Down
22 changes: 20 additions & 2 deletions src/jit.c
Original file line number Diff line number Diff line change
Expand Up @@ -2336,6 +2336,25 @@ struct jit_state *jit_state_init(size_t size)

state->offset = 0;
state->size = size;
#if defined(TSAN_ENABLED) && defined(__x86_64__)
/* ThreadSanitizer compatibility: Allocate JIT code buffer at a fixed
* address above the main memory region to avoid conflicts.
*
* Main memory: 0x7d0000000000 - 0x7d0100000000 (4GB for FULL4G)
* JIT buffer: 0x7d1000000000 + size
*
* This keeps both allocations in TSAN's app range (0x7cf000000000 -
* 0x7ffffffff000) and prevents overlap with main memory or TSAN shadow.
*/
void *jit_addr = (void *) 0x7d1000000000UL;
state->buf = mmap(jit_addr, size, PROT_READ | PROT_WRITE | PROT_EXEC,
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When TSAN is enabled on macOS x86_64, this mmap call loses the MAP_JIT flag that the standard path uses, so hardened macOS failures return MAP_FAILED and the JIT never initializes. Please keep MAP_JIT on macOS even in the TSAN path.

Prompt for AI agents
Address the following comment on src/jit.c at line 2350:

<comment>When TSAN is enabled on macOS x86_64, this mmap call loses the MAP_JIT flag that the standard path uses, so hardened macOS failures return MAP_FAILED and the JIT never initializes. Please keep MAP_JIT on macOS even in the TSAN path.</comment>

<file context>
@@ -2336,6 +2336,25 @@ struct jit_state *jit_state_init(size_t size)
+     * 0x7ffffffff000) and prevents overlap with main memory or TSAN shadow.
+     */
+    void *jit_addr = (void *) 0x7d1000000000UL;
+    state-&gt;buf = mmap(jit_addr, size, PROT_READ | PROT_WRITE | PROT_EXEC,
+                      MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
+    if (state-&gt;buf == MAP_FAILED) {
</file context>

✅ Addressed in f915bc2

MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
if (state->buf == MAP_FAILED) {
free(state);
return NULL;
}
#else
/* Standard allocation without TSAN */
state->buf = mmap(0, size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS
#if defined(__APPLE__)
Expand All @@ -2347,8 +2366,7 @@ struct jit_state *jit_state_init(size_t size)
free(state);
return NULL;
}
assert(state->buf != MAP_FAILED);

#endif
state->n_blocks = 0;
set_reset(&state->set);
reset_reg();
Expand Down
22 changes: 22 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@
#include "riscv.h"
#include "utils.h"

/* ThreadSanitizer configuration for FULL4G compatibility
*
* We use MAP_FIXED to allocate emulated memory at 0x7d0000000000, which is
* within TSAN's application memory range (0x7cf000000000 - 0x7ffffffff000).
* This avoids conflicts with TSAN's shadow memory and allows race detection
* to work with FULL4G's 4GB address space.
*
* Configuration optimizes for race detection with minimal overhead.
*/
#if defined(__SANITIZE_THREAD__)
const char *__tsan_default_options()
{
return "halt_on_error=0" /* Continue after errors */
":report_bugs=1" /* Report data races */
":second_deadlock_stack=1" /* Full deadlock info */
":verbosity=0" /* Reduce noise */
":memory_limit_mb=0" /* No memory limit */
":history_size=7" /* Larger race detection window */
":io_sync=0"; /* Don't sync on I/O */
}
#endif

/* enable program trace mode */
#if !RV32_HAS(SYSTEM) || (RV32_HAS(SYSTEM) && RV32_HAS(ELF_LOADER))
static bool opt_trace = false;
Expand Down
59 changes: 48 additions & 11 deletions src/riscv.c
Original file line number Diff line number Diff line change
Expand Up @@ -206,19 +206,41 @@ static pthread_t t2c_thread;
static void *t2c_runloop(void *arg)
{
riscv_t *rv = (riscv_t *) arg;
pthread_mutex_lock(&rv->wait_queue_lock);
while (!rv->quit) {
if (!list_empty(&rv->wait_queue)) {
queue_entry_t *entry =
list_last_entry(&rv->wait_queue, queue_entry_t, list);
pthread_mutex_lock(&rv->wait_queue_lock);
list_del_init(&entry->list);
pthread_mutex_unlock(&rv->wait_queue_lock);
pthread_mutex_lock(&rv->cache_lock);
t2c_compile(rv, entry->block);
pthread_mutex_unlock(&rv->cache_lock);
free(entry);
}
/* Wait for work or quit signal */
while (list_empty(&rv->wait_queue) && !rv->quit)
pthread_cond_wait(&rv->wait_queue_cond, &rv->wait_queue_lock);

if (rv->quit)
break;

/* Extract work item while holding the lock */
queue_entry_t *entry =
list_last_entry(&rv->wait_queue, queue_entry_t, list);
list_del_init(&entry->list);
pthread_mutex_unlock(&rv->wait_queue_lock);

/* Perform compilation with cache lock */
pthread_mutex_lock(&rv->cache_lock);
/* Look up block from cache using the key (might have been evicted) */
uint32_t pc = (uint32_t) entry->key;
block_t *block = (block_t *) cache_get(rv->block_cache, pc, false);
#if RV32_HAS(SYSTEM)
/* Verify SATP matches (for system mode) */
uint32_t satp = (uint32_t) (entry->key >> 32);
if (block && block->satp != satp)
block = NULL;
#endif
/* Compile only if block still exists in cache */
if (block)
t2c_compile(rv, block);
pthread_mutex_unlock(&rv->cache_lock);
free(entry);

pthread_mutex_lock(&rv->wait_queue_lock);
}
pthread_mutex_unlock(&rv->wait_queue_lock);
return NULL;
}
#endif
Expand Down Expand Up @@ -745,6 +767,7 @@ riscv_t *rv_create(riscv_user_t rv_attr)
/* prepare wait queue. */
pthread_mutex_init(&rv->wait_queue_lock, NULL);
pthread_mutex_init(&rv->cache_lock, NULL);
pthread_cond_init(&rv->wait_queue_cond, NULL);
INIT_LIST_HEAD(&rv->wait_queue);
/* activate the background compilation thread. */
pthread_create(&t2c_thread, NULL, t2c_runloop, rv);
Expand Down Expand Up @@ -866,10 +889,24 @@ void rv_delete(riscv_t *rv)
block_map_destroy(rv);
#else
#if RV32_HAS(T2C)
/* Signal the thread to quit */
pthread_mutex_lock(&rv->wait_queue_lock);
rv->quit = true;
pthread_cond_signal(&rv->wait_queue_cond);
pthread_mutex_unlock(&rv->wait_queue_lock);

pthread_join(t2c_thread, NULL);

/* Clean up any remaining entries in wait queue */
queue_entry_t *entry, *safe;
list_for_each_entry_safe (entry, safe, &rv->wait_queue, list) {
list_del(&entry->list);
free(entry);
}

pthread_mutex_destroy(&rv->wait_queue_lock);
pthread_mutex_destroy(&rv->cache_lock);
pthread_cond_destroy(&rv->wait_queue_cond);
jit_cache_exit(rv->jit_cache);
#endif
jit_state_exit(rv->jit_state);
Expand Down
3 changes: 2 additions & 1 deletion src/riscv_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ typedef struct block {

#if RV32_HAS(JIT) && RV32_HAS(T2C)
typedef struct {
block_t *block;
uint64_t key; /**< cache key (PC or PC|SATP) to look up block */
struct list_head list;
} queue_entry_t;
#endif
Expand Down Expand Up @@ -197,6 +197,7 @@ struct riscv_internal {
#if RV32_HAS(T2C)
struct list_head wait_queue;
pthread_mutex_t wait_queue_lock, cache_lock;
pthread_cond_t wait_queue_cond;
volatile bool quit; /**< Determine the main thread is terminated or not */
#endif
void *jit_state;
Expand Down
4 changes: 3 additions & 1 deletion src/t2c.c
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ void t2c_compile(riscv_t *rv, block_t *block)

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

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

struct jit_cache *jit_cache_init()
Expand Down
Loading