Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
126 changes: 61 additions & 65 deletions src/jsc/bindings/JSBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
#include <JavaScriptCore/JSArray.h>
#include <JavaScriptCore/JSGenericTypedArrayViewInlines.h>
#include <JavaScriptCore/JSObjectInlines.h>
#include <JavaScriptCore/ObjectConstructor.h>

extern "C" bool Bun__Node__ZeroFillBuffers;

Expand All @@ -96,9 +97,6 @@ extern "C" size_t highway_index_of_char(const uint8_t* haystack, size_t haystack
extern "C" size_t highway_last_index_of_char(const uint8_t* haystack, size_t haystack_len, uint8_t needle);
static constexpr size_t kHighwayNotFound = ~static_cast<size_t>(0);

// export fn Bun__inspect_singleline(globalThis: *JSGlobalObject, value: JSValue) bun.String
extern "C" BunString Bun__inspect_singleline(JSC::JSGlobalObject* globalObject, JSC::JSValue value);

using namespace JSC;
using namespace WebCore;

Expand Down Expand Up @@ -1144,53 +1142,26 @@ static JSC::EncodedJSValue jsBufferPrototypeFunction_compareBody(JSC::JSGlobalOb
size_t sourceEndInit = castedThis->byteLength();
size_t sourceEnd = sourceEndInit;

JSValue targetStartValue = jsUndefined();
JSValue targetEndValue = jsUndefined();
JSValue sourceStartValue = jsUndefined();
JSValue sourceEndValue = jsUndefined();

switch (callFrame->argumentCount()) {
default:
sourceEndValue = callFrame->uncheckedArgument(4);
if (sourceEndValue != jsUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceEndValue, "sourceEnd"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength), &sourceEnd);
RETURN_IF_EXCEPTION(throwScope, {});
}
[[fallthrough]];
case 4:
sourceStartValue = callFrame->uncheckedArgument(3);
if (sourceStartValue != jsUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceStartValue, "sourceStart"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength), &sourceStart);
RETURN_IF_EXCEPTION(throwScope, {});
}
[[fallthrough]];
case 3:
targetEndValue = callFrame->uncheckedArgument(2);
if (targetEndValue != jsUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetEndValue, "targetEnd"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength), &targetEnd);
RETURN_IF_EXCEPTION(throwScope, {});
}
[[fallthrough]];
case 2:
targetStartValue = callFrame->uncheckedArgument(1);
if (targetStartValue != jsUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetStartValue, "targetStart"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength), &targetStart);
RETURN_IF_EXCEPTION(throwScope, {});
}
break;
case 1:
case 0:
break;
// Node's validateOffset, in argument order: a start by kMaxOffset, an end by its buffer.
JSValue targetStartValue = callFrame->argument(1);
if (!targetStartValue.isUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetStartValue, "targetStart"_s, jsNumber(0), jsDoubleNumber(Bun::Buffer::kMaxOffset), &targetStart);
RETURN_IF_EXCEPTION(throwScope, {});
}

// Validate end values against their respective buffer lengths to prevent OOB access.
// This matches Node.js behavior where targetEnd is validated against target.length
// and sourceEnd is validated against source.length.
if (targetEnd > targetEndInit) {
return Bun::ERR::OUT_OF_RANGE(throwScope, lexicalGlobalObject, "targetEnd"_s, 0, targetEndInit, targetEndValue);
JSValue targetEndValue = callFrame->argument(2);
if (!targetEndValue.isUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, targetEndValue, "targetEnd"_s, jsNumber(0), jsNumber(targetEndInit), &targetEnd);
RETURN_IF_EXCEPTION(throwScope, {});
}
if (sourceEnd > sourceEndInit) {
return Bun::ERR::OUT_OF_RANGE(throwScope, lexicalGlobalObject, "sourceEnd"_s, 0, sourceEndInit, sourceEndValue);
JSValue sourceStartValue = callFrame->argument(3);
if (!sourceStartValue.isUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceStartValue, "sourceStart"_s, jsNumber(0), jsDoubleNumber(Bun::Buffer::kMaxOffset), &sourceStart);
RETURN_IF_EXCEPTION(throwScope, {});
}
JSValue sourceEndValue = callFrame->argument(4);
if (!sourceEndValue.isUndefined()) {
Bun::V::validateInteger(throwScope, lexicalGlobalObject, sourceEndValue, "sourceEnd"_s, jsNumber(0), jsNumber(sourceEndInit), &sourceEnd);
RETURN_IF_EXCEPTION(throwScope, {});
}

// When start >= end for either side, return early per Node.js semantics.
Expand Down Expand Up @@ -1446,7 +1417,7 @@ static JSC::EncodedJSValue jsBufferPrototypeFunction_fillBody(JSC::JSGlobalObjec
// byteLength read further down.
// https://github.com/nodejs/node/blob/v22.9.0/lib/buffer.js#L1066-L1079
if (!offsetValue.isUndefined()) {
Bun::V::validateInteger(scope, lexicalGlobalObject, offsetValue, "offset"_s, jsNumber(0), jsNumber(Bun::Buffer::kMaxLength), &offset);
Bun::V::validateInteger(scope, lexicalGlobalObject, offsetValue, "offset"_s, jsNumber(0), jsDoubleNumber(Bun::Buffer::kMaxOffset), &offset);
RETURN_IF_EXCEPTION(scope, {});
// Node only reads `end` once `offset` is present: fill(v, undefined, end)
// ignores end (it is never validated) and fills the whole buffer.
Expand Down Expand Up @@ -1815,18 +1786,16 @@ static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& sco
// and byteLength from the buffer right before use, and check for detachment
// after all JS calls in each code path are complete.

// Helper: re-fetch buffer state after JS calls. Returns false if the buffer
// was detached; the caller returns -1 (matches Node.js for a detached
// haystack). A merely EMPTY haystack is not -1; see computeIndexOfRange.
auto refetchBufferState = [&](const uint8_t*& typedVector, size_t& len) -> bool {
// Helper: re-fetch buffer state after JS calls. A detached haystack reads
// as empty, like in Node; computeIndexOfRange never touches a length-0 vector.
Comment thread
robobun marked this conversation as resolved.
Resolved
auto refetchBufferState = [&](const uint8_t*& typedVector, size_t& len) {
if (buffer->isDetached()) [[unlikely]] {
typedVector = nullptr;
len = 0;
return false;
return;
}
typedVector = buffer->typedVector();
len = buffer->byteLength();
return true;
};

if (std::isnan(byteOffsetD)) byteOffsetD = dir ? 0 : byteLength;
Expand All @@ -1835,7 +1804,7 @@ static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& sco
auto byteValue = static_cast<uint8_t>((valueValue.toInt32(lexicalGlobalObject)) % 256);
RETURN_IF_EXCEPTION(scope, -1);
const uint8_t* typedVector;
if (!refetchBufferState(typedVector, byteLength)) return -1;
refetchBufferState(typedVector, byteLength);
return indexOfNumber(lexicalGlobalObject, last, typedVector, byteLength, byteOffsetD, endD, byteValue);
}

Expand All @@ -1855,14 +1824,14 @@ static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& sco
auto* str = valueValue.toString(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, -1);
const uint8_t* typedVector;
if (!refetchBufferState(typedVector, byteLength)) return -1;
refetchBufferState(typedVector, byteLength);
return indexOfString(lexicalGlobalObject, last, typedVector, byteLength, byteOffsetD, endD, str, encoding.value());
}

if (auto* array = dynamicDowncast<JSC::JSUint8Array>(valueValue)) {
if (!encoding.has_value()) encoding = BufferEncodingType::utf8;
const uint8_t* typedVector;
if (!refetchBufferState(typedVector, byteLength)) return -1;
refetchBufferState(typedVector, byteLength);
// A needle whose backing buffer was detached by a valueOf/toPrimitive
// callback above reports byteLength()==0; computeIndexOfRange's
// empty-needle path handles that (clamped to `end`) without touching
Expand Down Expand Up @@ -1952,17 +1921,44 @@ static JSC::EncodedJSValue jsBufferPrototypeFunction_inspectBody(JSC::JSGlobalOb
result.append(',');
}
result.append(' ');
size_t i = 0;

// Like Node: util.inspect a null-prototype copy of the extras with
// the caller's options, then strip "[Object: null prototype] { " and " }".
Comment thread
robobun marked this conversation as resolved.
Resolved
JSObject* extras = JSC::constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure());
for (auto ident : array) {
if (i > 0) result.append(", "_s);
result.append(ident.string());
result.append(": "_s);
auto value = castedThis->get(globalObject, ident);
RETURN_IF_EXCEPTION(scope, {});
auto inspected = Bun__inspect_singleline(globalObject, value).transferToWTFString();
extras->putDirect(vm, ident, value);
}

// Define the copies (like Node's spread), so no Object.prototype setter runs.
JSObject* options = JSC::constructEmptyObject(globalObject);
if (auto* ctxObject = dynamicDowncast<JSObject>(ctx)) {
PropertyNameArrayBuilder names(vm, PropertyNameMode::StringsAndSymbols, PrivateSymbolMode::Exclude);
ctxObject->methodTable()->getOwnPropertyNames(ctxObject, globalObject, names, DontEnumPropertiesMode::Exclude);
RETURN_IF_EXCEPTION(scope, {});
result.append(inspected);
i++;
for (const auto& name : names) {
JSValue value = ctxObject->get(globalObject, name);
RETURN_IF_EXCEPTION(scope, {});
options->putDirectMayBeIndex(globalObject, name, value);
RETURN_IF_EXCEPTION(scope, {});
}
}
options->putDirect(vm, Identifier::fromString(vm, "breakLength"_s), jsDoubleNumber(std::numeric_limits<double>::infinity()));
options->putDirect(vm, Identifier::fromString(vm, "compact"_s), jsBoolean(true));

JSFunction* inspectFn = globalObject->utilInspectFunction();
RETURN_IF_EXCEPTION(scope, {});
MarkedArgumentBuffer args;
args.append(extras);
args.append(options);
JSValue inspected = JSC::call(globalObject, inspectFn, ArgList(args), "util.inspect"_s);
RETURN_IF_EXCEPTION(scope, {});
auto inspectedString = inspected.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
constexpr size_t prefixLength = sizeof("[Object: null prototype] { ") - 1;
if (inspectedString.length() > prefixLength + 2) {
result.append(StringView(inspectedString).substring(prefixLength, inspectedString.length() - prefixLength - 2));
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/jsc/bindings/JSBuffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ bool rejectBytesNoCopyAboveArrayBufferLimit(JSC::JSGlobalObject*, JSC::ThrowScop
namespace Buffer {

const size_t kMaxLength = MAX_ARRAY_BUFFER_SIZE;
// Node's validateOffset bound (its kMaxLength, 2**53 - 1). Separate from
// kMaxLength here because JSC caps an ArrayBuffer at 2**32 bytes, and a
// start offset past the end of a buffer is an empty range, not an error.
Comment thread
robobun marked this conversation as resolved.
Resolved
const size_t kMaxOffset = (size_t(1) << 53) - 1;
const size_t kStringMaxLength = WTF::String::MaxLength;
const size_t MAX_LENGTH = MAX_ARRAY_BUFFER_SIZE;
const size_t MAX_STRING_LENGTH = WTF::String::MaxLength;
Expand Down
Loading