Skip to content

Commit 3a973a5

Browse files
authored
Fix memory leaks in CertificateManager by improving certificate disposal patterns (#63321)
1 parent bb01877 commit 3a973a5

1 file changed

Lines changed: 84 additions & 39 deletions

File tree

src/Shared/CertificateGeneration/CertificateManager.cs

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -286,29 +286,36 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
286286
{
287287
var result = EnsureCertificateResult.Succeeded;
288288

289-
var currentUserCertificates = ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true);
290-
var localMachineCertificates = ListCertificates(StoreName.My, StoreLocation.LocalMachine, isValid: true, requireExportable: true);
291-
var certificates = currentUserCertificates.Concat(localMachineCertificates);
289+
var allCurrentUserCertificates = ListCertificates(StoreName.My, StoreLocation.CurrentUser, isValid: true, requireExportable: true);
290+
var allLocalMachineCertificates = ListCertificates(StoreName.My, StoreLocation.LocalMachine, isValid: true, requireExportable: true);
292291

293-
var filteredCertificates = certificates.Where(c => c.Subject == Subject);
292+
var currentUserCertificates = allCurrentUserCertificates.Where(c => c.Subject == Subject).ToList();
293+
var localMachineCertificates = allLocalMachineCertificates.Where(c => c.Subject == Subject).ToList();
294+
var filteredCertificates = currentUserCertificates.Concat(localMachineCertificates).ToList();
295+
294296
if (isInteractive)
295297
{
296298
// For purposes of updating the dev cert, only consider certificates with the current version or higher as valid
297299
// Only applies to interactive scenarios where we want to ensure we're generating the latest certificate
298300
// For non-interactive scenarios (e.g. first run experience), we want to accept older versions of the certificate as long as they meet the minimum version requirement
299301
// This will allow us to respond to scenarios where we need to invalidate older certificates due to security issues, etc. but not leave users
300302
// with a partially valid certificate after their first run experience.
301-
filteredCertificates = filteredCertificates.Where(c => GetCertificateVersion(c) >= AspNetHttpsCertificateVersion);
303+
filteredCertificates = filteredCertificates.Where(c => GetCertificateVersion(c) >=
304+
AspNetHttpsCertificateVersion).ToList();
302305
}
303306

304307
if (Log.IsEnabled())
305308
{
306-
var excludedCertificates = certificates.Except(filteredCertificates);
309+
var excludedCertificates = allCurrentUserCertificates.Concat(allLocalMachineCertificates).Except(filteredCertificates);
307310
Log.FilteredCertificates(ToCertificateDescription(filteredCertificates));
308311
Log.ExcludedCertificates(ToCertificateDescription(excludedCertificates));
309312
}
310313

311-
certificates = filteredCertificates;
314+
// Dispose certificates we're not going to use
315+
DisposeCertificates(allCurrentUserCertificates.Except(currentUserCertificates));
316+
DisposeCertificates(allLocalMachineCertificates.Except(localMachineCertificates));
317+
318+
var certificates = filteredCertificates;
312319

313320
X509Certificate2? certificate = null;
314321
var isNewCertificate = false;
@@ -380,6 +387,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
380387
Log.CreateDevelopmentCertificateError(e.ToString());
381388
}
382389
result = EnsureCertificateResult.ErrorCreatingTheCertificate;
390+
DisposeCertificates(certificates);
383391
return result;
384392
}
385393
Log.CreateDevelopmentCertificateEnd();
@@ -391,6 +399,11 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
391399
catch (Exception e)
392400
{
393401
Log.SaveCertificateInStoreError(e.ToString());
402+
if (isNewCertificate)
403+
{
404+
certificate?.Dispose();
405+
}
406+
DisposeCertificates(certificates);
394407
result = EnsureCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
395408
return result;
396409
}
@@ -448,6 +461,11 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
448461
result :
449462
EnsureCertificateResult.ErrorExportingTheCertificate;
450463

464+
if (isNewCertificate)
465+
{
466+
certificate?.Dispose();
467+
}
468+
DisposeCertificates(certificates);
451469
return result;
452470
}
453471
}
@@ -464,21 +482,41 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
464482
break;
465483
case TrustLevel.Partial:
466484
result = EnsureCertificateResult.PartiallyFailedToTrustTheCertificate;
485+
if (isNewCertificate)
486+
{
487+
certificate?.Dispose();
488+
}
489+
DisposeCertificates(certificates);
467490
return result;
468491
case TrustLevel.None:
469492
default: // Treat unknown status (should be impossible) as failure
470493
result = EnsureCertificateResult.FailedToTrustTheCertificate;
494+
if (isNewCertificate)
495+
{
496+
certificate?.Dispose();
497+
}
498+
DisposeCertificates(certificates);
471499
return result;
472500
}
473501
}
474502
catch (UserCancelledTrustException)
475503
{
476504
result = EnsureCertificateResult.UserCancelledTrustStep;
505+
if (isNewCertificate)
506+
{
507+
certificate?.Dispose();
508+
}
509+
DisposeCertificates(certificates);
477510
return result;
478511
}
479512
catch
480513
{
481514
result = EnsureCertificateResult.FailedToTrustTheCertificate;
515+
if (isNewCertificate)
516+
{
517+
certificate?.Dispose();
518+
}
519+
DisposeCertificates(certificates);
482520
return result;
483521
}
484522

@@ -515,51 +553,58 @@ internal ImportCertificateResult ImportCertificate(string certificatePath, strin
515553
return ImportCertificateResult.ExistingCertificatesPresent;
516554
}
517555

518-
X509Certificate2 certificate;
556+
X509Certificate2? certificate = null;
519557
try
520558
{
521-
Log.LoadCertificateStart(certificatePath);
522-
certificate = new X509Certificate2(certificatePath, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
523-
if (Log.IsEnabled())
559+
try
524560
{
525-
Log.LoadCertificateEnd(GetDescription(certificate));
561+
Log.LoadCertificateStart(certificatePath);
562+
certificate = new X509Certificate2(certificatePath, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.EphemeralKeySet);
563+
if (Log.IsEnabled())
564+
{
565+
Log.LoadCertificateEnd(GetDescription(certificate));
566+
}
526567
}
527-
}
528-
catch (Exception e)
529-
{
530-
if (Log.IsEnabled())
568+
catch (Exception e)
569+
{
570+
if (Log.IsEnabled())
571+
{
572+
Log.LoadCertificateError(e.ToString());
573+
}
574+
return ImportCertificateResult.InvalidCertificate;
575+
}
576+
577+
// Note that we're checking Subject, rather than LocalhostHttpsDistinguishedName,
578+
// because the tests use a different subject.
579+
if (!string.Equals(certificate.Subject, Subject, StringComparison.Ordinal) || // Kestrel requires this
580+
!IsHttpsDevelopmentCertificate(certificate))
531581
{
532-
Log.LoadCertificateError(e.ToString());
582+
if (Log.IsEnabled())
583+
{
584+
Log.NoHttpsDevelopmentCertificate(GetDescription(certificate));
585+
}
586+
return ImportCertificateResult.NoDevelopmentHttpsCertificate;
533587
}
534-
return ImportCertificateResult.InvalidCertificate;
535-
}
536588

537-
// Note that we're checking Subject, rather than LocalhostHttpsDistinguishedName,
538-
// because the tests use a different subject.
539-
if (!string.Equals(certificate.Subject, Subject, StringComparison.Ordinal) || // Kestrel requires this
540-
!IsHttpsDevelopmentCertificate(certificate))
541-
{
542-
if (Log.IsEnabled())
589+
try
543590
{
544-
Log.NoHttpsDevelopmentCertificate(GetDescription(certificate));
591+
certificate = SaveCertificate(certificate);
592+
}
593+
catch (Exception e)
594+
{
595+
if (Log.IsEnabled())
596+
{
597+
Log.SaveCertificateInStoreError(e.ToString());
598+
}
599+
return ImportCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
545600
}
546-
return ImportCertificateResult.NoDevelopmentHttpsCertificate;
547-
}
548601

549-
try
550-
{
551-
SaveCertificate(certificate);
602+
return ImportCertificateResult.Succeeded;
552603
}
553-
catch (Exception e)
604+
finally
554605
{
555-
if (Log.IsEnabled())
556-
{
557-
Log.SaveCertificateInStoreError(e.ToString());
558-
}
559-
return ImportCertificateResult.ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore;
606+
certificate?.Dispose();
560607
}
561-
562-
return ImportCertificateResult.Succeeded;
563608
}
564609

565610
public void CleanupHttpsCertificates()

0 commit comments

Comments
 (0)