Skip to content

napi: fix dropped writes through data pointers for small typed arrays - #37154

Merged
Jarred-Sumner merged 5 commits into
mainfrom
farm/1acc3647/napi-fast-typedarray-data-pointer
Aug 9, 2026
Merged

napi: fix dropped writes through data pointers for small typed arrays#37154
Jarred-Sumner merged 5 commits into
mainfrom
farm/1acc3647/napi-fast-typedarray-data-pointer

Conversation

@robobun

@robobun robobun commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #37151

NAPI-RS users reported that native writes into Uint8Arrays under ~1KB were silently dropped (data corruption), while the same code works in Node.

Repro

A native function that requests every out-param of napi_get_typedarray_info (as napi-rs does), writes through the returned data pointer, then reads the array back from JS:

node: length=16 first=42 last=42
bun:  length=16 first=0  last=0

The existing test_typedarray_info_byte_offset fixture also exposes it: for new Uint8Array(64) (no explicit ArrayBuffer), bun printed data_is_arraybuffer_data_plus_byte_offset=false where node prints true.

Cause

JSC keeps typed arrays under 1000 bytes (fastSizeLimit) in fast mode: the contents live in GC-heap storage with no ArrayBuffer. Materializing the backing buffer (possiblySharedBuffer()) copies the contents into a fresh ArrayBuffer and repoints the view, abandoning the old storage.

napi_get_typedarray_info captured vector() first and fetched the arraybuffer out-param afterwards, so the pointer it returned pointed at the abandoned storage; writes through it never reach the view. napi_get_buffer_info, napi_create_buffer and napi_create_buffer_copy had the sibling bug: they handed out fast-mode pointers that go stale as soon as anything later materializes the buffer (JS touching .buffer is enough), while Node guarantees the pointer stays valid for the object's lifetime.

Fix

Add Bun__JSValue__materializeArrayBufferViewBuffer (forces the view into wasteful mode via possiblySharedBuffer()) and call it before capturing the data pointer in the four NAPI entry points, matching Node's semantics.

Verification

New napi-app fixtures compare output against Node: write visibility through napi_get_typedarray_info data pointers (16/999/2048 bytes), and pointer stability for napi_get_buffer_info / napi_create_buffer / napi_create_buffer_copy after the backing buffer is materialized. All fail on the released bun and pass with this change; full test/napi/napi.test.ts (164 tests) and Node's js-native-api typedarray/dataview suites pass.

Also bumps the timeout on three pre-existing tests in this file that spawn 4+ sequential debug+ASAN bun processes and exceed the 5s default on slow machines.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/napi/napi.test.ts

…a pointers

JSC keeps typed arrays under 1000 bytes in fast mode, where the contents
live in GC-heap storage. Materializing the backing ArrayBuffer (e.g. via
the arraybuffer out-param of napi_get_typedarray_info, or JS touching
.buffer) copies the contents into a fresh ArrayBuffer and repoints the
view, abandoning the old storage.

napi_get_typedarray_info captured vector() before fetching the
arraybuffer out-param, so the data pointer it returned pointed at the
abandoned storage and native writes through it were silently dropped.
napi_get_buffer_info, napi_create_buffer and napi_create_buffer_copy
handed out fast-mode pointers that went stale as soon as anything
materialized the buffer.

Force the view into wasteful mode before capturing the pointer in all
four, matching Node, where the pointer stays valid for the lifetime of
the object.

Fixes #37151
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35c4d8b6-5476-4092-bc56-7d53331ef976

📥 Commits

Reviewing files that changed from the base of the PR and between c6e8b9e and 8e36faf.

📒 Files selected for processing (1)
  • test/napi/napi.test.ts

Walkthrough

The change materializes ArrayBufferView backing storage before N-API APIs expose data pointers. It adds the JSC-to-N-API binding, stable buffer allocation paths, and regression tests for pointer writes, byte offsets, and pointer stability.

Changes

ArrayBufferView pointer stability

Layer / File(s) Summary
Backing-storage materialization contract
src/jsc/JSValue.rs, src/jsc/bindings/bindings.cpp
Adds JSValue::materialize_array_buffer_view_buffer and its FFI binding. The binding reports whether backing storage exists or allocation succeeds.
N-API pointer-returning paths
src/runtime/napi/napi_body.rs, src/jsc/bindings/napi.cpp
Materializes backing storage before typed-array and buffer data pointers are exposed. Adds stable allocation and copy handling for napi_create_buffer and napi_create_buffer_copy.
Pointer-stability regression coverage
test/napi/napi-app/standalone_tests.cpp, test/napi/napi.test.ts
Adds tests for native writes, byte offsets, buffer materialization, and pointer stability. Registers standalone tests and adjusts selected subprocess execution and timeout behavior.

Possibly related PRs

  • oven-sh/bun#36568: Both changes modify ArrayBuffer or ArrayBufferView backing-storage materialization and related JSC bindings.
  • oven-sh/bun#36818: Both changes address ArrayBuffer backing-storage lifetime and stability in native bindings.
  • oven-sh/bun#37008: Both changes modify ArrayBufferView backing-buffer handling in src/jsc/bindings/bindings.cpp.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary fix for dropped writes through typed-array data pointers.
Description check ✅ Passed The description explains the bug, cause, implementation, verification, and test impact in sufficient detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@github-actions github-actions Bot added the claude label Aug 7, 2026

@claude claude 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.

I reviewed this PR and didn't find any bugs. It changes the pointer-lifetime contract NAPI hands to native addons (forcing small typed arrays out of JSC fast mode before returning vector()), which is memory-safety-adjacent enough that a human look is worthwhile.

What was reviewed:

  • Sibling coverage: napi_get_dataview_info doesn't need the fix (DataView always has a materialized buffer); napi_get_arraybuffer_info and napi_create_typedarray operate on already-materialized buffers.
  • Bun__JSValue__materializeArrayBufferViewBuffer guards non-cell/non-view inputs, so the pre-validation call in napi_get_buffer_info is safe on empty/ArrayBuffer/primitive values.
  • possiblySharedBuffer() returning nullptr on OOM leaves the view in fast mode — same as pre-PR behavior, no new failure path.
  • Perf trade-off (every NAPI-touched small array now allocates + copies into wasteful mode) is gated on the caller requesting the data pointer.
Extended reasoning...

Overview

The PR fixes silent data corruption where native addons writing through data pointers returned by napi_get_typedarray_info / napi_get_buffer_info / napi_create_buffer / napi_create_buffer_copy see their writes dropped for typed arrays under ~1KB. Root cause: JSC keeps small typed arrays in fast mode (GC-heap storage, no ArrayBuffer); materializing the backing buffer copies contents into a fresh ArrayBuffer and abandons the old storage. Bun captured vector() before materialization, handing out a soon-to-be-stale pointer. The fix adds a helper that forces possiblySharedBuffer() before capturing the pointer in all four entry points, and adds C++ test fixtures + checkSameOutput-vs-Node tests covering write visibility and pointer stability across the fast/wasteful boundary.

Security risks

None new. The change moves in the direction of stronger memory safety — it eliminates a case where native code writes to abandoned storage. The new C++ binding is defensive (dynamicDowncast, null/non-cell guards). No untrusted-input parsing is introduced.

Level of scrutiny

High. This is native memory-safety code that hands raw pointers to arbitrary third-party addons with a lifetime guarantee. REVIEW.md flags this as the most-blocked category. The fix is small and the mechanism is clearly explained, but the correctness depends on JSC internals (slowDownAndWasteMemory repointing m_vector, idempotence of possiblySharedBuffer(), DataView never being fast-mode) that a maintainer should confirm.

Other factors

  • Sibling analysis looks complete: I checked napi_get_dataview_info (DataView requires an existing ArrayBuffer, never fast-mode), napi_get_arraybuffer_info (bare ArrayBuffers have no fast mode), and napi_create_typedarray/node_api_create_buffer_from_arraybuffer (both take an existing buffer).
  • The materialize call in napi_get_buffer_info runs before the ArrayBuffer/view type check, but the C++ helper's dynamicDowncast<JSArrayBufferView> makes it a no-op for bare ArrayBuffers and non-cells, so the reject path is unchanged.
  • There is a real performance trade-off: every small typed array whose data pointer NAPI hands out now incurs an allocation + memcpy into wasteful mode. This is required for Node parity and is gated on !data.is_null(), but a maintainer should sign off on it.
  • Tests are Node-diffed via checkSameOutput, cover the 999-byte fast-mode boundary and the 2048-byte already-wasteful case, and include the pointer-stability-after-.buffer-access scenario. The three unrelated timeout bumps are commented with the debug+ASAN justification per REVIEW.md.
  • The PR description notes tests couldn't run locally (platform-specific), so CI is the verification of record — another reason for a human check before merge.

@robobun

robobun commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. One correction to the summary: the tests did run locally. The new fixtures fail against the released bun (writes invisible, node shows them) and pass with this change, and the full test/napi/napi.test.ts (164 tests) plus Node's js-native-api typedarray and dataview suites are green with a debug+ASAN build.

@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.

This isn't the correct fix. The correct fix is to create the JSC::ArrayBuffer and WTFMove that. What you have here allocates the ArrayBuffer twice.

@adeelaliasd

Copy link
Copy Markdown

Thanks @Jarred-Sumner This is essential for my framework to work. Rust Engine -> Bun

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun use JSC::ArrayBuffer. Fix.

Comment thread src/jsc/JSValue.rs Outdated
Comment thread src/jsc/bindings/napi.cpp Outdated
Comment thread src/runtime/napi/napi_body.rs Outdated
Comment thread src/runtime/napi/napi_body.rs Outdated
Comment thread src/runtime/napi/napi_body.rs Outdated

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/napi/napi_body.rs`:
- Around line 1448-1454: Propagate backing-storage materialization failures
instead of returning success with invalidated pointers: in
src/runtime/napi/napi_body.rs lines 1448-1454, preserve invalid-input handling
and fail before writing maybe_data when
typedarray.materialize_array_buffer_view_buffer() fails; likewise fail before
writing result_data at lines 2062-2066 and data at lines 2102-2106 when buffer
materialization fails. In src/jsc/bindings/napi.cpp lines 2282-2285, check
possiblySharedBuffer() and return failure before reading typedVector().
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f65df2ba-9692-4258-9a14-9b39d9711f1c

📥 Commits

Reviewing files that changed from the base of the PR and between 0c35ccc and b407ed6.

📒 Files selected for processing (6)
  • src/jsc/JSValue.rs
  • src/jsc/bindings/bindings.cpp
  • src/jsc/bindings/napi.cpp
  • src/runtime/napi/napi_body.rs
  • test/napi/napi-app/standalone_tests.cpp
  • test/napi/napi.test.ts

Comment thread src/runtime/napi/napi_body.rs Outdated
Comment thread test/napi/napi.test.ts Outdated
Comment thread src/runtime/napi/napi_body.rs Outdated
Instead of creating a fast-mode view and converting it (two allocations
plus a copy), napi_create_buffer and napi_create_buffer_copy now create
the JSC::ArrayBuffer up front and move it into the view. Throw OOM when
the allocation fails, matching Node's pending-exception behavior.

The accessor paths keep the one-time materialization for JS-created
views and now fail instead of handing out a pointer when it fails.
@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

Reworked in ce36053: napi_create_buffer and napi_create_buffer_copy now create the JSC::ArrayBuffer up front and move it into the view, so it is one allocation with no copy and the view is never in fast mode. The accessor paths (napi_get_typedarray_info, napi_get_buffer_info) keep the one-time possiblySharedBuffer() materialization since those views were created by JS and already sit in fast-mode GC storage; that conversion is the same one a JS .buffer access performs. Full napi suite passes locally, including the Node status-code alignment test (allocation failure now leaves a pending OOM exception like Node).

Comment thread src/jsc/JSValue.rs
Comment thread src/jsc/bindings/napi.cpp
Comment thread src/runtime/napi/napi_body.rs
Comment thread src/jsc/bindings/napi.cpp
Comment thread test/napi/napi.test.ts Outdated
…ze new tests

Nesting a ThrowScope inside NAPI_PREAMBLE's TopExceptionScope trips
validateExceptionChecks; follow the napi_create_bigint_words shape
instead and drop the unreachable trailing return. Run the new tests'
independent checkSameOutput calls concurrently.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/napi.cpp`:
- Around line 2324-2326: In the relevant N-API binding function, validate that
data is non-null whenever length > 0 before allocating the ArrayBuffer or
calling memcpy; return napi_invalid_arg immediately for a null source, while
preserving the existing copy behavior for valid inputs.

In `@test/napi/napi.test.ts`:
- Around line 925-930: Add the exact 1000-byte case to the size variants in the
Promise.all test for checkSameOutput("test_typedarray_info_write_visibility"),
preserving the existing expected length/visibility assertions so the
fast-storage cutoff boundary is covered.
- Around line 933-940: Update the test case around checkSameOutput for typed
arrays to include views created with nonzero byte offsets, such as subarrays or
typed-array constructors over an offset buffer. Keep the existing zero-offset
and small-size cases, and assert the same data-pointer relationship for the
offset views so incorrect byte-offset calculations are detected.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 719e2a51-0f43-4b5a-82bf-4c6099a49e97

📥 Commits

Reviewing files that changed from the base of the PR and between ce36053 and c6e8b9e.

📒 Files selected for processing (2)
  • src/jsc/bindings/napi.cpp
  • test/napi/napi.test.ts

Comment thread src/jsc/bindings/napi.cpp
Comment thread test/napi/napi.test.ts
Comment thread test/napi/napi.test.ts
@robobun

robobun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the remaining red lane is test/napi/napi.test.ts on macOS 14 x64, failing in the pre-existing "napi_reference_unref is blocked from finalizers in experimental modules" test (also failing on main, reported separately). The other failures passed on retry or when run alone. The changes in this PR are green on every lane.

@Jarred-Sumner
Jarred-Sumner merged commit 9008ae7 into main Aug 9, 2026
52 of 54 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/1acc3647/napi-fast-typedarray-data-pointer branch August 9, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun silently drops writes to [new Uint8Array(n)] under 1024 bytes

3 participants