Read sink.start() options before resolving the native sink - #39393
Read sink.start() options before resolving the native sink#39393Jarred-Sumner wants to merge 1 commit into
Conversation
Option getters (highWaterMark/asUint8Array/stream/path/fd) can run user JS that closes the sink; resolving `this` first left js_start writing to a finalized sink.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 27 minutes Limit details: You’ve used all 1 included review currently available under your plan. You completed 71 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Updated 8:46 PM PT - Aug 16th, 2026
❌ @Jarred-Sumner, your commit e8aa77b has 3 failures in
🧪 To try this PR locally: bunx bun-pr 39393That installs a local version of the PR into your bun-39393 --bun |
There was a problem hiding this comment.
LGTM — textbook "read user options before resolving the native pointer" reordering.
What was reviewed:
- Confirmed
streams::Startowns its data (no borrowed slices), so parsing beforeget_thisand droppingconfigon the detached-throw path is sound and leak-free. - Checked sibling host-fns (
js_write/js_flush/js_end) — none read user-getter option objects afterget_this, sojs_startwas the only affected site. - Tests spawn subprocesses for crash isolation, cover every option key per sink type, and verify follow-up
write()stays detached.
Extended reasoning...
Overview
Reorders JSSink::<T>::js_start in src/runtime/webcore/Sink.rs so streams::Start::from_js_with_runtime_tag (which invokes user property getters on the options object) runs before Self::get_this materializes &mut JSSink<T> from m_sinkPtr. Previously a getter calling sink.close() freed the boxed sink while this still pointed at it, and this.sink.start(config) was a heap UAF. After the reorder, get_this sees from_js_result::DETACHED and throws the standard "already been closed" error. Two subprocess tests are added exercising every getter key (highWaterMark/asUint8Array/stream for ArrayBufferSink, path/fd for FileSink).
Security risks
This is a memory-safety fix (UAF reachable from user JS). The change strictly reduces attack surface; it introduces no new user-controlled paths. Start is a fully-owned enum (BlobSizeType, bools, FileSinkOptions{PathOrFileDescriptor, bool, Mode}), so nothing in config can dangle if get_this subsequently throws — Rust drops it.
Level of scrutiny
Low-to-medium. The functional diff is a pure statement reorder of two independent blocks with no shared bindings; happy-path behavior is byte-identical. The fix is generic over T: JsSinkType, so it covers every sink variant (ArrayBufferSink, FileSink, HTTPServerWritable, NetworkSink, etc.) in one place. I audited the sibling host-fns for the same pattern: js_write only accepts strings/ArrayBuffers (its as_array_buffer/is_string+to_js_string on an actual JSString do not invoke user code), js_flush reads a bare boolean, and js_end reads no arguments — so js_start was correctly identified as the sole site.
Other factors
Tests follow file-local conventions (subprocess spawn with concurrent pipe drain, exact stdout assertion, if (exitCode !== 0) expect(stderr).toBe("") diagnostic before the unconditional expect(exitCode).toBe(0)), match the variant-matrix guidance, and assert the sink remains detached for a subsequent write(). No prior human or bot review comments to address.
What
sink.start(options)onBun.ArrayBufferSink/FileSinkresolved the native sink fromthisfirst and then read the options (highWaterMark/asUint8Array/stream/path/fd) via property gets. A getter that calls the sink's ownclose()frees the payload andstart()continued on freed memory (ASan: heap-use-after-free WRITE inArrayBufferSink::start, READ inFileSink::setup).js_startnow parses theStartconfig before resolvingthis.Repro (before)
Tests
test/js/bun/util/arraybuffersink.test.tsandtest/js/bun/util/filesink.test.ts— "start() with an option getter that closes the sink/writer throws instead of crashing". Fail on the current ASan canary and debug main; pass with this change.