diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509Chain.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509Chain.cs index 0e5a9a7e724266..e763f586d9de8a 100644 --- a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509Chain.cs +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509Chain.cs @@ -42,15 +42,33 @@ internal static X509Certificate2[] X509ChainGetCertificates(SafeX509ChainContext var certPtrs = new IntPtr[count]; int res = Interop.AndroidCrypto.X509ChainGetCertificates(ctx, certPtrs, certPtrs.Length); - if (res == 0) - throw new CryptographicException(); - - Debug.Assert(res <= certPtrs.Length); var certs = new X509Certificate2[certPtrs.Length]; - for (int i = 0; i < res; i++) + try + { + if (res == 0) + throw new CryptographicException(); + + Debug.Assert(res <= certPtrs.Length); + + for (int i = 0; i < res; i++) + { + // X509Certificate2 duplicates these JNI global refs; the native-returned refs remain caller-owned. + certs[i] = new X509Certificate2(certPtrs[i]); + } + } + finally { - certs[i] = new X509Certificate2(certPtrs[i]); + // The native side can populate part of certPtrs and then fail (returning 0) if a JNI + // exception is thrown mid-loop, so release every non-null entry rather than only the + // first `res` entries. + for (int i = 0; i < certPtrs.Length; i++) + { + if (certPtrs[i] != IntPtr.Zero) + { + Interop.JObjectLifetime.DeleteGlobalReference(certPtrs[i]); + } + } } if (res == certPtrs.Length) diff --git a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs index 7f8fe69f086c35..14278b303009b5 100644 --- a/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs +++ b/src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs @@ -25,6 +25,7 @@ public static partial class PlatformDetection private static readonly Lazy s_IsInHelix = new Lazy(() => Environment.GetEnvironmentVariables().Keys.Cast().Any(key => key.StartsWith("HELIX"))); public static bool IsInHelix => s_IsInHelix.Value; + public static bool IsNotInHelix => !IsInHelix; public static bool IsNetCore => Environment.Version.Major >= 5 || RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase); public static bool IsMonoRuntime => Type.GetType("Mono.RuntimeStructs") != null; diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.cs index 74a3351622e899..4b5ac85e6a1d63 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/ChainTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography.X509Certificates.Tests.Common; using System.Text; using System.Threading; using Test.Cryptography; @@ -376,6 +377,82 @@ public static void BuildChainCustomTrustStore( } } + [PlatformSpecific(TestPlatforms.Android)] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInHelix))] + public static void BuildChainRepeatedly_DoesNotExhaustGlobalReferences() + { + // Android aborts the process when its JNI global reference table overflows. This + // 6-certificate chain leaks 6 JNI global refs per successful build without the Android + // PAL cleanup, so 8,600 builds would leak 51,600 certificate refs. 8,400 iterations + // completed without the fix during threshold testing, while 8,500 iterations crashed + // with "global reference table overflow (max=51200)". + // This tests runs for ~10 minutes on an Android emulator. + const int Iterations = 8_600; + + CertificateAuthority.BuildPrivatePki( + PkiOptions.AllRevocation, + out RevocationResponder responder, + out CertificateAuthority root, + out CertificateAuthority[] intermediates, + out X509Certificate2 endCert, + intermediateAuthorityCount: 4, + registerAuthorities: false, + keyFactory: CertificateAuthority.KeyFactory.RSASize(2048)); + + using (responder) + using (root) + using (CertificateAuthority intermediate1 = intermediates[0]) + using (CertificateAuthority intermediate2 = intermediates[1]) + using (CertificateAuthority intermediate3 = intermediates[2]) + using (CertificateAuthority intermediate4 = intermediates[3]) + using (endCert) + using (ImportedCollection issuerHolder = new ImportedCollection(new X509Certificate2Collection + { + intermediate4.CloneIssuerCert(), + intermediate3.CloneIssuerCert(), + intermediate2.CloneIssuerCert(), + intermediate1.CloneIssuerCert(), + root.CloneIssuerCert(), + })) + using (ChainHolder chainHolder = new ChainHolder()) + { + X509Certificate2Collection issuers = issuerHolder.Collection; + X509Chain chain = CreateChain(chainHolder, endCert, issuers); + + // Each successful Android chain build materializes the chain from caller-owned JNI + // global references. Without releasing those native-returned references, this + // sequential public-API loop eventually exhausts Android process resources. + for (int i = 0; i < Iterations; i++) + { + if (!chain.Build(endCert)) + { + Assert.Fail($"Chain build failed on iteration {i} with '{chain.AllStatusFlags()}'."); + } + + if (i == 0) + { + Assert.Equal(issuers.Count + 1, chain.ChainElements.Count); + } + + chainHolder.DisposeChainElements(); + } + } + + static X509Chain CreateChain(ChainHolder chainHolder, X509Certificate2 endCert, X509Certificate2Collection issuers) + { + X509Chain chain = chainHolder.Chain; + + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationTime = endCert.NotBefore.AddSeconds(1); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.DisableCertificateDownloads = true; + chain.ChainPolicy.ExtraStore.AddRange(issuers); + chain.ChainPolicy.CustomTrustStore.Add(issuers[issuers.Count - 1]); + + return chain; + } + } + [Fact] public static void BuildChainWithSystemTrustAndCustomTrustCertificates() {