Skip to content

Fix certificate disposal in RemoteCertificateValidationCallback causing NRE on reuse#123875

Merged
rzikm merged 5 commits intomainfrom
copilot/fix-x509chain-nre-issue
Feb 3, 2026
Merged

Fix certificate disposal in RemoteCertificateValidationCallback causing NRE on reuse#123875
rzikm merged 5 commits intomainfrom
copilot/fix-x509chain-nre-issue

Conversation

Copy link
Contributor

Copilot AI commented Feb 2, 2026

Plan to Fix X509Chain.Build NRE Issue

This issue occurs because certificates in ExtraStore are being disposed even when added by the RemoteCertificateValidationCallback, causing NullReferenceException on subsequent uses.

Root Cause

  • In SslStream.Protocol.cs VerifyRemoteCertificate method (lines 1167-1171), certificates added to ExtraStore after the initial count are always disposed
  • This cleanup doesn't account for certificates that users add during the RemoteCertificateValidationCallback
  • Users' certificates or ChainElements certificates may be disposed even though they're not owned by SslStream

Solution

Only cleanup ExtraStore certificates when no RemoteCertificateValidationCallback was provided, as suggested by @bartonjs in the issue comments.

Checklist

  • Understand the issue and analyze the problematic code
  • Explore the repository structure and understand build/test procedures
  • Identify the exact code location that needs to be changed
  • Make minimal code change to fix the disposal issue in SslStream.Protocol.cs
  • Create a focused test case that reproduces the issue and validates the fix
  • Run baseline build to establish known-good state
  • Build and test the specific library changes
  • Verify the fix works correctly with new test
  • Run broader test suite to ensure no regressions (769 SslStream tests passed)
  • Address code review feedback on resource disposal
  • Run final code review (no issues found)
  • Run CodeQL security check (no issues found)
  • Address PR review comments: refactor test to use for loop, fix unused variable
  • Enhance test to verify ChainElements certificates are not disposed
  • Complete final validation
Original prompt

This section details on the original issue you should resolve

<issue_title>X509Chain.Build fails with NRE when ExtraStore certificates are reused within SslStream RemoteCertificateValidationCallback</issue_title>
<issue_description>### Description

Within the SslStream RemoteCertificateValidationCallback, you can mutate the X509Chain and revalidate it. Starting in .NET 10, if you do so and add a global, shared certificate to the ExtraStore, this certificate is disposed even though it isn't owned by the RemoteCertificateValidationCallback. This results in subsequent requests to the validation callback failing due to trying to use a disposed certificate. The confusion is compounded by the fact that the exception isn't an ObjectDisposedException, but instead is a NullReferenceException because X509Certificate.Pal is null after dispose but assumed to be non-null in X509Chain.Build.

Reproduction Steps

using System.Security.Cryptography.X509Certificates;

using var httpClient = new HttpClient(
    new SocketsHttpHandler
    {
        SslOptions =
        {
            RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
            {
                chain!.ChainPolicy.ExtraStore.Add(Certificates.CapellaCaCert);
                chain.Reset();
                var built = chain.Build((X509Certificate2)cert!);

                return built && errors == System.Net.Security.SslPolicyErrors.None;
            }
        },
        PooledConnectionLifetime = TimeSpan.Zero,
    });

await httpClient.GetAsync("https://microsoft.com/");

static class Certificates
{
    public static ReadOnlySpan<byte> CapellaCaCertPem =>
        @"-----BEGIN CERTIFICATE-----
        MIIDFTCCAf2gAwIBAgIRANLVkgOvtaXiQJi0V6qeNtswDQYJKoZIhvcNAQELBQAw
        JDESMBAGA1UECgwJQ291Y2hiYXNlMQ4wDAYDVQQLDAVDbG91ZDAeFw0xOTEyMDYy
        MjEyNTlaFw0yOTEyMDYyMzEyNTlaMCQxEjAQBgNVBAoMCUNvdWNoYmFzZTEOMAwG
        A1UECwwFQ2xvdWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfvOIi
        enG4Dp+hJu9asdxEMRmH70hDyMXv5ZjBhbo39a42QwR59y/rC/sahLLQuNwqif85
        Fod1DkqgO6Ng3vecSAwyYVkj5NKdycQu5tzsZkghlpSDAyI0xlIPSQjoORA/pCOU
        WOpymA9dOjC1bo6rDyw0yWP2nFAI/KA4Z806XeqLREuB7292UnSsgFs4/5lqeil6
        rL3ooAw/i0uxr/TQSaxi1l8t4iMt4/gU+W52+8Yol0JbXBTFX6itg62ppb/Eugmn
        mQRMgL67ccZs7cJ9/A0wlXencX2ohZQOR3mtknfol3FH4+glQFn27Q4xBCzVkY9j
        KQ20T1LgmGSngBInAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
        FJQOBPvrkU2In1Sjoxt97Xy8+cKNMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B
        AQsFAAOCAQEARgM6XwcXPLSpFdSf0w8PtpNGehmdWijPM3wHb7WZiS47iNen3oq8
        m2mm6V3Z57wbboPpfI+VEzbhiDcFfVnK1CXMC0tkF3fnOG1BDDvwt4jU95vBiNjY
        xdzlTP/Z+qr0cnVbGBSZ+fbXstSiRaaAVcqQyv3BRvBadKBkCyPwo+7svQnScQ5P
        Js7HEHKVms5tZTgKIw1fbmgR2XHleah1AcANB+MAPBCcTgqurqr5G7W2aPSBLLGA
        fRIiVzm7VFLc7kWbp7ENH39HVG6TZzKnfl9zJYeiklo5vQQhGSMhzBsO70z4RRzi
        DPFAN/4qZAgD5q3AFNIq2WWADFQGSwVJhg==
        -----END CERTIFICATE-----"u8;

    public static X509Certificate2 CapellaCaCert { get; } =
        X509CertificateLoader.LoadCertificate(CapellaCaCertPem);
}

Expected behavior

The certificate added to ExtraStore is not disposed by the SslStream when added by the callback. Additionally, if the ExtraStore does receive a disposed certificate, an ObjectDisposedException should be thrown rather than a NullReferenceException.

Actual behavior

There is a NullReferenceException thrown, on both Windows and Linux, internally when building the certificate chain. The stack trace below is on Windows, it is different but similar on Linux.

System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Security.Cryptography.X509Certificates.StorePal.LinkFromCertificateCollection(X509Certificate2Collection certificates)
   at System.Security.Cryptography.X509Certificates.ChainPal.BuildChain(Boolean useMachineContext, ICertificatePal cert, X509Certificate2Collection extraStore, OidCollection applicationPolicy, OidCollection certificatePolicy, X509RevocationMode revocationMode, X509RevocationFlag revocationFlag, X509Certificate2Collection customTrustStore, X509ChainTrustMode trustMode, DateTime verificationTime, TimeSpan timeout, Boolean disableAia)
   at System.Security.Cryptography.X509Certificates.X509Chain.Build(X509Certificate2 certificate, Boolean throwOnException)
   at Couchbase.Core.IO.Authentication.X509.CertificateFactory.<>c__DisplayClass7_0.<GetValidatorWithPredefinedCertificates>b__0(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) in E:\repos\couchbase-net-client\src\Couchbase\Core\IO\Authentication\X509\CertificateFactory.cs:line 147
   at Couchbase.Core.IO.Connections.CallbackCreator.Callback(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) in E:\repos\couc...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes dotnet/runtime#123058

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/dotnet/runtime/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 2, 2026 09:03
Only cleanup ExtraStore and ChainElements certificates when no RemoteCertificateValidationCallback is provided. When a callback is provided, users might add their own certificates to ExtraStore or keep references to certificates from ChainElements, and these should not be disposed by SslStream.

Co-authored-by: rzikm <[email protected]>
Changed shared certificate to use 'using' declaration to ensure proper cleanup even if test fails.

Co-authored-by: rzikm <[email protected]>
Copilot AI changed the title [WIP] Fix X509Chain.Build failure with NRE in SslStream callback Fix certificate disposal in RemoteCertificateValidationCallback causing NRE on reuse Feb 2, 2026
Copilot AI requested a review from rzikm February 2, 2026 09:10
Copy link
Member

@rzikm rzikm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, with one comment

@rzikm rzikm marked this pull request as ready for review February 2, 2026 09:36
Copilot AI review requested due to automatic review settings February 2, 2026 09:36
@rzikm rzikm requested a review from a team February 2, 2026 09:37
@dotnet-policy-service
Copy link
Contributor

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an SslStream certificate lifetime bug where certificates provided/added during RemoteCertificateValidationCallback could be disposed by SslStream, causing subsequent reuse to fail (including NullReferenceException in X509Chain.Build).

Changes:

  • Adjust SslStream cleanup behavior for certificates in X509Chain.ChainPolicy.ExtraStore and X509Chain.ChainElements based on whether a remote certificate validation callback is present.
  • Add a regression test to validate that a shared X509Certificate2 reused across multiple handshakes via callback does not get disposed.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs Modifies certificate cleanup logic in VerifyRemoteCertificate to avoid disposing certificates when a validation callback is provided.
src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationClientServer.cs Adds a regression test covering reuse of a certificate added to ExtraStore inside RemoteCertificateValidationCallback.

@rzikm
Copy link
Member

rzikm commented Feb 2, 2026

@copilot Please address code review comments

…nused variable

- Refactored test to use a for loop instead of duplicating connection code
- Fixed unused 'built' variable by discarding return value
- Removed issue reference from test comment as requested

Co-authored-by: rzikm <[email protected]>
@rzikm rzikm requested a review from a team February 2, 2026 14:22
Copy link
Member

@bartonjs bartonjs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Product changes look good.

Added verification that certificates from chain.ChainElements are not disposed when RemoteCertificateValidationCallback is provided, as suggested by @bartonjs. The test now:
- Saves certificates from ChainElements on first connection
- Verifies they remain valid after connection closes
- Verifies they remain valid after multiple connections

Co-authored-by: rzikm <[email protected]>
@rzikm
Copy link
Member

rzikm commented Feb 3, 2026

/ba-g test failures are unrelated

@rzikm rzikm merged commit 4fa6991 into main Feb 3, 2026
81 of 88 checks passed
lewing pushed a commit to lewing/runtime that referenced this pull request Feb 9, 2026
…ng NRE on reuse (dotnet#123875)

## Plan to Fix X509Chain.Build NRE Issue

This issue occurs because certificates in ExtraStore are being disposed
even when added by the RemoteCertificateValidationCallback, causing
NullReferenceException on subsequent uses.

### Root Cause
- In `SslStream.Protocol.cs` `VerifyRemoteCertificate` method (lines
1167-1171), certificates added to ExtraStore after the initial count are
always disposed
- This cleanup doesn't account for certificates that users add during
the RemoteCertificateValidationCallback
- Users' certificates or ChainElements certificates may be disposed even
though they're not owned by SslStream

### Solution
Only cleanup ExtraStore certificates when no
RemoteCertificateValidationCallback was provided, as suggested by
@bartonjs in the issue comments.

### Checklist
- [x] Understand the issue and analyze the problematic code
- [x] Explore the repository structure and understand build/test
procedures
- [x] Identify the exact code location that needs to be changed
- [x] Make minimal code change to fix the disposal issue in
SslStream.Protocol.cs
- [x] Create a focused test case that reproduces the issue and validates
the fix
- [x] Run baseline build to establish known-good state
- [x] Build and test the specific library changes
- [x] Verify the fix works correctly with new test
- [x] Run broader test suite to ensure no regressions (769 SslStream
tests passed)
- [x] Address code review feedback on resource disposal
- [x] Run final code review (no issues found)
- [x] Run CodeQL security check (no issues found)
- [x] Address PR review comments: refactor test to use for loop, fix
unused variable
- [x] Enhance test to verify ChainElements certificates are not disposed
- [x] Complete final validation

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>


----

*This section details on the original issue you should resolve*

<issue_title>X509Chain.Build fails with NRE when ExtraStore certificates
are reused within SslStream
RemoteCertificateValidationCallback</issue_title>
<issue_description>### Description

Within the `SslStream` `RemoteCertificateValidationCallback`, you can
mutate the `X509Chain` and revalidate it. Starting in .NET 10, if you do
so and add a global, shared certificate to the `ExtraStore`, this
certificate is disposed even though it isn't owned by the
`RemoteCertificateValidationCallback`. This results in subsequent
requests to the validation callback failing due to trying to use a
disposed certificate. The confusion is compounded by the fact that the
exception isn't an `ObjectDisposedException`, but instead is a
`NullReferenceException` because `X509Certificate.Pal` is null after
dispose but assumed to be non-null in `X509Chain.Build`.

### Reproduction Steps

```c#
using System.Security.Cryptography.X509Certificates;

using var httpClient = new HttpClient(
    new SocketsHttpHandler
    {
        SslOptions =
        {
            RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
            {
                chain!.ChainPolicy.ExtraStore.Add(Certificates.CapellaCaCert);
                chain.Reset();
                var built = chain.Build((X509Certificate2)cert!);

                return built && errors == System.Net.Security.SslPolicyErrors.None;
            }
        },
        PooledConnectionLifetime = TimeSpan.Zero,
    });

await httpClient.GetAsync("https://microsoft.com/");

static class Certificates
{
    public static ReadOnlySpan<byte> CapellaCaCertPem =>
        @"-----BEGIN CERTIFICATE-----
        MIIDFTCCAf2gAwIBAgIRANLVkgOvtaXiQJi0V6qeNtswDQYJKoZIhvcNAQELBQAw
        JDESMBAGA1UECgwJQ291Y2hiYXNlMQ4wDAYDVQQLDAVDbG91ZDAeFw0xOTEyMDYy
        MjEyNTlaFw0yOTEyMDYyMzEyNTlaMCQxEjAQBgNVBAoMCUNvdWNoYmFzZTEOMAwG
        A1UECwwFQ2xvdWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfvOIi
        enG4Dp+hJu9asdxEMRmH70hDyMXv5ZjBhbo39a42QwR59y/rC/sahLLQuNwqif85
        Fod1DkqgO6Ng3vecSAwyYVkj5NKdycQu5tzsZkghlpSDAyI0xlIPSQjoORA/pCOU
        WOpymA9dOjC1bo6rDyw0yWP2nFAI/KA4Z806XeqLREuB7292UnSsgFs4/5lqeil6
        rL3ooAw/i0uxr/TQSaxi1l8t4iMt4/gU+W52+8Yol0JbXBTFX6itg62ppb/Eugmn
        mQRMgL67ccZs7cJ9/A0wlXencX2ohZQOR3mtknfol3FH4+glQFn27Q4xBCzVkY9j
        KQ20T1LgmGSngBInAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
        FJQOBPvrkU2In1Sjoxt97Xy8+cKNMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B
        AQsFAAOCAQEARgM6XwcXPLSpFdSf0w8PtpNGehmdWijPM3wHb7WZiS47iNen3oq8
        m2mm6V3Z57wbboPpfI+VEzbhiDcFfVnK1CXMC0tkF3fnOG1BDDvwt4jU95vBiNjY
        xdzlTP/Z+qr0cnVbGBSZ+fbXstSiRaaAVcqQyv3BRvBadKBkCyPwo+7svQnScQ5P
        Js7HEHKVms5tZTgKIw1fbmgR2XHleah1AcANB+MAPBCcTgqurqr5G7W2aPSBLLGA
        fRIiVzm7VFLc7kWbp7ENH39HVG6TZzKnfl9zJYeiklo5vQQhGSMhzBsO70z4RRzi
        DPFAN/4qZAgD5q3AFNIq2WWADFQGSwVJhg==
        -----END CERTIFICATE-----"u8;

    public static X509Certificate2 CapellaCaCert { get; } =
        X509CertificateLoader.LoadCertificate(CapellaCaCertPem);
}
```

### Expected behavior

The certificate added to `ExtraStore` is not disposed by the `SslStream`
when added by the callback. Additionally, if the `ExtraStore` does
receive a disposed certificate, an `ObjectDisposedException` should be
thrown rather than a `NullReferenceException`.

### Actual behavior

There is a NullReferenceException thrown, on both Windows and Linux,
internally when building the certificate chain. The stack trace below is
on Windows, it is different but similar on Linux.

```
System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Security.Cryptography.X509Certificates.StorePal.LinkFromCertificateCollection(X509Certificate2Collection certificates)
   at System.Security.Cryptography.X509Certificates.ChainPal.BuildChain(Boolean useMachineContext, ICertificatePal cert, X509Certificate2Collection extraStore, OidCollection applicationPolicy, OidCollection certificatePolicy, X509RevocationMode revocationMode, X509RevocationFlag revocationFlag, X509Certificate2Collection customTrustStore, X509ChainTrustMode trustMode, DateTime verificationTime, TimeSpan timeout, Boolean disableAia)
   at System.Security.Cryptography.X509Certificates.X509Chain.Build(X509Certificate2 certificate, Boolean throwOnException)
   at Couchbase.Core.IO.Authentication.X509.CertificateFactory.<>c__DisplayClass7_0.<GetValidatorWithPredefinedCertificates>b__0(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) in E:\repos\couchbase-net-client\src\Couchbase\Core\IO\Authentication\X509\CertificateFactory.cs:line 147
   at Couchbase.Core.IO.Connections.CallbackCreator.Callback(Object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) in E:\repos\couc...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes dotnet#123058

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/dotnet/runtime/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: rzikm <[email protected]>
@rzikm
Copy link
Member

rzikm commented Feb 19, 2026

/backport to release/10.0

@github-actions
Copy link
Contributor

Started backporting to release/10.0 (link to workflow run)

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants