Skip to content

Commit ada401f

Browse files
authored
Make sure that MegolmEncryption.setupPromise always resolves (#2960)
ensureOutboundSession uses and modifies the setupPromise of the MegolmEncryption class. Some comments suggest that setupPromise will always resolve, in other words it should never contain a promise that will get rejected. Other comments also seem to suggest that the return value of ensureOutboundSession, a promise as well, may fail. The critical error here is that the promise that gets set as the next setupPromise, as well as the promise that ensureOutboundSession returns, is the same promise. It seems that the intention was for setupPromise to contain a promise that will always resolve to either `null` or `OutboundSessionInfo`. We can see that a couple of lines before we set setupPromise to its new value we construct a promise that logs and discards errors using the `Promise.catch()` method. The `Promise.catch()` method does not mutate the promise, instead it returns a new promise. The intention of the original author might have been to set the next setupPromise to the promise which `Promise.catch()` produces. This patch modifies the updating of setupPromise in the ensureOutboundSession so that setupPromise discards errors correctly. Using `>>=` to represent the promise chaining operation, setupPromise is now updated using the following logic: setupPromise = previousSetupPromise >>= setup >>= discardErrors
1 parent 41d7621 commit ada401f

File tree

2 files changed

+46
-7
lines changed

2 files changed

+46
-7
lines changed

spec/unit/crypto/algorithms/megolm.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,26 @@ describe("MegolmDecryption", function () {
490490

491491
expect(mockBaseApis.queueToDevice).not.toHaveBeenCalled();
492492
});
493+
494+
it("shouldn't wedge the setup promise if sharing a room key fails", async () => {
495+
// @ts-ignore - private field access
496+
const initialSetupPromise = await megolmEncryption.setupPromise;
497+
expect(initialSetupPromise).toBe(null);
498+
499+
// @ts-ignore - private field access
500+
megolmEncryption.prepareSession = () => {
501+
throw new Error("Can't prepare session");
502+
};
503+
504+
await expect(() =>
505+
// @ts-ignore - private field access
506+
megolmEncryption.ensureOutboundSession(mockRoom, {}, {}, true),
507+
).rejects.toThrow();
508+
509+
// @ts-ignore - private field access
510+
const finalSetupPromise = await megolmEncryption.setupPromise;
511+
expect(finalSetupPromise).toBe(null);
512+
});
493513
});
494514
});
495515

src/crypto/algorithms/megolm.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,23 @@ export class MegolmEncryption extends EncryptionAlgorithm {
248248
* @param singleOlmCreationPhase - Only perform one round of olm
249249
* session creation
250250
*
251+
* This method updates the setupPromise field of the class by chaining a new
252+
* call on top of the existing promise, and then catching and discarding any
253+
* errors that might happen while setting up the outbound group session. This
254+
* is done to ensure that `setupPromise` always resolves to `null` or the
255+
* `OutboundSessionInfo`.
256+
*
257+
* Using `>>=` to represent the promise chaining operation, it does the
258+
* following:
259+
*
260+
* ```
261+
* setupPromise = previousSetupPromise >>= setup >>= discardErrors
262+
* ```
263+
*
264+
* The initial value for the `setupPromise` is a promise that resolves to
265+
* `null`. The forceDiscardSession() resets setupPromise to this initial
266+
* promise.
267+
*
251268
* @returns Promise which resolves to the
252269
* OutboundSessionInfo when setup is complete.
253270
*/
@@ -278,18 +295,20 @@ export class MegolmEncryption extends EncryptionAlgorithm {
278295
};
279296

280297
// first wait for the previous share to complete
281-
const prom = this.setupPromise.then(setup);
298+
const fallible = this.setupPromise.then(setup);
282299

283-
// Ensure any failures are logged for debugging
284-
prom.catch((e) => {
300+
// Ensure any failures are logged for debugging and make sure that the
301+
// promise chain remains unbroken
302+
//
303+
// setupPromise resolves to `null` or the `OutboundSessionInfo` whether
304+
// or not the share succeeds
305+
this.setupPromise = fallible.catch((e) => {
285306
logger.error(`Failed to setup outbound session in ${this.roomId}`, e);
307+
return null;
286308
});
287309

288-
// setupPromise resolves to `session` whether or not the share succeeds
289-
this.setupPromise = prom;
290-
291310
// but we return a promise which only resolves if the share was successful.
292-
return prom;
311+
return fallible;
293312
}
294313

295314
private async prepareSession(

0 commit comments

Comments
 (0)