fix(html): report errors in the receiver Window realm

This commit is contained in:
ldm0
2026-09-22 15:49:35 +08:00
parent 2793b2407f
commit e097fdddaf
2 changed files with 166 additions and 58 deletions
@@ -65,66 +65,46 @@ pub(super) fn ensure_window_reflecting_body_onerror_handler(scope: &mut v8::PinS
);
}
fn dispatch_window_error_event_for_reason<'s>(
struct ReportedExceptionDetails {
message: String,
filename: String,
lineno: u32,
colno: u32,
error: v8::Global<v8::Value>,
}
fn reported_exception_details<'s>(
scope: &mut v8::PinScope<'s, '_>,
host_ptr: *mut JsContextHost,
reason: v8::Local<'s, v8::Value>,
) -> std::result::Result<(), String> {
let mut message = String::new();
let mut filename = String::new();
let mut lineno = 0.0;
let mut colno = 0.0;
let mut error_value: v8::Local<'s, v8::Value> = v8::null(scope).into();
) -> ReportedExceptionDetails {
// `reportError()` feeds the exception directly into HTML's exception
// reporting algorithm. V8's Message preserves the engine's internal
// exception metadata without observing author-defined `name`, `message`,
// or location getters on the value.
let exception_message = v8::Exception::create_message(scope, reason);
let message = exception_message.get(scope).to_rust_string_lossy(scope);
let filename = exception_message
.get_script_resource_name(scope)
.and_then(|value| value.to_string(scope))
.map(|value| value.to_rust_string_lossy(scope))
.unwrap_or_default();
let lineno = exception_message
.get_line_number(scope)
.and_then(|line| u32::try_from(line).ok())
.unwrap_or(0);
let colno = exception_message
.get_start_column()
.checked_add(1)
.and_then(|column| u32::try_from(column).ok())
.unwrap_or(0);
if reason.is_string() {
message = reason
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope))
.unwrap_or_default();
} else if !reason.is_null_or_undefined() {
if reason.is_object() {
error_value = reason;
if let Ok(obj) = v8::Local::<v8::Object>::try_from(reason) {
if let Some(v) = obj.get(scope, v8str(scope, "message").into())
&& !v.is_null_or_undefined()
{
message = v
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope))
.unwrap_or_default();
}
if let Some(v) = obj.get(scope, v8str(scope, "fileName").into())
&& !v.is_null_or_undefined()
{
filename = v
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope))
.unwrap_or_default();
}
if let Some(v) = obj.get(scope, v8str(scope, "lineNumber").into()) {
lineno = v.number_value(scope).unwrap_or(0.0);
}
if let Some(v) = obj.get(scope, v8str(scope, "columnNumber").into()) {
colno = v.number_value(scope).unwrap_or(0.0);
}
}
} else {
message = reason
.to_string(scope)
.map(|s| s.to_rust_string_lossy(scope))
.unwrap_or_default();
}
ReportedExceptionDetails {
message,
filename,
lineno,
colno,
error: v8::Global::new(scope, reason),
}
dispatch_window_error_event_with_details(
scope,
host_ptr,
&message,
&filename,
lineno as u32,
colno as u32,
Some(error_value),
)
}
pub(crate) fn dispatch_window_report_error_message<'s>(
@@ -146,7 +126,15 @@ pub(crate) fn dispatch_window_report_error_message<'s>(
filename_value.into(),
);
}
dispatch_window_error_event_for_reason(scope, host_ptr, reason)
dispatch_window_error_event_with_details(
scope,
host_ptr,
message,
filename.unwrap_or(""),
0,
0,
Some(reason),
)
}
pub(crate) fn dispatch_window_error_event_with_details<'s>(
@@ -219,7 +207,53 @@ pub(in crate::context_bootstrap) fn window_report_error_callback<'s>(
return;
};
if let Err(message) = dispatch_window_error_event_for_reason(scope, host_ptr, args.get(0)) {
let receiver = match crate::native_bridge::WindowOperationReceiver::capture_and_authorize(
scope,
args.this(),
unsafe { &*host_ptr },
) {
Ok(receiver) => receiver,
Err(crate::native_bridge::WindowOperationReceiverCaptureError::IllegalInvocation) => {
throw_type_error(scope, "Illegal invocation");
return;
}
Err(crate::native_bridge::WindowOperationReceiverCaptureError::CrossOrigin) => {
crate::native_bridge::throw_cross_origin_location_security_error(scope);
return;
}
};
if args.length() < 1 {
throw_type_error(
scope,
&crate::webidl::WebIdlError::missing_required(crate::webidl::Context::argument(
"Window.reportError",
1,
))
.to_string(),
);
return;
}
let details = reported_exception_details(scope, args.get(0));
let Some(binding) = receiver.resolve_live_binding(unsafe { &*host_ptr }) else {
// A discarded Window remains a valid receiver, but there is no live
// global left on which to report the exception.
return;
};
let dispatch_result = binding.with_current_scope(scope, host_ptr, |scope, _dispatch_scope| {
let error = v8::Local::new(scope, &details.error);
dispatch_window_error_event_with_details(
scope,
host_ptr,
&details.message,
&details.filename,
details.lineno,
details.colno,
Some(error),
)
});
if let Some(Err(message)) = dispatch_result {
throw_type_error(scope, &message);
}
}
@@ -9178,3 +9178,77 @@ fn modal_dialog_selection_inertness_is_scoped_to_its_document() {
r#"{"parentWithParentModal":"parent dialog","childWithParentModal":"child outside","childCommandWithParentModal":true,"childCommandTextWithParentModal":"child outside","parentWithChildModal":"parent outside","childWithChildModal":"child dialog","parentCommandWithChildModal":true,"parentCommandTextWithChildModal":"parent outside"}"#
);
}
#[test]
fn report_error_uses_the_receiver_realm_without_observing_exception_getters() {
let mut vm = new_storage_test_vm("https://report-error-receiver-realm.test/");
vm.eval(
r#"
(() => {
const frame = document.createElement("iframe");
(document.body || document.documentElement || document).appendChild(frame);
globalThis.__reportErrorRealmFrame = frame;
return "ready";
})()
"#,
)
.expect("reportError receiver Realm setup should evaluate");
vm.drain_pending_child_frame_work_for_test();
let result = vm
.eval(
r#"
(() => {
const child = globalThis.__reportErrorRealmFrame.contentWindow;
const childEvents = [];
let topEvents = 0;
let getterCalls = 0;
window.addEventListener("error", () => topEvents++);
child.addEventListener("error", event => childEvents.push(event));
const error = new TypeError("receiver failure");
window.reportError.call(child, error);
const opaqueReason = {};
for (const name of ["name", "message", "fileName", "lineNumber", "columnNumber"]) {
Object.defineProperty(opaqueReason, name, {
get() {
getterCalls++;
throw new Error(`unexpected ${name} getter`);
}
});
}
window.reportError.call(child, opaqueReason);
let missingArgumentError = null;
try {
window.reportError();
} catch (exception) {
missingArgumentError = exception.name;
}
globalThis.__reportErrorRealmFrame.remove();
child.reportError("detached failure");
const detachedCallCompleted = true;
return JSON.stringify({
topEvents,
eventCount: childEvents.length,
exactErrors: [childEvents[0].error === error, childEvents[1].error === opaqueReason],
receiverRealm: childEvents.every(event => event instanceof child.ErrorEvent),
messagesAreNonEmpty: childEvents.every(event => event.message.length > 0),
getterCalls,
missingArgumentError,
detachedCallCompleted
});
})()
"#,
)
.expect("cross-Realm reportError probe should evaluate");
assert_eq!(
result,
r#"{"topEvents":0,"eventCount":2,"exactErrors":[true,true],"receiverRealm":true,"messagesAreNonEmpty":true,"getterCalls":0,"missingArgumentError":"TypeError","detachedCallCompleted":true}"#,
);
}