IndexedDB: Handle missing object stores in object store operations (#38115)

These changes fix a large number of panics that can manifest as
intermittent test failures. They also add more specification text to
various IDBObjectStore methods and implement missing steps that check
for whether an object store is deleted.

Testing: Existing test coverage.

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
Josh Matthews 2025-08-22 18:04:34 -04:00 committed by GitHub
parent f334a56b07
commit b7cdd88b8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 96 additions and 92 deletions

View file

@ -92,6 +92,13 @@ impl IDBDatabase {
) )
} }
pub(crate) fn object_store_exists(&self, name: &DOMString) -> bool {
self.object_store_names
.borrow()
.iter()
.any(|store_name| store_name == name)
}
pub fn version(&self) -> u64 { pub fn version(&self) -> u64 {
let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap(); let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap();
let operation = SyncOperation::Version( let operation = SyncOperation::Version(

View file

@ -10,11 +10,14 @@ use net_traits::indexeddb_thread::{
IndexedDBThreadMsg, SyncOperation, IndexedDBThreadMsg, SyncOperation,
}; };
use profile_traits::ipc; use profile_traits::ipc;
use script_bindings::error::ErrorResult;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::IDBObjectStoreParameters; use crate::dom::bindings::codegen::Bindings::IDBDatabaseBinding::IDBObjectStoreParameters;
use crate::dom::bindings::codegen::Bindings::IDBObjectStoreBinding::IDBObjectStoreMethods; use crate::dom::bindings::codegen::Bindings::IDBObjectStoreBinding::IDBObjectStoreMethods;
use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::IDBTransactionMode; use crate::dom::bindings::codegen::Bindings::IDBTransactionBinding::{
IDBTransactionMethods, IDBTransactionMode,
};
// We need to alias this name, otherwise test-tidy complains at &String reference. // We need to alias this name, otherwise test-tidy complains at &String reference.
use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence as StrOrStringSequence; use crate::dom::bindings::codegen::UnionTypes::StringOrStringSequence as StrOrStringSequence;
use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::error::{Error, Fallible};
@ -171,6 +174,14 @@ impl IDBObjectStore {
self.key_path.is_some() self.key_path.is_some()
} }
fn verify_not_deleted(&self) -> ErrorResult {
let db = self.transaction.Db();
if !db.object_store_exists(&self.name.borrow()) {
return Err(Error::InvalidState);
}
Ok(())
}
/// Checks if the transation is active, throwing a "TransactionInactiveError" DOMException if not. /// Checks if the transation is active, throwing a "TransactionInactiveError" DOMException if not.
fn check_transaction_active(&self) -> Fallible<()> { fn check_transaction_active(&self) -> Fallible<()> {
// Let transaction be this object store handle's transaction. // Let transaction be this object store handle's transaction.
@ -191,9 +202,7 @@ impl IDBObjectStore {
let transaction = &self.transaction; let transaction = &self.transaction;
// If transaction is not active, throw a "TransactionInactiveError" DOMException. // If transaction is not active, throw a "TransactionInactiveError" DOMException.
if !transaction.is_active() { self.check_transaction_active()?;
return Err(Error::TransactionInactive);
}
if let IDBTransactionMode::Readonly = transaction.get_mode() { if let IDBTransactionMode::Readonly = transaction.get_mode() {
return Err(Error::ReadOnly); return Err(Error::ReadOnly);
@ -210,14 +219,14 @@ impl IDBObjectStore {
overwrite: bool, overwrite: bool,
can_gc: CanGc, can_gc: CanGc,
) -> Fallible<DomRoot<IDBRequest>> { ) -> Fallible<DomRoot<IDBRequest>> {
// Step 1: Unneeded, handled by self.check_readwrite_transaction_active() // Step 1. Let transaction be handles transaction.
// Step 2: Let store be this object store handle's object store. // Step 2: Let store be this object store handle's object store.
// This is resolved in the `execute_async` function. // This is resolved in the `execute_async` function.
// Step 3: If store has been deleted, throw an "InvalidStateError" DOMException. // Step 3: If store has been deleted, throw an "InvalidStateError" DOMException.
// FIXME:(rasviitanen) self.verify_not_deleted()?;
// Steps 4-5 // Step 4. If transactions state is not active, then throw a "TransactionInactiveError" DOMException.
// Step 5. If transaction is a read-only transaction, throw a "ReadOnlyError" DOMException.
self.check_readwrite_transaction_active()?; self.check_readwrite_transaction_active()?;
// Step 6: If store uses in-line keys and key was given, throw a "DataError" DOMException. // Step 6: If store uses in-line keys and key was given, throw a "DataError" DOMException.
@ -253,10 +262,13 @@ impl IDBObjectStore {
} }
} }
// Step 10. Let clone be a clone of value in targetRealm during transaction. Rethrow any exceptions.
let serialized_value = structuredclone::write(cx, value, None)?; let serialized_value = structuredclone::write(cx, value, None)?;
let (sender, receiver) = indexed_db::create_channel(self.global()); let (sender, receiver) = indexed_db::create_channel(self.global());
// Step 12. Let operation be an algorithm to run store a record into an object store with store, clone, key, and no-overwrite flag.
// Step 13. Return the result (an IDBRequest) of running asynchronously execute a request with handle and operation.
IDBRequest::execute_async( IDBRequest::execute_async(
self, self,
AsyncOperation::ReadWrite(AsyncReadWriteOperation::PutItem { AsyncOperation::ReadWrite(AsyncReadWriteOperation::PutItem {
@ -295,15 +307,20 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-delete // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-delete
fn Delete(&self, cx: SafeJSContext, query: HandleValue) -> Fallible<DomRoot<IDBRequest>> { fn Delete(&self, cx: SafeJSContext, query: HandleValue) -> Fallible<DomRoot<IDBRequest>> {
// Step 1: Unneeded, handled by self.check_readwrite_transaction_active() // Step 1. Let transaction be thiss transaction.
// TODO: Step 2 // Step 2. Let store be this's object store.
// TODO: Step 3 // Step 3. If store has been deleted, throw an "InvalidStateError" DOMException.
// Steps 4-5 self.verify_not_deleted()?;
// Step 4. If transactions state is not active, then throw a "TransactionInactiveError" DOMException.
// Step 5. If transaction is a read-only transaction, throw a "ReadOnlyError" DOMException.
self.check_readwrite_transaction_active()?; self.check_readwrite_transaction_active()?;
// Step 6 // Step 6
// TODO: Convert to key range instead // TODO: Convert to key range instead
let serialized_query = convert_value_to_key(cx, query, None); let serialized_query = convert_value_to_key(cx, query, None);
// Step 7 // Step 7. Let operation be an algorithm to run delete records from an object store with store and range.
// Stpe 8. Return the result (an IDBRequest) of running asynchronously execute a request with this and operation.
let (sender, receiver) = indexed_db::create_channel(self.global()); let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| { serialized_query.and_then(|q| {
IDBRequest::execute_async( IDBRequest::execute_async(
@ -318,11 +335,17 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-clear // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-clear
fn Clear(&self) -> Fallible<DomRoot<IDBRequest>> { fn Clear(&self) -> Fallible<DomRoot<IDBRequest>> {
// Step 1: Unneeded, handled by self.check_readwrite_transaction_active() // Step 1. Let transaction be thiss transaction.
// TODO: Step 2 // Step 2. Let store be this's object store.
// TODO: Step 3 // Step 3. If store has been deleted, throw an "InvalidStateError" DOMException.
// Steps 4-5 self.verify_not_deleted()?;
// Step 4. If transactions state is not active, then throw a "TransactionInactiveError" DOMException.
// Step 5. If transaction is a read-only transaction, throw a "ReadOnlyError" DOMException.
self.check_readwrite_transaction_active()?; self.check_readwrite_transaction_active()?;
// Step 6. Let operation be an algorithm to run clear an object store with store.
// Stpe 7. Return the result (an IDBRequest) of running asynchronously execute a request with this and operation.
let (sender, receiver) = indexed_db::create_channel(self.global()); let (sender, receiver) = indexed_db::create_channel(self.global());
IDBRequest::execute_async( IDBRequest::execute_async(
@ -336,14 +359,19 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-get // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-get
fn Get(&self, cx: SafeJSContext, query: HandleValue) -> Fallible<DomRoot<IDBRequest>> { fn Get(&self, cx: SafeJSContext, query: HandleValue) -> Fallible<DomRoot<IDBRequest>> {
// Step 1: Unneeded, handled by self.check_transaction_active() // Step 1. Let transaction be thiss transaction.
// TODO: Step 2 // Step 2. Let store be this's object store.
// TODO: Step 3 // Step 3. If store has been deleted, throw an "InvalidStateError" DOMException.
// Step 4 self.verify_not_deleted()?;
// Step 4. If transactions state is not active, then throw a "TransactionInactiveError" DOMException.
self.check_transaction_active()?; self.check_transaction_active()?;
// Step 5
// Step 5. Let range be the result of converting a value to a key range with query and true. Rethrow any exceptions.
let serialized_query = convert_value_to_key_range(cx, query, None); let serialized_query = convert_value_to_key_range(cx, query, None);
// Step 6
// Step 6. Let operation be an algorithm to run retrieve a value from an object store with the current Realm record, store, and range.
// Step 7. Return the result (an IDBRequest) of running asynchronously execute a request with this and operation.
let (sender, receiver) = indexed_db::create_channel(self.global()); let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| { serialized_query.and_then(|q| {
IDBRequest::execute_async( IDBRequest::execute_async(
@ -361,14 +389,20 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-getkey // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-getkey
fn GetKey(&self, cx: SafeJSContext, query: HandleValue) -> Result<DomRoot<IDBRequest>, Error> { fn GetKey(&self, cx: SafeJSContext, query: HandleValue) -> Result<DomRoot<IDBRequest>, Error> {
// Step 1: Unneeded, handled by self.check_transaction_active() // Step 1. Let transaction be thiss transaction.
// TODO: Step 2 // Step 2. Let store be this's object store.
// TODO: Step 3 // Step 3. If store has been deleted, throw an "InvalidStateError" DOMException.
// Step 4 self.verify_not_deleted()?;
// Step 4. If transactions state is not active, then throw a "TransactionInactiveError" DOMException.
self.check_transaction_active()?; self.check_transaction_active()?;
// Step 5
// Step 5. Let range be the result of running the steps to convert a value to a key range with query and null disallowed flag set. Rethrow any exceptions.
let serialized_query = convert_value_to_key_range(cx, query, None); let serialized_query = convert_value_to_key_range(cx, query, None);
// Step 6
// Step 6. Run the steps to asynchronously execute a request and return the IDBRequest created by these steps.
// The steps are run with this object store handle as source and the steps to retrieve a key from an object
// store as operation, using store and range.
let (sender, receiver) = indexed_db::create_channel(self.global()); let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| { serialized_query.and_then(|q| {
IDBRequest::execute_async( IDBRequest::execute_async(
@ -406,16 +440,19 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-count // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-count
fn Count(&self, cx: SafeJSContext, query: HandleValue) -> Fallible<DomRoot<IDBRequest>> { fn Count(&self, cx: SafeJSContext, query: HandleValue) -> Fallible<DomRoot<IDBRequest>> {
// Step 1: Unneeded, handled by self.check_transaction_active() // Step 1. Let transaction be thiss transaction.
// TODO: Step 2 // Step 2. Let store be this's object store.
// TODO: Step 3 // Step 3. If store has been deleted, throw an "InvalidStateError" DOMException.
// Steps 4 self.verify_not_deleted()?;
// Step 4. If transactions state is not active, then throw a "TransactionInactiveError" DOMException.
self.check_transaction_active()?; self.check_transaction_active()?;
// Step 5 // Step 5. Let range be the result of converting a value to a key range with query. Rethrow any exceptions.
let serialized_query = convert_value_to_key_range(cx, query, None); let serialized_query = convert_value_to_key_range(cx, query, None);
// Step 6 // Step 6. Let operation be an algorithm to run count the records in a range with store and range.
// Step 7. Return the result (an IDBRequest) of running asynchronously execute a request with this and operation.
let (sender, receiver) = indexed_db::create_channel(self.global()); let (sender, receiver) = indexed_db::create_channel(self.global());
serialized_query.and_then(|q| { serialized_query.and_then(|q| {
IDBRequest::execute_async( IDBRequest::execute_async(
@ -437,8 +474,23 @@ impl IDBObjectStoreMethods<crate::DomTypeHolder> for IDBObjectStore {
} }
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-setname // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-setname
fn SetName(&self, value: DOMString) { fn SetName(&self, value: DOMString) -> ErrorResult {
// Step 2. Let transaction be thiss transaction.
let transaction = &self.transaction;
// Step 3. Let store be this's object store.
// Step 4. If store has been deleted, throw an "InvalidStateError" DOMException.
self.verify_not_deleted()?;
// Step 5. If transaction is not an upgrade transaction, throw an "InvalidStateError" DOMException.
if transaction.Mode() != IDBTransactionMode::Versionchange {
return Err(Error::InvalidState);
}
// Step 6. If transactions state is not active, throw a "TransactionInactiveError" DOMException.
self.check_transaction_active()?;
*self.name.borrow_mut() = value; *self.name.borrow_mut() = value;
Ok(())
} }
// https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-keypath // https://www.w3.org/TR/IndexedDB-2/#dom-idbobjectstore-keypath

View file

@ -10,7 +10,7 @@
// https://w3c.github.io/IndexedDB/#idbobjectstore // https://w3c.github.io/IndexedDB/#idbobjectstore
[Pref="dom_indexeddb_enabled", Exposed=(Window,Worker)] [Pref="dom_indexeddb_enabled", Exposed=(Window,Worker)]
interface IDBObjectStore { interface IDBObjectStore {
attribute DOMString name; [SetterThrows] attribute DOMString name;
// readonly attribute any keyPath; // readonly attribute any keyPath;
// readonly attribute DOMStringList indexNames; // readonly attribute DOMStringList indexNames;
[SameObject] readonly attribute IDBTransaction transaction; [SameObject] readonly attribute IDBTransaction transaction;

View file

@ -5,28 +5,16 @@
expected: ERROR expected: ERROR
[idbobjectstore-add-put-exception-order.any.html] [idbobjectstore-add-put-exception-order.any.html]
[IDBObjectStore.put exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.put exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.put exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL
[IDBObjectStore.add exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.add exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.add exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL
[idbobjectstore-add-put-exception-order.any.worker.html] [idbobjectstore-add-put-exception-order.any.worker.html]
[IDBObjectStore.put exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.put exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.put exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL
[IDBObjectStore.add exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.add exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.add exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL

View file

@ -1,7 +1,4 @@
[idbobjectstore-clear-exception-order.any.worker.html] [idbobjectstore-clear-exception-order.any.worker.html]
[IDBObjectStore.clear exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.clear exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.clear exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL
@ -10,9 +7,6 @@
expected: ERROR expected: ERROR
[idbobjectstore-clear-exception-order.any.html] [idbobjectstore-clear-exception-order.any.html]
[IDBObjectStore.clear exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.clear exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.clear exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL

View file

@ -2,9 +2,6 @@
expected: ERROR expected: ERROR
[idbobjectstore-delete-exception-order.any.html] [idbobjectstore-delete-exception-order.any.html]
[IDBObjectStore.delete exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.delete exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.delete exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL
@ -13,8 +10,5 @@
expected: ERROR expected: ERROR
[idbobjectstore-delete-exception-order.any.worker.html] [idbobjectstore-delete-exception-order.any.worker.html]
[IDBObjectStore.delete exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.delete exception order: TransactionInactiveError vs. ReadOnlyError] [IDBObjectStore.delete exception order: TransactionInactiveError vs. ReadOnlyError]
expected: FAIL expected: FAIL

View file

@ -1,7 +1,4 @@
[idbobjectstore-query-exception-order.any.html] [idbobjectstore-query-exception-order.any.html]
[IDBObjectStore.get exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.get exception order: TransactionInactiveError vs. DataError] [IDBObjectStore.get exception order: TransactionInactiveError vs. DataError]
expected: FAIL expected: FAIL
@ -17,9 +14,6 @@
[IDBObjectStore.getAllKeys exception order: TransactionInactiveError vs. DataError] [IDBObjectStore.getAllKeys exception order: TransactionInactiveError vs. DataError]
expected: FAIL expected: FAIL
[IDBObjectStore.count exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.count exception order: TransactionInactiveError vs. DataError] [IDBObjectStore.count exception order: TransactionInactiveError vs. DataError]
expected: FAIL expected: FAIL
@ -40,9 +34,6 @@
expected: ERROR expected: ERROR
[idbobjectstore-query-exception-order.any.worker.html] [idbobjectstore-query-exception-order.any.worker.html]
[IDBObjectStore.get exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.get exception order: TransactionInactiveError vs. DataError] [IDBObjectStore.get exception order: TransactionInactiveError vs. DataError]
expected: FAIL expected: FAIL
@ -58,9 +49,6 @@
[IDBObjectStore.getAllKeys exception order: TransactionInactiveError vs. DataError] [IDBObjectStore.getAllKeys exception order: TransactionInactiveError vs. DataError]
expected: FAIL expected: FAIL
[IDBObjectStore.count exception order: InvalidStateError vs. TransactionInactiveError]
expected: FAIL
[IDBObjectStore.count exception order: TransactionInactiveError vs. DataError] [IDBObjectStore.count exception order: TransactionInactiveError vs. DataError]
expected: FAIL expected: FAIL

View file

@ -5,9 +5,6 @@
[Clear removes all records from an index ] [Clear removes all records from an index ]
expected: FAIL expected: FAIL
[If the object store has been deleted, the implementation must throw a DOMException of type InvalidStateError ]
expected: FAIL
[idbobjectstore_clear.any.sharedworker.html] [idbobjectstore_clear.any.sharedworker.html]
expected: ERROR expected: ERROR
@ -21,6 +18,3 @@
[Clear removes all records from an index ] [Clear removes all records from an index ]
expected: FAIL expected: FAIL
[If the object store has been deleted, the implementation must throw a DOMException of type InvalidStateError ]
expected: FAIL

View file

@ -5,9 +5,6 @@
[Returns the number of records that have keys with the key] [Returns the number of records that have keys with the key]
expected: FAIL expected: FAIL
[If the object store has been deleted, the implementation must throw a DOMException of type InvalidStateError ]
expected: FAIL
[idbobjectstore_count.any.sharedworker.html] [idbobjectstore_count.any.sharedworker.html]
expected: ERROR expected: ERROR
@ -19,9 +16,6 @@
[Returns the number of records that have keys with the key] [Returns the number of records that have keys with the key]
expected: FAIL expected: FAIL
[If the object store has been deleted, the implementation must throw a DOMException of type InvalidStateError ]
expected: FAIL
[idbobjectstore_count.any.serviceworker.html] [idbobjectstore_count.any.serviceworker.html]
expected: ERROR expected: ERROR

View file

@ -12,9 +12,6 @@
[delete() removes all of the records in the range] [delete() removes all of the records in the range]
expected: FAIL expected: FAIL
[If the object store has been deleted, the implementation must throw a DOMException of type InvalidStateError]
expected: FAIL
[idbobjectstore_delete.any.serviceworker.html] [idbobjectstore_delete.any.serviceworker.html]
expected: ERROR expected: ERROR
@ -33,9 +30,6 @@
[delete() removes all of the records in the range] [delete() removes all of the records in the range]
expected: FAIL expected: FAIL
[If the object store has been deleted, the implementation must throw a DOMException of type InvalidStateError]
expected: FAIL
[idbobjectstore_delete.any.sharedworker.html] [idbobjectstore_delete.any.sharedworker.html]
expected: ERROR expected: ERROR

View file

@ -2,7 +2,6 @@
expected: ERROR expected: ERROR
[nested-cloning-basic.any.worker.html] [nested-cloning-basic.any.worker.html]
expected: CRASH
[small typed array] [small typed array]
expected: FAIL expected: FAIL