Repository navigation
X509EnhancedKeyUsageExtension.EnhancedKeyUsages returns an empty collection when first read concurrently #136863
Description
Description
X509EnhancedKeyUsageExtension.EnhancedKeyUsages can return an empty collection when two threads read it for the first time concurrently on the same extension instance. Once the property has been read once, concurrent reads are correct.
We hit this with several Kestrel hosts started in parallel (xUnit) that share one X509Certificate2 as server certificate whose Extended Key Usage is Server Authentication only. Intermittently one host failed to start because its certificate check saw no usages and reported that the certificate's usages do not include Server Authentication.
Reproduction Steps
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
var warm = args.Contains("warm");
const int trials = 20000;
var empty = 0;
using var key = RSA.Create(2048);
var request = new CertificateRequest("CN=race", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension([new Oid("1.3.6.1.5.5.7.3.1")], false));
using var created = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddHours(1));
var der = created.RawData;
for (var i = 0; i < trials; i++)
{
using var certificate = X509CertificateLoader.LoadCertificate(der);
var extension = certificate.Extensions.OfType<X509EnhancedKeyUsageExtension>().Single();
if (warm) _ = extension.EnhancedKeyUsages.Count;
using var barrier = new Barrier(2);
var counts = new int[2];
var threads = Enumerable.Range(0, 2).Select(t => new Thread(() =>
{
barrier.SignalAndWait();
counts[t] = extension.EnhancedKeyUsages.Count;
})).ToArray();
foreach (var thread in threads) thread.Start();
foreach (var thread in threads) thread.Join();
empty += counts.Count(c => c == 0);
}
Console.WriteLine($"runtime={Environment.Version} os={Environment.OSVersion} processors={Environment.ProcessorCount} warm={warm} trials={trials} reads-with-empty-usages={empty}");dotnet run -c Release and dotnet run -c Release -- warm (net10.0 console project).
Expected behavior
Every read returns one usage (1.3.6.1.5.5.7.3.1): reads-with-empty-usages=0 with and without warm.
Actual behavior
| Environment | warm |
Result |
|---|---|---|
| Windows 11 (10.0.26300), .NET runtime 10.0.13, 8 processors | no | reads-with-empty-usages=66 of 40000 reads |
| same | yes | reads-with-empty-usages=0 |
| Linux (CI sandbox), .NET SDK 10.0.403, 2 processors | no | 2006 empty reads in 20000 trials |
| same | yes | 0 |
Regression?
Not known; observed on .NET 10.
Known Workarounds
Read EnhancedKeyUsages once (on one thread) before the extension or its certificate is shared between threads.
Configuration
.NET 10 (runtime 10.0.13 on Windows x64; SDK 10.0.403 on Linux x64).
Other information
Possible cause (from reading the implementation, not verified with a debugger): the first read decodes RawData lazily without synchronization; the decoder assigns a new, still empty OidCollection to the backing field before it adds the decoded usages and sets the decoded flag afterwards. A second concurrent first read can replace the field with its own empty collection while the first reader copies it, so that reader returns an empty list.
Activity
dotnet-policy-service commentedon Nov 2, 2026
Tagging subscribers to this area: @bartonjs, @vcsjones, @dotnet/area-system-security
See info in area-owners.md if you want to be subscribed.
bartonjs commentedon Nov 2, 2026
Technically speaking, it's not described as thread-safe, so the bug is in your concurrent usage.
But since @vcsjones has already assigned this to himself, I'm guessing he's going to fix it out of the goodness of his heart.