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
24 changes: 15 additions & 9 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,16 +230,20 @@ mod static_adapters {
// re-enters the VM).
let _a0_guard = a0.protected();
let _a1_guard = a1.protected();
let Some(input) = BlobOrStringOrBuffer::from_js(g, a0)? else {
return Err(g.throw_invalid_arguments(format_args!(
"expected string, buffer, TypedArray, or Blob",
)));
};
// Coerce the output argument first: `StringOrBuffer::from_js` can call a
// boxed String's `toString`, which may detach the input's ArrayBuffer.
let output = if a1.is_undefined_or_null() {
None
} else {
StringOrBuffer::from_js(g, a1)?
};
// `from_js_no_string_object` never runs user JS, so `output`'s buffer
// (captured above) stays valid.
let Some(input) = BlobOrStringOrBuffer::from_js_no_string_object(g, a0)? else {
return Err(g.throw_invalid_arguments(format_args!(
"expected string, buffer, TypedArray, or Blob",
)));
};
Crypto::SHA512_256::hash_(g, &input, output)
}
}
Expand Down Expand Up @@ -1517,16 +1521,18 @@ pub(crate) fn index_of_line(
return Ok(JSValue::js_number_from_int32(-1));
}

let Some(buffer) = arguments[0].as_array_buffer(global_this) else {
return Ok(JSValue::js_number_from_int32(-1));
};

// Coerce `offset` before snapshotting the buffer: `coerce_to_int64` can run
// `valueOf`/`toString`, which may detach `arguments[0]` (use-after-free).
let mut offset: usize = 0;
if arguments.len() > 1 {
let offset_value = arguments[1].coerce_to_int64(global_this)?;
offset = offset_value.max(0) as usize;
}

let Some(buffer) = arguments[0].as_array_buffer(global_this) else {
return Ok(JSValue::js_number_from_int32(-1));
};

let bytes = buffer.byte_slice();
let mut current_offset = offset;
let end = bytes.len() as u32;
Expand Down
119 changes: 54 additions & 65 deletions src/runtime/crypto/CryptoHasher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,47 +242,30 @@ impl CryptoHasher {
/// Hand-expanded static-method argument decode for the parameter list
/// `(algorithm string, input, optional output buffer/encoding)`.
pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult<JSValue> {
let arguments = callframe.arguments_old::<3>();
let mut i = 0usize;
let mut next_eat = || {
if i < arguments.len {
let v = arguments.ptr[i];
i += 1;
Some(v)
} else {
None
}
};
let arguments = callframe.arguments_undef::<3>();

let algorithm = {
let Some(string_value) = next_eat() else {
return Err(global.throw_invalid_arguments(format_args!("Missing argument")));
};
let string_value = arguments.ptr[0];
if string_value.is_undefined_or_null() {
if arguments.len == 0 {
return Err(global.throw_invalid_arguments(format_args!("Missing argument")));
}
return Err(global.throw_invalid_arguments(format_args!("Expected string")));
}
string_value.get_zig_string(global)?
};

// Node.BlobOrStringOrBuffer
let input = {
let Some(arg) = next_eat() else {
return Err(
global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))
);
};
match BlobOrStringOrBuffer::from_js(global, arg)? {
Some(b) => b,
None => {
return Err(global
.throw_invalid_arguments(format_args!("expected blob, string or buffer")));
}
}
};
if arguments.len < 2 {
return Err(
global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))
);
}

// ?Node.StringOrBuffer (static-method arm: only `undefined` → None)
let output: Option<StringOrBuffer> = match next_eat() {
Some(arg) => match StringOrBuffer::from_js(global, arg)? {
// Coerce the output argument first: `StringOrBuffer::from_js` can call a
// boxed String's `toString`, which may detach the input's ArrayBuffer.
let output: Option<StringOrBuffer> = if arguments.len > 2 {
let arg = arguments.ptr[2];
match StringOrBuffer::from_js(global, arg)? {
Some(v) => Some(v),
None => {
if arg.is_undefined() {
Expand All @@ -292,10 +275,22 @@ impl CryptoHasher {
.throw_invalid_arguments(format_args!("expected string or buffer")));
}
}
},
None => None,
}
} else {
None
};

// `from_js_no_string_object` never runs user JS, so `output`'s buffer
// (captured above) stays valid.
let input =
match BlobOrStringOrBuffer::from_js_no_string_object(global, arguments.ptr[1])? {
Some(b) => b,
None => {
return Err(global
.throw_invalid_arguments(format_args!("expected blob, string or buffer")));
}
};

Self::hash_(global, algorithm, &input, output)
}

Expand Down Expand Up @@ -1223,37 +1218,19 @@ impl<H: StaticHasher> StaticCryptoHasher<H> {
/// Hand-expanded `wrapStaticMethod` decode for the parameter list
/// `(*JSGlobalObject, Node.BlobOrStringOrBuffer, ?Node.StringOrBuffer)`.
pub fn hash(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult<JSValue> {
let arguments = callframe.arguments_old::<2>();
let mut i = 0usize;
let mut next_eat = || {
if i < arguments.len {
let v = arguments.ptr[i];
i += 1;
Some(v)
} else {
None
}
};
let arguments = callframe.arguments_undef::<2>();

// Node.BlobOrStringOrBuffer
let input = {
let Some(arg) = next_eat() else {
return Err(
global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))
);
};
match BlobOrStringOrBuffer::from_js(global, arg)? {
Some(b) => b,
None => {
return Err(global
.throw_invalid_arguments(format_args!("expected blob, string or buffer")));
}
}
};
if arguments.len == 0 {
return Err(
global.throw_invalid_arguments(format_args!("expected blob, string or buffer"))
);
}

// ?Node.StringOrBuffer (static-method arm: only `undefined` → None)
let output: Option<StringOrBuffer> = match next_eat() {
Some(arg) => match StringOrBuffer::from_js(global, arg)? {
// Coerce the output argument first: `StringOrBuffer::from_js` can call a
// boxed String's `toString`, which may detach the input's ArrayBuffer.
let output: Option<StringOrBuffer> = if arguments.len > 1 {
let arg = arguments.ptr[1];
match StringOrBuffer::from_js(global, arg)? {
Some(v) => Some(v),
None => {
if arg.is_undefined() {
Expand All @@ -1263,10 +1240,22 @@ impl<H: StaticHasher> StaticCryptoHasher<H> {
.throw_invalid_arguments(format_args!("expected string or buffer")));
}
}
},
None => None,
}
} else {
None
};

// `from_js_no_string_object` never runs user JS, so `output`'s buffer
// (captured above) stays valid.
let input =
match BlobOrStringOrBuffer::from_js_no_string_object(global, arguments.ptr[0])? {
Some(b) => b,
None => {
return Err(global
.throw_invalid_arguments(format_args!("expected blob, string or buffer")));
}
};

Self::hash_(global, &input, output)
}

Expand Down
16 changes: 11 additions & 5 deletions src/runtime/crypto/PasswordObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,19 +929,25 @@ pub(crate) fn js_password_object_verify_sync(
};
}

let Some(password) = StringOrBuffer::from_js(global_object, arguments[0])? else {
// Coerce the hash argument first: `StringOrBuffer::from_js` can call a
// boxed String's `toString`, which may detach the password's ArrayBuffer.
let Some(hash_) = StringOrBuffer::from_js(global_object, arguments[1])? else {
return Err(global_object.throw_invalid_argument_type(
"verify",
"password",
"hash",
"string or TypedArray",
));
};

let Some(hash_) = StringOrBuffer::from_js(global_object, arguments[1])? else {
drop(password);
// `allow_string_object = false`: the password decode never runs user JS, so
// `hash_`'s buffer (captured above) stays valid.
let Some(password) =
StringOrBuffer::from_js_maybe_async(global_object, arguments[0], false, false)?
else {
drop(hash_);
return Err(global_object.throw_invalid_argument_type(
"verify",
"hash",
"password",
"string or TypedArray",
));
};
Expand Down
22 changes: 19 additions & 3 deletions src/runtime/node/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,15 @@ impl BlobOrStringOrBuffer {
value: JSValue,
allow_file: bool,
is_async: bool,
allow_string_object: bool,
) -> JsResult<Option<BlobOrStringOrBuffer>> {
// Check StringOrBuffer first because it's more common and cheaper.
let str = match StringOrBuffer::from_js_maybe_async(global, value, is_async, true)? {
let str = match StringOrBuffer::from_js_maybe_async(
global,
value,
is_async,
allow_string_object,
)? {
Some(s) => s,
None => {
// `as_class_ref` is the safe shared-borrow downcast (centralised
Expand Down Expand Up @@ -141,7 +147,7 @@ impl BlobOrStringOrBuffer {
value: JSValue,
allow_file: bool,
) -> JsResult<Option<BlobOrStringOrBuffer>> {
Self::from_js_maybe_file_maybe_async(global, value, allow_file, false)
Self::from_js_maybe_file_maybe_async(global, value, allow_file, false, true)
}

pub fn from_js(
Expand All @@ -151,11 +157,21 @@ impl BlobOrStringOrBuffer {
Self::from_js_maybe_file(global, value, true)
}

/// [`from_js`] with `allow_string_object = false`: boxed `String` inputs are
/// rejected, so this never calls user `toString` and is safe to call after
/// an earlier argument's ArrayBuffer slice has been captured.
pub fn from_js_no_string_object(
global: &JSGlobalObject,
value: JSValue,
) -> JsResult<Option<BlobOrStringOrBuffer>> {
Self::from_js_maybe_file_maybe_async(global, value, true, false, false)
}

pub fn from_js_async(
global: &JSGlobalObject,
value: JSValue,
) -> JsResult<Option<BlobOrStringOrBuffer>> {
Self::from_js_maybe_file_maybe_async(global, value, true, true)
Self::from_js_maybe_file_maybe_async(global, value, true, true, true)
}

pub fn from_js_with_encoding_value(
Expand Down
45 changes: 24 additions & 21 deletions src/runtime/webcore/Crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,27 +351,9 @@ pub(crate) fn bun_random_uuid_v5(
let name_value = arguments.ptr[0];
let namespace_value = arguments.ptr[1];

// `bun_core::ZigStringSlice` is a borrow-or-own UTF-8 slice.
let name: bun_core::ZigStringSlice = 'brk: {
if name_value.is_string() {
let name_str = bun_core::OwnedString::new(name_value.to_bun_string(global)?);
let result = name_str.to_utf8();

break 'brk result;
} else if let Some(array_buffer) = name_value.as_array_buffer(global) {
let bytes: &[u8] = array_buffer.byte_slice();
break 'brk bun_core::ZigStringSlice::from_utf8_never_free(bytes);
} else {
return Err(global
.err(
bun_jsc::ErrorCode::INVALID_ARG_TYPE,
format_args!("The \"name\" argument must be of type string or BufferSource"),
)
.throw());
}
};
// `defer name.deinit()` — Utf8Slice's Drop handles cleanup.

// Decode `namespace` first: its `to_bun_string` can call a boxed String's
// `toString`, which may detach `name`'s backing ArrayBuffer. `namespace`
// is copied to a local `[u8; 16]`, so it's safe against the reverse.
let namespace: [u8; 16] = 'brk: {
if namespace_value.is_string() {
let namespace_str = bun_core::OwnedString::new(namespace_value.to_bun_string(global)?);
Expand Down Expand Up @@ -420,6 +402,27 @@ pub(crate) fn bun_random_uuid_v5(
.throw());
};

// `bun_core::ZigStringSlice` is a borrow-or-own UTF-8 slice.
let name: bun_core::ZigStringSlice = 'brk: {
if name_value.is_string() {
let name_str = bun_core::OwnedString::new(name_value.to_bun_string(global)?);
let result = name_str.to_utf8();

break 'brk result;
} else if let Some(array_buffer) = name_value.as_array_buffer(global) {
let bytes: &[u8] = array_buffer.byte_slice();
break 'brk bun_core::ZigStringSlice::from_utf8_never_free(bytes);
} else {
return Err(global
.err(
bun_jsc::ErrorCode::INVALID_ARG_TYPE,
format_args!("The \"name\" argument must be of type string or BufferSource"),
)
.throw());
}
};
// `defer name.deinit()` — Utf8Slice's Drop handles cleanup.

let uuid = UUID5::init(&namespace, name.slice());

if encoding == Encoding::Hex {
Expand Down
Loading
Loading