Files
servo/components/script/dom/workers/serviceworkerglobalscope.rs
Simon Wülker e73c010bb1 Force callers to claim blob url before making a fetch request (#43746)
`blob` URLs have a implicit blob URL entry attached, which stores the
data contained in the blob. The specification requires this entry to be
resolved as the URL is parsed. We only resolve it inside `net` when
loading the URL. That causes problems if the blob entry has been revoked
in the meantime - see https://github.com/servo/servo/issues/25226.

Ideally we would want to resolve blobs at parse-time as required. But
because `ServoUrl` is such a fundamental type, I've not managed to do
this change without having to touch hundreds of files at once.

Thus, we now require passing a `UrlWithBlobClaim` instead of a
`ServoUrl` when `fetch`-ing. This type proves that the caller has
acquired the blob beforehand.

As a temporary escape hatch, I've added
`UrlWithBlobClaim::from_url_without_having_claimed_blob`. That method
logs a warning if its used unsafely. This method is currently used in
most places to keep this change small. Only workers now acquire the blob
beforehand.

Testing: A new test starts to pass
Part of https://github.com/servo/servo/issues/43326
Part of https://github.com/servo/servo/issues/25226

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
Co-authored-by: Josh Matthews <josh@joshmatthews.net>
2026-04-06 14:21:55 +00:00

613 lines
24 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crossbeam_channel::{Receiver, Sender, after};
use devtools_traits::{DebuggerValue, DevtoolScriptControlMsg, EvaluateJSReply};
use dom_struct::dom_struct;
use fonts::FontContext;
use js::jsapi::{JS_AddInterruptCallback, JSContext};
use js::jsval::UndefinedValue;
use net_traits::CustomResponseMediator;
use net_traits::blob_url_store::UrlWithBlobClaim;
use net_traits::request::{
CredentialsMode, Destination, InsecureRequestsPolicy, ParserMetadata, Referrer, RequestBuilder,
};
use rand::random;
use servo_base::generic_channel::{GenericReceiver, GenericSend, GenericSender, RoutedReceiver};
use servo_base::id::PipelineId;
use servo_config::pref;
use servo_constellation_traits::{
ScopeThings, ServiceWorkerMsg, WorkerGlobalScopeInit, WorkerScriptLoadOrigin,
};
use servo_url::ServoUrl;
use style::thread_state::{self, ThreadState};
use crate::dom::abstractworker::WorkerScriptMsg;
use crate::dom::abstractworkerglobalscope::{WorkerEventLoopMethods, run_worker_event_loop};
use crate::dom::bindings::codegen::Bindings::ServiceWorkerGlobalScopeBinding;
use crate::dom::bindings::codegen::Bindings::ServiceWorkerGlobalScopeBinding::ServiceWorkerGlobalScopeMethods;
use crate::dom::bindings::codegen::Bindings::WorkerBinding::WorkerType;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::str::DOMString;
use crate::dom::bindings::structuredclone;
use crate::dom::bindings::trace::CustomTraceable;
use crate::dom::bindings::utils::define_all_exposed_interfaces;
use crate::dom::csp::Violation;
use crate::dom::debugger::debuggerglobalscope::DebuggerGlobalScope;
use crate::dom::dedicatedworkerglobalscope::AutoWorkerReset;
use crate::dom::event::Event;
use crate::dom::eventtarget::EventTarget;
use crate::dom::extendableevent::ExtendableEvent;
use crate::dom::extendablemessageevent::ExtendableMessageEvent;
use crate::dom::global_scope_script_execution::{ErrorReporting, RethrowErrors};
use crate::dom::globalscope::GlobalScope;
#[cfg(feature = "webgpu")]
use crate::dom::webgpu::identityhub::IdentityHub;
use crate::dom::worker::TrustedWorkerAddress;
use crate::dom::workerglobalscope::WorkerGlobalScope;
use crate::fetch::{CspViolationsProcessor, load_whole_resource};
use crate::messaging::{CommonScriptMsg, ScriptEventLoopSender};
use crate::realms::{AlreadyInRealm, InRealm, enter_auto_realm, enter_realm};
use crate::script_module::ScriptFetchOptions;
use crate::script_runtime::{
CanGc, IntroductionType, JSContext as SafeJSContext, Runtime, ThreadSafeJSContext,
};
use crate::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue};
use crate::task_source::TaskSourceName;
/// Messages used to control service worker event loop
pub(crate) enum ServiceWorkerScriptMsg {
/// Message common to all workers
CommonWorker(WorkerScriptMsg),
/// Message to request a custom response by the service worker
Response(CustomResponseMediator),
/// Wake-up call from the task queue.
WakeUp,
}
impl QueuedTaskConversion for ServiceWorkerScriptMsg {
fn task_source_name(&self) -> Option<&TaskSourceName> {
let script_msg = match self {
ServiceWorkerScriptMsg::CommonWorker(WorkerScriptMsg::Common(script_msg)) => script_msg,
_ => return None,
};
match script_msg {
CommonScriptMsg::Task(_category, _boxed, _pipeline_id, task_source) => {
Some(task_source)
},
_ => None,
}
}
fn pipeline_id(&self) -> Option<PipelineId> {
// Workers always return None, since the pipeline_id is only used to check for document activity,
// and this check does not apply to worker event-loops.
None
}
fn into_queued_task(self) -> Option<QueuedTask> {
let script_msg = match self {
ServiceWorkerScriptMsg::CommonWorker(WorkerScriptMsg::Common(script_msg)) => script_msg,
_ => return None,
};
let (event_category, task, pipeline_id, task_source) = match script_msg {
CommonScriptMsg::Task(category, boxed, pipeline_id, task_source) => {
(category, boxed, pipeline_id, task_source)
},
_ => return None,
};
Some(QueuedTask {
worker: None,
event_category,
task,
pipeline_id,
task_source,
})
}
fn from_queued_task(queued_task: QueuedTask) -> Self {
let script_msg = CommonScriptMsg::Task(
queued_task.event_category,
queued_task.task,
queued_task.pipeline_id,
queued_task.task_source,
);
ServiceWorkerScriptMsg::CommonWorker(WorkerScriptMsg::Common(script_msg))
}
fn inactive_msg() -> Self {
// Inactive is only relevant in the context of a browsing-context event-loop.
panic!("Workers should never receive messages marked as inactive");
}
fn wake_up_msg() -> Self {
ServiceWorkerScriptMsg::WakeUp
}
fn is_wake_up(&self) -> bool {
matches!(self, ServiceWorkerScriptMsg::WakeUp)
}
}
/// Messages sent from the owning registration.
pub(crate) enum ServiceWorkerControlMsg {
/// Shutdown.
Exit,
}
pub(crate) enum MixedMessage {
ServiceWorker(ServiceWorkerScriptMsg),
Devtools(DevtoolScriptControlMsg),
Control(ServiceWorkerControlMsg),
Timer,
}
struct ServiceWorkerCspProcessor {}
impl CspViolationsProcessor for ServiceWorkerCspProcessor {
fn process_csp_violations(&self, _violations: Vec<Violation>) {}
}
#[dom_struct]
pub(crate) struct ServiceWorkerGlobalScope {
workerglobalscope: WorkerGlobalScope,
#[ignore_malloc_size_of = "Defined in std"]
#[no_trace]
task_queue: TaskQueue<ServiceWorkerScriptMsg>,
own_sender: Sender<ServiceWorkerScriptMsg>,
/// A port on which a single "time-out" message can be received,
/// indicating the sw should stop running,
/// while still draining the task-queue
// and running all enqueued, and not cancelled, tasks.
#[no_trace]
time_out_port: Receiver<Instant>,
#[no_trace]
swmanager_sender: GenericSender<ServiceWorkerMsg>,
#[no_trace]
scope_url: ServoUrl,
/// A receiver of control messages,
/// currently only used to signal shutdown.
#[no_trace]
control_receiver: Receiver<ServiceWorkerControlMsg>,
debugger_global: Option<Dom<DebuggerGlobalScope>>,
}
impl WorkerEventLoopMethods for ServiceWorkerGlobalScope {
type WorkerMsg = ServiceWorkerScriptMsg;
type ControlMsg = ServiceWorkerControlMsg;
type Event = MixedMessage;
fn task_queue(&self) -> &TaskQueue<ServiceWorkerScriptMsg> {
&self.task_queue
}
fn handle_event(&self, event: MixedMessage, cx: &mut js::context::JSContext) -> bool {
self.handle_mixed_message(event, cx)
}
fn handle_worker_post_event(
&self,
_worker: &TrustedWorkerAddress,
) -> Option<AutoWorkerReset<'_>> {
None
}
fn from_control_msg(msg: ServiceWorkerControlMsg) -> MixedMessage {
MixedMessage::Control(msg)
}
fn from_worker_msg(msg: ServiceWorkerScriptMsg) -> MixedMessage {
MixedMessage::ServiceWorker(msg)
}
fn from_devtools_msg(msg: DevtoolScriptControlMsg) -> MixedMessage {
MixedMessage::Devtools(msg)
}
fn from_timer_msg() -> MixedMessage {
MixedMessage::Timer
}
fn control_receiver(&self) -> &Receiver<ServiceWorkerControlMsg> {
&self.control_receiver
}
}
impl ServiceWorkerGlobalScope {
#[allow(clippy::too_many_arguments)]
fn new_inherited(
init: WorkerGlobalScopeInit,
worker_url: ServoUrl,
from_devtools_receiver: RoutedReceiver<DevtoolScriptControlMsg>,
runtime: Runtime,
own_sender: Sender<ServiceWorkerScriptMsg>,
receiver: Receiver<ServiceWorkerScriptMsg>,
time_out_port: Receiver<Instant>,
swmanager_sender: GenericSender<ServiceWorkerMsg>,
scope_url: ServoUrl,
control_receiver: Receiver<ServiceWorkerControlMsg>,
closing: Arc<AtomicBool>,
font_context: Arc<FontContext>,
debugger_global: Option<&DebuggerGlobalScope>,
) -> ServiceWorkerGlobalScope {
ServiceWorkerGlobalScope {
workerglobalscope: WorkerGlobalScope::new_inherited(
init,
DOMString::new(),
WorkerType::Classic, // FIXME(cybai): Should be provided from `Run Service Worker`
worker_url,
runtime,
from_devtools_receiver,
closing,
#[cfg(feature = "webgpu")]
Arc::new(IdentityHub::default()),
// FIXME: investigate what environment this value comes from for service workers.
InsecureRequestsPolicy::DoNotUpgrade,
Some(font_context),
),
task_queue: TaskQueue::new(receiver, own_sender.clone()),
own_sender,
time_out_port,
swmanager_sender,
scope_url,
control_receiver,
debugger_global: debugger_global.map(Dom::from_ref),
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
init: WorkerGlobalScopeInit,
worker_url: ServoUrl,
from_devtools_receiver: RoutedReceiver<DevtoolScriptControlMsg>,
runtime: Runtime,
own_sender: Sender<ServiceWorkerScriptMsg>,
receiver: Receiver<ServiceWorkerScriptMsg>,
time_out_port: Receiver<Instant>,
swmanager_sender: GenericSender<ServiceWorkerMsg>,
scope_url: ServoUrl,
control_receiver: Receiver<ServiceWorkerControlMsg>,
closing: Arc<AtomicBool>,
font_context: Arc<FontContext>,
debugger_global: Option<&DebuggerGlobalScope>,
cx: &mut js::context::JSContext,
) -> DomRoot<ServiceWorkerGlobalScope> {
let scope = Box::new(ServiceWorkerGlobalScope::new_inherited(
init,
worker_url,
from_devtools_receiver,
runtime,
own_sender,
receiver,
time_out_port,
swmanager_sender,
scope_url,
control_receiver,
closing,
font_context,
debugger_global,
));
ServiceWorkerGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(cx, scope)
}
/// <https://w3c.github.io/ServiceWorker/#run-service-worker-algorithm>
#[expect(unsafe_code)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_serviceworker_scope(
scope_things: ScopeThings,
own_sender: Sender<ServiceWorkerScriptMsg>,
receiver: Receiver<ServiceWorkerScriptMsg>,
devtools_receiver: GenericReceiver<DevtoolScriptControlMsg>,
swmanager_sender: GenericSender<ServiceWorkerMsg>,
scope_url: ServoUrl,
control_receiver: Receiver<ServiceWorkerControlMsg>,
context_sender: Sender<ThreadSafeJSContext>,
closing: Arc<AtomicBool>,
font_context: Arc<FontContext>,
) -> JoinHandle<()> {
let ScopeThings {
script_url,
init,
worker_load_origin,
..
} = scope_things;
let serialized_worker_url = script_url.to_string();
let origin = scope_url.origin();
thread::Builder::new()
.name(format!("SW:{}", script_url.debug_compact()))
.spawn(move || {
thread_state::initialize(ThreadState::SCRIPT | ThreadState::IN_WORKER);
let runtime = Runtime::new(None);
// SAFETY: We are in a new thread, so this first cx.
// It is OK to have it separated of runtime here,
// because it will never outlive it (runtime destruction happens at the end of this function
let mut cx = unsafe { runtime.cx() };
let cx = &mut cx;
let context_for_interrupt = runtime.thread_safe_js_context();
let _ = context_sender.send(context_for_interrupt);
let WorkerScriptLoadOrigin {
referrer_url,
referrer_policy,
pipeline_id,
} = worker_load_origin;
let debugger_global =
init.from_devtools_sender
.clone()
.map(|from_devtools_sender| {
let debugger_global = DebuggerGlobalScope::new(
pipeline_id,
init.to_devtools_sender.clone(),
from_devtools_sender,
init.mem_profiler_chan.clone(),
init.time_profiler_chan.clone(),
init.script_to_constellation_chan.clone(),
init.script_to_embedder_chan.clone(),
init.resource_threads.clone(),
init.storage_threads.clone(),
#[cfg(feature = "webgpu")]
Arc::new(IdentityHub::default()),
cx,
);
debugger_global.execute(cx);
debugger_global
});
// Service workers are time limited
// https://w3c.github.io/ServiceWorker/#service-worker-lifetime
let sw_lifetime_timeout = pref!(dom_serviceworker_timeout_seconds) as u64;
let time_out_port = after(Duration::new(sw_lifetime_timeout, 0));
let devtools_mpsc_port = devtools_receiver.route_preserving_errors();
let resource_threads_sender = init.resource_threads.sender();
let global = ServiceWorkerGlobalScope::new(
init,
script_url.clone(),
devtools_mpsc_port,
runtime,
own_sender,
receiver,
time_out_port,
swmanager_sender,
scope_url,
control_receiver,
closing,
font_context,
debugger_global.as_deref(),
cx,
);
let worker_scope = global.upcast::<WorkerGlobalScope>();
let global_scope = global.upcast::<GlobalScope>();
if let Some(debugger_global) = debugger_global.as_deref() {
debugger_global.fire_add_debuggee(
CanGc::from_cx(cx),
global_scope,
pipeline_id,
Some(worker_scope.worker_id()),
);
}
let referrer = referrer_url
.map(Referrer::ReferrerUrl)
.unwrap_or_else(|| global_scope.get_referrer());
let request = RequestBuilder::new(
None,
UrlWithBlobClaim::from_url_without_having_claimed_blob(script_url),
referrer,
)
.destination(Destination::ServiceWorker)
.credentials_mode(CredentialsMode::Include)
.parser_metadata(ParserMetadata::NotParserInserted)
.use_url_credentials(true)
.pipeline_id(Some(pipeline_id))
.referrer_policy(referrer_policy)
.insecure_requests_policy(worker_scope.insecure_requests_policy())
// TODO: Use policy container from ScopeThings
.policy_container(global_scope.policy_container())
.origin(origin);
let (url, source) = match load_whole_resource(
request,
&resource_threads_sender,
global.upcast(),
&ServiceWorkerCspProcessor {},
cx,
) {
Err(_) => {
error!("error loading script {}", serialized_worker_url);
worker_scope.clear_js_runtime();
return;
},
Ok((metadata, bytes, _)) => (metadata.final_url, bytes),
};
unsafe {
// Handle interrupt requests
JS_AddInterruptCallback(cx.raw_cx(), Some(interrupt_callback));
}
{
// TODO: use AutoWorkerReset as in dedicated worker?
let mut realm = enter_auto_realm(cx, worker_scope);
let mut realm = realm.current_realm();
define_all_exposed_interfaces(&mut realm, global_scope);
let script = global_scope.create_a_classic_script(
&mut realm,
String::from_utf8_lossy(&source),
url,
ScriptFetchOptions::default_classic_script(),
ErrorReporting::Unmuted,
Some(IntroductionType::WORKER),
1,
true,
);
_ = global_scope.run_a_classic_script(&mut realm, script, RethrowErrors::No);
let in_realm_proof = (&mut realm).into();
global.dispatch_activate(
CanGc::from_cx(&mut realm),
InRealm::Already(&in_realm_proof),
);
}
let reporter_name = format!("service-worker-reporter-{}", random::<u64>());
global_scope.mem_profiler_chan().run_with_memory_reporting(
|| {
// Step 18, Run the responsible event loop specified
// by inside settings until it is destroyed.
// The worker processing model remains on this step
// until the event loop is destroyed,
// which happens after the closing flag is set to true,
// or until the worker has run beyond its allocated time.
while !worker_scope.is_closing() && !global.has_timed_out() {
run_worker_event_loop(&*global, None, cx);
}
},
reporter_name,
global.event_loop_sender(),
CommonScriptMsg::CollectReports,
);
worker_scope.clear_js_runtime();
})
.expect("Thread spawning failed")
}
fn handle_mixed_message(&self, msg: MixedMessage, cx: &mut js::context::JSContext) -> bool {
match msg {
MixedMessage::Devtools(msg) => match msg {
DevtoolScriptControlMsg::WantsLiveNotifications(_pipe_id, wants_updates) => {
self.upcast::<GlobalScope>()
.set_devtools_wants_updates(wants_updates);
},
DevtoolScriptControlMsg::Eval(code, id, frame_actor_id, reply) => {
if let Some(debugger_global) = self.debugger_global.as_deref() {
debugger_global.fire_eval(
CanGc::from_cx(cx),
code.into(),
id,
Some(self.upcast::<WorkerGlobalScope>().worker_id()),
frame_actor_id,
reply,
);
} else {
let _ = reply.send(EvaluateJSReply {
value: DebuggerValue::VoidValue,
has_exception: true,
});
}
},
_ => debug!("got an unusable devtools control message inside the worker!"),
},
MixedMessage::ServiceWorker(msg) => {
self.handle_script_event(msg, cx);
},
MixedMessage::Control(ServiceWorkerControlMsg::Exit) => {
return false;
},
MixedMessage::Timer => {},
}
true
}
fn has_timed_out(&self) -> bool {
// TODO: https://w3c.github.io/ServiceWorker/#service-worker-lifetime
false
}
fn handle_script_event(&self, msg: ServiceWorkerScriptMsg, cx: &mut js::context::JSContext) {
use self::ServiceWorkerScriptMsg::*;
match msg {
CommonWorker(WorkerScriptMsg::DOMMessage(msg)) => {
let scope = self.upcast::<WorkerGlobalScope>();
let target = self.upcast();
let _ac = enter_realm(scope);
rooted!(&in(cx) let mut message = UndefinedValue());
if let Ok(ports) = structuredclone::read(
scope.upcast(),
*msg.data,
message.handle_mut(),
CanGc::from_cx(cx),
) {
ExtendableMessageEvent::dispatch_jsval(
target,
scope.upcast(),
message.handle(),
ports,
CanGc::from_cx(cx),
);
} else {
ExtendableMessageEvent::dispatch_error(
target,
scope.upcast(),
CanGc::from_cx(cx),
);
}
},
CommonWorker(WorkerScriptMsg::Common(msg)) => {
self.upcast::<WorkerGlobalScope>().process_event(msg, cx);
},
Response(mediator) => {
// TODO XXXcreativcoder This will eventually use a FetchEvent interface to fire event
// when we have the Request and Response dom api's implemented
// https://w3c.github.io/ServiceWorker/#fetchevent-interface
self.upcast::<EventTarget>()
.fire_event(atom!("fetch"), CanGc::from_cx(cx));
let _ = mediator.response_chan.send(None);
},
WakeUp => {},
}
}
pub(crate) fn event_loop_sender(&self) -> ScriptEventLoopSender {
ScriptEventLoopSender::ServiceWorker(self.own_sender.clone())
}
fn dispatch_activate(&self, can_gc: CanGc, _realm: InRealm) {
let event = ExtendableEvent::new(self, atom!("activate"), false, false, can_gc);
let event = (*event).upcast::<Event>();
self.upcast::<EventTarget>().dispatch_event(event, can_gc);
}
}
#[expect(unsafe_code)]
unsafe extern "C" fn interrupt_callback(cx: *mut JSContext) -> bool {
let in_realm_proof = AlreadyInRealm::assert_for_cx(unsafe { SafeJSContext::from_ptr(cx) });
let global = unsafe { GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof)) };
let worker =
DomRoot::downcast::<WorkerGlobalScope>(global).expect("global is not a worker scope");
assert!(worker.is::<ServiceWorkerGlobalScope>());
// A false response causes the script to terminate
!worker.is_closing()
}
impl ServiceWorkerGlobalScopeMethods<crate::DomTypeHolder> for ServiceWorkerGlobalScope {
// https://w3c.github.io/ServiceWorker/#dom-serviceworkerglobalscope-onmessage
event_handler!(message, GetOnmessage, SetOnmessage);
// https://w3c.github.io/ServiceWorker/#dom-serviceworkerglobalscope-onmessageerror
event_handler!(messageerror, GetOnmessageerror, SetOnmessageerror);
}