From 13a0dbe5ff017749e32b27d747f70568f5cf2d44 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:07:55 +0000 Subject: [PATCH 1/6] bake: exit a successful bun build --app through the build VM build_command only ran vm.on_exit() and vm.global_exit() when the build threw. A build that rendered returned Ok instead, so the process exited without emitting 'exit', without applying process.exitCode, and, under BUN_DESTRUCT_VM_ON_EXIT, without destroying the JSC VM, which left every native still owned by a JS wrapper created during prerendering (immediates, blobs, text decoders) unfreed. Run the same exit sequence on both arms. --- src/runtime/bake/production.rs | 38 ++++++------ test/bake/dev/production.test.ts | 99 +++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 18 deletions(-) diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index 4f8b053a5f0d..d943bb965c5c 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -112,11 +112,13 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { // SAFETY: `init_bake` returns a freshly-allocated VM owned by this thread; // unique access for the rest of this function. let vm = unsafe { &mut *vm_ptr }; - // defer vm.deinit() — handled by `vm.destroy()` on the unwind path below. + // Runs only on the `Err` return below (a bundler or I/O failure). Every + // other way out of this function exits the process, and the normal one, + // `global_exit()` at the end, tears the VM down itself. // Note: pass `vm_ptr` by value into the guard so the drop closure does // not borrow the local (`defer!` would capture `&vm_ptr`, which under // edition-2024 disjoint-capture rules collides with the `&mut *vm_ptr` - // re-borrows on the JSError path). + // re-borrows below). let _vm_guard = scopeguard::guard(vm_ptr, |p| { // SAFETY: p is the unique live VM on this thread; its loop is alive, so // queued work is released here rather than by a thread teardown. @@ -211,28 +213,32 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { // LIFO order — under the API lock, before the VM is destroyed. let mut pt = PerThread::placeholder(vm_ptr); - match build_with_vm(ctx, &cwd, &mut pt) { + let result = build_with_vm(ctx, &cwd, &mut pt); + // SAFETY: the reborrows `build_with_vm` made through `pt.vm` died when it + // returned, so this frame has exclusive access again; the VM stays + // allocated until `global_exit` (or `_vm_guard`) destroys it. + let vm = unsafe { &mut *vm_ptr }; + match result { Ok(()) => {} Err(crate::Error::JSError) => { - // SAFETY: vm.global is live for VM lifetime. - let global = unsafe { &*(*vm_ptr).global }; - let err_value = global.take_exception(jsc::JsError::Thrown); - // SAFETY: see above. - unsafe { - (*vm_ptr) - .print_error_like_object_to_console(err_value.to_error().unwrap_or(err_value)) - }; - // SAFETY: see above. - let vm = unsafe { &mut *vm_ptr }; + let err_value = vm.global().take_exception(jsc::JsError::Thrown); + vm.print_error_like_object_to_console(err_value.to_error().unwrap_or(err_value)); if vm.exit_handler.exit_code == 0 { vm.exit_handler.exit_code = 1; } - vm.on_exit(); - vm.global_exit(); } Err(e) => return Err(e), } - Ok(()) + + // A rendered build exits the same way a failed one does: `on_exit` runs the + // `process.on("exit")` handlers the config and prerender registered, and + // `global_exit` exits with `process.exitCode`, tearing the VM down first + // under BUN_DESTRUCT_VM_ON_EXIT. Returning `Ok` instead would free only the + // Rust side of the VM (`_vm_guard`); the JSC heap would never be destroyed, + // so the natives still owned by the prerender's JS objects (timers, blobs, + // decoders, ...) would never be freed. + vm.on_exit(); + vm.global_exit() } /// Ported inline from `bun.bun_js.failWithBuildError` to avoid the diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 1335e3244c00..a1bad3e21104 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from "bun:test"; import { existsSync } from "fs"; -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, isASAN } from "harness"; import path from "path"; -import { tempDirWithBakeDeps } from "../bake-harness"; +import { tempDirWithBakeDeps, WAIT_MULTIPLIER } from "../bake-harness"; const normalizePath = (path: string) => (process.platform === "win32" ? path.replaceAll("\\", "/") : path); const platformPath = (path: string) => (process.platform === "win32" ? path.replaceAll("/", "\\") : path); @@ -594,4 +594,99 @@ export default function IndexPage() { // Verify NO JavaScript imports are included in the HTML expect(htmlContent).not.toContain('