Skip to content

Commit dc22773

Browse files
committed
fix(thread): live daemon at exit forbids graceful teardown (JVM-exact hard exit)
Smell-audited: 1: the ubuntunote alignment panic was the daemon-teardown race made reachable by D6 (rt.deinit freeing the heap under a sleeping daemon worker's registered GC context — Linux timing exposed it, mac passed by luck). The JVM-exact rule IS the structural fix: daemon threads never see graceful teardown — if one is still running after the non-daemon join pass, flush stdout and exit(0) immediately (no rt.deinit). All started threads now register (the join pass filters daemon); 30x deterministic on mac, thread e2e + smoke green.
1 parent d2b4150 commit dc22773

1 file changed

Lines changed: 32 additions & 10 deletions

File tree

‎src/runtime/thread.zig‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ pub fn start(rt: *Runtime, thread_val: Value, loc: SourceLocation) !Value {
141141
// Pin: the worker writes to the Thread value after main may have
142142
// dropped every reference (fire-and-forget start) — future.zig's rule.
143143
try rt.gc.pin(thread_val);
144-
if (!st.daemon) try registerNonDaemon(rt, thread_val);
144+
// ALL started threads register (daemon included): the exit path joins
145+
// the non-daemon entries and hard-exits if a daemon is still running.
146+
try registerStarted(rt, thread_val);
145147
var t = std.Thread.spawn(.{}, worker, .{thread_val}) catch |e| {
146148
io_default.lockMutex(&st.cell.mutex);
147149
st.run_state = .done;
@@ -263,33 +265,53 @@ pub fn setDaemon(thread_val: Value, daemon: bool, loc: SourceLocation) !Value {
263265
return Value.nil_val;
264266
}
265267

266-
// --- join-at-exit registry (non-daemon threads; JVM-faithful main wait) ---
268+
// --- join-at-exit registry (JVM-faithful main-exit rule) ---
267269

268-
fn registerNonDaemon(rt: *Runtime, thread_val: Value) !void {
270+
fn registerStarted(rt: *Runtime, thread_val: Value) !void {
269271
io_default.lockMutex(&rt.user_threads_mutex);
270272
defer io_default.unlockMutex(&rt.user_threads_mutex);
271273
try rt.user_threads.append(rt.gpa, thread_val);
272274
}
273275

274-
/// Called by the app entry after the program body: wait for every live
275-
/// non-daemon Thread (the JVM's main-exit rule). Entries are join'd in
276-
/// registration order; a done thread's join returns immediately. Threads
277-
/// STARTED BY the joined threads land in the registry too — loop until a
278-
/// pass sees no new entries.
276+
/// Called by the app entry after the program body — the JVM main-exit rule
277+
/// in both halves:
278+
/// 1. WAIT for every live non-daemon Thread (join in registration order;
279+
/// threads started BY joined threads land in the registry too — the
280+
/// loop re-reads the length each pass).
281+
/// 2. If a DAEMON thread is still running afterwards, exit the process
282+
/// IMMEDIATELY (stdout flushed, no Runtime teardown) — exactly the
283+
/// JVM, which kills daemon threads abruptly with no cleanup. This is
284+
/// also the structural fix for the teardown race a sleeping daemon
285+
/// worker exposes (rt.deinit freeing the heap under a live worker's
286+
/// registered GC context — the 2026-07-17 ubuntunote alignment panic).
279287
pub fn joinAllNonDaemon(rt: *Runtime) void {
280288
var i: usize = 0;
281289
while (true) {
282290
io_default.lockMutex(&rt.user_threads_mutex);
283291
const n = rt.user_threads.items.len;
284292
if (i >= n) {
285293
io_default.unlockMutex(&rt.user_threads_mutex);
286-
return;
294+
break;
287295
}
288296
const tv = rt.user_threads.items[i];
289297
io_default.unlockMutex(&rt.user_threads_mutex);
290-
join(tv, null);
298+
if (!isDaemon(tv)) join(tv, null);
291299
i += 1;
292300
}
301+
// Half 2: a live daemon forbids graceful teardown (JVM-exact).
302+
io_default.lockMutex(&rt.user_threads_mutex);
303+
var live_daemon = false;
304+
for (rt.user_threads.items) |tv| {
305+
if (isDaemon(tv) and runState(tv) == .running) {
306+
live_daemon = true;
307+
break;
308+
}
309+
}
310+
io_default.unlockMutex(&rt.user_threads_mutex);
311+
if (live_daemon) {
312+
if (rt.stdout) |out| out.flush() catch {};
313+
std.process.exit(0);
314+
}
293315
}
294316

295317
// --- tests ---

0 commit comments

Comments
 (0)