Skip to content

X509EnhancedKeyUsageExtension.EnhancedKeyUsages returns an empty collection when first read concurrently #136863

Open

Description

@wolfgangcolsman

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

  1. dotnet-policy-service commented on Nov 2, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @bartonjs, @vcsjones, @dotnet/area-system-security
    See info in area-owners.md if you want to be subscribed.

  2. self-assigned this
    on Nov 2, 2026
  3. bartonjs commented on Nov 2, 2026

    @bartonjs
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions