diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 1b8b415755..cc79cb8971 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -280,6 +280,17 @@ pub const Interpreter = struct { exit_code: ?ExitCode = 0, this_jsvalue: JSValue = .zero, + /// Tracks which resources have been cleaned up to avoid double-free. + /// When the interpreter finishes normally via finish(), it cleans up + /// the runtime resources (IO, shell env) and sets this to .runtime_cleaned. + /// The GC finalizer then only cleans up what remains (args, interpreter itself). + cleanup_state: enum(u8) { + /// Nothing has been cleaned up yet - need full cleanup + needs_full_cleanup, + /// Runtime resources (IO, shell env) have been cleaned up via finish() + runtime_cleaned, + } = .needs_full_cleanup, + __alloc_scope: if (bun.Environment.enableAllocScopes) bun.AllocationScope else void, estimated_size_for_gc: usize = 0, @@ -1222,6 +1233,11 @@ pub const Interpreter = struct { } fn #derefRootShellAndIOIfNeeded(this: *ThisInterpreter, free_buffered_io: bool) void { + // Check if already cleaned up to prevent double-free + if (this.cleanup_state == .runtime_cleaned) { + return; + } + if (free_buffered_io) { // Can safely be called multiple times. if (this.root_shell._buffered_stderr == .owned) { @@ -1240,10 +1256,26 @@ pub const Interpreter = struct { } this.this_jsvalue = .zero; + // Mark that runtime resources have been cleaned up + this.cleanup_state = .runtime_cleaned; } fn deinitFromFinalizer(this: *ThisInterpreter) void { - this.#derefRootShellAndIOIfNeeded(true); + log("Interpreter(0x{x}) deinitFromFinalizer (cleanup_state={s})", .{ @intFromPtr(this), @tagName(this.cleanup_state) }); + + switch (this.cleanup_state) { + .needs_full_cleanup => { + // The interpreter never finished normally (e.g., early error or never started), + // so we need to clean up IO and shell env here + this.root_io.deref(); + this.root_shell.deinitImpl(false, true); + }, + .runtime_cleaned => { + // finish() already cleaned up IO and shell env via #derefRootShellAndIOIfNeeded, + // nothing more to do for those resources + }, + } + this.keep_alive.disable(); this.args.deinit(); this.allocator.destroy(this);