Mark transactions inactive during key serialization#479
Mark transactions inactive during key serialization#479nolanlawson wants to merge 7 commits intomainfrom
Conversation
|
Note I only added the minimal logic to mark the transaction inactive when a key is provided to
|
|
Thanks for calling out other places where the spec converts a key to a value! (And for filing the spec issue in the first place!) For places like indexedDB.cmp and the IDBKeyRange methods (only/lowerBound/upperBound/bound/includes) where we don't have a transaction context to mark inactive and there's no complex state management going on it seems fine to not change the logic. But for continue and continuePrimaryKey we do explicitly have transactions and we do check that they are active and I think there is enough potentially interesting state management going on that it makes sense to consistently apply the same rationale we're using for add/put. In particular, it seems like we could be in an upgrade transaction when calling one of the continue methods and then nefarious code could attempt to delete the object store or index that the cursor is against with that method on the stack. I do think it's much less likely for code to be written in a way that could cause a security bug in this situation, but I do think it still applies that there's no reasonable use-case for content to be doing tricky things here and I do think it also simplifies things from a spec perspective. For example, step 3 of both continue methods is to throw if the underlying source/object store has been deleted. If we don't mark the transaction as inactive, then deleteObjectStore currently is defined to synchronously delete the store; we "destroy store" without ever going "in parallel" or doing async hand-waving. deleteIndex also synchronously does "destroy index" although the 2nd para afterwards does do some hand-waving: "Although this method does not return an IDBRequest object, the index destruction itself is processed as an asynchronous request within the upgrade transaction." So we would side-step these edge-cases where the index/objectStore could conceptually be deleted partway through the algorithm and any need to test/specify that by making the transaction inactive during the key conversions. |
|
Thanks for the feedback @asutherland! I just checked, and it appears that none of Firefox/Chromium/WebKit actually throw a Click to see test draftpromise_test(async testCase => {
const db = await createDatabase(testCase, database => {
database.createObjectStore('store');
});
const transaction = db.transaction(['store'], 'readwrite');
const objectStore = transaction.objectStore('store');
objectStore.put({}, 0);
objectStore.put({}, 1);
const cursor = await new Promise((resolve, reject) => {
const cursorReq = objectStore.openCursor();
cursorReq.onerror = reject;
cursorReq.onsuccess = e => resolve(e.target.result);
});
let getterCalled = false;
const activeKey = ['value that should not be used'];
Object.defineProperty(activeKey, '0', {
enumerable: true,
get: testCase.step_func(() => {
getterCalled = true;
assert_throws_dom('TransactionInactiveError', () => {
objectStore.get('key');
}, 'transaction should not be active during key serialization');
return 'value that should not be used';
}),
});
cursor.continue(activeKey);
await promiseForTransaction(testCase, transaction);
db.close();
assert_true(getterCalled,
"activeKey's getter should be called during test");
}, 'Transaction inactive during key serialization in IDBCursor.continue()');
promise_test(async testCase => {
const db = await createDatabase(testCase, database => {
const objectStore = database.createObjectStore('store');
objectStore.createIndex('idx', 'name');
});
const transaction = db.transaction(['store'], 'readwrite');
const objectStore = transaction.objectStore('store');
objectStore.put({ name: 'a' }, 0);
objectStore.put({ name: 'b' }, 1);
const idx = objectStore.index('idx')
const cursor = await new Promise((resolve, reject) => {
const cursorReq = idx.openCursor();
cursorReq.onerror = reject;
cursorReq.onsuccess = e => resolve(e.target.result);
});
let getterCalled = false;
const activeKey = ['value that should not be used'];
Object.defineProperty(activeKey, '0', {
enumerable: true,
get: testCase.step_func(() => {
getterCalled = true;
assert_throws_dom('TransactionInactiveError', () => {
objectStore.get('key');
}, 'transaction should not be active during key serialization');
return 'value that should not be used';
}),
});
cursor.continuePrimaryKey(activeKey, 0);
await promiseForTransaction(testCase, transaction);
db.close();
assert_true(getterCalled,
"activeKey's getter should be called during test");
}, 'Transaction inactive during key serialization in IDBCursor.continuePrimaryKey()');Firefox does throw for |
|
After re-reading your comment, I realized you weren't talking specifically about Firefox's implementation. I agree we can make this work with |
Yes.
Thank you; I like the introduction of the wrapper, this seems very clean! @evanstade and @SteveBeckerMSFT in #476 I think we were initially only talking about matching Firefox; are you okay with the (consistent) expansion to also cover the cursor continue methods? |
|
There are a lot of methods that take a key as an argument and are associated with a transaction --- many of the ones in Just based on the name alone, |
|
Yes, I agree we should expand the mechanism to cover convert a value to a key range too if you're on board since the same rationale applies. I somehow had a lot of tunnel vision going on and ignored that algorithm and its many uses. |
|
Yeah it makes sense to apply the change more broadly. (Sorry for slow reply.) |
|
Sorry for the delay. I've made the following changes:
The WPT tests will be a bit tricky to cover all these new scenarios, so I wanted to check if the spec changes look good first. (For example, "Creating a request to retrieve multiple items" calls the new transaction-checking logic multiple times, so you could imagine writing a test where the getter uses a counter to only trigger the |
|
Friendly ping @asutherland @evanstade 🙂 |
|
@nolanlawson thanks for the PR! I'll also review this soon. |
Closes #476
The following tasks have been completed:
Implementation commitment:
Preview | Diff