-
Notifications
You must be signed in to change notification settings - Fork 71
Mark transactions inactive during key serialization #479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
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.) |
Closes #476
The following tasks have been completed:
Implementation commitment:
Preview | Diff