Fix certificate disposal in RemoteCertificateValidationCallback causing NRE on reuse#123875
Merged
Fix certificate disposal in RemoteCertificateValidationCallback causing NRE on reuse#123875
Conversation
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
rzikm
approved these changes
Feb 2, 2026
src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationClientServer.cs
Outdated
Show resolved
Hide resolved
Contributor
|
Tagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones |
Contributor
There was a problem hiding this comment.
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
SslStreamcleanup behavior for certificates inX509Chain.ChainPolicy.ExtraStoreandX509Chain.ChainElementsbased on whether a remote certificate validation callback is present. - Add a regression test to validate that a shared
X509Certificate2reused 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. |
src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs
Show resolved
Hide resolved
src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationClientServer.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationClientServer.cs
Show resolved
Hide resolved
Member
|
@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]>
This was referenced Feb 2, 2026
rzikm
approved these changes
Feb 2, 2026
ManickaP
approved these changes
Feb 2, 2026
bartonjs
reviewed
Feb 2, 2026
src/libraries/System.Net.Security/tests/FunctionalTests/CertificateValidationClientServer.cs
Show resolved
Hide resolved
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]>
3 tasks
Member
|
/ba-g test failures are unrelated |
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]>
Member
|
/backport to release/10.0 |
Contributor
|
Started backporting to |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
SslStream.Protocol.csVerifyRemoteCertificatemethod (lines 1167-1171), certificates added to ExtraStore after the initial count are always disposedSolution
Only cleanup ExtraStore certificates when no RemoteCertificateValidationCallback was provided, as suggested by @bartonjs in the issue comments.
Checklist
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
SslStreamRemoteCertificateValidationCallback, you can mutate theX509Chainand revalidate it. Starting in .NET 10, if you do so and add a global, shared certificate to theExtraStore, this certificate is disposed even though it isn't owned by theRemoteCertificateValidationCallback. 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 anObjectDisposedException, but instead is aNullReferenceExceptionbecauseX509Certificate.Palis null after dispose but assumed to be non-null inX509Chain.Build.Reproduction Steps
Expected behavior
The certificate added to
ExtraStoreis not disposed by theSslStreamwhen added by the callback. Additionally, if theExtraStoredoes receive a disposed certificate, anObjectDisposedExceptionshould be thrown rather than aNullReferenceException.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.