Skip to content

Commit e5562b7

Browse files
Fix thread teardown and pause races in async/thread workers
Initialize the per-worker interpreter's background-thread registry mutex and leak the shared global_env/modules at teardown while live workers exist, so a running worker never touches freed memory without blocking on long-running threads. Also pause-check while/for loops at each iteration so a paused worker freezes instead of finishing and clearing its paused flag.
1 parent 5e3125d commit e5562b7

2 files changed

Lines changed: 105 additions & 13 deletions

File tree

src/interpreter.c

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -436,9 +436,51 @@ typedef struct {
436436
Value thread_val;
437437
} ThrStart;
438438

439+
// Initialize the background-thread registry on an interpreter. The per-worker
440+
// interpreters spawned for async/thread are built with memset(0) rather than
441+
// interpreter_init(), so their registry mutex must be initialized explicitly
442+
// or any lock/unlock on it is undefined behavior.
443+
static void bg_threads_init(Interpreter *interp) {
444+
if (!interp) {
445+
return;
446+
}
447+
interp->bg_threads = NULL;
448+
interp->bg_thread_count = 0;
449+
interp->bg_thread_cap = 0;
450+
mtx_init(&interp->bg_threads_lock, 0);
451+
}
452+
453+
// Register a background thread on the interpreter that spawned it. The registry
454+
// is only used to detect, at teardown, whether workers might still be using
455+
// shared state (global_env/modules). We deliberately do NOT join such threads:
456+
// async/thread workers may be long-running or infinite by design, so blocking
457+
// on them at process exit would hang. Instead the interpreter skips freeing
458+
// shared state while any worker is live and lets the OS reclaim it on exit.
459+
static void bg_threads_register(Interpreter *interp, struct Thread *th) {
460+
if (!interp || !th) {
461+
return;
462+
}
463+
mtx_lock(&interp->bg_threads_lock);
464+
if (interp->bg_thread_count + 1 > interp->bg_thread_cap) {
465+
size_t newcap = interp->bg_thread_cap ? interp->bg_thread_cap * 2 : 8;
466+
struct Thread **nb = realloc(interp->bg_threads, newcap * sizeof(*nb));
467+
if (!nb) {
468+
mtx_unlock(&interp->bg_threads_lock);
469+
return;
470+
}
471+
interp->bg_threads = nb;
472+
interp->bg_thread_cap = newcap;
473+
}
474+
interp->bg_threads[interp->bg_thread_count++] = th;
475+
mtx_unlock(&interp->bg_threads_lock);
476+
}
477+
439478
static int thread_worker(void *arg) {
440479
ThrStart *start = (ThrStart *)arg;
441480
LabelMap labels = {0};
481+
// The worker's interpreter was zero-initialized, not via interpreter_init,
482+
// so its background-thread registry mutex must be set up explicitly.
483+
bg_threads_init(start->interp);
442484
start->interp->current_thread = start->thread_val.as.thread;
443485
ExecResult res = exec_stmt(start->interp, start->body, start->env, &labels);
444486

@@ -461,6 +503,13 @@ static int thread_worker(void *arg) {
461503
* worker must not free it. Ownership remains with the parent
462504
* interpreter which will free the env when appropriate.
463505
*/
506+
// The worker's own registry (used if it spawned nested async/thread
507+
// workers) is just bookkeeping; we do not join those threads here
508+
// because they may be long-running or infinite. Free the array and
509+
// destroy the lock now that exec_stmt has returned and no further
510+
// registrations can occur on this interpreter.
511+
free(start->interp->bg_threads);
512+
mtx_destroy(&start->interp->bg_threads_lock);
464513
free(start->interp);
465514
free(start);
466515
return 0;
@@ -808,6 +857,9 @@ static int start_deferred_async_handle(Interpreter *interp, Value thread_val, in
808857
}
809858

810859
value_thread_set_started(thread_val, 1);
860+
if (interp) {
861+
bg_threads_register(interp, thread_val.as.thread);
862+
}
811863
return 0;
812864
}
813865

@@ -2381,6 +2433,7 @@ Value eval_expr(Interpreter *interp, Expr *expr, Env *env) {
23812433
value_free(thread_val);
23822434
return value_null();
23832435
}
2436+
bg_threads_register(interp, thread_for_worker.as.thread);
23842437
return thread_val;
23852438
}
23862439
case EXPR_MAP: {
@@ -3787,6 +3840,7 @@ static ExecResult exec_stmt(Interpreter *interp, Stmt *stmt, Env *env, LabelMap
37873840
free(start);
37883841
return make_error("Failed to start thread", stmt->line, stmt->column);
37893842
}
3843+
bg_threads_register(interp, thread_for_worker.as.thread);
37903844
return make_ok(value_null());
37913845
}
37923846

@@ -3823,6 +3877,7 @@ static ExecResult exec_stmt(Interpreter *interp, Stmt *stmt, Env *env, LabelMap
38233877
free(start);
38243878
return make_error("Failed to start async", stmt->line, stmt->column);
38253879
}
3880+
bg_threads_register(interp, thread_for_worker.as.thread);
38263881
return make_ok(value_null());
38273882
}
38283883

@@ -3871,6 +3926,9 @@ static ExecResult exec_stmt(Interpreter *interp, Stmt *stmt, Env *env, LabelMap
38713926
if (interpreter_thr_should_stop(interp)) {
38723927
break;
38733928
}
3929+
// A paused worker must not evaluate the loop condition or advance;
3930+
// freeze at the iteration boundary until it is resumed.
3931+
wait_if_paused(interp);
38743932
if (++iteration_count > max_iterations) {
38753933
interp->loop_depth--;
38763934
return make_error("Infinite loop detected", stmt->line, stmt->column);
@@ -4013,6 +4071,9 @@ static ExecResult exec_stmt(Interpreter *interp, Stmt *stmt, Env *env, LabelMap
40134071
if (interpreter_thr_should_stop(interp)) {
40144072
break;
40154073
}
4074+
// A paused worker must not evaluate the loop condition or advance;
4075+
// freeze at the iteration boundary until it is resumed.
4076+
wait_if_paused(interp);
40164077
if (++iteration_count > max_iterations) {
40174078
/* cleanup alias/temp and restore previous local if needed */
40184079
env_delete(env, stmt->as.for_stmt.counter);
@@ -4404,6 +4465,7 @@ void interpreter_init(Interpreter *interp, const char *source_path, bool verbose
44044465
interp->trace_next_step_index = 0;
44054466
snprintf(interp->trace_last_state_id, sizeof(interp->trace_last_state_id), "seed");
44064467
interp->trace_last_rule[0] = '\0';
4468+
bg_threads_init(interp);
44074469

44084470
if (!interp->private_mode) {
44094471
if (trace_push_frame(interp, "<top-level>", interp->global_env, 0, 0, 0) != 0) {
@@ -4432,22 +4494,38 @@ void interpreter_destroy(Interpreter *interp) {
44324494
return;
44334495
}
44344496

4435-
if (interp->global_env) {
4436-
env_free(interp->global_env);
4437-
interp->global_env = NULL;
4438-
}
4497+
// If background threads (async/thread) are still live, they execute
4498+
// against this interpreter's shared global_env and modules. Freeing those
4499+
// now would be a use-after-free if a worker touches them during teardown
4500+
// (observed intermittently under CI load). Such workers may be long-running
4501+
// or infinite by design, so we must not block waiting for them. Instead we
4502+
// deliberately leak the shared state; the OS reclaims it when the process
4503+
// exits and forcibly terminates the remaining threads.
4504+
int has_live_threads = (interp->bg_thread_count > 0);
4505+
4506+
free(interp->bg_threads);
4507+
interp->bg_threads = NULL;
4508+
interp->bg_thread_cap = 0;
4509+
mtx_destroy(&interp->bg_threads_lock);
44394510

4440-
ModuleEntry *me = interp->modules;
4441-
while (me) {
4442-
ModuleEntry *next = me->next;
4443-
free(me->name);
4444-
if (me->owns_env) {
4445-
env_free(me->env);
4511+
if (!has_live_threads) {
4512+
if (interp->global_env) {
4513+
env_free(interp->global_env);
4514+
interp->global_env = NULL;
44464515
}
4447-
free(me);
4448-
me = next;
4516+
4517+
ModuleEntry *me = interp->modules;
4518+
while (me) {
4519+
ModuleEntry *next = me->next;
4520+
free(me->name);
4521+
if (me->owns_env) {
4522+
env_free(me->env);
4523+
}
4524+
free(me);
4525+
me = next;
4526+
}
4527+
interp->modules = NULL;
44494528
}
4450-
interp->modules = NULL;
44514529

44524530
if (interp->error) {
44534531
free(interp->error);
@@ -4524,6 +4602,9 @@ int interpreter_restart_thread(Interpreter *interp, Value thread_val, int line,
45244602
}
45254603
return -1;
45264604
}
4605+
if (interp) {
4606+
bg_threads_register(interp, th);
4607+
}
45274608
return 0;
45284609
}
45294610

src/interpreter.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include "ast.h"
55
#include "env.h"
66

7+
struct Thread; // forward declaration (defined in value.h)
8+
79
typedef struct {
810
DeclType type;
911
char *name;
@@ -98,6 +100,15 @@ typedef struct Interpreter {
98100
int trace_next_step_index;
99101
char trace_last_state_id[24];
100102
char trace_last_rule[32];
103+
// Registry of live background threads (async/thread) spawned by this
104+
// interpreter. interpreter_destroy() checks it and, while any worker is
105+
// still live, skips freeing shared state (global_env/modules) because a
106+
// still-running worker could touch freed memory; the OS reclaims it at
107+
// process exit.
108+
struct Thread **bg_threads;
109+
size_t bg_thread_count;
110+
size_t bg_thread_cap;
111+
mtx_t bg_threads_lock;
101112
} Interpreter;
102113

103114
// Initialize/destroy a reusable interpreter session.

0 commit comments

Comments
 (0)