Previously, `napi_create_external_buffer` used a two-stage finalization
approach: it created the ArrayBuffer with an empty finalizer, then added
the real finalizer separately via `vm.heap.addFinalizer()`. This caused
a race condition where native code could free/reuse the underlying memory
while Bun still held a reference to the ArrayBuffer.
The fix passes the finalizer directly to `ArrayBuffer::createFromBytes`,
matching the approach already used by `napi_create_external_arraybuffer`.
This ties the cleanup correctly to the ArrayBuffer's lifecycle.
Fixes#26423
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- WTFMove → WTF::move / std::move: Replaced WTFMove() macro with
WTF::move() function for WTF types, std::move() for std types
- SortedArrayMap removed: Replaced with if-else chains in
EventFactory.cpp, JSCryptoKeyUsage.cpp
- Wasm::Memory::create signature changed: Removed VM parameter
- URLPattern allocation: Changed from WTF_MAKE_ISO_ALLOCATED to
WTF_MAKE_TZONE_ALLOCATED
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## Summary
Fixes an issue where loading the same native module
(NODE_MODULE_CONTEXT_AWARE) multiple times would fail with:
```
symbol 'napi_register_module_v1' not found in native module
```
Fixes https://github.com/oven-sh/bun/issues/23136
Fixes https://github.com/oven-sh/bun/issues/21432
## Root Cause
When a native module is loaded for the first time:
1. `dlopen()` loads the shared library
2. Static constructors run and call `node_module_register()`
3. The module registers successfully
On subsequent loads of the same module:
1. `dlopen()` returns the same handle (library already loaded)
2. Static constructors **do not run again**
3. No registration occurs, leading to the "symbol not found" error
## Solution
Implemented a thread-safe `DLHandleMap` to cache and replay module
registrations:
1. **Thread-local storage** captures the `node_module*` during static
constructor execution
2. **After successful first load**, save the registration to the global
map
3. **On subsequent loads**, look up the cached registration and replay
it
This approach matches Node.js's `global_handle_map` implementation.
## Changes
- Created `src/bun.js/bindings/DLHandleMap.h` - thread-safe singleton
cache
- Added thread-local storage in `src/bun.js/bindings/v8/node.cpp`
- Modified `src/bun.js/bindings/BunProcess.cpp` to save/lookup cached
modules
- Also includes the exports fix (using `toObject()` to match Node.js
behavior)
## Test Plan
Added `test/js/node/process/dlopen-duplicate-load.test.ts` with tests
that:
- Build a native addon using node-gyp
- Load it twice with `process.dlopen`
- Verify both loads succeed
- Test with different exports objects
All tests pass.
## Related Issue
Fixes the second bug discovered in the segfault investigation.
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
## Summary
- Fix `napi_typeof` to return `napi_object` for boxed String objects
(`new String("hello")`) instead of incorrectly returning `napi_string`
- Add regression test for boxed primitive objects (String, Number,
Boolean)
The issue was that `StringObjectType` and `DerivedStringObjectType` JSC
cell types were falling through to return `napi_string`, but these
represent object wrappers around strings, not primitive strings.
## Test plan
- [x] `bun bd test test/napi/napi.test.ts -t "napi_typeof"` passes
- [x] Test fails with `USE_SYSTEM_BUN=1` (confirming the bug exists in
released version)
Fixes#25351🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Fixes a segfault that occurred when calling `process.dlopen` with
`null`, `undefined`, or primitive values for `exports`.
Previously, this would cause a crash at address `0x00000000` in
`node_module_register` due to dereferencing an uninitialized
`strongExportsObject`.
## Changes
- Modified `src/bun.js/bindings/v8/node.cpp` to use JSC's `toObject()`
instead of manual type checking
- This matches Node.js `ToObject()` behavior:
- Throws `TypeError` for `null`/`undefined`
- Creates wrapper objects for primitives
- Preserves existing objects
## Test Plan
Added `test/js/node/process/dlopen-non-object-exports.test.ts` with
three test cases:
- Null exports (should throw)
- Undefined exports (should throw)
- Primitive exports (should create wrapper)
All tests pass with the fix.
## Related Issue
Fixes the first bug discovered in the segfault investigation.
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
### What does this PR do?
Refactored NAPI property and element access to use inline methods and
improved error handling. Added comprehensive tests for default value
behavior and numeric string key operations in NAPI, ensuring correct
handling of missing properties, integer keys, and property deletion.
Updated TypeScript tests to cover new scenarios.
### How did you verify your code works?
Tests
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
### What does this PR do?
When `napi_create_external_buffer` receives empty input, the returned
buffer should be detached.
This fixes the remaining tests in `ref-napi` other than three that use a
few uv symbols
<img width="329" height="159" alt="Screenshot 2025-11-01 at 8 38 01 PM"
src="https://github.com/user-attachments/assets/2c75f937-79c5-467a-bde3-44e45e05d9a0"
/>
### How did you verify your code works?
Added tests for correct values from `napi_get_buffer_info`,
`napi_get_arraybuffer_info`, and `napi_is_detached_arraybuffer` when
given an empty buffer from `napi_create_external_buffer`
---------
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
### What does this PR do?
If the input was empty `ArrayBuffer::createFromBytes` would create a
buffer that `JSUint8Array::create` would see as detached, so it would
throw an exception. This would likely cause crashes in `node-addon-api`
because finalize data is freed if `napi_create_external_buffer` fails,
and we already setup the finalizer.
Example of creating empty buffer:
a7f62a4caa/src/binding.cc (L687)fixes#6737fixes#10965fixes#12331fixes#12937fixes#13622
most likely fixes#14822
### How did you verify your code works?
Manually and added tests.
### What does this PR do?
Replaces raw napi_env pointers with WTF::Ref<NapiEnv> for improved
memory management and safety. Updates related classes, function
signatures, and finalizer handling to use reference counting. Adds
ref/deref methods to NapiEnv and integrates them in Zig and C++ code
paths, ensuring proper lifecycle management for N-API environments.
### How did you verify your code works?
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Alistair Smith <hi@alistair.sh>
### What does this PR do?
- **Use `Latin1Character` instead of `LChar`**
- **Fix for
0875bc8f62**
### How did you verify your code works?
---
# WebKit Update Summary (September 2025)
## Overview
This document summarizes the major changes in WebKit/JavaScriptCore from
the September 2025 update. The update includes approximately 254
JSC-related commits with significant improvements to performance,
stability, and developer experience.
## Critical Bug Fixes
### Memory Safety
- **operationMaterializeObjectInOSR fix** (5c7aadfa0a96): Fixed
uninitialized Butterfly storage during OSR exits with sunk Array
allocations. This prevents potential crashes when arrays with holes are
materialized during OSR exit.
- **FTL materialization fixes** (a72d19840714, ed1e6fe03899): Added
missing internal object type handling in FTL materialization, improving
stability during optimization bailouts.
### Promise and Async Improvements
- **JSPromiseReaction object** (a1cb5e087a46, later reverted in
b0566a4db201): Initially introduced to improve promise reaction handling
but was reverted due to compatibility issues with Bun's modifications.
- **Async stack traces enhancements**:
- Added support for `Promise.any` in async stack traces (d9a997b3edaa)
- Added empty JSValue checking for async stack trace safety
(9d26223d4bcb)
- Promise.all support was added and later reverted due to performance
concerns
## Performance Optimizations
### JIT Compiler Improvements
- **B3 Immutable Loads** (570a3530f949, 62300f8db3d9): Added
immutability annotations and CSE optimizations for loads that can look
for targets in dominators
- **BBQ JIT enhancements**:
- Fixed callee-save register handling (c7ae05719045)
- Simplified F32 copysign operations (e0651af57025)
- **DFG optimizations**:
- Fixed RegExp constant folding with materialized NewRegExp nodes
(7b53a04a5afa)
- Improved RegExp object node handling in strength reduction
(eeb65e05095b)
### WebAssembly Improvements
- **WASM SIMD Support**:
- Added v128 support for IPInt call and tail-call instructions
(73f0c9d430cb)
- Implemented v128 support in local.get, local.set, global.get,
global.set (67d7bf15139a)
- Added x86_64 SIMD integer arithmetic and float instructions
- **WASM Memory Management**:
- Introduced WasmInstanceAnchor for better instance lifecycle management
(f9f1ed183bf7)
- Attached AbstractHeap to wasm memory access for better optimization
(f183c6f7def4)
- Added signal handling for null checks in wasm (bf18b5b709f3)
- **WASM Debugging**: Added LLDB debugging infrastructure for
WebAssembly (e03c10225cc8)
## API and Language Features
### Iterator Helpers
- Merged `Iterator.prototype.sliding` into `Iterator.prototype.windows`
(1d49e823702d)
- Optimized iterator next method calls using CachedCall (5ee92514060c)
### Math Extensions
- Improved performance of `Math.sumPrecise` implementation
(602294057337)
### Error Handling
- Enhanced error messages for for-of loops without Symbol.iterator
(0051bbf2491f)
## Infrastructure Changes
### Character Type Refactoring
- **LChar to Latin1Character rename** (63b97b511366, 1424f0687876):
Major refactoring replacing the `LChar` type with `Latin1Character`
throughout the codebase for better clarity
- Additional fixes for Latin1Character usage (711eab3243f0,
50bf8e6fd4ca, 88e29ab76aec)
### Build System
- Fixed builds with GCC 15.x (e33b18bc59d6)
- Added gitattributes for JSC test files (82c4cc796da6)
- Improved test runner with comprehensive verbose logging (7ef95c177a42)
- Added memory-limited annotations for tests using excessive memory
(b991cd17d612)
### Testing Infrastructure
- Improved handling of missing test executables (db1e3bbb3be2)
- Added support for non-customized ICU 74.2 in intl tests (c922a28b6642)
- Fixed various test configuration issues and timeouts
## Bun-Specific Modifications
### Preserved Customizations
- Maintained `BUN_JSC_ADDITIONS` for Bun-specific features
- Kept async context support for AsyncLocalStorage
- Preserved V8 heap snapshot compatibility layer
- Maintained custom inspector extensions
### Conflicts Resolved
- Successfully merged upstream changes while preserving Bun's event loop
integration
- Resolved conflicts in promise handling while maintaining Bun's async
behavior
- Fixed re-declaration issues with `isAsyncFrame` for async stack traces
## Breaking Changes and Reverts
### Reverted Features
1. **JSPromiseReaction object**: Reverted due to conflicts with Bun's
promise handling
2. **Promise.all async stack trace support**: Reverted due to ~4%
performance regression in JetStream3/doxbee-async benchmark
3. **Array.prototype.flat C++ implementation**: Reverted (reason not
specified in commit)
## Security Improvements
- Type safety improvements with uncheckedDowncast for Wasm::Callee
(48425afd643d)
- Added bounds checking and validation for Wasm array operations
(b5148db1c4c1)
- Improved memory safety with proper initialization of materialized
objects
## Platform Support
- macOS: Continued support for x64/arm64
- Linux: Maintained glibc/musl compatibility
- Windows: Preserved x64 support
- Fixed platform-specific alignment issues for x86_64 (94a60eb123c5)
## Notable Debugging Enhancements
- LLDB infrastructure for WebAssembly debugging
- Improved verbose command logging in test runners
- Enhanced stack trace capabilities for async functions
- Better error reporting for missing Symbol.iterator
## Performance Metrics
- Several memory optimizations for test execution
- JIT memory reservation size adjustments for debug builds
- Optimized iterator operations with cached calls
- Improved Math.sumPrecise performance
## Future Considerations
- The JSPromiseReaction implementation may need revisiting with adjusted
architecture
- Async stack trace support for Promise.all requires performance
optimization
- Continued work on WASM SIMD support for additional operations
## Migration Notes for Bun Team
1. **LChar usage**: All references to `LChar` have been replaced with
`Latin1Character`
2. **Promise handling**: The reverted JSPromiseReaction changes indicate
potential architectural conflicts that may need addressing
3. **Test configuration**: New memory-limited annotations should be used
for memory-intensive tests
4. **Build flags**: Ensure USE_BUN_JSC_ADDITIONS and USE_BUN_EVENT_LOOP
remain enabled
## Summary
- Fixed napi_strict_equals to use JavaScript === operator semantics
instead of Object.is()
- Added missing recv parameter validation in napi_call_function
- Fixed napi_create_array_with_length boundary handling to match Node.js
behavior
## Changes
### napi_strict_equals
- Changed from isSameValue (Object.is semantics) to isStrictEqual (===
semantics)
- Now correctly implements JavaScript strict equality: NaN !== NaN and
-0 === 0
- Added new JSC binding JSC__JSValue__isStrictEqual to support this
### napi_call_function
- Added NAPI_CHECK_ARG(env, recv) validation to match Node.js behavior
- Prevents crashes when recv parameter is null/undefined
### napi_create_array_with_length
- Fixed boundary value handling for negative and oversized lengths
- Now correctly clamps negative signed values to 0 (e.g., when size_t
0x80000000 becomes negative in i32)
- Matches Node.js V8 implementation which casts size_t to int then
clamps to min 0
## Test plan
- [x] Added comprehensive C++ tests in
test/napi/napi-app/standalone_tests.cpp
- [x] Added corresponding JavaScript tests in test/napi/napi.test.ts
- [x] Tests verify:
- Strict equality semantics (NaN, -0/0, normal values)
- Null recv parameter handling
- Array creation with boundary values (negative, oversized, edge cases)
🤖 Generated with [Claude Code](https://claude.ai/code)
Reverts oven-sh/bun#21962
`vm.ensureTerminationException` allocates a JSString, which is not safe
to do from a thread that doesn't own the API lock.
```ts
Bun Canary v1.2.21-canary.1 (f706382a) Linux x64 (baseline)
Linux Kernel v6.12.38 | musl
CPU: sse42 popcnt avx avx2 avx512
Args: "/var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/release/bun-linux-x64-musl-baseline-profile/bun-profile" "/var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/test/js/node/worker_threads"...
Features: bunfig http_server jsc tsconfig(3) tsconfig_paths workers_spawned(40) workers_terminated(34)
Builtins: "bun:main" "node:worker_threads"
Elapsed: 362ms | User: 518ms | Sys: 63ms
RSS: 0.34GB | Peak: 100.36MB | Commit: 0.34GB | Faults: 0 | Machine: 8.17GB
panic(main thread): Segmentation fault at address 0x0
oh no: Bun has crashed. This indicates a bug in Bun, not your code.
To send a redacted crash report to Bun's team,
please file a GitHub issue using the link below:
http://localhost:38809/1.2.21/Ba2f706382wNgkgUu11luEm6yX+lwy+Dgtt+oEurthoD8214mE___07+09DA2AA
6 | describe("Worker destruction", () => {
7 | const method = ["Bun.connect", "Bun.listen", "fetch"];
8 | describe.each(method)("bun when %s is used in a Worker that is terminating", method => {
9 | // fetch: ASAN failure
10 | test.skipIf(isBroken && method == "fetch")("exits cleanly", () => {
11 | expect([join(import.meta.dir, "worker_thread_check.ts"), method]).toRun();
^
error:
Command /var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/test/js/node/worker_threads/worker_thread_check.ts Bun.connect failed:
Spawned 10 workers RSS 79 MB
Spawned 10 workers RSS 87 MB
Spawned 10 workers RSS 90 MB
at <anonymous> (/var/lib/buildkite-agent/builds/ip-172-31-38-185/bun/bun/test/js/node/worker_threads/worker_destruction.test.ts:11:73)
✗ Worker destruction > bun when Bun.connect is used in a Worker that is terminating > exits cleanly [597.56ms]
✓ Worker destruction > bun when Bun.listen is used in a Worker that is terminating > exits cleanly [503.47ms]
» Worker destruction > bun when fetch is used in a Worker that is terminating > exits cleanly
1 pass
1 skip
1 fail
2 expect() calls
Ran 3 tests across 1 file. [1125.00ms]
======== Stack trace from GDB for bun-profile-28234.core: ========
Program terminated with signal SIGILL, Illegal instruction.
#0 crash_handler.crash () at crash_handler.zig:1523
[Current thread is 1 (LWP 28234)]
#0 crash_handler.crash () at crash_handler.zig:1523
#1 0x0000000002db77aa in crash_handler.crashHandler (reason=..., error_return_trace=0x0, begin_addr=...) at crash_handler.zig:471
#2 0x0000000002db2b55 in crash_handler.handleSegfaultPosix (sig=<optimized out>, info=<optimized out>) at crash_handler.zig:792
#3 0x0000000004716b58 in WTF::jscSignalHandler (sig=11, info=0x7ffe54051e90, ucontext=0x0) at vendor/WebKit/Source/WTF/wtf/threads/Signals.cpp:548
#4 <signal handler called>
#5 JSC::VM::currentThreadIsHoldingAPILock (this=0x148296c30000) at vendor/WebKit/Source/JavaScriptCore/runtime/VM.h:840
#6 JSC::sanitizeStackForVM (vm=...) at vendor/WebKit/Source/JavaScriptCore/runtime/VM.cpp:1369
#7 0x0000000003f4a060 in JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::{lambda()#1}::operator()() const (this=<optimized out>) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/LocalAllocatorInlines.h:46
#8 JSC::FreeList::allocateWithCellSize<JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::{lambda()#1}>(JSC::LocalAllocator::allocate(JSC::Heap&, unsigned long, JSC::GCDeferralContext*, JSC::AllocationFailureMode)::{lambda()#1} const&, unsigned long) (this=0x148296c38e48, cellSize=16, slowPath=...) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/FreeListInlines.h:46
#9 JSC::LocalAllocator::allocate (this=0x148296c38e30, heap=..., cellSize=16, deferralContext=0x0, failureMode=JSC::AllocationFailureMode::Assert) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/LocalAllocatorInlines.h:44
#10 JSC::GCClient::IsoSubspace::allocate (this=0x148296c38e30, vm=..., cellSize=16, deferralContext=0x0, failureMode=JSC::AllocationFailureMode::Assert) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/IsoSubspaceInlines.h:34
#11 JSC::tryAllocateCellHelper<JSC::JSString, (JSC::AllocationFailureMode)0> (vm=..., size=16, deferralContext=0x0) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/JSCellInlines.h:192
#12 JSC::allocateCell<JSC::JSString> (vm=..., size=16) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/JSCellInlines.h:212
#13 JSC::JSString::create (vm=..., value=...) at cache/webkit-a73e665a39b281c5/include/JavaScriptCore/JSString.h:204
#14 0x0000000004479ad1 in JSC::jsNontrivialString (vm=..., s=...) at vendor/WebKit/Source/JavaScriptCore/runtime/JSString.h:846
#15 JSC::VM::ensureTerminationException (this=0x148296c30000) at vendor/WebKit/Source/JavaScriptCore/runtime/VM.cpp:627
#16 JSGlobalObject__requestTermination (globalObject=<optimized out>) at ./build/release/./src/bun.js/bindings/ZigGlobalObject.cpp:3979
#17 0x0000000003405ab8 in bun.js.web_worker.notifyNeedTermination (this=0x542904f0d80) at /var/lib/buildkite-agent/builds/ip-172-31-16-28/bun/bun/src/bun.js/web_worker.zig:558
#18 0x0000000004362b6f in WebCore::Worker::terminate (this=0x984c900000000000) at ./src/bun.js/bindings/webcore/Worker.cpp:266
#19 WebCore::jsWorkerPrototypeFunction_terminateBody(JSC::JSGlobalObject*, JSC::CallFrame*, WebCore::JSWorker*)::{lambda()#1}::operator()() const (this=<optimized out>) at ./build/release/./src/bun.js/bindings/webcore/JSWorker.cpp:549
#20 WebCore::toJS<WebCore::IDLUndefined, WebCore::jsWorkerPrototypeFunction_terminateBody(JSC::JSGlobalObject*, JSC::CallFrame*, WebCore::JSWorker*)::{lambda()#1}>(JSC::JSGlobalObject&, JSC::ThrowScope&, WebCore::jsWorkerPrototypeFunction_terminateBody(JSC::JSGlobalObject*, JSC::CallFrame*, WebCore::JSWorker*)::{lambda()#1}&&) (lexicalGlobalObject=..., throwScope=..., valueOrFunctor=...) at ./src/bun.js/bindings/webcore/JSDOMConvertBase.h:174
#21 WebCore::jsWorkerPrototypeFunction_terminateBody (lexicalGlobalObject=<optimized out>, callFrame=<optimized out>, castedThis=<optimized out>) at ./build/release/./src/bun.js/bindings/webcore/JSWorker.cpp:549
#22 WebCore::IDLOperation<WebCore::JSWorker>::call<&WebCore::jsWorkerPrototypeFunction_terminateBody, (WebCore::CastedThisErrorBehavior)0> (lexicalGlobalObject=..., operationName=..., callFrame=...) at ./src/bun.js/bindings/webcore/JSDOMOperation.h:63
#23 WebCore::jsWorkerPrototypeFunction_terminate (lexicalGlobalObject=<optimized out>, callFrame=0x7ffe540536b8) at ./build/release/./src/bun.js/bindings/webcore/JSWorker.cpp:554
#24 0x000014825580c038 in ?? ()
#25 0x00007ffe540537b0 in ?? ()
#26 0x0000148255a626cb in ?? ()
#27 0x0000000000000000 in ?? ()
1 crashes reported during this test
```
### What does this PR do?
Calling `process.exit` inside a Web Worker now immediately notifies the
VM that it needs to terminate. Previously, `napi_call_function` would
return success in a Web Worker even if the JS code called
`process.exit`. Now it'll return `napi_pending_exception`.
### How did you verify your code works?
Ran Node's NAPI tests (`node-api/test_worker_terminate/test.js` now
passes) in addition to our own NAPI tests.
## Summary
Fixes a crash in `napi_is_exception_pending` that occurs during
environment cleanup when finalizers call this function.
The crash manifested as:
```
panic: Aborted
- napi.h:192: napi_is_exception_pending
- napi.h:516: wrap_cleanup
- napi.h:273: napi_env__::cleanup
```
## Root Cause
Bun's implementation was using `DECLARE_THROW_SCOPE` during cleanup when
JavaScript execution is not safe, and didn't follow Node.js's approach
of avoiding `NAPI_PREAMBLE` for this function.
## Changes Made
1. **Remove `NAPI_PREAMBLE_NO_THROW_SCOPE`** - Node.js explicitly states
this function "must execute when there is a pending exception"
2. **Use `DECLARE_CATCH_SCOPE`** instead of `DECLARE_THROW_SCOPE` for
safety during cleanup
3. **Add safety check** `!env->isFinishingFinalizers()` before accessing
VM
4. **Add `napi_clear_last_error` function** to match Node.js
implementation
5. **Use `napi_clear_last_error`** instead of `napi_set_last_error` for
consistent behavior
## Test Plan
Created comprehensive test that:
- ✅ **Reproduces the original crash scenario** (finalizers calling
`napi_is_exception_pending`)
- ✅ **Verifies it no longer crashes in Bun**
- ✅ **Confirms behavior matches Node.js exactly**
### Test Results
**Before fix:** Would crash with `panic: Aborted` during cleanup
**After fix:**
```
Testing napi_is_exception_pending behavior...
1. Testing basic napi_is_exception_pending:
Status: 0 (should be 0 for napi_ok)
Result: false (should be false - no exception pending)
2. Testing with pending exception:
Exception was thrown as expected: Test exception
3. Testing finalizer scenario (the crash case):
Creating object with finalizer that calls napi_is_exception_pending...
Objects created. Forcing garbage collection...
Garbage collection completed.
napi_is_exception_pending in finalizer: status=0, result=false
[...5 finalizers ran successfully...]
SUCCESS: napi_is_exception_pending works correctly in all scenarios!
```
**Node.js comparison:** Identical output and behavior confirmed.
## Impact
- **Fixes crashes** in native addons that call
`napi_is_exception_pending` in finalizers
- **Improves Node.js compatibility** by aligning implementation approach
- **No breaking changes** - only fixes crash scenario, normal usage
unchanged
The fix aligns Bun's NAPI implementation with Node.js's proven approach
for safe exception checking during environment cleanup.
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
# Fix NAPI cleanup hook behavior to match Node.js
This PR addresses critical differences in NAPI cleanup hook
implementation that cause crashes when native modules attempt to remove
cleanup hooks. The fixes ensure Bun's behavior matches Node.js exactly.
## Issues Fixed
Fixes#20835Fixes#18827Fixes#21392Fixes#21682Fixes#13253
All these issues show crashes related to NAPI cleanup hook management:
- #20835, #18827, #21392, #21682: Show "Attempted to remove a NAPI
environment cleanup hook that had never been added" crashes with
`napi_remove_env_cleanup_hook`
- #13253: Shows `napi_remove_async_cleanup_hook` crashes in the stack
trace during Vite dev server cleanup
## Key Behavioral Differences Addressed
### 1. Error Handling for Non-existent Hook Removal
- **Node.js**: Silently ignores removal of non-existent hooks (see
`node/src/cleanup_queue-inl.h:27-30`)
- **Bun Before**: Crashes with `NAPI_PERISH` error
- **Bun After**: Silently ignores, matching Node.js behavior
### 2. Duplicate Hook Prevention
- **Node.js**: Uses `CHECK_EQ` which crashes in ALL builds when adding
duplicate hooks (see `node/src/cleanup_queue-inl.h:24`)
- **Bun Before**: Used debug-only assertions
- **Bun After**: Uses `NAPI_RELEASE_ASSERT` to crash in all builds,
matching Node.js
### 3. VM Termination Checks
- **Node.js**: No VM termination checks in cleanup hook APIs
- **Bun Before**: Had VM termination checks that could cause spurious
failures
- **Bun After**: Removed VM termination checks to match Node.js
### 4. Async Cleanup Hook Handle Validation
- **Node.js**: Validates handle is not NULL before processing
- **Bun Before**: Missing NULL handle validation
- **Bun After**: Added proper NULL handle validation with
`napi_invalid_arg` return
## Execution Order Verified
Both Bun and Node.js execute cleanup hooks in LIFO order (Last In, First
Out) as expected.
## Additional Architectural Differences Identified
Two major architectural differences remain that affect compatibility but
don't cause crashes:
1. **Queue Architecture**: Node.js uses a single unified queue for all
cleanup hooks, while Bun uses separate queues for regular vs async
cleanup hooks
2. **Iteration Safety**: Different behavior when hooks are added/removed
during cleanup iteration
These will be addressed in future work as they require more extensive
architectural changes.
## Testing
- Added comprehensive test suite covering all cleanup hook scenarios
- Tests verify identical behavior between Bun and Node.js
- Includes edge cases like duplicate hooks, non-existent removal, and
execution order
- All tests pass with the current fixes
The changes ensure NAPI modules using cleanup hooks (like LMDB, native
Rust modules, etc.) work reliably without crashes.
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Kai Tamkun <kai@tamkun.io>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
### What does this PR do?
Defers exceptions thrown by NAPI code until execution returns/flows to
JS code.
### How did you verify your code works?
Ran existing NAPI tests and added to napi.test.ts.
## Summary
Fixes a bug in napi_get_value_bigint_words where the function would
return the number of words copied instead of the actual word count
needed when the provided buffer is smaller than required.
## The Problem
When napi_get_value_bigint_words was called with a buffer smaller than
the actual BigInt size, it would incorrectly return the buffer size
instead of the actual word count needed. This doesn't match Node.js
behavior.
### Example
BigInt that requires 2 words: 0x123456789ABCDEF0123456789ABCDEFn
Call with buffer for only 1 word
- Before fix: word_count = 1 (buffer size)
- After fix: word_count = 2 (actual words needed)
## The Fix
Changed napi_get_value_bigint_words to always set word_count to the
actual number of words in the BigInt, regardless of buffer size.
## Test Plan
- Added test test_bigint_word_count that verifies the word count is
correctly returned
- Added test test_ref_unref_underflow for the existing
napi_reference_unref underflow protection
- Both tests pass with the fix and match Node.js behavior
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude <noreply@anthropic.com>