Skip to content
Merged
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
60 changes: 57 additions & 3 deletions crates/coop-worker/src/plugin_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,31 @@ impl LoadedPlugin {
// the error message; if it's not a string, we just
// include the raw bits.
let reason = unsafe { (api.js_promise_reason)(promise_ptr) };
let reason_str = read_perry_string(reason)
.unwrap_or_else(|| format!("0x{:016x}", reason.to_bits()));
// Never render an empty reason. `handler promise rejected: `
// with nothing after it is what an operator actually sees,
// and it names neither the failure nor where to look. An
// Error object reads back as a zero-length string here, so
// "" and "not a string" must stay distinguishable, and the
// raw bits are always worth keeping for a bug report.
let bits = reason.to_bits();
let reason_str = match read_perry_string(reason) {
Some(text) if !text.is_empty() => text,
Some(_) => format!("<empty string> (raw 0x{bits:016x})"),
None => {
// Not a string: ask Perry to stringify it, so an
// `Error` object reports as "Error: <message>"
// instead of an opaque tag. This is what turned an
// unattributable 500 into
// "TypeError: value is not a function".
let header = unsafe { (api.js_jsvalue_to_string)(reason) };
read_string_header(header as *const StringHeader).unwrap_or_else(|| {
format!(
"<non-string rejection value, tag 0x{:04x}> (raw 0x{bits:016x})",
bits >> 48
)
})
}
};
return Err(anyhow!("handler promise rejected: {}", reason_str));
}
_ => {
Expand Down Expand Up @@ -633,10 +656,41 @@ fn make_perry_buffer(bytes: &[u8]) -> Result<f64> {
Ok(value)
}

/// NaN-box tags for the two string representations. See Perry's
/// `crates/perry-runtime/src/value/nanbox.rs`.
const STRING_TAG: u64 = 0x7fff;
const SHORT_STRING_TAG: u64 = 0x7ff9;

/// Materialize a `StringHeader` the runtime handed us. Split out so the
/// rejection path can reuse it for `js_jsvalue_to_string`'s result.
fn read_string_header(header_ptr: *const StringHeader) -> Option<String> {
if header_ptr.is_null() {
return None;
}
unsafe {
let len = (*header_ptr).byte_len as usize;
let data = (header_ptr as *const u8).add(std::mem::size_of::<StringHeader>());
let slice = std::slice::from_raw_parts(data, len);
Some(String::from_utf8_lossy(slice).into_owned())
}
}

fn read_perry_string(value: f64) -> Option<String> {
// Check the tag OURSELVES first. `js_get_string_pointer_unified` does NOT
// return 0 for every non-string: it deliberately returns the payload for a
// POINTER_TAG (0x7ffd) value too, "used for cross-module returns"
// (nanbox.rs). Handing it an object therefore yields a non-null pointer
// that is NOT a StringHeader, and reading through it is a wild read — it
// produced a bogus one-character reason ("\t") for a rejected handler
// promise carrying an Error object.
let tag = value.to_bits() >> 48;
if tag != STRING_TAG && tag != SHORT_STRING_TAG {
return None;
}

// js_get_string_pointer_unified handles both heap-allocated strings
// (STRING_TAG) and inline SSO strings (SHORT_STRING_TAG) by
// materializing the latter to the heap. Returns 0 if not a string.
// materializing the latter to the heap.
let header_ptr =
unsafe { (crate::runtime_libraries::runtime_api().js_get_string_pointer_unified)(value) }
as *const StringHeader;
Expand Down
5 changes: 5 additions & 0 deletions crates/coop-worker/src/runtime_libraries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use std::time::Instant;
pub(crate) type JsGcInit = unsafe extern "C" fn();
pub(crate) type JsPromiseState = unsafe extern "C" fn(*mut u8) -> i32;
pub(crate) type JsPromiseValue = unsafe extern "C" fn(*mut u8) -> f64;
/// `js_jsvalue_to_string(value) -> *mut StringHeader`: stringify ANY JS value,
/// so a non-string rejection (an `Error` object) can still be reported.
pub(crate) type JsValueToString = unsafe extern "C" fn(f64) -> *const u8;
pub(crate) type PerryPoll = unsafe extern "C" fn() -> i32;
pub(crate) type JsWaitForEvent = unsafe extern "C" fn();
pub(crate) type JsValueIsPromise = unsafe extern "C" fn(f64) -> i32;
Expand All @@ -37,6 +40,7 @@ pub(crate) struct RuntimeApi {
pub js_promise_state: JsPromiseState,
pub js_promise_value: JsPromiseValue,
pub js_promise_reason: JsPromiseValue,
pub js_jsvalue_to_string: JsValueToString,
pub perry_poll: PerryPoll,
pub js_wait_for_event: JsWaitForEvent,
pub js_value_is_promise: JsValueIsPromise,
Expand Down Expand Up @@ -181,6 +185,7 @@ pub fn initialize_runtime_libraries_with_verification(
js_promise_state: load_symbol(runtime_handle, "js_promise_state")?,
js_promise_value: load_symbol(runtime_handle, "js_promise_value")?,
js_promise_reason: load_symbol(runtime_handle, "js_promise_reason")?,
js_jsvalue_to_string: load_symbol(runtime_handle, "js_jsvalue_to_string")?,
perry_poll: load_symbol(runtime_handle, "perry_poll")?,
js_wait_for_event: load_symbol(runtime_handle, "js_wait_for_event")?,
js_value_is_promise: load_symbol(runtime_handle, "js_value_is_promise")?,
Expand Down
Loading