Skip to content

Commit 4ccb72c

Browse files
authored
Element-R: Fix resource leaks in verification logic (#4263)
* Move `RustVerificationRequest.onChange` out to a method The only reason it was an inner function in the first place was to avoid storing a reference in the class to `outgoingRequestProcessor`. That changed with d1dec4c. * Fix reference cycles in rust verification code
1 parent 9f1aebb commit 4ccb72c

File tree

1 file changed

+42
-26
lines changed

1 file changed

+42
-26
lines changed

src/rust-crypto/verification.ts

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,40 @@ export class RustVerificationRequest
7272
private readonly supportedVerificationMethods: string[],
7373
) {
7474
super();
75-
7675
this.reEmitter = new TypedReEmitter(this);
7776

78-
const onChange = async (): Promise<void> => {
79-
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
80-
81-
// Set the _verifier object (wrapping the rust `Verification` as a js-sdk Verifier) if:
82-
// - we now have a `Verification` where we lacked one before
83-
// - we have transitioned from QR to SAS
84-
// - we are verifying with SAS, but we need to replace our verifier with a new one because both parties
85-
// tried to start verification at the same time, and we lost the tie breaking
86-
if (verification instanceof RustSdkCryptoJs.Sas) {
87-
if (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) {
88-
this.setVerifier(new RustSASVerifier(verification, this, outgoingRequestProcessor));
89-
} else if (this._verifier instanceof RustSASVerifier) {
90-
this._verifier.replaceInner(verification);
91-
}
92-
} else if (verification instanceof RustSdkCryptoJs.Qr && this._verifier === undefined) {
93-
this.setVerifier(new RustQrCodeVerifier(verification, outgoingRequestProcessor));
77+
// Obviously, the Rust object maintains a reference to the callback function. If the callback function maintains
78+
// a reference to the Rust object, then we have a reference cycle which means that `RustVerificationRequest`
79+
// will never be garbage-collected, and hence the underlying rust object will never be freed.
80+
//
81+
// To avoid this reference cycle, use a weak reference in the callback function. If the `RustVerificationRequest`
82+
// gets garbage-collected, then there is nothing to update!
83+
const weakThis = new WeakRef(this);
84+
inner.registerChangesCallback(async () => weakThis.deref()?.onChange());
85+
}
86+
87+
/**
88+
* Hook which is called when the underlying rust class notifies us that there has been a change.
89+
*/
90+
private onChange(): void {
91+
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
92+
93+
// Set the _verifier object (wrapping the rust `Verification` as a js-sdk Verifier) if:
94+
// - we now have a `Verification` where we lacked one before
95+
// - we have transitioned from QR to SAS
96+
// - we are verifying with SAS, but we need to replace our verifier with a new one because both parties
97+
// tried to start verification at the same time, and we lost the tie breaking
98+
if (verification instanceof RustSdkCryptoJs.Sas) {
99+
if (this._verifier === undefined || this._verifier instanceof RustQrCodeVerifier) {
100+
this.setVerifier(new RustSASVerifier(verification, this, this.outgoingRequestProcessor));
101+
} else if (this._verifier instanceof RustSASVerifier) {
102+
this._verifier.replaceInner(verification);
94103
}
104+
} else if (verification instanceof RustSdkCryptoJs.Qr && this._verifier === undefined) {
105+
this.setVerifier(new RustQrCodeVerifier(verification, this.outgoingRequestProcessor));
106+
}
95107

96-
this.emit(VerificationRequestEvent.Change);
97-
};
98-
inner.registerChangesCallback(onChange);
108+
this.emit(VerificationRequestEvent.Change);
99109
}
100110

101111
private setVerifier(verifier: RustSASVerifier | RustQrCodeVerifier): void {
@@ -473,9 +483,12 @@ abstract class BaseRustVerifer<InnerType extends RustSdkCryptoJs.Qr | RustSdkCry
473483
super();
474484

475485
this.completionDeferred = defer();
476-
inner.registerChangesCallback(async () => {
477-
this.onChange();
478-
});
486+
487+
// As with RustVerificationRequest, we need to avoid a reference cycle.
488+
// See the comments in RustVerificationRequest.
489+
const weakThis = new WeakRef(this);
490+
inner.registerChangesCallback(async () => weakThis.deref()?.onChange());
491+
479492
// stop the runtime complaining if nobody catches a failure
480493
this.completionDeferred.promise.catch(() => null);
481494
}
@@ -752,9 +765,12 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
752765
public replaceInner(inner: RustSdkCryptoJs.Sas): void {
753766
if (this.inner != inner) {
754767
this.inner = inner;
755-
inner.registerChangesCallback(async () => {
756-
this.onChange();
757-
});
768+
769+
// As with RustVerificationRequest, we need to avoid a reference cycle.
770+
// See the comments in RustVerificationRequest.
771+
const weakThis = new WeakRef(this);
772+
inner.registerChangesCallback(async () => weakThis.deref()?.onChange());
773+
758774
// replaceInner will only get called if we started the verification at the same time as the other side, and we lost
759775
// the tie breaker. So we need to re-accept their verification.
760776
this.sendAccept();

0 commit comments

Comments
 (0)