fix(renderer): execute javascript URL navigations internally

This commit is contained in:
ldm0
2026-08-30 16:36:55 +08:00
committed by Donough Liu
parent 8eab95a494
commit 4cd690931e
20 changed files with 1380 additions and 412 deletions
+8 -2
View File
@@ -22,6 +22,7 @@ mod history_mutation;
mod history_runtime;
mod idle_detection;
mod image_data;
mod javascript_url;
pub(crate) use self::idle_detection::apply_idle_override_to_current_context;
mod indexed_db;
#[cfg(test)]
@@ -285,6 +286,10 @@ pub(crate) use self::indexed_db::{
pub(in crate::context_bootstrap) use self::indexed_db::{
indexed_db_usage_bytes_for_storage_key, scoped_storage_bucket_indexed_db_factory,
};
pub(crate) use self::javascript_url::{
arm_internal_javascript_url_eval, consume_internal_javascript_url_eval,
restore_internal_javascript_url_eval,
};
pub(crate) use self::location_runtime::sync_global_location_runtime_state;
pub(crate) use self::location_runtime::{
sync_document_location_runtime_state_from_window,
@@ -422,8 +427,9 @@ pub(crate) use self::streams::{
#[cfg(test)]
pub(crate) use self::trusted_types::trusted_types_lazy_state_materialized;
pub(crate) use self::trusted_types::{
TrustedTypesCodeGenerationCheck, install_trusted_types_eval_runtime_state,
install_trusted_types_runtime_state, trusted_html_string_or_throw, trusted_html_value_string,
TrustedTypesCodeGenerationCheck, check_javascript_url_trusted_types,
install_trusted_types_eval_runtime_state, install_trusted_types_runtime_state,
trusted_html_string_or_throw, trusted_html_value_string,
trusted_script_string_for_script_element_execution, trusted_script_string_or_type_error,
trusted_script_url_string_or_throw, trusted_types_code_generation_check,
trusted_types_code_generation_check_callback,
@@ -0,0 +1,67 @@
use crate::util::{get_private_value, set_private_value};
const INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT: &str = "__moliInternalJavascriptUrlEvalPermit";
/// Scoped permission for the direct eval used by the lightweight-popup
/// javascript-URL adapter.
///
/// Lightweight popups do not own a V8 Context, so their adapter uses direct
/// eval to recover Script completion semantics while resolving globals
/// through the popup Window object. The source has already passed the target
/// Document's CSP and Trusted Types checks. The isolate callback consumes the
/// private marker on the first compilation; nested page-authored eval calls
/// therefore go through the normal policy path.
pub(crate) struct InternalJavascriptUrlEvalPermit {
previous: Option<v8::Global<v8::Value>>,
}
pub(crate) fn arm_internal_javascript_url_eval(
scope: &mut v8::PinScope<'_, '_>,
) -> InternalJavascriptUrlEvalPermit {
let global = scope.get_current_context().global(scope);
let previous = get_private_value(scope, global, INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT)
.map(|value| v8::Global::new(scope, value));
let marker = v8::Object::new(scope);
set_private_value(
scope,
global,
INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT,
marker.into(),
);
InternalJavascriptUrlEvalPermit { previous }
}
pub(crate) fn restore_internal_javascript_url_eval(
scope: &mut v8::PinScope<'_, '_>,
permit: InternalJavascriptUrlEvalPermit,
) {
let global = scope.get_current_context().global(scope);
let previous = permit
.previous
.as_ref()
.map(|value| v8::Local::new(scope, value))
.unwrap_or_else(|| v8::undefined(scope).into());
set_private_value(
scope,
global,
INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT,
previous,
);
}
pub(crate) fn consume_internal_javascript_url_eval(scope: &mut v8::PinScope<'_, '_>) -> bool {
let global = scope.get_current_context().global(scope);
let armed = get_private_value(scope, global, INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT)
.is_some_and(|marker| marker.is_object());
if !armed {
return false;
}
let empty = v8::undefined(scope);
set_private_value(
scope,
global,
INTERNAL_JAVASCRIPT_URL_EVAL_PERMIT_SLOT,
empty.into(),
);
true
}
@@ -265,6 +265,59 @@ pub(crate) fn trusted_script_string_for_script_element_execution(
None
}
/// Result of applying the Trusted Types pre-navigation conversion to a
/// decoded `javascript:` source.
///
/// Owner-specific CSP event dispatch deliberately remains outside this
/// module. This layer owns only Trusted Type values and default-policy calls.
pub(crate) struct JavascriptUrlTrustedTypesCheck {
pub(crate) source: Option<String>,
pub(crate) violated: bool,
}
/// Apply the Trusted Types pre-navigation conversion for a decoded
/// `javascript:` source. Policy exceptions are consumed because this runs in
/// the later navigation task, not in the API call that queued the navigation.
pub(crate) fn check_javascript_url_trusted_types(
scope: &mut v8::PinScope<'_, '_>,
original: &str,
requirements: TrustedTypesForScriptRequirements,
) -> JavascriptUrlTrustedTypesCheck {
if !requirements.requires_conversion() {
return JavascriptUrlTrustedTypesCheck {
source: Some(original.to_owned()),
violated: false,
};
}
let outcome = {
let try_catch = std::pin::pin!(v8::TryCatch::new(scope));
let mut scope = try_catch.init();
apply_default_trusted_type_policy_outcome(
&mut scope,
original,
TrustedTypeKind::Script,
"Location href",
)
};
match outcome {
DefaultTrustedTypePolicyOutcome::Value(value)
if url::Url::parse(&format!("javascript:{value}")).is_ok() =>
{
JavascriptUrlTrustedTypesCheck {
source: Some(value),
violated: false,
}
}
DefaultTrustedTypePolicyOutcome::Value(_)
| DefaultTrustedTypePolicyOutcome::Unavailable
| DefaultTrustedTypePolicyOutcome::Rejected
| DefaultTrustedTypePolicyOutcome::Exception => JavascriptUrlTrustedTypesCheck {
source: (!requirements.is_enforced()).then(|| original.to_owned()),
violated: true,
},
}
}
pub(crate) enum TrustedTypesCodeGenerationCheck {
AllowOriginal,
AllowModified(String),
@@ -777,7 +777,7 @@ impl DocumentRuntime {
kind: ContentSecurityPolicyNonUrlKind,
source: &str,
) -> DocumentContentSecurityPolicyCheck {
self.inline_source_csp_check_for_child_document(
self.inline_source_csp_check_for_document(
Some(self.document_handle()),
self.document_url(),
&self.policy_container.response_content_security_policies,
@@ -795,7 +795,7 @@ impl DocumentRuntime {
source: &str,
request: ContentSecurityPolicyScriptElementRequest<'_>,
) -> DocumentContentSecurityPolicyCheck {
self.inline_script_element_csp_check_for_child_document(
self.inline_script_element_csp_check_for_document(
Some(self.document_handle()),
self.document_url(),
&self.policy_container.response_content_security_policies,
@@ -929,7 +929,7 @@ impl DocumentRuntime {
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn inline_script_element_csp_check_for_child_document(
pub(crate) fn inline_script_element_csp_check_for_document(
&self,
document_handle: Option<DomHandle>,
document_url: &Url,
@@ -971,7 +971,7 @@ impl DocumentRuntime {
}
}
pub(crate) fn inline_source_csp_check_for_child_document(
pub(crate) fn inline_source_csp_check_for_document(
&self,
document_handle: Option<DomHandle>,
document_url: &Url,
@@ -1011,6 +1011,7 @@ impl DocumentRuntime {
}
}
#[cfg(test)]
pub(crate) fn document_frame_csp_violation(
&self,
request_url: &Url,
@@ -1030,22 +1031,6 @@ impl DocumentRuntime {
)
}
pub(crate) fn document_frame_csp_report_only_violation(
&self,
request_url: &Url,
) -> Option<DocumentContentSecurityPolicyViolation> {
document_frame_policy_violation(
&self
.policy_container
.response_content_security_report_only_policies,
&self.policy_container.content_security_reporting_endpoints,
self.document_url(),
request_url,
ContentSecurityPolicyRedirectStatus::NoRedirect,
ContentSecurityPolicyDisposition::Report,
)
}
pub(crate) fn script_element_request_csp_violation_for_child_document(
&self,
document_handle: Option<DomHandle>,
@@ -1164,7 +1149,7 @@ impl DocumentRuntime {
}
}
pub(crate) fn document_frame_csp_violation_for_child_document(
pub(crate) fn document_frame_csp_violation_for_document(
&self,
document_handle: Option<DomHandle>,
document_url: &Url,
@@ -1186,7 +1171,7 @@ impl DocumentRuntime {
)
}
pub(crate) fn document_frame_csp_report_only_violation_for_child_document(
pub(crate) fn document_frame_csp_report_only_violation_for_document(
&self,
document_url: &Url,
response_report_only_policies: &[String],
@@ -1396,6 +1381,44 @@ impl DocumentRuntime {
)
}
pub(crate) fn trusted_types_sink_csp_violations_for_document(
&self,
document_handle: Option<DomHandle>,
document_url: &Url,
response_policies: &[String],
report_only_policies: &[String],
reporting_endpoints: &ContentSecurityPolicyReportingEndpoints,
sink: &str,
sample: &str,
) -> Vec<DocumentContentSecurityPolicyViolation> {
let report_only_policies = document_response_content_security_policy_strings(
report_only_policies,
reporting_endpoints,
);
let enforced_policies = self
.document_content_security_policy_strings_for_optional_document(
document_handle,
response_policies,
reporting_endpoints,
);
let mut violations = iter_document_trusted_types_sink_policy_violations(
enforced_policies,
document_url,
sink,
sample,
ContentSecurityPolicyDisposition::Enforce,
)
.collect::<Vec<_>>();
violations.extend(iter_document_trusted_types_sink_policy_violations(
report_only_policies,
document_url,
sink,
sample,
ContentSecurityPolicyDisposition::Report,
));
violations
}
pub(crate) fn requires_trusted_types_for_script(&self) -> bool {
let policies = self.document_content_security_policy_strings_for_optional_document(
Some(self.document_handle()),
@@ -2131,7 +2154,24 @@ fn document_trusted_types_sink_policy_violation_from_document_policies(
sample: &str,
disposition: ContentSecurityPolicyDisposition,
) -> Option<DocumentContentSecurityPolicyViolation> {
policies.into_iter().find_map(|policy| {
iter_document_trusted_types_sink_policy_violations(
policies,
document_url,
sink,
sample,
disposition,
)
.next()
}
fn iter_document_trusted_types_sink_policy_violations<'a>(
policies: Vec<DocumentContentSecurityPolicyString>,
document_url: &'a Url,
sink: &'a str,
sample: &'a str,
disposition: ContentSecurityPolicyDisposition,
) -> impl Iterator<Item = DocumentContentSecurityPolicyViolation> + 'a {
policies.into_iter().filter_map(move |policy| {
let single_policy = [policy.policy.clone()];
let mut violation =
content_security_policy_trusted_types_sink_violation_with_disposition_and_reporting_endpoints(
@@ -200,6 +200,21 @@ enum LightweightPopupClassicScriptAdvance {
Pending(PopupDocumentLoadBodyActivity),
}
struct LightweightPopupDocumentProjectionSource<'a> {
task: LightweightPopupNavigationTaskToken,
document_url: &'a Url,
markup: &'a str,
content_type: Option<&'a str>,
character_set: Option<&'a str>,
policy_container: &'a DocumentPolicyContainer,
}
struct LightweightPopupDocumentProjectionApplication {
document_handle: Option<DomHandle>,
body_activity: PopupDocumentLoadBodyActivity,
classic_script_load_pending: bool,
}
pub(super) struct PendingLightweightPopupClassicScriptLoad {
target: LightweightPopupClassicScriptFetchTarget,
script_handle: DomHandle,
@@ -2408,81 +2423,25 @@ impl JsContextHost {
&base_url,
&document_referrer,
);
if let Some(document) =
crate::dom_parser::parse_child_document_projection_from_source(
scope,
final_url.clone(),
&loaded.markup,
loaded.content_type.as_deref(),
Some(&loaded.character_set),
crate::parser::HtmlParser::with_scripting_enabled(
self.lightweight_popup_scripting_enabled(popup_id),
),
)
{
let host_ptr = self as *mut JsContextHost;
let document_base_url = lightweight_popup_parsed_document_base_url(
host_ptr, scope, document, &final_url,
);
if let Some(document_record) =
self.lightweight_popup_document_record_mut(popup_id)
{
document_record.state.base_url = document_base_url.clone();
}
let document_referrer = self
.lightweight_popup_document_record(popup_id)
.map(|document| document.state.policy_container.document_referrer.clone())
.unwrap_or_default();
sync_lightweight_popup_document_window_slots(
scope,
document,
window,
&document_base_url,
&document_referrer,
);
set_object_slot(scope, window, "document", document.into());
self.forget_lightweight_popup_document_handle(popup_id);
let popup_document_handle =
self.remember_lightweight_popup_document_handle(scope, popup_id, document);
if let Some(document_handle) = popup_document_handle {
let _ = crate::context_bootstrap::install_css_runtime_state_for_document(
scope,
window,
Some(document_handle),
);
install_lightweight_popup_get_computed_style(
scope,
window,
document_handle,
);
}
let _ = self.set_lightweight_popup_document_wrapper(
popup_id,
v8::Global::new(scope, document),
);
let script_advance = self.execute_lightweight_popup_document_scripts(
scope,
if let Some(application) = self.install_lightweight_popup_document_projection(
scope,
window,
LightweightPopupDocumentProjectionSource {
task,
popup_document_handle,
loaded.policy_container.sandbox.allows_scripts,
&loaded.policy_container.response_content_security_policies,
&loaded
.policy_container
.response_content_security_report_only_policies,
&loaded.policy_container.content_security_reporting_endpoints,
);
body_activity = match script_advance {
LightweightPopupClassicScriptAdvance::Completed(activity) => activity,
LightweightPopupClassicScriptAdvance::Pending(activity) => {
classic_script_load_pending = true;
activity
}
};
document_url: &final_url,
markup: &loaded.markup,
content_type: loaded.content_type.as_deref(),
character_set: Some(&loaded.character_set),
policy_container: &loaded.policy_container,
},
) {
body_activity = application.body_activity;
classic_script_load_pending = application.classic_script_load_pending;
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
return PopupDocumentLoadApplication::Applied { body_activity };
}
if !classic_script_load_pending
&& let Some(document_handle) = popup_document_handle
&& let Some(document_handle) = application.document_handle
{
self.sync_child_browsing_context_subtree(scope, document_handle);
}
@@ -2801,6 +2760,83 @@ impl JsContextHost {
self.dispatch_lightweight_popup_window_event(scope, popup_id, "load", event);
}
fn install_lightweight_popup_document_projection<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
source: LightweightPopupDocumentProjectionSource<'_>,
) -> Option<LightweightPopupDocumentProjectionApplication> {
let popup_id = source.task.popup_id();
let document = crate::dom_parser::parse_child_document_projection_from_source(
scope,
source.document_url.clone(),
source.markup,
source.content_type,
source.character_set,
crate::parser::HtmlParser::with_scripting_enabled(
self.lightweight_popup_scripting_enabled(popup_id),
),
)?;
let host_ptr = self as *mut JsContextHost;
let document_base_url = lightweight_popup_parsed_document_base_url(
host_ptr,
scope,
document,
source.document_url,
);
if let Some(document_record) = self.lightweight_popup_document_record_mut(popup_id) {
document_record.state.base_url = document_base_url.clone();
}
// The committed record owns the navigation-derived referrer. The
// response policy container only carries response-derived policy and
// can still have an empty referrer here.
let document_referrer = self
.lightweight_popup_document_record(popup_id)
.map(|document| document.state.policy_container.document_referrer.clone())
.unwrap_or_default();
sync_lightweight_popup_document_window_slots(
scope,
document,
window,
&document_base_url,
&document_referrer,
);
set_object_slot(scope, window, "document", document.into());
self.forget_lightweight_popup_document_handle(popup_id);
let document_handle =
self.remember_lightweight_popup_document_handle(scope, popup_id, document);
if let Some(document_handle) = document_handle {
let _ = crate::context_bootstrap::install_css_runtime_state_for_document(
scope,
window,
Some(document_handle),
);
install_lightweight_popup_get_computed_style(scope, window, document_handle);
}
let _ =
self.set_lightweight_popup_document_wrapper(popup_id, v8::Global::new(scope, document));
let script_advance = self.execute_lightweight_popup_document_scripts(
scope,
source.task,
document_handle,
source.policy_container.sandbox.allows_scripts,
&source.policy_container.response_content_security_policies,
&source
.policy_container
.response_content_security_report_only_policies,
&source.policy_container.content_security_reporting_endpoints,
);
let (body_activity, classic_script_load_pending) = match script_advance {
LightweightPopupClassicScriptAdvance::Completed(activity) => (activity, false),
LightweightPopupClassicScriptAdvance::Pending(activity) => (activity, true),
};
Some(LightweightPopupDocumentProjectionApplication {
document_handle,
body_activity,
classic_script_load_pending,
})
}
fn execute_lightweight_popup_document_scripts(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
@@ -3543,9 +3579,10 @@ impl JsContextHost {
fn execute_lightweight_popup_window_javascript_url_source(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
popup_id: u64,
task: LightweightPopupNavigationTaskToken,
source: &str,
) -> Result<()> {
let popup_id = task.popup_id();
let Some(window) = self.lightweight_popup_window(scope, popup_id) else {
anyhow::bail!("popup javascript URL has no window");
};
@@ -3562,7 +3599,7 @@ impl JsContextHost {
}
self.execute_lightweight_popup_window_javascript_url_source_in_context(
popup_scope,
popup_id,
task,
window,
source,
)
@@ -3571,37 +3608,133 @@ impl JsContextHost {
fn execute_lightweight_popup_window_javascript_url_source_in_context<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
popup_id: u64,
task: LightweightPopupNavigationTaskToken,
window: v8::Local<'s, v8::Object>,
source: &str,
) -> Result<()> {
let Some(source_value) = v8_string(scope, source) else {
let popup_id = task.popup_id();
let owner = OwnerDispatchScope::LightweightPopup(popup_id);
let Some(source) = self.prepare_javascript_url_source_for_execution(scope, owner, source)
else {
return Ok(());
};
let Some(source_value) = v8_string(scope, &source) else {
anyhow::bail!("failed to allocate popup javascript URL source");
};
let mut script_source = v8::script_compiler::Source::new(source_value, None);
let Some(function) = v8::script_compiler::compile_function(
let Some(wrapper_source) = v8_string(
scope,
&mut script_source,
&[],
&[window],
v8::script_compiler::CompileOptions::NoCompileOptions,
v8::script_compiler::NoCacheReason::BecauseInlineScript,
"(function(window, __moliSource) { with (window) { return eval(__moliSource); } })",
) else {
anyhow::bail!("v8 failed to compile popup javascript URL");
anyhow::bail!("failed to allocate popup javascript URL wrapper");
};
let Some(function) = v8::Script::compile(scope, wrapper_source, None)
.and_then(|script| script.run(scope))
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
else {
anyhow::bail!("v8 failed to compile popup javascript URL wrapper");
};
let previous_popup = enter_active_lightweight_popup_scope(scope, popup_id);
let previous_message_source = self.enter_window_message_source_scope(
super::PendingWindowMessageEndpoint::LightweightPopup(popup_id),
);
let run_succeeded = function.call(scope, window.into(), &[]).is_some();
let eval_permit = crate::context_bootstrap::arm_internal_javascript_url_eval(scope);
let completion = function.call(scope, window.into(), &[window.into(), source_value.into()]);
crate::context_bootstrap::restore_internal_javascript_url_eval(scope, eval_permit);
self.restore_window_message_source_scope(previous_message_source);
restore_active_lightweight_popup_scope(scope, previous_popup);
if !run_succeeded {
let Some(completion) = completion else {
anyhow::bail!("v8 failed to execute popup javascript URL");
};
if completion.is_string()
&& self.lightweight_popup_committed_navigation_task_is_current(task)
{
let markup = completion
.to_string(scope)
.map(|value| value.to_rust_string_lossy(scope))
.unwrap_or_default();
self.commit_lightweight_popup_javascript_url_string_completion(
scope, task, window, markup,
);
}
Ok(())
}
fn commit_lightweight_popup_javascript_url_string_completion<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
task: LightweightPopupNavigationTaskToken,
window: v8::Local<'s, v8::Object>,
markup: String,
) -> bool {
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
return false;
}
let popup_id = task.popup_id();
let Some(document_url) = self
.lightweight_popup_document_record(popup_id)
.map(|document| document.url.clone())
else {
return false;
};
let Some((document_state, storage_scope)) =
self.lightweight_popup_document_commit_seed(popup_id)
else {
return false;
};
let policy_container = document_state.policy_container.clone();
if !self.commit_lightweight_popup_document(
scope,
window,
LightweightPopupDocumentCommit {
owner: task.document_owner(),
location_url: document_url.clone(),
origin: LightweightPopupDocumentCommitOrigin::RetainCurrent,
state: document_state,
storage_scope,
navigation_loader: None,
},
) || !self.lightweight_popup_committed_navigation_task_is_current(task)
{
return false;
}
let base_url = self
.lightweight_popup_base_url(scope, popup_id)
.unwrap_or_else(|| document_url.clone());
sync_lightweight_popup_window_location(
scope,
window,
document_url.as_str(),
&base_url,
&policy_container.document_referrer,
);
let Some(application) = self.install_lightweight_popup_document_projection(
scope,
window,
LightweightPopupDocumentProjectionSource {
task,
document_url: &document_url,
markup: &markup,
content_type: Some("text/html"),
character_set: Some("UTF-8"),
policy_container: &policy_container,
},
) else {
return false;
};
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
return true;
}
if !application.classic_script_load_pending {
if let Some(document_handle) = application.document_handle {
self.sync_child_browsing_context_subtree(scope, document_handle);
}
if self.lightweight_popup_committed_navigation_task_is_current(task) {
self.queue_lightweight_popup_load_event(task);
}
}
true
}
fn queue_lightweight_popup_load_event(&mut self, task: LightweightPopupNavigationTaskToken) {
if !self.lightweight_popup_committed_navigation_task_is_current(task) {
return;
@@ -4658,7 +4791,7 @@ fn lightweight_popup_javascript_url_callback<'s>(
return;
}
if let Err(error) =
host.execute_lightweight_popup_window_javascript_url_source(scope, popup_id, &source)
host.execute_lightweight_popup_window_javascript_url_source(scope, task, &source)
{
tracing::debug!(
popup_id,
@@ -8,9 +8,9 @@ use crate::{
context_bootstrap::CHILD_BROWSING_CONTEXT_HANDLE_SLOT,
document_runtime::{
DocumentContentSecurityPolicyCheck, DocumentContentSecurityPolicyViolation,
DocumentSubresourceCspKind, DomHandle, create_content_security_policy_violation_event,
DocumentPolicyContainer, DocumentSubresourceCspKind, DomHandle,
create_content_security_policy_violation_event,
},
dom::native::Node,
native_bridge::{
active_child_window_handle, active_lightweight_popup_id,
child_window_handle_from_marker_data, entered_child_window_handle,
@@ -26,6 +26,15 @@ pub(crate) enum DocumentCspOutcome {
SkippedChildContext,
}
#[derive(Debug, Clone)]
struct OwnerDocumentPolicySnapshot {
document_handle: Option<DomHandle>,
document_url: url::Url,
policy_container: DocumentPolicyContainer,
}
const JAVASCRIPT_URL_TRUSTED_TYPES_SINK: &str = "Location href";
fn policy_child_window_handle(scope: &mut v8::PinScope<'_, '_>) -> Option<DomHandle> {
if let Some(handle) = active_child_window_handle(scope) {
return Some(handle);
@@ -49,6 +58,43 @@ impl DocumentCspOutcome {
}
impl JsContextHost {
fn owner_document_policy_snapshot(
&self,
owner: OwnerDispatchScope,
) -> Option<OwnerDocumentPolicySnapshot> {
match owner {
OwnerDispatchScope::Top => {
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
let runtime = unsafe { &*self.runtime };
Some(OwnerDocumentPolicySnapshot {
document_handle: Some(runtime.document_handle()),
document_url: runtime.document_url().clone(),
policy_container: runtime.document_policy_container().clone(),
})
}
OwnerDispatchScope::Child(handle) => {
let mut policy_container =
self.child_browsing_context_policy_container_snapshot(handle)?;
policy_container.response_content_security_policies =
self.child_effective_response_content_security_policies(handle);
policy_container.response_content_security_report_only_policies =
self.child_effective_response_content_security_report_only_policies(handle);
policy_container.content_security_reporting_endpoints =
self.child_effective_content_security_reporting_endpoints(handle);
Some(OwnerDocumentPolicySnapshot {
document_handle: self.child_browsing_context_document_handle(handle),
document_url: self.child_browsing_context_current_url(handle)?,
policy_container,
})
}
OwnerDispatchScope::LightweightPopup(popup_id) => Some(OwnerDocumentPolicySnapshot {
document_handle: self.lightweight_popup_document_handle(popup_id),
document_url: self.lightweight_popup_document_url(popup_id)?,
policy_container: self.lightweight_popup_policy_container(popup_id)?.clone(),
}),
}
}
pub(crate) fn document_policy_container(
&self,
) -> &crate::document_runtime::DocumentPolicyContainer {
@@ -60,57 +106,91 @@ impl JsContextHost {
&self,
owner: OwnerDispatchScope,
) -> Option<crate::document_runtime::DocumentConnectPolicySnapshot> {
let policy = match owner {
OwnerDispatchScope::Top => self.document_policy_container().clone(),
OwnerDispatchScope::Child(handle) => {
self.child_browsing_context_policy_container_snapshot(handle)?
}
OwnerDispatchScope::LightweightPopup(popup_id) => {
self.lightweight_popup_policy_container(popup_id)?.clone()
}
};
Some(crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(&policy))
let snapshot = self.owner_document_policy_snapshot(owner)?;
Some(
crate::document_runtime::DocumentConnectPolicySnapshot::from_policy_container(
&snapshot.policy_container,
),
)
}
pub(crate) fn trusted_types_for_script_requirements_for_owner(
&self,
owner: OwnerDispatchScope,
) -> Option<TrustedTypesForScriptRequirements> {
match owner {
OwnerDispatchScope::Top => {
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(unsafe { &*self.runtime }.trusted_types_for_script_requirements())
}
OwnerDispatchScope::Child(handle) => {
let response_policies =
self.child_effective_response_content_security_policies(handle);
let report_only_policies =
self.child_effective_response_content_security_report_only_policies(handle);
let reporting_endpoints =
self.child_effective_content_security_reporting_endpoints(handle);
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
self.child_browsing_context_document_handle(handle),
&response_policies,
&report_only_policies,
&reporting_endpoints,
),
)
}
OwnerDispatchScope::LightweightPopup(popup_id) => {
let policy = self.lightweight_popup_policy_container(popup_id)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
self.lightweight_popup_document_handle(popup_id),
&policy.response_content_security_policies,
&policy.response_content_security_report_only_policies,
&policy.content_security_reporting_endpoints,
),
)
}
let snapshot = self.owner_document_policy_snapshot(owner)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.trusted_types_for_script_requirements_for_document(
snapshot.document_handle,
&snapshot.policy_container.response_content_security_policies,
&snapshot
.policy_container
.response_content_security_report_only_policies,
&snapshot
.policy_container
.content_security_reporting_endpoints,
),
)
}
fn trusted_types_sink_csp_violations_for_owner(
&self,
owner: OwnerDispatchScope,
sink: &str,
sample: &str,
) -> Vec<DocumentContentSecurityPolicyViolation> {
let Some(snapshot) = self.owner_document_policy_snapshot(owner) else {
return Vec::new();
};
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &*self.runtime }.trusted_types_sink_csp_violations_for_document(
snapshot.document_handle,
&snapshot.document_url,
&snapshot.policy_container.response_content_security_policies,
&snapshot
.policy_container
.response_content_security_report_only_policies,
&snapshot
.policy_container
.content_security_reporting_endpoints,
sink,
sample,
)
}
/// Run the target Document checks immediately before a `javascript:` URL
/// executes. Source-context admission checks may already have happened at
/// the navigation API boundary; this is the Chromium-style target check
/// shared by top-level, child-frame, and lightweight-popup executors.
pub(crate) fn prepare_javascript_url_source_for_execution<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
owner: OwnerDispatchScope,
source: &str,
) -> Option<String> {
let csp_source = format!("javascript:{source}");
if !self.allows_inline_javascript_navigation_by_csp(scope, owner, &csp_source) {
return None;
}
let requirements = self.trusted_types_for_script_requirements_for_owner(owner)?;
let check = crate::context_bootstrap::check_javascript_url_trusted_types(
scope,
source,
requirements,
);
if check.violated {
let host_ptr: *mut JsContextHost = self;
self.dispatch_trusted_types_sink_csp_violation_event_for_owner_without_stack_best_effort(
scope,
host_ptr,
owner,
JAVASCRIPT_URL_TRUSTED_TYPES_SINK,
source,
);
}
check.source
}
pub(crate) fn cross_origin_embedder_policy(
@@ -256,7 +336,7 @@ impl JsContextHost {
kind: ContentSecurityPolicyNonUrlKind,
source: &str,
) -> bool {
let Some(check) = self.inline_source_csp_check_for_owner(scope, owner, kind, source) else {
let Some(check) = self.inline_source_csp_check_for_owner(owner, kind, source) else {
// Deliberately fail open only while a child or lightweight popup
// has no active document URL/policy context (for example during a
// navigation swap). CSP is document-scoped: applying the top-level
@@ -282,54 +362,27 @@ impl JsContextHost {
fn inline_source_csp_check_for_owner(
&self,
_scope: &mut v8::PinScope<'_, '_>,
owner: OwnerDispatchScope,
kind: ContentSecurityPolicyNonUrlKind,
source: &str,
) -> Option<DocumentContentSecurityPolicyCheck> {
match owner {
OwnerDispatchScope::Top => {
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(unsafe { &*self.runtime }.inline_source_csp_check(kind, source))
}
OwnerDispatchScope::Child(handle) => {
let document_url = self.child_browsing_context_current_url(handle)?;
let response_policies =
self.child_effective_response_content_security_policies(handle);
let response_report_only_policies =
self.child_effective_response_content_security_report_only_policies(handle);
let response_reporting_endpoints =
self.child_effective_content_security_reporting_endpoints(handle);
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.inline_source_csp_check_for_child_document(
self.child_browsing_context_document_handle(handle),
&document_url,
&response_policies,
&response_report_only_policies,
&response_reporting_endpoints,
kind,
source,
),
)
}
OwnerDispatchScope::LightweightPopup(popup_id) => {
let document_url = self.lightweight_popup_document_url(popup_id)?;
let policy_container = self.lightweight_popup_policy_container(popup_id)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.inline_source_csp_check_for_child_document(
self.lightweight_popup_document_handle(popup_id),
&document_url,
&policy_container.response_content_security_policies,
&policy_container.response_content_security_report_only_policies,
&policy_container.content_security_reporting_endpoints,
kind,
source,
),
)
}
}
let snapshot = self.owner_document_policy_snapshot(owner)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.inline_source_csp_check_for_document(
snapshot.document_handle,
&snapshot.document_url,
&snapshot.policy_container.response_content_security_policies,
&snapshot
.policy_container
.response_content_security_report_only_policies,
&snapshot
.policy_container
.content_security_reporting_endpoints,
kind,
source,
),
)
}
fn inline_script_element_csp_check_for_owner(
@@ -338,47 +391,23 @@ impl JsContextHost {
source: &str,
request: ContentSecurityPolicyScriptElementRequest<'_>,
) -> Option<DocumentContentSecurityPolicyCheck> {
match owner {
OwnerDispatchScope::Top => {
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(unsafe { &*self.runtime }.inline_script_element_csp_check(source, request))
}
OwnerDispatchScope::Child(handle) => {
let document_url = self.child_browsing_context_current_url(handle)?;
let response_policies =
self.child_effective_response_content_security_policies(handle);
let response_report_only_policies =
self.child_effective_response_content_security_report_only_policies(handle);
let response_reporting_endpoints =
self.child_effective_content_security_reporting_endpoints(handle);
Some(
unsafe { &*self.runtime }.inline_script_element_csp_check_for_child_document(
self.child_browsing_context_document_handle(handle),
&document_url,
&response_policies,
&response_report_only_policies,
&response_reporting_endpoints,
source,
request,
),
)
}
OwnerDispatchScope::LightweightPopup(popup_id) => {
let document_url = self.lightweight_popup_document_url(popup_id)?;
let policy_container = self.lightweight_popup_policy_container(popup_id)?;
Some(
unsafe { &*self.runtime }.inline_script_element_csp_check_for_child_document(
self.lightweight_popup_document_handle(popup_id),
&document_url,
&policy_container.response_content_security_policies,
&policy_container.response_content_security_report_only_policies,
&policy_container.content_security_reporting_endpoints,
source,
request,
),
)
}
}
let snapshot = self.owner_document_policy_snapshot(owner)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
Some(
unsafe { &*self.runtime }.inline_script_element_csp_check_for_document(
snapshot.document_handle,
&snapshot.document_url,
&snapshot.policy_container.response_content_security_policies,
&snapshot
.policy_container
.response_content_security_report_only_policies,
&snapshot
.policy_container
.content_security_reporting_endpoints,
source,
request,
),
)
}
pub(crate) fn entered_owner_dispatch_scope(
@@ -545,27 +574,18 @@ impl JsContextHost {
frame_handle: DomHandle,
request_url: &url::Url,
) -> Option<DocumentContentSecurityPolicyViolation> {
match self.frame_navigation_source_child_handle(frame_handle)? {
Some(source_child) => {
let document_url = self.child_browsing_context_current_url(source_child)?;
let response_policies =
self.child_effective_response_content_security_policies(source_child);
let response_reporting_endpoints =
self.child_effective_content_security_reporting_endpoints(source_child);
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &*self.runtime }.document_frame_csp_violation_for_child_document(
self.child_browsing_context_document_handle(source_child),
&document_url,
&response_policies,
&response_reporting_endpoints,
request_url,
)
}
None => {
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &*self.runtime }.document_frame_csp_violation(request_url)
}
}
let owner = self.owner_dispatch_scope_for_node(frame_handle)?;
let snapshot = self.owner_document_policy_snapshot(owner)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &*self.runtime }.document_frame_csp_violation_for_document(
snapshot.document_handle,
&snapshot.document_url,
&snapshot.policy_container.response_content_security_policies,
&snapshot
.policy_container
.content_security_reporting_endpoints,
request_url,
)
}
pub(crate) fn frame_navigation_csp_report_only_violation(
@@ -573,42 +593,19 @@ impl JsContextHost {
frame_handle: DomHandle,
request_url: &url::Url,
) -> Option<DocumentContentSecurityPolicyViolation> {
match self.frame_navigation_source_child_handle(frame_handle)? {
Some(source_child) => {
let document_url = self.child_browsing_context_current_url(source_child)?;
let response_report_only_policies = self
.child_effective_response_content_security_report_only_policies(source_child);
let response_reporting_endpoints =
self.child_effective_content_security_reporting_endpoints(source_child);
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &*self.runtime }
.document_frame_csp_report_only_violation_for_child_document(
&document_url,
&response_report_only_policies,
&response_reporting_endpoints,
request_url,
)
}
None => {
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &*self.runtime }.document_frame_csp_report_only_violation(request_url)
}
}
}
fn frame_navigation_source_child_handle(
&self,
frame_handle: DomHandle,
) -> Option<Option<DomHandle>> {
let owner_document = self
.dom_host()
.node(frame_handle)
.and_then(Node::owner_document)?;
if owner_document == self.document_handle() {
return Some(None);
}
self.child_browsing_context_host_for_document_handle(owner_document)
.map(Some)
let owner = self.owner_dispatch_scope_for_node(frame_handle)?;
let snapshot = self.owner_document_policy_snapshot(owner)?;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &*self.runtime }.document_frame_csp_report_only_violation_for_document(
&snapshot.document_url,
&snapshot
.policy_container
.response_content_security_report_only_policies,
&snapshot
.policy_container
.content_security_reporting_endpoints,
request_url,
)
}
fn child_effective_response_content_security_policies(
@@ -1072,6 +1069,23 @@ impl JsContextHost {
);
}
pub(crate) fn dispatch_trusted_types_sink_csp_violation_event_for_owner_without_stack_best_effort<
's,
>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
host_ptr: *mut JsContextHost,
owner: OwnerDispatchScope,
sink: &str,
sample: &str,
) {
for violation in self.trusted_types_sink_csp_violations_for_owner(owner, sink, sample) {
self.dispatch_content_security_policy_violation_event_for_owner_best_effort(
scope, host_ptr, owner, &violation,
);
}
}
fn dispatch_trusted_types_sink_csp_violation_event_with_location_best_effort<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
@@ -1270,15 +1284,15 @@ impl JsContextHost {
frame_handle: DomHandle,
violation: &DocumentContentSecurityPolicyViolation,
) {
match self.frame_navigation_source_child_handle(frame_handle) {
Some(Some(source_child)) => {
match self.owner_dispatch_scope_for_node(frame_handle) {
Some(OwnerDispatchScope::Child(source_child)) => {
self.dispatch_child_content_security_policy_violation_event_best_effort(
scope,
source_child,
violation,
);
}
Some(None) => {
Some(OwnerDispatchScope::Top) => {
let host_ptr: *mut JsContextHost = self;
// SAFETY: JsContextHost is owned by the ScriptVm that owns this DocumentRuntime.
unsafe { &mut *self.runtime }
@@ -1286,6 +1300,11 @@ impl JsContextHost {
scope, host_ptr, violation,
);
}
Some(OwnerDispatchScope::LightweightPopup(popup_id)) => {
self.dispatch_lightweight_popup_content_security_policy_violation_event_best_effort(
scope, popup_id, violation,
);
}
None => {
tracing::error!(
blocked_uri = violation.blocked_uri.as_str(),
+51 -28
View File
@@ -628,6 +628,17 @@ struct RuntimeExpressionAwaitSpec {
navigation_policy: RuntimeEvaluationNavigationPolicy,
}
struct RendererPostResponseOwnerWork {
document_lifecycle: Option<RendererDocumentLifecycleIdentity>,
navigation_handoff: Option<RendererTopLevelNavigationHandoff>,
}
impl RendererPostResponseOwnerWork {
const fn is_empty(&self) -> bool {
self.document_lifecycle.is_none() && self.navigation_handoff.is_none()
}
}
fn checked_live_page_wait_deadline(timeout_ms: u64, operation: &str) -> Result<Instant> {
let timeout = std::time::Duration::from_millis(timeout_ms);
Instant::now()
@@ -1597,7 +1608,7 @@ impl RendererOwnerHandle {
Ok(entry) => entry,
Err(error) => return Err(error).into(),
};
entry.begin_standalone_navigation_follow();
entry.begin_renderer_navigation_follow();
self.restore_live_page_entry(token, entry);
}
match self
@@ -1957,16 +1968,23 @@ impl RendererOwnerHandle {
);
}
fn post_response_document_lifecycle_continuation(
fn post_response_owner_work_continuation(
&self,
token: RendererPageToken,
document: RendererDocumentLifecycleIdentity,
work: RendererPostResponseOwnerWork,
) -> RendererPageCommandPostResponseContinuation {
let page_wake_tx = self.state.page_wake_tx.clone();
RendererPageCommandPostResponseContinuation::new(move || {
let _ = page_wake_tx.send(RendererOwnerWake::post_response_document_lifecycle(
token, document,
));
if let Some(document) = work.document_lifecycle {
let _ = page_wake_tx.send(RendererOwnerWake::post_response_document_lifecycle(
token, document,
));
}
if let Some(handoff) = work.navigation_handoff {
let _ = page_wake_tx.send(RendererOwnerWake::top_level_navigation_handoff(
token, handoff,
));
}
})
}
@@ -3380,7 +3398,7 @@ impl RendererOwnerHandle {
mut entry: RetiringPageEntry,
error: anyhow::Error,
) -> RenderRuntimeDispatchOutcome {
entry.settle_standalone_navigation_follow(false);
entry.settle_renderer_navigation_follow(false);
tracing::warn!(
page_id = token.page_id.as_u64(),
failure = %error,
@@ -3407,7 +3425,7 @@ impl RendererOwnerHandle {
"retiring page after committed navigation failed to bootstrap"
);
let cleanup_failure = disposition.to_string();
entry.settle_standalone_navigation_follow(false);
entry.settle_renderer_navigation_follow(false);
remove_page_on_bound_owner_local_store(token);
let entry = entry.reject_and_retire(&cleanup_failure);
restore_retiring_entry_after_command_on_bound_owner_local_store(token, entry);
@@ -3458,7 +3476,7 @@ impl RendererOwnerHandle {
);
remove_page_on_bound_owner_local_store(token);
}
entry.settle_standalone_navigation_follow(false);
entry.settle_renderer_navigation_follow(false);
self.restore_live_page_entry(token, entry);
disposition.into_dispatch_outcome(token, page_creation_publication)
}
@@ -3784,7 +3802,7 @@ impl RendererOwnerHandle {
return RenderRuntimeDispatchOutcome::BackgroundComplete(Ok(()));
}
};
let claimed = entry.begin_standalone_navigation_follow_from_handoff(handoff);
let claimed = entry.begin_renderer_navigation_follow_from_handoff(handoff);
self.restore_live_page_entry(token, entry);
if !claimed {
tracing::trace!(
@@ -4212,7 +4230,7 @@ impl RendererOwnerHandle {
action,
DocumentLifecycleTurnAction::RequestedTopLevelNavigation { .. }
) && entry
.begin_standalone_navigation_follow();
.begin_renderer_navigation_follow();
let delegated_top_level_navigation = if matches!(
action,
DocumentLifecycleTurnAction::RequestedTopLevelNavigation { .. }
@@ -4885,7 +4903,7 @@ impl RendererOwnerHandle {
mut entry: LivePageEntry,
completion: LivePagePendingNavigationCompletion,
) -> RenderRuntimeDispatchOutcome {
entry.settle_standalone_navigation_follow(true);
entry.settle_renderer_navigation_follow(true);
match completion {
LivePagePendingNavigationCompletion::Background
| LivePagePendingNavigationCompletion::PublishedPageCreation { .. } => {
@@ -5001,7 +5019,7 @@ impl RendererOwnerHandle {
completion: LivePagePendingNavigationCompletion,
download: RendererPendingDownloadActivation,
) -> RenderRuntimeDispatchOutcome {
entry.settle_standalone_navigation_follow(true);
entry.settle_renderer_navigation_follow(true);
match completion {
LivePagePendingNavigationCompletion::Background
| LivePagePendingNavigationCompletion::PublishedPageCreation { .. } => {
@@ -5051,7 +5069,7 @@ impl RendererOwnerHandle {
completion: LivePagePendingNavigationCompletion,
) -> RenderRuntimeDispatchOutcome {
if follow_count == 0 {
entry.begin_standalone_navigation_follow();
entry.begin_renderer_navigation_follow();
}
self.restore_live_page_entry(token, entry);
RenderRuntimeDispatchOutcome::ContinueNextTurn(Box::new(
@@ -5450,20 +5468,25 @@ impl RendererOwnerHandle {
},
);
}
let post_response_continuation =
match replacement_lifecycle.map(|outcome| outcome.readiness) {
Some(
DocumentLifecycleTurnReadiness::Runnable { document }
| DocumentLifecycleTurnReadiness::Blocked { document },
) => {
if let Err(error) = entry.defer_document_lifecycle_until_response(document) {
self.restore_live_page_entry(token, entry);
return Err(error).into();
}
Some(self.post_response_document_lifecycle_continuation(token, document))
let deferred_document = match replacement_lifecycle.map(|outcome| outcome.readiness) {
Some(
DocumentLifecycleTurnReadiness::Runnable { document }
| DocumentLifecycleTurnReadiness::Blocked { document },
) => {
if let Err(error) = entry.defer_document_lifecycle_until_response(document) {
self.restore_live_page_entry(token, entry);
return Err(error).into();
}
Some(DocumentLifecycleTurnReadiness::Idle) | None => None,
};
Some(document)
}
Some(DocumentLifecycleTurnReadiness::Idle) | None => None,
};
let post_response_work = RendererPostResponseOwnerWork {
document_lifecycle: deferred_document,
navigation_handoff: entry.pending_javascript_navigation_handoff(),
};
let post_response_continuation = (!post_response_work.is_empty())
.then(|| self.post_response_owner_work_continuation(token, post_response_work));
self.finish_live_page_entry_with_page_state_and_continuation(
token,
entry,
@@ -6086,7 +6109,7 @@ impl RendererOwnerHandle {
Err(error) => return Err(error).into(),
};
if follow_count == 0 {
entry.begin_standalone_navigation_follow();
entry.begin_renderer_navigation_follow();
}
let retire_page_on_failure = completion.retires_page_on_navigation_failure();
if follow_count >= MAX_PENDING_LOCATION_NAVIGATION_TURNS {
@@ -233,7 +233,7 @@ impl RendererOwnerHandle {
));
}
if entry.page_vm().vm().has_pending_location_navigation()
&& entry.begin_standalone_navigation_follow()
&& entry.begin_renderer_navigation_follow()
{
return self.continue_live_page_pending_navigation(
token,
@@ -2,7 +2,7 @@ use super::navigation_follow::CommittedNavigationBootstrapCompletion;
use super::*;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) enum StandaloneNavigationFollowState {
pub(super) enum RendererNavigationFollowState {
#[default]
Idle,
Following {
@@ -21,7 +21,7 @@ pub(in crate::runtime) struct PublishedReplacementDocument {
pub(in crate::runtime) view_generation: u64,
}
impl StandaloneNavigationFollowState {
impl RendererNavigationFollowState {
pub(super) fn claim(
&mut self,
current: Option<crate::page_task_queue::RendererTopLevelNavigationHandoff>,
@@ -65,7 +65,7 @@ impl StandaloneNavigationFollowState {
pub(in crate::runtime) struct LivePageEntry {
pub(in crate::runtime) slot: RendererPageSlotHandle,
pub(super) top_level_navigation_dispatch: RendererTopLevelNavigationDispatch,
pub(super) standalone_navigation_follow: StandaloneNavigationFollowState,
pub(super) renderer_navigation_follow: RendererNavigationFollowState,
// Keep executable continuation state before `vm`: Rust drops fields in
// declaration order, so an exceptional entry drop still releases any
// ScriptVm-bound task before releasing the PageVm itself.
@@ -180,9 +180,9 @@ impl CommittedNavigationEntry {
RetiringPageEntry::new(self.entry)
}
pub(in crate::runtime) fn settle_standalone_navigation_follow(&mut self, succeeded: bool) {
pub(in crate::runtime) fn settle_renderer_navigation_follow(&mut self, succeeded: bool) {
self.entry
.standalone_navigation_follow
.renderer_navigation_follow
.settle(None, succeeded);
}
}
@@ -196,8 +196,8 @@ impl RetiringPageEntry {
Self { entry }
}
pub(in crate::runtime) fn settle_standalone_navigation_follow(&mut self, succeeded: bool) {
self.entry.settle_standalone_navigation_follow(succeeded);
pub(in crate::runtime) fn settle_renderer_navigation_follow(&mut self, succeeded: bool) {
self.entry.settle_renderer_navigation_follow(succeeded);
}
}
@@ -210,8 +210,8 @@ impl std::fmt::Debug for LivePageEntry {
&self.top_level_navigation_dispatch,
)
.field(
"standalone_navigation_follow",
&self.standalone_navigation_follow,
"renderer_navigation_follow",
&self.renderer_navigation_follow,
)
.field("vm", &self.vm)
.field(
@@ -244,7 +244,7 @@ impl LivePageEntry {
slot,
top_level_navigation_dispatch:
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter,
standalone_navigation_follow: StandaloneNavigationFollowState::Idle,
renderer_navigation_follow: RendererNavigationFollowState::Idle,
pending_document_lifecycle_turn: None,
post_response_document_lifecycle: None,
vm: Some(vm),
@@ -264,7 +264,7 @@ impl LivePageEntry {
slot,
top_level_navigation_dispatch:
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter,
standalone_navigation_follow: StandaloneNavigationFollowState::Idle,
renderer_navigation_follow: RendererNavigationFollowState::Idle,
pending_document_lifecycle_turn: None,
post_response_document_lifecycle: None,
vm: None,
@@ -294,45 +294,60 @@ impl LivePageEntry {
self.top_level_navigation_dispatch
}
/// Claim the single standalone owner chain for the current pending
/// location navigation. A failed chain remains suppressed while that same
/// descriptor is pending, so a duplicate producer handoff cannot restart
/// the chain with a fresh limit.
pub(in crate::runtime) fn begin_standalone_navigation_follow(&mut self) -> bool {
self.begin_standalone_navigation_follow_for_handoff(None)
pub(in crate::runtime) fn pending_javascript_navigation_handoff(
&self,
) -> Option<crate::page_task_queue::RendererTopLevelNavigationHandoff> {
let vm = self.active_page_vm()?.vm();
vm.pending_location_navigation_scheme_is("javascript")
.then(|| vm.pending_location_navigation_handoff())
.flatten()
}
fn renderer_owned_pending_navigation_handoff(
&self,
) -> Option<crate::page_task_queue::RendererTopLevelNavigationHandoff> {
if matches!(
self.top_level_navigation_dispatch,
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter
) {
return self
.active_page_vm()
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
}
self.pending_javascript_navigation_handoff()
}
/// Claim the renderer-owned chain for the current pending location
/// navigation. Standalone pages keep every navigation in the renderer;
/// delegated pages retain schemes such as `javascript:` that must not
/// cross the browser navigation boundary.
pub(in crate::runtime) fn begin_renderer_navigation_follow(&mut self) -> bool {
self.begin_renderer_navigation_follow_for_handoff(None)
}
/// Claim a producer handoff only while the same request still occupies
/// the ScriptVm's unique pending navigation slot. A delayed wake for an
/// overwritten request therefore cannot start the replacement request.
pub(in crate::runtime) fn begin_standalone_navigation_follow_from_handoff(
pub(in crate::runtime) fn begin_renderer_navigation_follow_from_handoff(
&mut self,
handoff: crate::page_task_queue::RendererTopLevelNavigationHandoff,
) -> bool {
self.begin_standalone_navigation_follow_for_handoff(Some(handoff))
self.begin_renderer_navigation_follow_for_handoff(Some(handoff))
}
pub(super) fn begin_standalone_navigation_follow_for_handoff(
pub(super) fn begin_renderer_navigation_follow_for_handoff(
&mut self,
requested: Option<crate::page_task_queue::RendererTopLevelNavigationHandoff>,
) -> bool {
if !matches!(
self.top_level_navigation_dispatch,
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter
) {
return false;
}
let current = self
.active_page_vm()
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
self.standalone_navigation_follow.claim(current, requested)
let current = self.renderer_owned_pending_navigation_handoff();
self.renderer_navigation_follow.claim(current, requested)
}
pub(in crate::runtime) fn settle_standalone_navigation_follow(&mut self, succeeded: bool) {
pub(in crate::runtime) fn settle_renderer_navigation_follow(&mut self, succeeded: bool) {
let current = self
.active_page_vm()
.and_then(|page_vm| page_vm.vm().pending_location_navigation_handoff());
self.standalone_navigation_follow.settle(current, succeeded);
self.renderer_navigation_follow.settle(current, succeeded);
}
/// Commit the source Document synchronously. The navigation task guard
@@ -55,7 +55,7 @@ mod tests;
pub(in crate::runtime) use bound::*;
#[cfg(test)]
use entry::StandaloneNavigationFollowState;
use entry::RendererNavigationFollowState;
pub(in crate::runtime) use entry::{
CommittedNavigationEntry, LivePageEntry, PublishedReplacementDocument, RetiringPageEntry,
};
@@ -87,7 +87,7 @@ mod navigation_dispatch_tests {
}
fn phase_one_entry_shell_without_active_page_vm(
standalone_navigation_follow: StandaloneNavigationFollowState,
renderer_navigation_follow: RendererNavigationFollowState,
) -> LivePageEntry {
let page_id = PageId::new_for_testing(1);
let (page_context_cancel_tx, _page_context_cancel_rx) =
@@ -102,7 +102,7 @@ mod navigation_dispatch_tests {
slot,
top_level_navigation_dispatch:
RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter,
standalone_navigation_follow,
renderer_navigation_follow,
pending_document_lifecycle_turn: None,
post_response_document_lifecycle: None,
vm: None,
@@ -116,15 +116,15 @@ mod navigation_dispatch_tests {
let handoff = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(1);
for succeeded in [false, true] {
for mut state in [
StandaloneNavigationFollowState::Idle,
StandaloneNavigationFollowState::Following { handoff },
StandaloneNavigationFollowState::FailedWithPendingNavigation { handoff },
RendererNavigationFollowState::Idle,
RendererNavigationFollowState::Following { handoff },
RendererNavigationFollowState::FailedWithPendingNavigation { handoff },
] {
state.settle(None, succeeded);
assert_eq!(
state,
StandaloneNavigationFollowState::Idle,
RendererNavigationFollowState::Idle,
"a committed navigation must settle to Idle after succeeded={succeeded}"
);
}
@@ -134,7 +134,7 @@ mod navigation_dispatch_tests {
#[test]
fn failed_phase_one_advance_returns_a_retiring_entry() {
let advance = phase_one::classify_pending_phase_one_entry_advance(
phase_one_entry_shell_without_active_page_vm(StandaloneNavigationFollowState::Idle),
phase_one_entry_shell_without_active_page_vm(RendererNavigationFollowState::Idle),
Err(anyhow!("resume failed")),
);
@@ -152,7 +152,7 @@ mod navigation_dispatch_tests {
#[test]
fn successful_phase_one_advance_without_a_vm_is_retired_as_an_invariant_error() {
let advance = phase_one::classify_pending_phase_one_entry_advance(
phase_one_entry_shell_without_active_page_vm(StandaloneNavigationFollowState::Idle),
phase_one_entry_shell_without_active_page_vm(RendererNavigationFollowState::Idle),
Ok(
LivePagePendingNavigationPhaseOneAdvance::TriggeredNavigation {
stage: PageVmInitStage::DomContentLoaded,
@@ -179,14 +179,14 @@ mod navigation_dispatch_tests {
fn navigation_handoff_claim_rejects_stale_and_duplicate_requests() {
let first = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(1);
let second = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(2);
let mut state = StandaloneNavigationFollowState::Idle;
let mut state = RendererNavigationFollowState::Idle;
assert!(!state.claim(Some(second), Some(first)));
assert_eq!(state, StandaloneNavigationFollowState::Idle);
assert_eq!(state, RendererNavigationFollowState::Idle);
assert!(state.claim(Some(second), Some(second)));
assert_eq!(
state,
StandaloneNavigationFollowState::Following { handoff: second }
RendererNavigationFollowState::Following { handoff: second }
);
assert!(!state.claim(Some(second), Some(second)));
}
@@ -195,18 +195,18 @@ mod navigation_dispatch_tests {
fn failed_navigation_suppresses_only_the_same_request_identity() {
let first = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(1);
let second = crate::page_task_queue::RendererTopLevelNavigationHandoff::new(2);
let mut state = StandaloneNavigationFollowState::Following { handoff: first };
let mut state = RendererNavigationFollowState::Following { handoff: first };
state.settle(Some(first), false);
assert_eq!(
state,
StandaloneNavigationFollowState::FailedWithPendingNavigation { handoff: first }
RendererNavigationFollowState::FailedWithPendingNavigation { handoff: first }
);
assert!(!state.claim(Some(first), Some(first)));
assert!(state.claim(Some(second), Some(second)));
assert_eq!(
state,
StandaloneNavigationFollowState::Following { handoff: second }
RendererNavigationFollowState::Following { handoff: second }
);
}
@@ -1144,7 +1144,18 @@ impl PageVm {
);
self.vm_mut()
.restore_top_level_location_runtime_state(&initiator_url);
let replacement_html = self.vm_mut().eval_javascript_url_runtime_turn(&source)?;
if self.vm().script_execution_disabled() {
return Ok(PageVmFollowNavigationTurnOutcome::Completed);
}
let replacement_html = self
.vm_mut()
.eval_javascript_url_runtime_turn(&source, &initiator_url)?;
// Chromium suppresses a string completion when the script synchronously
// starts a normal navigation or submits a form. That newer navigation
// owns the target Document and must win over javascript: replacement.
if self.vm().has_pending_location_navigation() {
return Ok(PageVmFollowNavigationTurnOutcome::TriggeredNavigation { stage });
}
if let Some(replacement_html) = replacement_html {
self.document_lifecycle.set_next_document_open_start_reason(
RendererLifecycleStartReason::JavascriptDocumentReplacement,
@@ -867,6 +867,18 @@ fn page_diagnostics_snapshot_observes_but_does_not_drain_lifecycle_state() {
);
}
async fn follow_pending_javascript_url_for_test(
page_vm: &mut PageVm,
) -> anyhow::Result<crate::runtime::PageVmFollowNavigationTurnOutcome> {
let mut pending_document_lifecycle_turn = None;
page_vm
.follow_pending_location_navigation_one_turn_async(
&mut pending_document_lifecycle_turn,
PageVmInitStage::Load,
)
.await
}
#[tokio::test]
async fn javascript_location_navigation_executes_when_pending_navigation_is_followed() {
run_page_vm_async_test(async move {
@@ -878,7 +890,6 @@ async fn javascript_location_navigation_executes_when_pending_navigation_is_foll
let (outcome_is_completed, log, href, network_records) = local_executor
.run(async move {
let mut page_vm = page_vm;
let mut pending_document_lifecycle_turn = None;
page_vm.vm_mut().eval(
r#"
globalThis.__events = [];
@@ -892,12 +903,7 @@ globalThis.__events.push('after setter');
"javascript: location should remain pending until the owner follows it"
);
let outcome = page_vm
.follow_pending_location_navigation_one_turn_async(
&mut pending_document_lifecycle_turn,
PageVmInitStage::Load,
)
.await?;
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
let log = page_vm
.vm_mut()
.eval("JSON.stringify(globalThis.__events)")?;
@@ -929,7 +935,240 @@ globalThis.__events.push('after setter');
}
#[tokio::test]
async fn javascript_location_assignment_keeps_href_and_drops_pending_when_not_followed() {
async fn javascript_location_navigation_exception_is_reported_without_failing_navigation() {
run_page_vm_async_test(async move {
let page_vm = test_page_vm_with_document_url(
Url::parse("https://javascript-location-exception.test/start.html").unwrap(),
);
let local_executor = page_vm.local_executor.clone();
let (completed, error_count, href) = local_executor
.run(async move {
let mut page_vm = page_vm;
page_vm.vm_mut().eval(
r#"
globalThis.__javascriptUrlErrorCount = 0;
window.addEventListener("error", event => {
globalThis.__javascriptUrlErrorCount += 1;
event.preventDefault();
});
location.href = "javascript:throw new Error('expected javascript URL failure')";
"queued"
"#,
)?;
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
let error_count = page_vm
.vm_mut()
.eval("String(globalThis.__javascriptUrlErrorCount)")?;
let href = page_vm.vm_mut().eval("location.href")?;
Ok::<_, anyhow::Error>((
matches!(
outcome,
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
),
error_count,
href,
))
})
.await
.expect("javascript: exception should not fail the navigation task");
assert!(completed);
assert_eq!(error_count, "1");
assert_eq!(
href,
"https://javascript-location-exception.test/start.html"
);
})
.await;
}
#[tokio::test]
async fn javascript_location_navigation_obeys_trusted_types_pre_navigation_check() {
run_page_vm_async_test(async move {
let page_vm = test_page_vm_with_document_url(
Url::parse("https://javascript-location-trusted-types.test/start.html").unwrap(),
);
let local_executor = page_vm.local_executor.clone();
let ran = local_executor
.run(async move {
let mut page_vm = page_vm;
page_vm.vm_mut().set_response_content_security_policies(&[
"require-trusted-types-for 'script'".to_owned(),
]);
page_vm.vm_mut().eval(
r#"
globalThis.__blockedJavascriptUrlRan = false;
location.href = "javascript:globalThis.__blockedJavascriptUrlRan = true";
"queued"
"#,
)?;
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
assert!(matches!(
outcome,
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
));
page_vm
.vm_mut()
.eval("String(globalThis.__blockedJavascriptUrlRan)")
})
.await
.expect("Trusted Types should block the javascript: task without failing it");
assert_eq!(ran, "false");
})
.await;
}
#[tokio::test]
async fn javascript_location_navigation_executes_default_policy_modified_source() {
run_page_vm_async_test(async move {
let page_vm = test_page_vm_with_document_url(
Url::parse("https://javascript-location-default-policy.test/start.html").unwrap(),
);
let local_executor = page_vm.local_executor.clone();
let observed = local_executor
.run(async move {
let mut page_vm = page_vm;
page_vm
.vm_mut()
.set_response_content_security_policies(&[
"require-trusted-types-for 'script'".to_owned(),
]);
page_vm.vm_mut().eval(
r#"
globalThis.__originalJavascriptUrlRan = false;
globalThis.__modifiedJavascriptUrlRan = false;
globalThis.__javascriptUrlPolicyCalls = [];
trustedTypes.createPolicy("default", {
createScript(value, type, sink) {
globalThis.__javascriptUrlPolicyCalls.push([value, type, sink]);
return value.replace("__originalJavascriptUrlRan", "__modifiedJavascriptUrlRan");
}
});
location.href = "javascript:globalThis.__originalJavascriptUrlRan = true";
"queued"
"#,
)?;
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
assert!(matches!(
outcome,
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
));
page_vm.vm_mut().eval(
r#"JSON.stringify({
original: globalThis.__originalJavascriptUrlRan,
modified: globalThis.__modifiedJavascriptUrlRan,
calls: globalThis.__javascriptUrlPolicyCalls
})"#,
)
})
.await
.expect("Trusted Types default policy should rewrite javascript: source");
assert_eq!(
observed,
r#"{"original":false,"modified":true,"calls":[["globalThis.__originalJavascriptUrlRan = true","TrustedScript","Location href"]]}"#
);
})
.await;
}
#[tokio::test]
async fn queued_top_level_javascript_url_does_not_execute_after_scripting_is_disabled() {
run_page_vm_async_test(async move {
let page_vm = test_page_vm_with_document_url(
Url::parse("https://javascript-location-disabled.test/start.html").unwrap(),
);
let local_executor = page_vm.local_executor.clone();
let (completed, ran) = local_executor
.run(async move {
let mut page_vm = page_vm;
page_vm.vm_mut().eval(
r#"
globalThis.__disabledJavascriptUrlRan = false;
location.href = "javascript:globalThis.__disabledJavascriptUrlRan = true";
"queued"
"#,
)?;
page_vm.vm_mut().set_script_execution_disabled(true);
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
let ran = page_vm
.vm_mut()
.eval("String(globalThis.__disabledJavascriptUrlRan)")?;
Ok::<_, anyhow::Error>((
matches!(
outcome,
crate::runtime::PageVmFollowNavigationTurnOutcome::Completed
),
ran,
))
})
.await
.expect("disabled javascript: task should complete without execution");
assert!(completed);
assert_eq!(ran, "false");
})
.await;
}
#[tokio::test]
async fn javascript_string_completion_does_not_replace_after_new_navigation_is_triggered() {
run_page_vm_async_test(async move {
let page_vm = test_page_vm_with_document_url(
Url::parse("https://javascript-location-precedence.test/start.html").unwrap(),
);
let local_executor = page_vm.local_executor.clone();
let (triggered, pending_url, body_text) = local_executor
.run(async move {
let mut page_vm = page_vm;
page_vm.vm_mut().eval(
r#"
document.body.textContent = "original body";
location.href = "javascript:(location.href = 'https://javascript-location-precedence.test/winner.html', 'replacement body')";
"queued"
"#,
)?;
let outcome = follow_pending_javascript_url_for_test(&mut page_vm).await?;
let pending = page_vm
.vm_mut()
.take_pending_location_navigation_with_seed()
.expect("javascript: execution should leave the newer navigation pending");
let body_text = page_vm.vm_mut().eval("document.body.textContent")?;
Ok::<_, anyhow::Error>((
matches!(
outcome,
crate::runtime::PageVmFollowNavigationTurnOutcome::TriggeredNavigation { .. }
),
pending.url.to_string(),
body_text,
))
})
.await
.expect("newer navigation should suppress javascript: string replacement");
assert!(triggered);
assert_eq!(
pending_url,
"https://javascript-location-precedence.test/winner.html"
);
assert_eq!(body_text, "original body");
})
.await;
}
#[tokio::test]
async fn javascript_location_assignment_keeps_href_and_pending_when_not_delegated() {
run_page_vm_async_test(async move {
let page_vm = test_page_vm_with_document_url(
Url::parse("https://javascript-location-ghost.test/start.html").unwrap(),
@@ -960,8 +1199,8 @@ location.href
"a javascript: pending navigation must never be published to the browser"
);
assert!(
!page_vm.vm().has_pending_location_navigation(),
"dropping a javascript: pending navigation must clear the pending record"
page_vm.vm().has_pending_location_navigation(),
"browser publication must leave renderer-owned javascript: work pending"
);
let href = page_vm.vm_mut().eval("location.href")?;
assert_eq!(
+26 -5
View File
@@ -1873,7 +1873,7 @@ document.body.appendChild(frame);
}
#[tokio::test(flavor = "multi_thread")]
async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate_pages() {
async fn javascript_location_assignment_executes_renderer_owned_task_on_delegate_pages() {
let runtime = JsRuntime::initialize();
let loader =
ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("default loader");
@@ -1889,8 +1889,14 @@ async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate
let (set_reply, _) = page
.run_async_command(RendererPageCommand::EvaluateExpression {
expression: r#"location.href = "javascript:document.title = 'LOC-RAN'"; "set""#
.to_owned(),
expression: r#"
globalThis.__javascriptLocationDone = new Promise(resolve => {
globalThis.__resolveJavascriptLocation = resolve;
});
location.href = "javascript:document.title = 'LOC-RAN'; globalThis.__resolveJavascriptLocation('ran'); void 0";
"set"
"#
.to_owned(),
await_promise: false,
})
.await
@@ -1915,6 +1921,22 @@ async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate
"assigning a javascript: URL must not ghost location.href"
);
let (task_reply, _) = tokio::time::timeout(
Duration::from_secs(2),
page.run_async_command(RendererPageCommand::EvaluateExpression {
expression: "globalThis.__javascriptLocationDone".to_owned(),
await_promise: true,
}),
)
.await
.expect("renderer-owned javascript: task should settle its promise")
.expect("javascript: task promise should evaluate");
assert_eq!(
renderer_json_value(task_reply),
Some(serde_json::json!("ran")),
"the javascript: task must run after the assigning command"
);
let (title_reply, _) = page
.run_async_command(RendererPageCommand::EvaluateExpression {
expression: "document.title".to_owned(),
@@ -1924,8 +1946,7 @@ async fn javascript_location_assignment_does_not_ghost_location_href_on_delegate
.expect("document.title readback should complete");
assert_eq!(
renderer_json_value(title_reply),
Some(serde_json::json!("start")),
"a javascript: location assignment must not execute its script"
Some(serde_json::json!("LOC-RAN"))
);
page.close_async()
-5
View File
@@ -5568,11 +5568,6 @@ impl ScriptVm {
&mut self,
) -> Option<super::native_bridge::PendingLocationNavigation> {
if self.pending_location_navigation_scheme_is("javascript") {
let source_url = self.document_runtime.document_url().clone();
self._context_host
.borrow_mut()
.clear_pending_location_navigation();
self.restore_top_level_location_runtime_state(&source_url);
return None;
}
let source_url = self.document_runtime.document_url().clone();
+49 -12
View File
@@ -652,20 +652,57 @@ impl ScriptVm {
pub(crate) fn eval_javascript_url_runtime_turn(
&mut self,
source: &str,
base_url: &Url,
) -> Result<Option<String>> {
let source = source.to_owned();
let base_url = base_url.clone();
let context_ptr: *const v8::Global<v8::Context> = &self.page_default_context as *const _;
let completion = self
.execute_source_text_in_context_ptr_runtime_turn_with_base_url_and_current_window_error_report(
context_ptr,
source,
None,
None,
0,
None,
true,
false,
SourceTextScriptCompletionMode::ValueTypeAware,
)?;
let provenance = CompiledStringProvenance::at_url(base_url.clone());
let pending = PendingScriptTurn::new(
self.renderer_document_isolate
.with_renderer_document_isolate_mut::<
RawScriptExecutionResult<Option<SourceTextScriptCompletion>>,
>(|isolate| {
let scope = pin!(v8::HandleScope::new(isolate));
let scope = &mut scope.init();
let context = unsafe { v8::Local::new(scope, &*context_ptr) };
let scope = &mut v8::ContextScope::new(scope, context);
let host_ptr = context_host_ptr_from_global_bridge(scope).ok_or_else(|| {
anyhow!("javascript URL target context has no Window host")
})?;
let Some(source) = unsafe { &mut *host_ptr }
.prepare_javascript_url_source_for_execution(
scope,
crate::native_bridge::OwnerDispatchScope::Top,
&source,
)
else {
Self::perform_microtask_checkpoints(scope, Some(&base_url))?;
return Ok(None);
};
execute_source_text_on_current_stack_with_completion(
scope,
&source,
Some(&provenance),
0,
None,
true,
UncaughtScriptReportTarget::CurrentWindow,
SourceTextScriptCompletionMode::ValueTypeAware,
)
.map(Some)
}),
);
let completion = pending
.finish_with_style_drain(self, StyleInvalidationTurnExitBoundary::RuntimeEvaluate);
let completion = match completion {
Ok(Some(completion)) => completion,
Ok(None) => return Ok(None),
// A javascript: URL reports script exceptions to its Window, but
// the navigation itself completes without replacing the Document.
Err(RawScriptExecutionError::Exception { .. }) => return Ok(None),
Err(RawScriptExecutionError::Internal(error)) => return Err(error),
};
Ok(match completion {
SourceTextScriptCompletion::String(value) => Some(value),
SourceTextScriptCompletion::NonString => None,
@@ -87,7 +87,7 @@ impl ScriptVm {
}
SourceTextScriptCompletion::NonString => Ok(FrameScriptCompletionValue::NonString),
SourceTextScriptCompletion::Ignored => Err(anyhow!(
"source-text frame script job unexpectedly ignored completion"
"value-aware frame script execution unexpectedly ignored its completion"
)),
})
}
@@ -122,7 +122,11 @@ impl ScriptVm {
let Some(job) = self.prepare_inline_classic_frame_script_job(job)? else {
return Ok(SourceTextScriptCompletion::Ignored);
};
let mut job = job;
let realm_id = self.current_realm_id_for_frame_script_job(&job)?;
if !self.prepare_javascript_url_frame_script_job(realm_id, &mut job)? {
return Ok(SourceTextScriptCompletion::NonString);
}
let is_source_text = matches!(&job.source, FrameScriptSource::SourceText(_));
let context_ptr = is_source_text
.then(|| self.frame_realm_context_ptr(realm_id))
@@ -203,6 +207,40 @@ impl ScriptVm {
result
}
fn prepare_javascript_url_frame_script_job(
&mut self,
realm_id: FrameRealmId,
job: &mut FrameScriptJob,
) -> Result<bool> {
if job.kind != crate::frame_owner_model::FrameScriptJobKind::JavascriptUrl {
return Ok(true);
}
let child_handle = self
._context_host
.borrow()
.frame_owner_child_handle_for_script_job(job)
.ok_or_else(|| anyhow!("javascript URL frame job has no current child owner"))?;
#[cfg(not(test))]
let FrameScriptSource::SourceText(source) = &mut job.source;
#[cfg(test)]
let source = match &mut job.source {
FrameScriptSource::SourceText(source) => source,
FrameScriptSource::FunctionConstructor(_) => {
return Err(anyhow!("javascript URL frame job has no source text"));
}
};
self.with_frame_realm_scope(realm_id, |scope, host_ptr| {
let owner = crate::native_bridge::OwnerDispatchScope::Child(child_handle);
let Some(checked_source) = unsafe { &mut *host_ptr }
.prepare_javascript_url_source_for_execution(scope, owner, source)
else {
return Ok(false);
};
*source = checked_source;
Ok(true)
})
}
fn prepare_inline_classic_frame_script_job(
&mut self,
mut job: FrameScriptJob,
@@ -151,6 +151,9 @@ pub(super) unsafe extern "C" fn string_code_generation_check_callback(
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return true;
};
if crate::context_bootstrap::consume_internal_javascript_url_eval(scope) {
return true;
}
let requires_trusted_types_for_script =
unsafe { &*host_ptr }.requires_trusted_types_for_script(scope);
let action = if requires_trusted_types_for_script {
@@ -25035,6 +25035,162 @@ async fn lightweight_popup_javascript_url_navigation_runs_async_with_opener() {
);
}
#[tokio::test]
async fn lightweight_popup_javascript_url_string_completion_replaces_document() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm = new_storage_test_vm_with_loader("https://example.com/base/page.html", &loader);
let setup = vm
.eval(
r#"
(() => {
globalThis.__stringCompletionPopup = open();
__stringCompletionPopup.location.href =
"javascript:'<!doctype html><main id=javascript-url-result>replaced</main>'";
return `${__stringCompletionPopup.location.href}|${__stringCompletionPopup.document.body.textContent}`;
})()
"#,
)
.expect("popup javascript string completion setup should evaluate");
assert_eq!(setup, "about:blank|");
vm.drain_pending_child_frame_work_for_test();
assert!(
vm.run_next_due_timer_callback_for_test(&loader)
.await
.expect("popup javascript URL timer should run")
);
vm.drain_pending_child_frame_work_for_test();
assert_eq!(
vm.eval(
r##"JSON.stringify([
__stringCompletionPopup.location.href,
__stringCompletionPopup.document.querySelector("#javascript-url-result").textContent,
__stringCompletionPopup.document.defaultView === __stringCompletionPopup,
__stringCompletionPopup.opener === window
])"##,
)
.expect("popup javascript string completion should replace the document"),
r#"["about:blank","replaced",true,true]"#
);
}
#[tokio::test]
async fn loaded_lightweight_popup_can_replace_itself_from_javascript_url_string_completion() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm =
new_page_task_executor_test_vm_with_loader("https://example.com/base/page.html", &loader);
assert_eq!(
vm.eval(
r#"
(() => {
const html = `<!doctype html><script>
open("javascript:'<main id=javascript-url-self-result>self replaced</main>'", "_self");
<\/script>`;
globalThis.__selfReplacingPopup =
open(URL.createObjectURL(new Blob([html], { type: "text/html" })));
return __selfReplacingPopup.document.body.textContent;
})()
"#,
)
.expect("self-replacing popup setup should evaluate"),
""
);
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(Boolean(__selfReplacingPopup.document.querySelector('#javascript-url-self-result')))",
"true",
"loaded popup javascript URL string completion",
)
.await;
assert_eq!(
vm.eval(
"__selfReplacingPopup.document.querySelector('#javascript-url-self-result').textContent",
)
.expect("self-replaced popup document should remain observable"),
"self replaced"
);
}
#[tokio::test]
async fn popup_javascript_url_string_document_keeps_inherited_frame_src_policy() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
"https://popup-javascript-url-frame-csp.test/",
&loader,
);
vm.set_response_content_security_policies(&["frame-src 'none'".to_owned()]);
assert_eq!(
vm.eval(
r#"
(() => {
globalThis.__popupJavascriptUrlFrameCspEvents = [];
addEventListener("message", event => {
__popupJavascriptUrlFrameCspEvents.push(String(event.data));
});
globalThis.__javascriptUrlCspPopup = open();
__javascriptUrlCspPopup.addEventListener("securitypolicyviolation", event => {
__javascriptUrlCspPopup.opener.postMessage(
`${event.violatedDirective}:${event.blockedURI}`, "*");
});
const markup = '<iframe src="https://blocked-frame.test/fail.html"></iframe>';
__javascriptUrlCspPopup.location.href = "javascript:" + JSON.stringify(markup);
return __popupJavascriptUrlFrameCspEvents.length;
})()
"#,
)
.expect("popup javascript URL inherited CSP setup should evaluate"),
"0"
);
assert_eq!(
vm._context_host
.borrow()
.lightweight_popup_policy_container(1)
.expect("popup policy container")
.response_content_security_policies,
["frame-src 'none'".to_owned()]
);
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(Boolean(__javascriptUrlCspPopup.document.querySelector('iframe')))",
"true",
"popup javascript URL replacement iframe",
)
.await;
assert_eq!(
vm.eval(
r#"(() => {
const frame = __javascriptUrlCspPopup.document.querySelector('iframe');
return `${frame.contentWindow !== null}|${frame.contentDocument !== null}`;
})()"#,
)
.expect("popup javascript URL nested frame projection should be observable"),
"true|true"
);
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__popupJavascriptUrlFrameCspEvents.length)",
"1",
"popup javascript URL inherited frame-src violation",
)
.await;
assert_eq!(
vm.eval("__popupJavascriptUrlFrameCspEvents.join('|')")
.expect("popup javascript URL inherited CSP result should evaluate"),
"frame-src:https://blocked-frame.test/fail.html"
);
assert_eq!(
vm.eval("__javascriptUrlCspPopup.location.href")
.expect("popup javascript URL replacement should retain its prior URL"),
"about:blank"
);
}
#[tokio::test]
async fn lightweight_popup_javascript_url_uses_inline_navigation_csp_not_eval_csp() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
@@ -25120,6 +25276,47 @@ async fn lightweight_popup_javascript_url_uses_inline_navigation_csp_not_eval_cs
);
}
#[tokio::test]
async fn lightweight_popup_javascript_url_eval_permit_is_one_shot() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
"https://popup-javascript-url-eval.test/",
&loader,
);
vm.set_response_content_security_policies(&["script-src 'unsafe-inline'".to_owned()]);
vm.eval(
r#"
globalThis.__popupJavascriptUrlEvalResults = [];
onmessage = event => __popupJavascriptUrlEvalResults.push(String(event.data));
const popup = open();
popup.location.href = `javascript:
try {
eval("globalThis.__nestedEvalRan = true");
opener.postMessage("nested:allowed", "*");
} catch (error) {
opener.postMessage("nested:" + error.name, "*");
}
`;
"queued"
"#,
)
.expect("popup javascript URL nested eval should queue");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__popupJavascriptUrlEvalResults.length)",
"1",
"popup javascript URL nested eval policy result",
)
.await;
assert_eq!(
vm.eval("__popupJavascriptUrlEvalResults.sort().join('|')")
.expect("popup javascript URL nested eval result should evaluate"),
"nested:EvalError"
);
}
#[tokio::test]
async fn lightweight_popup_post_message_round_trips_with_wasm_module() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
@@ -5878,6 +5878,77 @@ async fn child_document_open_nested_frame_uses_inherited_frame_src_policy() {
);
}
#[tokio::test]
async fn child_javascript_url_nested_frame_uses_inherited_frame_src_policy() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
"https://child-javascript-url-frame-csp.test/",
&loader,
);
vm.document_runtime
.set_response_content_security_policies(&["frame-src 'none'".to_owned()]);
assert_eq!(
vm.eval(
r#"
(() => {
globalThis.__childJavascriptUrlFrameCspEvents = [];
addEventListener("message", event => {
__childJavascriptUrlFrameCspEvents.push(String(event.data));
});
const source = `javascript:
addEventListener("securitypolicyviolation", event => {
parent.postMessage(event.violatedDirective, "*");
});
const nested = document.createElement("iframe");
nested.src = "https://blocked-frame.test/fail.html";
document.body.appendChild(nested);
`;
const frame = document.createElement("iframe");
frame.src = encodeURI(source.replaceAll("\n", ""));
(document.body || document.documentElement || document).appendChild(frame);
return "queued";
})()
"#,
)
.expect("child javascript URL CSP setup should evaluate"),
"queued"
);
assert!(
vm.run_one_child_frame_task_executor_turn(
ChildFrameSemanticTurnKind::NavigationCommit,
&loader,
)
.await
.expect("javascript URL navigation commit should use the selected-task dispatcher")
);
run_page_realm_prerequisite_then_expected_child_frame_semantic_turn(
&mut vm,
&loader,
ChildFrameSemanticTurnKind::DocumentScriptReady,
"javascript URL should execute after its initial child realm materializes",
)
.await;
assert!(
vm.run_one_child_frame_task_executor_turn(
ChildFrameSemanticTurnKind::NavigationCommit,
&loader,
)
.await
.expect("nested frame navigation should reach the inherited CSP gate")
);
assert!(
vm.run_one_window_message_executor_turn(&loader)
.await
.expect("child CSP report should dispatch to the top Window")
);
assert_eq!(
vm.eval("JSON.stringify(globalThis.__childJavascriptUrlFrameCspEvents)")
.expect("child javascript URL CSP events should be observable"),
r#"["frame-src"]"#
);
}
#[tokio::test]
async fn child_document_write_inline_classic_obeys_inherited_response_csp() {
let mut vm = new_storage_test_vm("https://child-document-write-csp.test/");