Skip to content

Fix segmentation fault during building stack traces string - #22902

Merged
Jarred-Sumner merged 13 commits into
mainfrom
fix-segv-during-stack-frame-building
Oct 4, 2025
Merged

Jarred-Sumner merged 13 commits into
mainfrom
fix-segv-during-stack-frame-building

Conversation

@sosukesuzuki

@sosukesuzuki sosukesuzuki commented Sep 23, 2025 •

Copy link
Copy Markdown
Member

What does this PR do?

Bun sometimes crashes with a segmentation fault while generating stack traces.

the following might be happening in remapZigException:

  1. The first populateStackTrace (OnlyPosition) sets frames_len (e.g., frames_len = 5)
    trace.frames_len = frame_i;
[frame1, frame2, frame3, frame4, frame5]
  1. Frame filtering in remapZigException reduces frames_len (e.g., frames_len = 3)
    if (start_index) |k| {
    var j = k;
    for (frames[k..]) |frame| {
    if (frame.source_url.eqlComptime("bun:wrap") or
    frame.function_name.eqlComptime("::bunternal::"))
    {
    continue;
    }
    // Workaround for being unable to hide that specific frame without also hiding the frame before it
    if (frame.source_url.isEmpty() and NoisyBuiltinFunctionMap.getWithEql(frame.function_name, String.eqlComptime) != null) {
    continue;
    }
    frames[j] = frame;
    j += 1;
    }
    exception.stack.frames_len = @as(u8, @truncate(j));
    frames.len = j;
[frame1, frame4, frame5, (frame4, frame5)] 
// frame2 and frame3 are removed by filtering; frames_len is set to 3 here, but frame4 and frame5 remain in their original positions
  1. The second populateStackTrace (OnlySourceLine) increases frames_len (e.g., frames_len = 5)
    trace.frames_len = frame_i;
[frame1, frame4, frame5, frame4, frame5]

When deinit is executed on these frames, the ref count is excessively decremented (for frame4 and frame5), resulting in a UAF.

How did you verify your code works?

WIP. I'm working on creating minimal reproduction code.

However, I've confirmed that twenty-server tests passes with this PR.

@robobun

robobun commented Sep 23, 2025 •

Copy link
Copy Markdown
Collaborator
Updated 2:42 AM PT - Oct 3rd, 2025

❌ @Jarred-Sumner, your commit 8f3d7ea has 3 failures in Build #27858 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 22902

That installs a local version of the PR into your bun-22902 executable, so you can run:

bun-22902 --bun

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Note that in each of these three call sites in bindings.cpp, we are incrementing the reference count on the function name string.

4397,18:             frame.function_name = Bun::toStringRef(name);
4914,32:                         current.function_name = Bun::toStringRef(functionName);
4914,32:                         current.function_name = Bun::toStringRef(functionName);

So I think somewhere we are incorrectly double decrementing it, and this PR Is likely a workaround but not the correct fix

@sosukesuzuki

Copy link
Copy Markdown
Member Author

@Jarred-Sumner I know that, but don't we have to increment the ref count when we copy the frame as well as when we first set the name?

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

This leaks the non-builtin function names.
Adding this to: populateStackFrame in bindings.cpp:

   if (frame.function_name.tag == BunStringTag::WTFStringImpl) {
        printf("[%zu] function name: %s\n", frame.function_name.impl.wtf->refCount(), frame.function_name.toWTFString().utf8().data());
    }

This branch:

[10006] function name: loadAndEvaluateModule
[10004] function name: Hello
[20003] function name: moduleEvaluation
[20004] function name: moduleEvaluation
[10007] function name: loadAndEvaluateModule
[10005] function name: Hello
[20005] function name: moduleEvaluation
[20006] function name: moduleEvaluation

Main:

[20001] function name: moduleEvaluation
[20002] function name: moduleEvaluation
[9] function name: loadAndEvaluateModule
[6] function name: Hello
[20003] function name: moduleEvaluation
[20004] function name: moduleEvaluation
[9] function name: loadAndEvaluateModule
[6] function name: Hello
[20005] function name: moduleEvaluation
[20006] function name: moduleEvaluation
[9] function name: loadAndEvaluateModule

Script:

function Hello() {
  throw new Error("Hello");
}

try {
  Hello();
} catch (e) {
  Bun.gc(true);
  for (let i = 0; i < 10000; i++) {
    reportError(e);
  }
}

Run:

bun-debug error.js 2> /dev/null > out.txt

@sosukesuzuki
sosukesuzuki force-pushed the fix-segv-during-stack-frame-building branch from effb642 to e1d81c1 Compare September 27, 2025 09:36
@sosukesuzuki
sosukesuzuki force-pushed the fix-segv-during-stack-frame-building branch from e1d81c1 to 679524b Compare September 28, 2025 08:33
@sosukesuzuki
sosukesuzuki marked this pull request as ready for review September 28, 2025 09:15
@coderabbitai

coderabbitai Bot commented Sep 28, 2025 •

Copy link
Copy Markdown
Contributor

Walkthrough

Removed a Log method and a local alias in src/logger.zig. Added a per-frame JSC stack-frame index field and updated populateStackTrace control flow to set and consume that index across OnlyPosition and OnlySourceLines paths; no public function signatures changed.

Changes

Cohort / File(s) Summary of Changes
Logger API & imports
src/logger.zig
Removed toZigException(this: *const Log, allocator: std.mem.Allocator) *js.ZigException.Holder. Removed local alias const js = bun.jsc; and retained const jsc = bun.jsc;.
Stack-trace population logic
src/bun.js/bindings/bindings.cpp
Reworked populateStackTrace to branch on PopulateStackTraceFlags (OnlyPosition vs OnlySourceLines). In OnlyPosition path, assign per-frame jsc_stack_frame_index and manage separate frame counters; in OnlySourceLines path, consume stored jsc_stack_frame_index to fetch source lines with assertions. Internal control flow and indexing changed; public signatures unchanged.
ZigStackFrame record (Zig + C headers)
src/bun.js/bindings/ZigStackFrame.zig, src/bun.js/bindings/headers-handwritten.h
Added jsc_stack_frame_index field (i32 / int32_t) with default -1 and updated Zero/constructor initialization to set -1.

Possibly related PRs

Suggested reviewers

  • Jarred-Sumner
  • taylordotfish
  • alii

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title succinctly states the main goal of the changes, namely fixing a segmentation fault that occurs while building stack trace strings, which aligns directly with the reported issue and the code modifications in populateStackTrace and related structures. It is clear, specific, and concise without extraneous detail.
Description Check ✅ Passed The pull request description follows the required template by including clear sections for what the PR does and how it was verified, with a detailed explanation of the segmentation fault scenario and confirmation that the twenty-server tests pass. Both template headings are present and populated with relevant information, making the description complete and informative despite the minimal repro code being a work in progress.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-segv-during-stack-frame-building

Comment @coderabbitai help to get the list of available commands and usage tips.

@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

📜 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 cf13671 and 679524b.

📒 Files selected for processing (2)
  • src/bun.js/VirtualMachine.zig (1 hunks)
  • src/logger.zig (0 hunks)
💤 Files with no reviewable changes (1)
  • src/logger.zig
🧰 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/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
🧠 Learnings (7)
📓 Common learnings
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
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/building-bun.mdc:0-0
Timestamp: 2025-08-30T00:07:54.573Z
Learning: Applies to src/**/*.zig : Implement debug logs in Zig using `const log = bun.Output.scoped(.${SCOPE}, false);` and invoking `log("...", .{})`
Learnt from: CR
PR: oven-sh/bun#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-08T04:44:59.101Z
Learning: For platform-specific changes, run bun run zig:check-all to compile on all platforms
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 : Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions
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(...)
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/{**/js_*.zig,bun.js/api/**/*.zig} : Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions
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 : In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS<ClassName> and re-export toJS/fromJS/fromJSDirect
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 : Use parameter name globalObject (not ctx) and accept (*JSC.JSGlobalObject, *JSC.CallFrame) in binding methods/constructors
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 src/bun.js/bindings/ZigGlobalObject.h : When there is a class, prototype, and constructor, add a JSC::LazyClassStructure field for the class to ZigGlobalObject.h
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/**/js_*.zig : Implement JavaScript bindings in a Zig file named with a js_ prefix (e.g., js_smtp.zig, js_your_feature.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/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/bindings/generated_classes_list.zig : Update src/bun.js/bindings/generated_classes_list.zig to include new classes

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 : Constructors and prototype methods should return bun.JSError!JSC.JSValue to integrate Zig error handling with JS exceptions

Applied to files:

  • src/bun.js/VirtualMachine.zig
📚 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: For platform-specific changes, run bun run zig:check-all to compile on all platforms

Applied to files:

  • src/bun.js/VirtualMachine.zig
📚 Learning: 2025-09-02T18:29:58.304Z
Learnt from: taylordotfish
PR: oven-sh/bun#22227
File: src/memory.zig:37-43
Timestamp: 2025-09-02T18:29:58.304Z
Learning: In bun's `src/memory.zig`, the `initDefault` function intentionally calls `T.init()` without parameter validation to cause compile-time errors when `init` requires arguments or returns an error union. This design forces developers to add explicit `initDefault` methods for complex types rather than silently falling back to `{}`. The `{}` fallback is only intended for types that have no `init` function at all.

Applied to files:

  • src/bun.js/VirtualMachine.zig
📚 Learning: 2025-08-30T00:11:57.076Z
Learnt from: CR
PR: oven-sh/bun#0
File: .cursor/rules/registering-bun-modules.mdc:0-0
Timestamp: 2025-08-30T00:11:57.076Z
Learning: Applies to src/{**/js_*.zig,bun.js/api/**/*.zig} : Use bun.JSError!JSValue for proper error propagation in JS-exposed Zig functions

Applied to files:

  • src/bun.js/VirtualMachine.zig

Comment thread src/bun.js/VirtualMachine.zig Outdated
Comment on lines 2693 to 2695
@memset(frames[j..], jsc.ZigStackFrame.Zero);
exception.stack.frames_len = @as(u8, @truncate(j));
frames.len = j;

@coderabbitai coderabbitai Bot Sep 28, 2025 •

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 | 🟠 Major

Avoid zeroing ZigStackFrame without releasing its string refs.

ZigStackFrame owns bun.String fields (function_name, source_url, etc.) whose refcounts are maintained manually. Blasting the trailing slice with @memset(..., .Zero) drops those pointers without calling .deref(), so the underlying WTF::StringImpl instances never release their refs and the counts climb each time we build a stack trace. Please walk the discarded frames and deref their strings (or call the appropriate deinit helper) before zeroing them out.

🤖 Prompt for AI Agents
In src/bun.js/VirtualMachine.zig around lines 2693-2695, before zeroing the
trailing slice (frames[j..]) and shrinking the lengths, iterate over the frames
that are being discarded and release their owned bun.String fields (e.g.,
function_name, source_url, module_name, etc.) by calling the appropriate
deinit/deref helper for each field or a ZigStackFrame.deinit(&frame) helper,
then perform the @memset and update exception.stack.frames_len and frames.len;
this ensures the underlying WTF::StringImpl refs are decremented before the
pointers are overwritten.

@sosukesuzuki sosukesuzuki Sep 28, 2025 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The frames are copied WITHOUT incrementing refcounts during compaction:
frames[j] = frame; // Simple assignment, no ref() call

After compaction, frames[0..j] and frames[j..] may contain duplicate
pointers to the same StringImpl. Calling deref() on frames[j..] would
incorrectly decrement refcounts for StringImpls still referenced by
frames[0..j], causing use-after-free.

The memset(.Zero) is correct - it clears duplicate pointers without
touching refcounts, preventing double-deref when the Holder is reused.

What do you think?

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread src/bun.js/VirtualMachine.zig Outdated
j += 1;
}

@memset(frames[j..], jsc.ZigStackFrame.Zero);

@Jarred-Sumner Jarred-Sumner Sep 28, 2025 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is this the correct fix? remember that zig doesn't have automatic constructor/destructors, so unless we are calling deref on frames outside the bounds of .len, this change should not do anything. and if that is what's happening, then it should be fixed at those call sites and not at this call site.

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm still confused why this would fix it

@sosukesuzuki
sosukesuzuki force-pushed the fix-segv-during-stack-frame-building branch from 6840ea5 to 4a0ae70 Compare September 29, 2025 05:24
@sosukesuzuki
sosukesuzuki force-pushed the fix-segv-during-stack-frame-building branch from 4a0ae70 to b858187 Compare September 29, 2025 07:34
@sosukesuzuki
sosukesuzuki marked this pull request as draft October 3, 2025 02:51
@sosukesuzuki
sosukesuzuki marked this pull request as ready for review October 4, 2025 05:49

@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

🧹 Nitpick comments (1)
src/bun.js/bindings/bindings.cpp (1)

4798-4804: Guard jsc_stack_frame_index before use in release builds

At the moment we only ASSERT that jsc_stack_frame_index is within bounds before indexing frames[...]. Those ASSERTs disappear in release builds, so if this value ever remains at the sentinel -1 (or otherwise drifts out of range) we’ll index with an invalid size_t and immediately crash—exactly the class of failure this work is trying to eradicate. Let’s keep the assertion for debug, but add a runtime guard (or upgrade it to a RELEASE_ASSERT) so production builds fail safe.

     } else if (flags == PopulateStackTraceFlags::OnlySourceLines) {
         for (uint8_t i = 0; i < trace.frames_len; i++) {
             ZigStackFrame& frame = trace.frames_ptr[i];
-            // A call with flags set to OnlySourceLines always follows a call with flags set to OnlyPosition,
-            // so jsc_stack_frame_index is always a valid value here.
-            ASSERT(frame.jsc_stack_frame_index >= 0);
-            ASSERT(static_cast<size_t>(frame.jsc_stack_frame_index) < frames.size());
-            populateStackFrame(vm, trace, frames[frame.jsc_stack_frame_index], frame, i == 0, &trace.referenced_source_provider, globalObject, flags);
+            // A call with flags set to OnlySourceLines always follows a call with flags set to OnlyPosition,
+            // but guard in release builds as well.
+            ASSERT(frame.jsc_stack_frame_index >= 0);
+            ASSERT(static_cast<size_t>(frame.jsc_stack_frame_index) < frames.size());
+            int32_t stackFrameIndex = frame.jsc_stack_frame_index;
+            if (stackFrameIndex < 0 || static_cast<size_t>(stackFrameIndex) >= frames.size())
+                continue;
+            populateStackFrame(vm, trace, frames[stackFrameIndex], frame, i == 0, &trace.referenced_source_provider, globalObject, flags);
         }
     }
📜 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 15e54ce and 8f3d7ea.

📒 Files selected for processing (2)
  • src/bun.js/bindings/bindings.cpp (1 hunks)
  • src/bun.js/bindings/headers-handwritten.h (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/headers-handwritten.h
  • 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/headers-handwritten.h
  • 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
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 (3)
📚 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 src/bun.js/bindings/ZigGlobalObject.h : When there is a class, prototype, and constructor, add a JSC::LazyClassStructure field for the class to ZigGlobalObject.h

Applied to files:

  • src/bun.js/bindings/headers-handwritten.h
📚 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 : In Zig binding structs, expose generated bindings via pub const js = JSC.Codegen.JS<ClassName> and re-export toJS/fromJS/fromJSDirect

Applied to files:

  • src/bun.js/bindings/headers-handwritten.h
📚 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/headers-handwritten.h

@Jarred-Sumner
Jarred-Sumner merged commit 578a47c into main Oct 4, 2025
59 of 61 checks passed
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.

3 participants