fix(indexeddb): use intrinsic templates for factory creation

This commit is contained in:
ldm0
2026-09-23 03:02:18 +08:00
parent 6d08975a69
commit d2d046d444
4 changed files with 121 additions and 10 deletions
@@ -1,4 +1,5 @@
use super::*;
use crate::context_bootstrap::exposed_interfaces::build_intrinsic_interface_instance;
use crate::util::{get_private_object, get_private_value, set_private_value};
use crate::web_api_interfaces;
use moli_webapi_declare::WebApiObject;
@@ -14,7 +15,7 @@ const INDEXED_DB_READWRITE_TRANSACTION_QUEUE_FIELD: &str =
"moli.IndexedDb.runtime.readwriteTransactionQueue";
#[derive(Default, WebApiObject)]
#[webapi(interface = web_api_interfaces::IDBFactory, require_prototype)]
#[webapi(interface = web_api_interfaces::IDBFactory)]
struct IndexedDbFactoryRuntimeDeclaration {
#[webapi(slot = INDEXED_DB_EVENT_LISTENERS_SLOT, init = "null_object")]
event_listeners: (),
@@ -306,15 +307,12 @@ pub(in crate::context_bootstrap::indexed_db) fn indexed_db_factory_storage_scope
fn build_indexed_db_factory_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> Option<v8::Local<'s, v8::Object>> {
// The factory is script-visible and must receive IDBFactory.prototype;
// only the private owner state around it is null-prototype.
let factory = IndexedDbFactoryRuntimeDeclaration {
event_listeners: (),
}
.bind(scope)
.ok()?;
let factory_proto = global_constructor_prototype(scope, "IDBFactory")?;
let _ = factory.set_prototype(scope, factory_proto.into());
// Materialize the realm's intrinsic before constructing the factory, without
// reading an author-replaced public IDBFactory binding on first use.
let factory = build_intrinsic_interface_instance(scope, "IDBFactory").ok()?;
IndexedDbFactoryRuntimeDeclaration::default()
.initialize(scope, factory)
.ok()?;
let listeners = new_null_prototype_object(scope);
set_indexed_db_slot_value(
scope,
@@ -1,6 +1,31 @@
use super::*;
use moli_url::origin_ascii_serialization;
#[test]
fn indexed_db_first_use_ignores_public_constructor_overrides() {
let fixture = include_str!("../../../tests/fixtures/indexeddb-first-use.js");
for mode in ["number", "function", "getter", "delete"] {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-first-use.test/");
assert_eq!(
vm.lazy_constructor_materialization_count_for_test("IDBFactory")
.expect("initial IDBFactory materialization count"),
0,
"{mode}: IDBFactory must remain lazy before the probe"
);
assert_eq!(
vm.eval(&format!("({fixture})({mode:?})"))
.unwrap_or_else(|error| panic!("{mode}: first IndexedDB access failed: {error}")),
"ok"
);
assert_eq!(
vm.lazy_constructor_materialization_count_for_test("IDBFactory")
.expect("final IDBFactory materialization count"),
1,
"{mode}: first IndexedDB access must materialize the intrinsic exactly once"
);
}
}
#[test]
fn indexed_db_runtime_state_is_created_on_first_use_without_window_slots() {
let mut vm = new_storage_page_task_executor_test_vm("https://indexeddb-lazy-runtime.test/");
@@ -1,4 +1,44 @@
use super::*;
use crate::worker::WorkerGlobalKind;
#[tokio::test]
async fn worker_indexed_db_first_use_ignores_public_constructor_overrides() {
ensure_v8();
let script_url = url::Url::parse("https://indexeddb-first-use.test/worker.js").unwrap();
let fixture = include_str!("../../../../tests/fixtures/indexeddb-first-use.js");
for kind in [
WorkerGlobalKind::Dedicated {
name: String::new(),
},
WorkerGlobalKind::Shared {
name: String::new(),
storage_key: moli_storage_key::MoliStorageKey::first_party_from_url(&script_url, None),
},
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-first-use.test/").unwrap(),
},
] {
for mode in ["number", "function", "getter", "delete"] {
let (bootstrap_tx, mut bootstrap_rx) = tokio::sync::mpsc::unbounded_channel();
let handle = spawn_test_worker_with_options(
WorkerSpawnOptions::new(format!("({fixture})({mode:?})"), script_url.to_string())
.with_global_kind(kind.clone())
.with_bootstrap_completion_sender(bootstrap_tx),
);
let bootstrap = timeout(TIMEOUT, bootstrap_rx.recv()).await;
handle.terminate_and_join();
bootstrap
.expect("timed out waiting for IndexedDB first-use checks")
.expect("worker bootstrap channel closed")
.result
.unwrap_or_else(|error| {
panic!("{kind:?}, {mode}: first IndexedDB access failed: {error:?}")
});
}
}
}
async fn lazy_diagnostics(
handle: &WorkerHandle,
+48
View File
@@ -0,0 +1,48 @@
(mode => {
const check = (condition, message) => {
if (!condition) throw new Error(message);
};
let constructorReads = 0;
const replacement = mode === 'function' ? function Fake() {} : 123;
const poison = () => {
constructorReads++;
throw new Error('public IDBFactory getter must not run');
};
// Do not read IDBFactory, its descriptor, or indexedDB before the override:
// doing so could materialize the intrinsic and hide a first-use regression.
if (mode === 'getter') {
Object.defineProperty(globalThis, 'IDBFactory', {
get: poison,
configurable: false
});
} else if (mode === 'delete') {
check(delete globalThis.IDBFactory, 'constructor deletion');
} else {
globalThis.IDBFactory = replacement;
}
const factory = globalThis.indexedDB;
check(typeof factory === 'object' && factory !== null, 'factory exists');
const prototype = Object.getPrototypeOf(factory);
const intrinsic = prototype.constructor;
check(intrinsic !== replacement && intrinsic.name === 'IDBFactory', 'intrinsic constructor');
check(prototype === intrinsic.prototype, 'intrinsic prototype');
check(factory instanceof intrinsic, 'factory instance');
check(Object.prototype.toString.call(factory) === '[object IDBFactory]', 'factory tag');
check(factory.cmp(1, 2) === -1 && factory.cmp(2, 2) === 0, 'native cmp works');
for (const name of ['open', 'deleteDatabase', 'databases']) {
check(typeof factory[name] === 'function', name + ' method');
}
check(globalThis.indexedDB === factory, 'SameObject');
check(constructorReads === 0, 'no public constructor reads');
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'IDBFactory');
if (mode === 'getter') {
check(descriptor.get === poison && !descriptor.configurable, 'getter preserved');
} else if (mode === 'delete') {
check(descriptor === undefined, 'deleted constructor stays absent');
} else {
check(descriptor.value === replacement, 'replacement preserved');
}
return 'ok';
})