Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/js/internal/streams/native-readable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ function constructNativeReadable(readableStream: ReadableStream, options): Nativ
stream.debugId = ++debugId;
}

stream.$bunNativePtr = bunNativePtr;
// Define the private field directly: an ordinary put would consult the prototype
// chain, and user code can graft ReadableStream.prototype (which has a $bunNativePtr
// accessor meant for actual ReadableStreams) underneath node stream prototypes.
Comment thread
robobun marked this conversation as resolved.
Outdated
$putByIdDirectPrivate(stream, "bunNativePtr", bunNativePtr);
stream[kRefCount] = 0;
stream[kConstructed] = false;
stream[kPendingRead] = false;
Expand Down
56 changes: 44 additions & 12 deletions src/jsc/bindings/webcore/streams/JSReadableStream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -786,48 +786,80 @@ JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_blob, (JSGlobalObject
}

// Bun private-name accessors ($bunNativePtr / $bunNativeType / $disturbed).
// JSC brand-checks DOMAttribute getters centrally (PropertySlot::customGetter), but
// nothing checks custom setters: an ordinary put whose receiver merely inherits
// ReadableStream.prototype (e.g. a node stream after Object.setPrototypeOf surgery)
// reaches them with that foreign receiver as thisValue, so each accessor must
// verify the receiver itself.
Comment thread
robobun marked this conversation as resolved.
Outdated

JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_nativePtrGetter, (JSGlobalObject*, JSC::EncodedJSValue thisValue, PropertyName))
JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_nativePtrGetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName))
{
auto* stream = uncheckedDowncast<JSReadableStream>(JSValue::decode(thisValue));
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stream = dynamicDowncast<JSReadableStream>(JSValue::decode(thisValue));
if (!stream) [[unlikely]]
return throwVMDOMAttributeGetterTypeError(lexicalGlobalObject, scope, JSReadableStream::info(), propertyName);
JSValue nativePtr = stream->nativePtrForJS();
return JSValue::encode(nativePtr.isEmpty() ? jsUndefined() : nativePtr);
}

JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_nativePtrSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName))
JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_nativePtrSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName propertyName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto* stream = uncheckedDowncast<JSReadableStream>(JSValue::decode(thisValue));
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stream = dynamicDowncast<JSReadableStream>(JSValue::decode(thisValue));
if (!stream) [[unlikely]] {
throwDOMAttributeSetterTypeError(lexicalGlobalObject, scope, JSReadableStream::info(), propertyName);
return false;
}
stream->m_nativePtr.set(vm, stream, JSValue::decode(encodedValue));
return true;
}

JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_nativeTypeGetter, (JSGlobalObject*, JSC::EncodedJSValue thisValue, PropertyName))
JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_nativeTypeGetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName))
{
const auto* stream = uncheckedDowncast<JSReadableStream>(JSValue::decode(thisValue));
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
const auto* stream = dynamicDowncast<JSReadableStream>(JSValue::decode(thisValue));
if (!stream) [[unlikely]]
return throwVMDOMAttributeGetterTypeError(lexicalGlobalObject, scope, JSReadableStream::info(), propertyName);
return JSValue::encode(jsNumber(stream->m_nativeType));
}

JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_nativeTypeSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName))
JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_nativeTypeSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName propertyName))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stream = uncheckedDowncast<JSReadableStream>(JSValue::decode(thisValue));
auto* stream = dynamicDowncast<JSReadableStream>(JSValue::decode(thisValue));
if (!stream) [[unlikely]] {
throwDOMAttributeSetterTypeError(lexicalGlobalObject, scope, JSReadableStream::info(), propertyName);
return false;
}
int32_t nativeType = JSValue::decode(encodedValue).toInt32(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, false);
stream->m_nativeType = nativeType;
return true;
}

JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_disturbedGetter, (JSGlobalObject*, JSC::EncodedJSValue thisValue, PropertyName))
JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_disturbedGetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName))
{
const auto* stream = uncheckedDowncast<JSReadableStream>(JSValue::decode(thisValue));
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
const auto* stream = dynamicDowncast<JSReadableStream>(JSValue::decode(thisValue));
if (!stream) [[unlikely]]
return throwVMDOMAttributeGetterTypeError(lexicalGlobalObject, scope, JSReadableStream::info(), propertyName);
return JSValue::encode(jsBoolean(stream->m_disturbed));
}

JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_disturbedSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName))
JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_disturbedSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName propertyName))
{
auto* stream = uncheckedDowncast<JSReadableStream>(JSValue::decode(thisValue));
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto* stream = dynamicDowncast<JSReadableStream>(JSValue::decode(thisValue));
if (!stream) [[unlikely]] {
throwDOMAttributeSetterTypeError(lexicalGlobalObject, scope, JSReadableStream::info(), propertyName);
return false;
}
stream->m_disturbed = JSValue::decode(encodedValue).toBoolean(lexicalGlobalObject);
return true;
}
Expand Down
35 changes: 35 additions & 0 deletions test/js/node/stream/node-stream.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,41 @@ it("Readable.fromWeb", async () => {
expect(Buffer.concat(chunks).toString()).toBe("Hello World!\n");
});

// fromWeb assigns stream.$bunNativePtr on the node Readable. When user code grafts
// ReadableStream.prototype into the node stream prototype chain, that put used to
// reach ReadableStream's private custom setter with the Readable as the receiver,
// writing a JSValue through a type-confused pointer into the Readable's own
// property storage (observable below as a clobbered Symbol(kCapture) slot).
it("Readable.fromWeb with ReadableStream.prototype grafted into the prototype chain", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const { Readable } = require("node:stream");
const { EventEmitter } = require("node:events");
const snap = o => Object.fromEntries(Reflect.ownKeys(o).map(k => [String(k), Object.prototype.toString.call(o[k])]));
const before = snap(Readable.fromWeb(new Response("x").body));
Object.setPrototypeOf(EventEmitter.prototype, ReadableStream.prototype);
const stream = Readable.fromWeb(new Response("y").body);
const after = snap(stream);
for (const key in before) {
if (before[key] !== after[key]) throw new Error("clobbered own slot " + key + ": " + before[key] + " -> " + after[key]);
}
const chunks = [];
for await (const chunk of stream) chunks.push(chunk);
console.log("read:" + Buffer.concat(chunks).toString());
`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe("read:y\n");
expect(exitCode).toBe(0);
});

// An error from the underlying web stream must surface on the node Readable as an
// 'error' event (and destroy it), not as a global unhandled rejection.
it("Readable.fromWeb propagates web stream errors to 'error' and destroys", async () => {
Expand Down
Loading