diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index bbdc752f9c..fd3e76b845 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -307,6 +307,7 @@ IndexedDB/idbtransaction_objectStoreNames.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbtransaction_objectStoreNames.any.js?moli-wpt-any=window IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=dedicatedworker IndexedDB/idbversionchangeevent.any.js?moli-wpt-any=window +IndexedDB/idlharness.any.js?moli-wpt-any=dedicatedworker IndexedDB/idlharness.any.js?moli-wpt-any=window IndexedDB/index_sort_order.any.js?moli-wpt-any=dedicatedworker IndexedDB/index_sort_order.any.js?moli-wpt-any=window diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/install.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/install.rs index 60e2f12533..8e16654d72 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/install.rs @@ -35,7 +35,7 @@ use super::{ idb_object_store_transaction_getter, idb_transaction_abort_callback, idb_transaction_commit_callback, idb_transaction_object_store_callback, idb_transaction_object_store_names_getter, indexed_db_runtime_factory, - install_dom_string_list_template_bindings, v8str, + install_dom_string_list_template_bindings, }; use anyhow::{Result, anyhow}; diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/install/state.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/install/state.rs index 9bbcc4d941..d946770e31 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/install/state.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/install/state.rs @@ -1,5 +1,18 @@ use super::*; -use crate::{util::context_host_ptr_from_window_object, webidl}; +use crate::{util::context_host_ptr_from_window_object, web_api_interfaces, webidl}; +use moli_webapi_declare::WebApiObject; + +#[derive(Default, WebApiObject)] +#[webapi( + fragment, + prototype = "WorkerGlobalScope", + enumerable, + receiver = web_api_interfaces::WorkerGlobalScope::is_instance +)] +struct WorkerIndexedDbPrototypeDeclaration { + #[webapi(accessor_property = "indexedDB", getter = worker_indexed_db_getter)] + indexed_db: (), +} pub(in crate::context_bootstrap) fn ensure_indexed_db_runtime_state<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -9,27 +22,25 @@ pub(in crate::context_bootstrap) fn ensure_indexed_db_runtime_state<'s>( pub(crate) fn install_worker_indexed_db_runtime_state<'s>( scope: &mut v8::PinScope<'s, '_>, - global: v8::Local<'s, v8::Object>, ) -> Result<()> { - global - .set_lazy_data_property_with_configuration( - scope, - v8str(scope, "indexedDB").into(), - v8::LazyDataPropertyConfiguration::new(worker_indexed_db_lazy_getter) - .property_attribute(v8::PropertyAttribute::DONT_ENUM) - .getter_side_effect_type(v8::SideEffectType::HasNoSideEffect), - ) - .unwrap_or(false) - .then_some(()) - .ok_or_else(|| anyhow!("failed to install lazy worker IndexedDB factory")) + let prototype = crate::util::global_constructor_prototype(scope, "WorkerGlobalScope") + .ok_or_else(|| anyhow!("WorkerGlobalScope prototype missing during IndexedDB bootstrap"))?; + WorkerIndexedDbPrototypeDeclaration::default() + .initialize(scope, prototype) + .map_err(|error| anyhow!("failed to install WorkerGlobalScope indexedDB getter: {error}")) } -fn worker_indexed_db_lazy_getter<'s>( +fn worker_indexed_db_getter<'s>( scope: &mut v8::PinScope<'s, '_>, - _name: v8::Local<'s, v8::Name>, - _args: v8::PropertyCallbackArguments<'s>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + let Some(context) = args.this().get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + // Laziness and SameObject identity belong to the realm's private cache, + // independently of author properties shadowing the prototype getter. match ensure_indexed_db_runtime_state(scope) { Some(factory) => rv.set(factory.into()), None => rv.set(v8::undefined(scope).into()), diff --git a/moli-renderer-v8/src/worker/global_scope/mod.rs b/moli-renderer-v8/src/worker/global_scope/mod.rs index 2bfbaacc26..732a0cc31e 100644 --- a/moli-renderer-v8/src/worker/global_scope/mod.rs +++ b/moli-renderer-v8/src/worker/global_scope/mod.rs @@ -3194,7 +3194,7 @@ pub(super) fn install_worker_global_scope<'s>( secure_context, &identity, )?; - crate::context_bootstrap::install_worker_indexed_db_runtime_state(scope, global)?; + crate::context_bootstrap::install_worker_indexed_db_runtime_state(scope)?; crate::context_bootstrap::install_worker_base64_runtime_state(scope, global)?; install_simple_event_target_methods(scope, global, WORKER_GLOBAL_LISTENERS_SLOT, false); install_simple_event_target_ordered_handlers(scope, global); diff --git a/moli-renderer-v8/src/worker/thread/tests/indexed_db.rs b/moli-renderer-v8/src/worker/thread/tests/indexed_db.rs new file mode 100644 index 0000000000..c2164d1af3 --- /dev/null +++ b/moli-renderer-v8/src/worker/thread/tests/indexed_db.rs @@ -0,0 +1,62 @@ +use super::super::WorkerGlobalKind; +use super::*; + +#[tokio::test] +async fn indexed_db_global_attribute_preserves_receivers_and_identity_across_worker_kinds() { + ensure_v8(); + let storage_key = moli_storage_key::MoliStorageKey::new( + "https://indexeddb-attribute.test".to_owned(), + "https://indexeddb-attribute.test".to_owned(), + None, + moli_storage_key::StoragePartitionRelation::FirstParty, + ); + let cases = [ + ( + WorkerGlobalKind::Dedicated { + name: String::new(), + }, + "https://indexeddb-attribute.test/worker.js", + ), + ( + WorkerGlobalKind::Dedicated { + name: String::new(), + }, + "data:text/javascript,", + ), + ( + WorkerGlobalKind::Shared { + name: "indexedDB".to_owned(), + storage_key, + }, + "https://indexeddb-attribute.test/shared.js", + ), + ( + WorkerGlobalKind::Service { + registration_id: ServiceWorkerRegistrationId::from_u64_for_test(1), + version_id: ServiceWorkerVersionId::from_u64_for_test(1), + scope_url: url::Url::parse("https://indexeddb-attribute.test/").unwrap(), + }, + "https://indexeddb-attribute.test/sw.js", + ), + ]; + let fixture = include_str!("../../../../tests/fixtures/worker-indexeddb-attribute.js"); + for (kind, script_url) in cases { + let (bootstrap_tx, mut bootstrap_rx) = tokio::sync::mpsc::unbounded_channel(); + let source = format!( + "{fixture}\nconst result = workerIndexedDBAttributeProbe(); if (result.state !== 'pass' || result.checks.length !== 53) throw new Error(JSON.stringify(result));" + ); + let handle = spawn_test_worker_with_options( + WorkerSpawnOptions::new(source, script_url.to_owned()) + .with_global_kind(kind) + .with_bootstrap_completion_sender(bootstrap_tx), + ); + let bootstrap = timeout(TIMEOUT, bootstrap_rx.recv()) + .await + .expect("IndexedDB attribute probe should finish") + .expect("IndexedDB attribute probe should report completion"); + handle.terminate_and_join(); + bootstrap.result.unwrap_or_else(|error| { + panic!("IndexedDB attribute probe failed for {script_url}: {error:?}") + }); + } +} diff --git a/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs b/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs index 19a7164f6a..58252fe04b 100644 --- a/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/lazy_storage.rs @@ -176,17 +176,31 @@ async fn worker_storage_surfaces_materialize_in_independent_stages() { return; } + if (event.data === "indexedDBDescriptor") { + const descriptor = Object.getOwnPropertyDescriptor(WorkerGlobalScope.prototype, "indexedDB"); + let illegalReceiver; + try { descriptor.get.call({}); } catch (error) { illegalReceiver = error.name; } + postMessage({ + phase: "indexedDBDescriptor", + own: Object.hasOwn(self, "indexedDB"), + accessor: typeof descriptor.get === "function" && descriptor.set === undefined, + illegalReceiver + }); + return; + } + if (event.data === "indexedDB") { const factory = indexedDB; const descriptor = - Object.getOwnPropertyDescriptor(self, "indexedDB"); + Object.getOwnPropertyDescriptor(WorkerGlobalScope.prototype, "indexedDB"); postMessage({ phase: "indexedDB", same: factory === indexedDB, instance: factory instanceof IDBFactory, - dataDescriptor: - descriptor.value === factory && - typeof descriptor.get === "undefined" + prototypeAccessor: + !Object.hasOwn(self, "indexedDB") && + descriptor.get.call(self) === factory && + descriptor.set === undefined }); return; } @@ -379,10 +393,22 @@ async fn worker_storage_surfaces_materialize_in_independent_stages() { assert_eq!(diagnostics.storage_constructor_materializations, 0); assert!(!diagnostics.opfs_owner_state_materialized); + handle.post_message(serialize_test_string("indexedDBDescriptor")); + assert_eq!( + recv_post_json(&mut handle).await, + r#"{"phase":"indexedDBDescriptor","own":false,"accessor":true,"illegalReceiver":"TypeError"}"# + ); + let before_factory = lazy_diagnostics(&handle).await; + assert_eq!( + before_factory.materialized_interfaces, diagnostics.materialized_interfaces, + "inspecting the accessor and rejecting a receiver must not materialize IDBFactory" + ); + assert_eq!(materialization_count(&before_factory, "IDBFactory"), 0); + handle.post_message(serialize_test_string("indexedDB")); assert_eq!( recv_post_json(&mut handle).await, - r#"{"phase":"indexedDB","same":true,"instance":true,"dataDescriptor":true}"# + r#"{"phase":"indexedDB","same":true,"instance":true,"prototypeAccessor":true}"# ); let diagnostics = lazy_diagnostics(&handle).await; assert_eq!(materialization_count(&diagnostics, "IDBFactory"), 1); diff --git a/moli-renderer-v8/src/worker/thread/tests/mod.rs b/moli-renderer-v8/src/worker/thread/tests/mod.rs index 9e31fa020d..f4f0995471 100644 --- a/moli-renderer-v8/src/worker/thread/tests/mod.rs +++ b/moli-renderer-v8/src/worker/thread/tests/mod.rs @@ -1528,6 +1528,7 @@ mod abort_signal_events; mod abort_signal_statics; mod cors_redirects; mod imported_scripts; +mod indexed_db; mod lazy_storage; mod lifecycle; mod modules; diff --git a/moli-renderer-v8/tests/fixtures/worker-indexeddb-attribute.js b/moli-renderer-v8/tests/fixtures/worker-indexeddb-attribute.js new file mode 100644 index 0000000000..680c26a7d0 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/worker-indexeddb-attribute.js @@ -0,0 +1,104 @@ +function workerIndexedDBAttributeProbe() { + const checks = []; + const check = (label, pass, actual = '') => checks.push({label, pass, actual: String(actual)}); + const typeError = callback => { + try { callback(); } catch (error) { return Object.getPrototypeOf(error) === TypeError.prototype; } + return false; + }; + const prototype = WorkerGlobalScope.prototype; + const descriptor = Object.getOwnPropertyDescriptor(prototype, 'indexedDB'); + const getter = descriptor?.get; + const native = receiver => typeof getter === 'function' ? getter.call(receiver) : undefined; + check('readonly prototype accessor', typeof getter === 'function' && descriptor.set === undefined); + check('enumerable configurable prototype accessor', descriptor?.enumerable === true && descriptor.configurable === true); + check('getter metadata', getter?.name === 'get indexedDB' && getter.length === 0); + check('getter cannot construct', typeof getter === 'function' && typeError(() => new getter())); + check('no initial own attribute', !Object.hasOwn(self, 'indexedDB')); + check('invalid receiver before first factory access', typeError(() => native({}))); + + const factory = self.indexedDB; + const originalOwn = Object.getOwnPropertyDescriptor(self, 'indexedDB'); + const restoreOwn = () => { + delete self.indexedDB; + if (originalOwn) Object.defineProperty(self, 'indexedDB', originalOwn); + }; + check('SameObject', self.indexedDB === factory); + check('factory brand', factory instanceof IDBFactory); + check('factory operation', factory.cmp(1, 2) === -1); + check('read does not materialize own attribute', !Object.hasOwn(self, 'indexedDB')); + for (const [label, receiver] of [['self', self], ['undefined', undefined], ['null', null]]) { + check('getter receiver ' + label, native(receiver) === factory); + } + + const revoked = Proxy.revocable(self, {}); revoked.revoke(); + let traps = 0; + const proxy = new Proxy(self, { + get() { ++traps; throw new Error('get trap'); }, + getPrototypeOf() { ++traps; throw new Error('prototype trap'); } + }); + const invalid = [{}, prototype, Object.create(prototype), Object.create(self), 0, 'text', + true, Symbol('receiver'), 1n, factory, IDBFactory.prototype, new Proxy(self, {}), revoked.proxy, proxy]; + for (let i = 0; i < invalid.length; ++i) { + check('reject invalid receiver ' + i, typeError(() => native(invalid[i]))); + } + check('receiver check does not invoke Proxy traps', traps === 0, traps); + check('inherited attribute validates receiver', typeError(() => Object.create(self).indexedDB)); + check('author proxy property read validates receiver', typeError(() => new Proxy(self, {}).indexedDB)); + + let conversions = 0; + const replacement = {[Symbol.toPrimitive]() { ++conversions; throw new Error('conversion'); }}; + check('getter ignores extra arguments', typeof getter === 'function' && getter.call(self, replacement) === factory); + check('Reflect.set rejects assignment', Reflect.set(self, 'indexedDB', replacement) === false); + check('assignment preserves factory', self.indexedDB === factory); + restoreOwn(); + check('strict assignment throws TypeError', typeError(() => { 'use strict'; self.indexedDB = replacement; })); + check('strict assignment preserves factory', self.indexedDB === factory); + restoreOwn(); + check('assignment does not convert value', conversions === 0, conversions); + check('deleting absent own attribute succeeds', delete self.indexedDB); + check('deleting absent own attribute preserves factory', self.indexedDB === factory); + restoreOwn(); + + Object.defineProperty(self, 'indexedDB', {value: 42, writable: true, enumerable: true, configurable: true}); + check('author data shadow is visible', self.indexedDB === 42); + check('native getter ignores data shadow', native(self) === factory); + check('author data shadow can be deleted', delete self.indexedDB); + check('deleting data shadow restores same factory', self.indexedDB === factory); + restoreOwn(); + let reads = 0; + const marker = new Error('author getter'); + Object.defineProperty(self, 'indexedDB', {configurable: true, get() { ++reads; throw marker; }}); + check('native getter ignores accessor shadow', native(self) === factory && reads === 0, reads); + let error; + try { self.indexedDB; } catch (caught) { error = caught; } + check('author accessor remains observable', error === marker && reads === 1, reads); + delete self.indexedDB; + check('deleting accessor shadow restores same factory', self.indexedDB === factory); + restoreOwn(); + + // Changing the exposed prototype property must not recreate the factory. + Object.defineProperty(prototype, 'indexedDB', {value: 'prototype shadow', configurable: true}); + check('prototype property is configurable', self.indexedDB === 'prototype shadow'); + check('saved getter survives prototype replacement', native(self) === factory); + delete prototype.indexedDB; + check('saved getter survives prototype deletion', native(self) === factory); + if (descriptor) Object.defineProperty(prototype, 'indexedDB', descriptor); + check('restoring prototype restores same factory', self.indexedDB === factory); + + let constructorReads = 0; + const constructors = ['WorkerGlobalScope', 'IDBFactory']; + const originalConstructors = constructors.map(name => Object.getOwnPropertyDescriptor(self, name)); + try { + for (const name of constructors) { + Object.defineProperty(self, name, {configurable: true, get() { ++constructorReads; throw marker; }}); + } + check('getter ignores replaced constructors', native(self) === factory && constructorReads === 0, constructorReads); + } finally { + constructors.forEach((name, index) => Object.defineProperty(self, name, originalConstructors[index])); + } + check('factory remains usable', factory.cmp('a', 'b') === -1); + Object.defineProperty(self, 'indexedDB', {value: 'locked', writable: false, configurable: false}); + check('locked author shadow is visible', self.indexedDB === 'locked'); + check('native getter ignores locked shadow', native(self) === factory); + return {state: checks.every(check => check.pass) ? 'pass' : 'fail', checks}; +}