mirror of
https://github.com/lexmount/moli.git
synced 2026-09-27 16:01:31 +00:00
fix(blob): revoke object URLs with window realms
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
FileAPI/idlharness.html
|
||||
FileAPI/url/sandboxed-iframe.html
|
||||
FileAPI/url/url-lifetime.html
|
||||
acid/acid3/numbered-tests.html
|
||||
client-hints/service-workers/intercept-request.https.html
|
||||
client-hints/service-workers/new-request.https.html
|
||||
|
||||
@@ -7,6 +7,7 @@ FileAPI/blob/Blob-stream-byob-crash.html
|
||||
FileAPI/blob/Blob-stream-sync-xhr-crash.html
|
||||
FileAPI/file/File-constructor-endings.html
|
||||
FileAPI/filelist-section/filelist.html
|
||||
FileAPI/url/url-lifetime.html
|
||||
IndexedDB/database-names-by-origin.html
|
||||
IndexedDB/idb_webworkers.htm
|
||||
IndexedDB/idbfactory-databases-opaque-origin.html
|
||||
|
||||
@@ -43,6 +43,7 @@ impl<OwnerId, PartitionId> Default for BlobEntries<OwnerId, PartitionId> {
|
||||
#[derive(Debug)]
|
||||
struct ObjectUrlState<OwnerId, AccessKey> {
|
||||
owner_id: Option<OwnerId>,
|
||||
lifetime_id: Option<u64>,
|
||||
blob_id: BlobId,
|
||||
access_key: Option<AccessKey>,
|
||||
}
|
||||
@@ -169,14 +170,36 @@ where
|
||||
self.create_object_url_with_access_key(owner_id, blob_id, origin, None)
|
||||
}
|
||||
|
||||
/// Create an object URL with its creator environment’s access key.
|
||||
/// The key belongs to the URL, independently of the backing Blob.
|
||||
/// Create an object URL tied to a more specific execution-context lifetime.
|
||||
pub fn create_object_url_with_lifetime(
|
||||
&self,
|
||||
owner_id: Option<OwnerId>,
|
||||
lifetime_id: Option<u64>,
|
||||
blob_id: BlobId,
|
||||
origin: &str,
|
||||
) -> Option<String> {
|
||||
self.create_object_url_with_lifetime_and_access_key(owner_id, lifetime_id, blob_id, origin, None)
|
||||
}
|
||||
|
||||
/// Create an object URL with its creator environment's access key.
|
||||
pub fn create_object_url_with_access_key(
|
||||
&self,
|
||||
owner_id: Option<OwnerId>,
|
||||
blob_id: BlobId,
|
||||
origin: &str,
|
||||
access_key: Option<AccessKey>,
|
||||
) -> Option<String> {
|
||||
self.create_object_url_with_lifetime_and_access_key(owner_id, None, blob_id, origin, access_key)
|
||||
}
|
||||
|
||||
/// Associate the URL's independent creator key and execution-context lifetime.
|
||||
pub fn create_object_url_with_lifetime_and_access_key(
|
||||
&self,
|
||||
owner_id: Option<OwnerId>,
|
||||
lifetime_id: Option<u64>,
|
||||
blob_id: BlobId,
|
||||
origin: &str,
|
||||
access_key: Option<AccessKey>,
|
||||
) -> Option<String> {
|
||||
self.retain_blob_object_url_ref(blob_id)?;
|
||||
let mut object_urls = self.object_urls.lock();
|
||||
@@ -190,6 +213,7 @@ where
|
||||
object_url.clone(),
|
||||
ObjectUrlState {
|
||||
owner_id,
|
||||
lifetime_id,
|
||||
blob_id,
|
||||
access_key,
|
||||
},
|
||||
@@ -246,6 +270,28 @@ where
|
||||
Some((String::from_utf8_lossy(&bytes).into_owned(), mime_type))
|
||||
}
|
||||
|
||||
/// Revoke every object URL created by one execution-context lifetime.
|
||||
pub fn cleanup_object_url_lifetime(&self, lifetime_id: u64) -> usize {
|
||||
let removed_blob_ids = {
|
||||
let mut object_urls = self.object_urls.lock();
|
||||
let mut removed_blob_ids = Vec::new();
|
||||
object_urls.retain(|_, state| {
|
||||
if state.lifetime_id == Some(lifetime_id) {
|
||||
removed_blob_ids.push(state.blob_id);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
removed_blob_ids
|
||||
};
|
||||
let removed_count = removed_blob_ids.len();
|
||||
for blob_id in removed_blob_ids {
|
||||
self.release_blob_object_url_ref(blob_id);
|
||||
}
|
||||
removed_count
|
||||
}
|
||||
|
||||
/// Remove Blob/object URL entries owned by a context.
|
||||
pub fn cleanup_owner_resources(&self, owner_id: OwnerId) {
|
||||
let removed_blob_ids = {
|
||||
@@ -263,9 +309,22 @@ where
|
||||
ids
|
||||
};
|
||||
|
||||
self.object_urls.lock().retain(|_, state| {
|
||||
state.owner_id != Some(owner_id) && !removed_blob_ids.contains(&state.blob_id)
|
||||
});
|
||||
let released_blob_ids = {
|
||||
let mut object_urls = self.object_urls.lock();
|
||||
let mut released_blob_ids = Vec::new();
|
||||
object_urls.retain(|_, state| {
|
||||
let remove =
|
||||
state.owner_id == Some(owner_id) || removed_blob_ids.contains(&state.blob_id);
|
||||
if remove && !removed_blob_ids.contains(&state.blob_id) {
|
||||
released_blob_ids.push(state.blob_id);
|
||||
}
|
||||
!remove
|
||||
});
|
||||
released_blob_ids
|
||||
};
|
||||
for blob_id in released_blob_ids {
|
||||
self.release_blob_object_url_ref(blob_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Retain a reader reference for a Blob.
|
||||
@@ -535,4 +594,32 @@ mod tests {
|
||||
Some((b"other".to_vec(), "text/plain".to_owned()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_object_url_lifetime_revokes_only_matching_urls() {
|
||||
let store = BlobStore::<u64, u64>::default();
|
||||
let blob = store.create_blob(
|
||||
Some(1),
|
||||
Some(10),
|
||||
b"shared".to_vec(),
|
||||
"text/plain".to_owned(),
|
||||
);
|
||||
let first_url = store
|
||||
.create_object_url_with_lifetime(Some(1), Some(101), blob, "https://example.test")
|
||||
.expect("first object URL");
|
||||
let second_url = store
|
||||
.create_object_url_with_lifetime(Some(1), Some(202), blob, "https://example.test")
|
||||
.expect("second object URL");
|
||||
|
||||
assert_eq!(store.cleanup_object_url_lifetime(101), 1);
|
||||
assert!(store.object_url_bytes_and_type(&first_url).is_none());
|
||||
assert_eq!(
|
||||
store.object_url_bytes_and_type(&second_url),
|
||||
Some((b"shared".to_vec(), "text/plain".to_owned()))
|
||||
);
|
||||
|
||||
store.release_blob_wrapper_ref(blob);
|
||||
assert_eq!(store.cleanup_object_url_lifetime(202), 1);
|
||||
assert!(store.blob_bytes(blob).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,14 +349,14 @@ pub(super) fn create_object_url_for_object<'s>(
|
||||
let partition = current_blob_storage_partition_identity(scope)?;
|
||||
let owner_id = current_resource_owner_id(scope);
|
||||
let origin = storage_key.origin().to_owned();
|
||||
blob_store().create_object_url_with_access_key(
|
||||
let lifetime_id = native_bridge::current_runtime_observable_context_token(scope)
|
||||
.map(native_bridge::RuntimeObservableContextToken::as_u64);
|
||||
blob_store().create_object_url_with_lifetime_and_access_key(
|
||||
owner_id,
|
||||
lifetime_id,
|
||||
blob_id,
|
||||
&origin,
|
||||
Some(ObjectUrlAccessKey {
|
||||
partition,
|
||||
storage_key,
|
||||
}),
|
||||
Some(ObjectUrlAccessKey { partition, storage_key }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -490,6 +490,12 @@ pub(crate) fn cleanup_owner_resources(owner_id: ResourceOwnerId) {
|
||||
blob_store().cleanup_owner_resources(owner_id);
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_object_urls_for_context(
|
||||
context_token: native_bridge::RuntimeObservableContextToken,
|
||||
) -> usize {
|
||||
blob_store().cleanup_object_url_lifetime(context_token.as_u64())
|
||||
}
|
||||
|
||||
fn release_blob_wrapper_ref(blob_id: BlobId) {
|
||||
blob_store().release_blob_wrapper_ref(blob_id);
|
||||
}
|
||||
|
||||
@@ -422,6 +422,8 @@ impl JsContextHost {
|
||||
&mut self,
|
||||
context_token: RuntimeObservableContextToken,
|
||||
) -> usize {
|
||||
let revoked_blob_object_url_count =
|
||||
crate::blob::cleanup_object_urls_for_context(context_token);
|
||||
crate::observer_runtime::retire_context_token(self, context_token);
|
||||
let indexed_db_retirement = self.retire_indexed_db_context(context_token);
|
||||
let retired_indexed_db_connections = indexed_db_retirement.retired_connections.len();
|
||||
@@ -449,13 +451,15 @@ impl JsContextHost {
|
||||
?context_token,
|
||||
retired_count,
|
||||
retired_indexed_db_connections,
|
||||
revoked_blob_object_url_count,
|
||||
"retired LocalWindow bindings with destroyed V8 execution context"
|
||||
);
|
||||
} else if retired_indexed_db_connections > 0 {
|
||||
} else if retired_indexed_db_connections > 0 || revoked_blob_object_url_count > 0 {
|
||||
tracing::debug!(
|
||||
?context_token,
|
||||
retired_indexed_db_connections,
|
||||
"retired IndexedDB state with destroyed V8 execution context"
|
||||
revoked_blob_object_url_count,
|
||||
"retired context-owned state with destroyed V8 execution context"
|
||||
);
|
||||
}
|
||||
retired_count
|
||||
@@ -473,6 +477,8 @@ impl JsContextHost {
|
||||
&mut self,
|
||||
context_token: RuntimeObservableContextToken,
|
||||
) -> usize {
|
||||
let revoked_blob_object_url_count =
|
||||
crate::blob::cleanup_object_urls_for_context(context_token);
|
||||
crate::observer_runtime::retire_context_token(self, context_token);
|
||||
let retired_realm_count = self
|
||||
.window_execution_context_realms
|
||||
@@ -480,6 +486,7 @@ impl JsContextHost {
|
||||
tracing::debug!(
|
||||
?context_token,
|
||||
retired_realm_count,
|
||||
revoked_blob_object_url_count,
|
||||
"retired isolated Window realm registration"
|
||||
);
|
||||
retired_realm_count
|
||||
|
||||
@@ -1045,3 +1045,54 @@ fn blob_url_revocation_respects_browser_partitions_and_allows_same_origin_realms
|
||||
assert!(crate::blob::object_url_body_and_type(&url).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_child_frame_revokes_only_its_blob_object_urls() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://blob-url-child-lifetime.test/",
|
||||
"<!doctype html><html><body></body></html>",
|
||||
);
|
||||
|
||||
let urls = vm
|
||||
.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const parentUrl = URL.createObjectURL(new Blob(["parent"]));
|
||||
const frame = document.createElement("iframe");
|
||||
document.body.appendChild(frame);
|
||||
const childUrl = frame.contentWindow.URL.createObjectURL(
|
||||
new frame.contentWindow.Blob(["child"])
|
||||
);
|
||||
globalThis.__blobUrlLifetimeFrame = frame;
|
||||
return `${parentUrl}|${childUrl}`;
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("child Blob object URL setup should evaluate");
|
||||
let (parent_url, child_url) = urls
|
||||
.split_once('|')
|
||||
.expect("setup should return both object URLs");
|
||||
|
||||
assert_eq!(
|
||||
crate::blob::object_url_body_and_type(parent_url),
|
||||
Some(("parent".to_owned(), String::new()))
|
||||
);
|
||||
assert_eq!(
|
||||
crate::blob::object_url_body_and_type(child_url),
|
||||
Some(("child".to_owned(), String::new()))
|
||||
);
|
||||
|
||||
vm.eval("globalThis.__blobUrlLifetimeFrame.remove()")
|
||||
.expect("child frame removal should evaluate");
|
||||
vm.drain_pending_child_frame_work_for_test();
|
||||
|
||||
assert_eq!(
|
||||
crate::blob::object_url_body_and_type(parent_url),
|
||||
Some(("parent".to_owned(), String::new())),
|
||||
"removing a child frame must preserve the parent realm's object URLs"
|
||||
);
|
||||
assert!(
|
||||
crate::blob::object_url_body_and_type(child_url).is_none(),
|
||||
"removing a child frame must revoke object URLs created by its realm"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user