Skip to content

ssg 3 - #22138

Merged
Jarred-Sumner merged 164 commits into
mainfrom
zack/ssg-3
Sep 30, 2025
Merged

ssg 3#22138
Jarred-Sumner merged 164 commits into
mainfrom
zack/ssg-3

Conversation

@zackradisic

@zackradisic zackradisic commented Aug 26, 2025 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a crash related to the dev server overwriting the uws user context
pointer when setting abort callback.

Adds support for return new Response(<jsx />, { ... }) and return Response.render(...) and return Response.redirect(...):

  • Created a SSRResponse class to handle this (see JSBakeResponse.{h,cpp})
  • SSRResponse is designed to "fake" being a React component
    • This is done in JSBakeResponse::create inside of src/bun.js/bindings/JSBakeResponse.cpp
    • And src/js/builtins/BakeSSRResponse.ts defines a wrapComponent function which wraps
      the passed in component (when doing new Response(<jsx />, ...)). It does
      this to throw an error (in redirect()/render() case) or return the
      component.
    • Created a BakeAdditionsToGlobal struct which contains some properties
      needed for this
    • Added some of the properties we need to fake to BunBuiltinNames.h (e.g.
      $$typeof), the rationale behind this is that we couldn't use
      structure->addPropertyTransition because JSBakeResponse is not a final
      JSObject.
  • When bake and server-side, bundler rewrites Response -> Bun.SSRResponse (see src/ast/P.zig and src/ast/visitExpr.zig)
  • Created a new WebCore body variant (Render: struct { path: []const u8 })
    • Created when return Response.render(...)
    • When handled, it re-invokes dev server to render the new path

Enables server-side sourcemaps for the dev server:

  • New source providers for server-side: (DevServerSourceProvider.{h,cpp})
  • IncrementalGraph and SourceMapStore are updated to support this

There are numerous other stuff:

  • allow app configuration from Bun.serve(...)
  • fix errors stopping dev server
  • fix use after free related to in RequestContext.finishRunningErrorHandler
  • Request.cookies
  • Make "use client"; components work
  • Fix some bugs using require(...) in dev server
  • Fix catch-all routes not working in the dev server
  • Updates findSourceMappingURL(...) to use std.mem.lastIndexOf(...) because
    the sourcemap that should be used is the last one anyway

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
src/bun.js/webcore/Response.zig (1)

444-514: Past review feedback not addressed: redirect implementation still in wrong file.

Maintainer feedback from previous reviews explicitly stated this redirect code should be in BakeResponse.zig, not Response.zig. While the refactoring separates constructRedirectImpl from the wrapper, the core issue remains: this SSR-specific redirect logic doesn't belong in the base Response implementation.

Consider moving constructRedirectImpl and the SSR-aware constructRedirect wrapper to src/bun.js/webcore/BakeResponse.zig as originally requested by the maintainer.

Based on past review comments.

src/bun.js/webcore/BakeResponse.zig (2)

21-29: Critical: Multiple issues from past reviews remain unaddressed.

Two bugs flagged in previous reviews are still present:

  1. Wrong parameter type: bake_ssr_has_jsx is declared as *c_int but should be ?*c_int since the constructor (line 31-48) checks for null and the C++ caller may pass null for function-call form.

  2. Missing pointer cast: Return type is ?*anyopaque but the code returns @as(*Response, ...) without casting to *anyopaque, which is UB across ABIs.

Apply this fix:

-pub export fn BakeResponseClass__constructForSSR(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, bake_ssr_has_jsx: *c_int) callconv(jsc.conv) ?*anyopaque {
-    return @as(*Response, constructor(globalObject, callFrame, bake_ssr_has_jsx) catch |err| switch (err) {
+pub export fn BakeResponseClass__constructForSSR(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFrame, bake_ssr_has_jsx: ?*c_int) callconv(jsc.conv) ?*anyopaque {
+    const resp = constructor(globalObject, callFrame, bake_ssr_has_jsx) catch |err| switch (err) {
         error.JSError => return null,
         error.OutOfMemory => {
             globalObject.throwOutOfMemory() catch {};
             return null;
         },
-    });
+    };
+    return @ptrCast(resp);
 }

Based on past review comments.


31-48: Critical: Null-pointer dereference remains unfixed.

Lines 37 and 43 unconditionally dereference bake_ssr_has_jsx without checking if it's null. The C++ caller passes null for the function-call form (non-constructor invocation), which will cause a crash.

Guard all writes to the optional pointer:

 pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, bake_ssr_has_jsx: ?*c_int) bun.JSError!*Response {
     var arguments = callframe.argumentsAsArray(2);
 
+    if (bake_ssr_has_jsx) |out| out.* = 0;
+
     // Allow `return new Response(<jsx> ... </jsx>, { ... }`
     // inside of a react component
     if (!arguments[0].isUndefinedOrNull() and arguments[0].isObject()) {
-        bake_ssr_has_jsx.* = 0;
         if (try arguments[0].isJSXElement(globalThis)) {
             const vm = globalThis.bunVM();
             if (try vm.getDevServerAsyncLocalStorage()) |async_local_storage| {
                 try assertStreamingDisabled(globalThis, async_local_storage, "new Response(<jsx />, { ... })");
             }
-            bake_ssr_has_jsx.* = 1;
+            if (bake_ssr_has_jsx) |out| out.* = 1;
         }
     }
 
     return Response.constructor(globalThis, callframe);
 }

Based on past review comments.

🧹 Nitpick comments (1)
src/bun.js/webcore/BakeResponse.zig (1)

128-136: Consider improving error messages for better debugging.

While the validation logic is correct (including the callable check on line 131), the error messages could be more specific to help developers understand what went wrong. Previous review feedback suggested clearer messages.

Consider applying these message improvements:

 fn assertStreamingDisabled(globalThis: *jsc.JSGlobalObject, async_local_storage: JSValue, display_function: []const u8) bun.JSError!void {
-    if (async_local_storage.isEmptyOrUndefinedOrNull() or !async_local_storage.isObject()) return globalThis.throwInvalidArguments("store value must be an object", .{});
-    const getStoreFn = (try async_local_storage.getPropertyValue(globalThis, "getStore")) orelse return globalThis.throwInvalidArguments("store value must have a \"getStore\" field", .{});
-    if (!getStoreFn.isCallable()) return globalThis.throwInvalidArguments("\"getStore\" must be a function", .{});
+    if (async_local_storage.isEmptyOrUndefinedOrNull() or !async_local_storage.isObject())
+        return globalThis.throwInvalidArguments("AsyncLocalStorage must be an object", .{});
+    const getStoreFn = (try async_local_storage.getPropertyValue(globalThis, "getStore"))
+        orelse return globalThis.throwInvalidArguments("AsyncLocalStorage must have a getStore() function", .{});
+    if (!getStoreFn.isCallable())
+        return globalThis.throwInvalidArguments("AsyncLocalStorage.getStore must be a function", .{});
     const store_value = try getStoreFn.call(globalThis, async_local_storage, &.{});
-    const streaming_val = (try store_value.getPropertyValue(globalThis, "streaming")) orelse return globalThis.throwInvalidArguments("store value must have a \"streaming\" field", .{});
+    if (store_value.isEmptyOrUndefinedOrNull() or !store_value.isObject())
+        return globalThis.throwInvalidArguments("getStore() must return an object", .{});
+    const streaming_val = (try store_value.getPropertyValue(globalThis, "streaming"))
+        orelse return globalThis.throwInvalidArguments("store must have a \"streaming\" field", .{});
     if (!streaming_val.isBoolean()) return globalThis.throwInvalidArguments("\"streaming\" field must be a boolean", .{});
     if (streaming_val.asBoolean()) return globalThis.throwInvalidArguments("\"{s}\" is not available when `export const streaming = true`", .{display_function});
 }

Based on past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0c90134 and c5e7973.

📒 Files selected for processing (3)
  • src/bun.js/VirtualMachine.zig (2 hunks)
  • src/bun.js/webcore/BakeResponse.zig (1 hunks)
  • src/bun.js/webcore/Response.zig (5 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/bun.js/webcore/BakeResponse.zig
  • src/bun.js/VirtualMachine.zig
  • src/bun.js/webcore/Response.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/bun.js/webcore/BakeResponse.zig
  • src/bun.js/VirtualMachine.zig
  • src/bun.js/webcore/Response.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/webcore/BakeResponse.zig
  • src/bun.js/VirtualMachine.zig
  • src/bun.js/webcore/Response.zig
🧠 Learnings (2)
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.zig : Wrap the Bun__<Type>__toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

Applied to files:

  • src/bun.js/VirtualMachine.zig
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/**/*.zig : Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)

Applied to files:

  • src/bun.js/webcore/Response.zig
🔇 Additional comments (1)
src/bun.js/VirtualMachine.zig (1)

192-198: LGTM! Well-structured ALS integration.

The extern declaration and wrapper follow Zig/JSC interop patterns correctly. The wrapper appropriately uses jsc.fromJSHostCall for error handling and returns null when the result is empty/undefined/null, providing a clean Zig-facing API for dev-server AsyncLocalStorage retrieval.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bun.js/bindings/bindings.cpp (1)

2386-2398: Use fully qualified JSType enumerator for consistency and to avoid lookup issues.

Other cases in this switch use JSC::JSType::…; switch discriminant is a uint8_t cast of JSType. Prefer qualifying the wrapper case the same way.

Apply this diff:

-    case JSDOMWrapperType: {
+    case JSC::JSType::JSDOMWrapperType: {
         if (jsDynamicCast<WebCore::JSFetchHeaders*>(cell))
             return static_cast<double>(jsCast<WebCore::JSFetchHeaders*>(cell)->wrapped().size());
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c5e7973 and 8a7deca.

📒 Files selected for processing (6)
  • src/ast/visitExpr.zig (1 hunks)
  • src/bun.js/VirtualMachine.zig (2 hunks)
  • src/bun.js/bindings/JSValue.zig (2 hunks)
  • src/bun.js/bindings/ZigGlobalObject.cpp (2 hunks)
  • src/bun.js/bindings/bindings.cpp (6 hunks)
  • test/no-validate-leaksan.txt (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • test/no-validate-leaksan.txt
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/ast/visitExpr.zig
  • src/bun.js/bindings/ZigGlobalObject.cpp
  • src/bun.js/bindings/JSValue.zig
🧰 Additional context used
📓 Path-based instructions (7)
src/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/building-bun.mdc)

Implement debug logs in Zig using const log = bun.Output.scoped(.${SCOPE}, false); and invoking log("...", .{})

Files:

  • src/bun.js/VirtualMachine.zig
**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.zig: Declare the extern C symbol in Zig and export a Zig-friendly alias for use
Wrap the Bun____toJS extern in a Zig method that takes a JSGlobalObject and returns JSC.JSValue

**/*.zig: Format Zig files with zig-format (bun run zig-format)
In Zig, manage memory carefully with allocators and use defer for cleanup

Files:

  • src/bun.js/VirtualMachine.zig
src/bun.js/**/*.zig

📄 CodeRabbit inference engine (.cursor/rules/zig-javascriptcore-classes.mdc)

src/bun.js/**/*.zig: In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS and re-export toJS/fromJS/fromJSDirect
Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
Implement getters as get(this, globalObject) returning JSC.JSValue and matching the .classes.ts interface
Provide deinit() for resource cleanup and finalize() that calls deinit(); use bun.destroy(this) or appropriate destroy pattern
Access JS call data via CallFrame (argument(i), argumentCount(), thisValue()) and throw errors with globalObject.throw(...)
For properties marked cache: true, use the generated Zig accessors (NameSetCached/GetCached) to work with GC-owned values
In finalize() for objects holding JS references, release them using .deref() before destroy

Files:

  • src/bun.js/VirtualMachine.zig
**/*.{cpp,h}

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.{cpp,h}: When exposing a JS class with public Constructor and Prototype, define three C++ types: class Foo : public JSC::DestructibleObject (if it has C++ fields), class FooPrototype : public JSC::JSNonFinalObject, and class FooConstructor : public JSC::InternalFunction
If the class has C++ data members, inherit from JSC::DestructibleObject and provide proper destruction; if it has no C++ fields (only JS properties), avoid a class and use JSC::constructEmptyObject(vm, structure) with putDirectOffset
Prefer placing the subspaceFor implementation in the .cpp file rather than the header when possible

Files:

  • src/bun.js/bindings/bindings.cpp
**/*.cpp

📄 CodeRabbit inference engine (.cursor/rules/javascriptcore-class.mdc)

**/*.cpp: Include "root.h" at the top of C++ binding files to satisfy lints
Define prototype properties using a const HashTableValue array and declare accessors/functions with JSC_DECLARE_* macros
Prototype classes should subclass JSC::JSNonFinalObject, provide create/createStructure, DECLARE_INFO, finishCreation that reifies static properties, and set mayBePrototype on the Structure
Custom getters should use JSC_DEFINE_CUSTOM_GETTER, jsDynamicCast to validate this, and throwThisTypeError on mismatch
Custom setters should use JSC_DEFINE_CUSTOM_SETTER, validate this via jsDynamicCast, and store via WriteBarrier/set semantics
Prototype functions should use JSC_DEFINE_HOST_FUNCTION, validate this with jsDynamicCast, and return encoded JSValue
Constructors should subclass JSC::InternalFunction, return internalFunctionSpace in subspaceFor, set the prototype property as non-configurable/non-writable, and provide create/createStructure
Provide a setup function that builds the Prototype, Constructor, and Structure, and assigns them to the LazyClassStructure initializer
Use the cached Structure via globalObject->m_.get(globalObject) when constructing instances
Expose constructors to Zig via an extern "C" function that returns the constructor from the LazyClassStructure
Provide an extern "C" Bun____toJS function that creates an instance using the cached Structure and returns an EncodedJSValue

Files:

  • src/bun.js/bindings/bindings.cpp
**/*.{cpp,cc,cxx,h,hpp,hxx}

📄 CodeRabbit inference engine (CLAUDE.md)

Format C/C++ sources/headers with clang-format (bun run clang-format)

Files:

  • src/bun.js/bindings/bindings.cpp
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: For JS classes with a public constructor, implement Foo, FooPrototype, and FooConstructor (JSC patterns)
Define class properties using HashTableValue arrays in JSC bindings
Add iso subspaces for classes with C++ fields in JSC bindings
Cache structures in ZigGlobalObject for JSC-bound classes

Files:

  • src/bun.js/bindings/bindings.cpp
🧠 Learnings (4)
📚 Learning: 2025-09-08T04:44:59.101Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T04:44:59.101Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Cache structures in ZigGlobalObject for JSC-bound classes

Applied to files:

  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-08-30T00:13:36.815Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/zig-javascriptcore-classes.mdc:0-0
Timestamp: 2025-08-30T00:13:36.815Z
Learning: Applies to src/bun.js/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

Applied to files:

  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-08-30T00:11:00.890Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/javascriptcore-class.mdc:0-0
Timestamp: 2025-08-30T00:11:00.890Z
Learning: Applies to **/*.cpp : Provide an extern "C" Bun__<Type>__toJS function that creates an instance using the cached Structure and returns an EncodedJSValue

Applied to files:

  • src/bun.js/bindings/bindings.cpp
📚 Learning: 2025-09-08T04:44:59.101Z
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T04:44:59.101Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Define class properties using HashTableValue arrays in JSC bindings

Applied to files:

  • src/bun.js/bindings/bindings.cpp
🧬 Code graph analysis (1)
src/bun.js/bindings/bindings.cpp (1)
src/bun.js/bindings/JSDOMExceptionHandling.h (1)
  • propagateException (86-90)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (5)
src/bun.js/VirtualMachine.zig (2)

192-198: LGTM! Clean ALS interop implementation.

The extern declaration and Zig wrapper follow best practices:

  • Proper error union return type for JS exception integration
  • Correct use of jsc.fromJSHostCall for C++ interop
  • Appropriate null-checking for empty/undefined/null values
  • Aligns with coding guidelines for Zig bindings

845-847: Verify shutdown sequence implications.

The commented-out eventLoop().tick() call prevents processing pending event loop tasks during VM exit. While the comment explains this causes 50+ test failures, this workaround may have implications:

  • Pending async operations may not complete
  • Resources held by queued tasks may not be properly released
  • Deferred cleanup handlers might not execute

Given the PR is already experiencing test failures (per reviewer comments), ensure this change doesn't introduce additional shutdown-related issues in production scenarios, especially for long-running server processes with the new SSR/Bake integration.

Consider opening an issue to track re-enabling this call once the underlying test issues are resolved, as proper event loop draining during shutdown is generally important for cleanup.

src/bun.js/bindings/bindings.cpp (3)

32-32: Includes look good.

root.h is present, and adding WebCoreJSBuiltins.h/Strong.h is fine for builtin names and JSC Strong usage. No issues.

Also applies to: 63-63


2005-2025: Native FetchHeaders creation path is correct.

  • Returns a native WebCore::FetchHeaders* as expected by Zig callers.
  • Handles exceptions via propagateException + deref on failure. LGTM.

6044-6045: Style-only change.

Brace repositioning is fine; no behavioral impact.

JSFetchHeaders* fetchHeaders = jsCast<JSFetchHeaders*>(value);
fetchHeaders->computeMemoryCost();
return JSC::JSValue::encode(value);
return JSC::JSValue::encode(fetchHeaders);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: returning the wrong value (encodes JSFetchHeaders instead of the JSValue).*

Return should encode the JSValue you created (variable “value”), not the JSFetchHeaders* pointer. Use encode(value) for correctness and consistency with neighboring helpers.

Apply this diff:

-    return JSC::JSValue::encode(fetchHeaders);
+    return JSC::JSValue::encode(value);
🤖 Prompt for AI Agents
In src/bun.js/bindings/bindings.cpp around line 2046, the function currently
returns JSC::JSValue::encode(fetchHeaders) which encodes the JSFetchHeaders*
pointer instead of the JSValue you constructed; change the return to encode the
JSValue variable (value) — i.e., return JSC::JSValue::encode(value) — so the
function returns the actual JSValue as done in neighboring helpers, ensuring
correct encoding and type safety.

@Jarred-Sumner
Jarred-Sumner merged commit a89e61f into main Sep 30, 2025
59 of 62 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the zack/ssg-3 branch September 30, 2025 12:26
@jlucaso1

Copy link
Copy Markdown
Contributor

We will have something like cache invalidation for static pages?

cc: @Jarred-Sumner @zackradisic

@robobun

robobun commented Jul 12, 2026 •

Copy link
Copy Markdown
Collaborator

~DevServerSourceProvider (introduced here) held a raw Zig::GlobalObject* and read through it in its destructor, which runs from ~CodeCache after the global object cell has already been swept during BUN_DESTRUCT_VM_ON_EXIT teardown. Fixed in #34035 by storing the Rust VM pointer directly, matching Zig::SourceProvider.

Jarred-Sumner pushed a commit that referenced this pull request Jul 12, 2026
…wn (#34035)

Fixes `test/bake/dev/request-cookies.test.ts` going red on the `debian
13 x64-asan` lane (seen in [build
72183](https://buildkite.com/bun/bun/builds/72183#019f55d5-6839-41cf-b0f5-3c56ada43ef9)
and [build 71964](https://buildkite.com/bun/bun/builds/71964)):

```
dev| ==1715==ERROR: AddressSanitizer: SEGV on unknown address 0x000000007490
error: DevServer panicked
      at gracefulExit (test/bake/bake-harness.ts:614:17)
✗  DEV:request-cookies-1: request.cookies.get() basic functionality
```

### Cause

`~DevServerSourceProvider` held a raw `Zig::GlobalObject*` and called
`m_globalObject->bunVM()` to reach `Bun__removeDevServerSourceProvider`.
Under `BUN_DESTRUCT_VM_ON_EXIT=1` (set by the CI runner for the asan
lane), the harness's `process.exit(0)` runs
`Zig__GlobalObject__destructOnExit`, which does
`gcUnprotect(globalObject)` then `collectNow(Sync, Full)` then two
`vm.derefSuppressingSaferCPPChecking()`. The global object cell is swept
during `collectNow`, but the provider's last `Ref` is only released
later from `~CodeCache` inside `~JSC::VM`, so the destructor read
`m_bunVM` out of a freed cell.

With bmalloc the freed cell usually still holds the old value and the
read happens to work, which is why this was ~0.5% in CI and never
reproduced locally. When the memory is reused with a zero at that offset
the Rust side receives a null `VirtualMachine*` and the next access is
`(null)->source_mappings.mutex`, which lands at exactly 0x7490.

Deterministic ASAN backtrace with `Malloc=1`:

```
==79839==ERROR: AddressSanitizer: heap-use-after-free ...
    #0  Zig::GlobalObject::bunVM() const  ZigGlobalObject.h:353
    #1  Bake::DevServerSourceProvider::~DevServerSourceProvider()  DevServerSourceProvider.h:65
    ...
    #7  JSC::SourceCodeKey::~SourceCodeKey()
    #12 JSC::CodeCacheMap::~CodeCacheMap()
    #16 JSC::VM::~VM()
    #18 Zig__GlobalObject__destructOnExit  ZigGlobalObject.cpp:4049
    #19 VirtualMachine::global_exit  VirtualMachine.rs:1603
    #20 Bun__Process__exit
```

### Fix

Store the Rust `VirtualMachine*` directly (`void* m_bunVM`), captured in
`create()`, so the destructor no longer indirects through a GC cell.
This mirrors `Zig::SourceProvider`, which already stores `m_bunVM` for
the same reason. The Rust `VirtualMachine` outlives every GC cell (step
10 of `global_exit` is `self.destroy()`, after `destructOnExit` has
finished).

### Verification

New ASAN-only case in `test/bake/dev/server-sourcemap.test.ts` runs the
dev server with `Malloc=1` + `BUN_DESTRUCT_VM_ON_EXIT=1` so ASAN poisons
the swept global-object cell, making the UAF deterministic. Added an
`env` option to the `devTest` harness so the test can set those for the
spawned dev server.

```
# fail-before (src/ stashed)
SUMMARY: AddressSanitizer: heap-use-after-free ZigGlobalObject.h:353:48 in Zig::GlobalObject::bunVM() const
(fail)  DEV:server-sourcemap-5: DevServerSourceProvider destructor does not touch the swept global object on process exit

# pass-after
(pass)  DEV:server-sourcemap-5: DevServerSourceProvider destructor does not touch the swept global object on process exit
```

`test/bake/dev/server-sourcemap.test.ts` (5 tests) and
`test/bake/dev/request-cookies.test.ts` (2 tests) are green.
`request-cookies.test.ts` now also passes under the full CI LeakSan
config (`BUN_DESTRUCT_VM_ON_EXIT=1` + `detect_leaks=1`).

The bug is from a89e61f (#22138), which introduced
`DevServerSourceProvider` with the raw global-object pointer.

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 1 · 3 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/request-cookies.test.ts test/bake/dev/server-sourcemap.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (722d6f0)

test/bake/dev/server-sourcemap.test.ts:
Dev server testing directory: /tmp/bun-dev-test-tI2Y82
bun add v1.4.0 (722d6f0)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

installed react@0.0.0-experimental-603e6108-20241029
installed react-dom@0.0.0-experimental-603e6108-20241029
installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029
installed react-refresh@0.0.0-experimental-603e6108-20241029

6 packages installed [462.00ms]
bun install v1.4.0 (722d6f0)

Checked 6 installs across 7 packages (no changes) [167.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:37377
�[0;30mdev|�[0m �[32mBundled page in 2125ms�[0m�[2m:�[0
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (1498d7b)

test/bake/dev/server-sourcemap.test.ts:
Dev server testing directory: /tmp/bun-dev-test-7LPeWv
bun add v1.4.0-canary.1 (1498d7b)
Resolving dependencies
Resolved, downloaded and extracted [0]
Saved lockfile

installed react@0.0.0-experimental-603e6108-20241029
installed react-dom@0.0.0-experimental-603e6108-20241029
installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029
installed react-refresh@0.0.0-experimental-603e6108-20241029

6 packages installed [9.00ms]
bun install v1.4.0-canary.1 (1498d7b)

Checked 6 installs across 7 packages (no changes) [0.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:43275
�[0;30mdev|�[0m �[32mBundled page in 47ms�[0m�[2m:�[0m pages/[...slug].tsx �[2m+ 2 more�[0m
�[0;30mdev|�[0m �[0m�[1m1 |�[0m �[0m�[35mexport�[0m �[0m�[35mdefault�[0m �[0m�[35masync�[0m �[0m�[35mfunction�[0m MyPage(params) {
�[0;30mdev|�[0m �[0m�[1m2 |�[0m   myFunc()�[0m�[2m;�[0m
�[0;30mdev|�[0m �[0m�[1m3 |�[0m   �[0m�[35mreturn�[0m �[0m<�[0mh1>{JSON�[0m�[3m�[1m.stringify�[0m(params)}�[0m<�[0m/h1>�[0m�[2m;�[0m
�[0;30mdev|�[0m �[0m�[1m4 |�[0m }
�[0;30mdev|�[0m �[0m�[1m5 |�[0m 
�[0;30mdev|�[0m �[0m�
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bake/dev/request-cookies.test.ts test/bake/dev/server-sourcemap.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (722d6f0)

test/bake/dev/server-sourcemap.test.ts:
Dev server testing directory: /tmp/bun-dev-test-dkR1se
bun add v1.4.0 (722d6f0)
Resolving dependencies
Resolved, downloaded and extracted [0]
Saved lockfile

installed react@0.0.0-experimental-603e6108-20241029
installed react-dom@0.0.0-experimental-603e6108-20241029
installed react-server-dom-bun@0.0.0-experimental-603e6108-20241029
installed react-refresh@0.0.0-experimental-603e6108-20241029

6 packages installed [119.00ms]
bun install v1.4.0 (722d6f0)

Checked 6 installs across 7 packages (no changes) [97.00ms]
�[0;30mdev|�[0m Started development server: http://localhost:44249
�[0;30mdev|�[0m �[32mBundled page in 2351ms�[0m�[2m:�[0m
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 690ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/runtime/bake/DevServerSourceProvider.h | 13 +++++++-----
 test/bake/bake-harness.ts                  |  5 +++++
 test/bake/dev/server-sourcemap.test.ts     | 34 ++++++++++++++++++++++++++++++
 3 files changed, 47 insertions(+), 5 deletions(-)
```

</details>

**gate history** · 1 passed · 0 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                        reads  edits  tests
src/runtime/bake/DevServerSourceProvider.h      1      2      0
test/bake/bake-harness.ts                       8      2      0
test/bake/dev/server-sourcemap.test.ts          1      4      0
```

</details>

<!-- robobun:evidence:end -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants