@@ -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+
439478static 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
0 commit comments