File: System\Net\Security\SslStream.IO.cs
Web Access
Project: src\src\libraries\System.Net.Security\src\System.Net.Security.csproj (System.Net.Security)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Security
{
    public partial class SslStream
    {
        private readonly SslAuthenticationOptions _sslAuthenticationOptions = new SslAuthenticationOptions();
        private NestedState _nestedAuth;
        private bool _isRenego;
 
        private TlsFrameHelper.TlsFrameInfo _lastFrame;
 
        private object _handshakeLock => _sslAuthenticationOptions;
        private volatile TaskCompletionSource<bool>? _handshakeWaiter;
 
        private const int HandshakeTypeOffsetSsl2 = 2;                       // Offset of HelloType in Sslv2 and Unified frames
        private const int HandshakeTypeOffsetTls = 5;                        // Offset of HelloType in Sslv3 and TLS frames
 
        private const int UnknownTlsFrameLength = int.MaxValue;              // frame too short to determine length
 
        private bool _receivedEOF;
 
        // Used by Telemetry to ensure we log connection close exactly once
        private enum ConnectionStatus
        {
            NoHandshake = 0,
            HandshakeCompleted = 1, // connection opened
            Disposed = 2, // connection closed
        }
 
        private ConnectionStatus _connectionOpenedStatus;
 
        private void SetException(Exception e)
        {
            Debug.Assert(e != null, $"Expected non-null Exception to be passed to {nameof(SetException)}");
 
            _exception ??= ExceptionDispatchInfo.Capture(e);
 
            CloseContext();
        }
 
        //
        // This is to not depend on GC&SafeHandle class if the context is not needed anymore.
        //
        private void CloseInternal()
        {
            _exception = s_disposedSentinel;
            CloseContext();
 
            // Ensure a Read or Auth operation is not in progress,
            // block potential future read and auth operations since SslStream is disposing.
            // This leaves the _nestedRead = StreamDisposed and _nestedAuth = StreamDisposed, but that's ok, since
            // subsequent operations check the _exception sentinel first
            if (Interlocked.Exchange(ref _nestedRead, NestedState.StreamDisposed) == NestedState.StreamNotInUse &&
                Interlocked.Exchange(ref _nestedAuth, NestedState.StreamDisposed) == NestedState.StreamNotInUse)
            {
                _buffer.ReturnBuffer();
            }
 
            if (!_buffer.IsValid)
            {
                // Suppress finalizer since the read buffer was returned.
                GC.SuppressFinalize(this);
            }
 
            if (NetSecurityTelemetry.Log.IsEnabled())
            {
                // Set the status to disposed. If it was opened before, log ConnectionClosed
                if (Interlocked.Exchange(ref _connectionOpenedStatus, ConnectionStatus.Disposed) == ConnectionStatus.HandshakeCompleted)
                {
                    NetSecurityTelemetry.Log.ConnectionClosed(GetSslProtocolInternal());
                }
            }
        }
 
        private ProtocolToken EncryptData(ReadOnlyMemory<byte> buffer)
        {
            ThrowIfExceptionalOrNotAuthenticated();
 
            lock (_handshakeLock)
            {
                if (_handshakeWaiter != null)
                {
                    ProtocolToken token = default;
                    // avoid waiting under lock.
                    token.Status = new SecurityStatusPal(SecurityStatusPalErrorCode.TryAgain);
                    return token;
                }
 
                return Encrypt(buffer);
            }
        }
 
        //
        // This method assumes that a SSPI context is already in a good shape.
        // For example it is either a fresh context or already authenticated context that needs renegotiation.
        //
        private Task ProcessAuthenticationAsync(bool isAsync = false, CancellationToken cancellationToken = default)
        {
            ThrowIfExceptional();
 
            if (NetSecurityTelemetry.AnyTelemetryEnabled())
            {
                return ProcessAuthenticationWithTelemetryAsync(isAsync, cancellationToken);
            }
            else
            {
                return isAsync ?
                    ForceAuthenticationAsync<AsyncReadWriteAdapter>(IsServer, null, cancellationToken) :
                    ForceAuthenticationAsync<SyncReadWriteAdapter>(IsServer, null, cancellationToken);
            }
        }
 
        private async Task ProcessAuthenticationWithTelemetryAsync(bool isAsync, CancellationToken cancellationToken)
        {
            long startingTimestamp;
            if (NetSecurityTelemetry.Log.IsEnabled())
            {
                NetSecurityTelemetry.Log.HandshakeStart(IsServer, _sslAuthenticationOptions.TargetHost);
                startingTimestamp = Stopwatch.GetTimestamp();
            }
            else
            {
                startingTimestamp = 0;
            }
 
            Activity? activity = NetSecurityTelemetry.StartActivity(this);
            Exception? exception = null;
            try
            {
                Task task = isAsync ?
                    ForceAuthenticationAsync<AsyncReadWriteAdapter>(IsServer, null, cancellationToken) :
                    ForceAuthenticationAsync<SyncReadWriteAdapter>(IsServer, null, cancellationToken);
 
                await task.ConfigureAwait(false);
 
                if (startingTimestamp is not 0)
                {
                    // SslStream could already have been disposed at this point, in which case _connectionOpenedStatus == ConnectionStatus.Disposed
                    // Make sure that we increment the open connection counter only if it is guaranteed to be decremented in dispose/finalize
                    bool connectionOpen = Interlocked.CompareExchange(ref _connectionOpenedStatus, ConnectionStatus.HandshakeCompleted, ConnectionStatus.NoHandshake) == ConnectionStatus.NoHandshake;
                    SslProtocols protocol = GetSslProtocolInternal();
                    NetSecurityTelemetry.Log.HandshakeCompleted(protocol, startingTimestamp, connectionOpen);
                }
            }
            catch (Exception ex)
            {
                exception = ex;
                if (startingTimestamp is not 0)
                {
                    NetSecurityTelemetry.Log.HandshakeFailed(IsServer, startingTimestamp, ex.Message);
                }
 
                throw;
            }
            finally
            {
                NetSecurityTelemetry.StopActivity(activity, exception, this);
            }
        }
 
        //
        // This is used to reply on re-handshake when received SEC_I_RENEGOTIATE on Read().
        //
        private async Task ReplyOnReAuthenticationAsync<TIOAdapter>(byte[]? buffer, CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
            try
            {
                await ForceAuthenticationAsync<TIOAdapter>(receiveFirst: false, buffer, cancellationToken).ConfigureAwait(false);
            }
            finally
            {
                _handshakeWaiter!.SetResult(true);
                _handshakeWaiter = null;
            }
        }
 
        // This will initiate renegotiation or PHA for Tls1.3
        private async Task RenegotiateAsync<TIOAdapter>(CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
            if (Interlocked.CompareExchange(ref _nestedAuth, NestedState.StreamInUse, NestedState.StreamNotInUse) != NestedState.StreamNotInUse)
            {
                ObjectDisposedException.ThrowIf(_nestedAuth == NestedState.StreamDisposed, this);
                throw new InvalidOperationException(SR.Format(SR.net_io_invalidnestedcall, "authenticate"));
            }
 
            if (Interlocked.CompareExchange(ref _nestedRead, NestedState.StreamInUse, NestedState.StreamNotInUse) != NestedState.StreamNotInUse)
            {
                ObjectDisposedException.ThrowIf(_nestedRead == NestedState.StreamDisposed, this);
                throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, "read"));
            }
 
            // Write is different since we do not do anything special in Dispose
            if (Interlocked.Exchange(ref _nestedWrite, NestedState.StreamInUse) != NestedState.StreamNotInUse)
            {
                _nestedRead = NestedState.StreamNotInUse;
                throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, "write"));
            }
 
            ProtocolToken token = default;
            token.RentBuffer = true;
            try
            {
                if (_buffer.ActiveLength > 0)
                {
                    throw new InvalidOperationException(SR.net_ssl_renegotiate_buffer);
                }
 
                _sslAuthenticationOptions.RemoteCertRequired = true;
                _isRenego = true;
 
 
                token = Renegotiate();
 
                if (token.Size > 0)
                {
                    await TIOAdapter.WriteAsync(InnerStream, token.AsMemory(), cancellationToken).ConfigureAwait(false);
                    await TIOAdapter.FlushAsync(InnerStream, cancellationToken).ConfigureAwait(false);
                }
 
                token.ReleasePayload();
 
                if (token.Status.ErrorCode != SecurityStatusPalErrorCode.OK)
                {
                    if (token.Status.ErrorCode == SecurityStatusPalErrorCode.NoRenegotiation)
                    {
                        // Peer does not want to renegotiate. That should keep session usable.
                        return;
                    }
 
                    throw SslStreamPal.GetException(token.Status);
                }
 
                do
                {
                    int frameSize = await ReceiveHandshakeFrameAsync<TIOAdapter>(cancellationToken).ConfigureAwait(false);
                    token = ProcessTlsFrame(frameSize);
 
                    if (token.Size > 0)
                    {
                        await TIOAdapter.WriteAsync(InnerStream, token.AsMemory(), cancellationToken).ConfigureAwait(false);
                        await TIOAdapter.FlushAsync(InnerStream, cancellationToken).ConfigureAwait(false);
                    }
                    token.ReleasePayload();
                }
                while (token.Status.ErrorCode == SecurityStatusPalErrorCode.ContinueNeeded);
 
                CompleteHandshake(_sslAuthenticationOptions);
            }
            finally
            {
                if (_buffer.ActiveLength == 0)
                {
                    _buffer.ReturnBuffer();
                }
 
                token.ReleasePayload();
 
                _nestedRead = NestedState.StreamNotInUse;
                _nestedWrite = NestedState.StreamNotInUse;
                _isRenego = false;
                // We will not release _nestedAuth at this point to prevent another renegotiation attempt.
            }
        }
 
        // reAuthenticationData is only used on Windows in case of renegotiation.
        private async Task ForceAuthenticationAsync<TIOAdapter>(bool receiveFirst, byte[]? reAuthenticationData, CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
            bool handshakeCompleted = false;
            ProtocolToken token = default;
 
            token.RentBuffer = true;
 
            if (reAuthenticationData == null)
            {
                // prevent nesting only when authentication functions are called explicitly. e.g. handle renegotiation transparently.
                if (Interlocked.Exchange(ref _nestedAuth, NestedState.StreamInUse) == NestedState.StreamInUse)
                {
                    throw new InvalidOperationException(SR.Format(SR.net_io_invalidnestedcall, "authenticate"));
                }
            }
            try
            {
                if (!receiveFirst)
                {
                    token = NextMessage(reAuthenticationData, out int consumed);
                    Debug.Assert(consumed == (reAuthenticationData?.Length ?? 0));
 
                    if (token.Size > 0)
                    {
                        Debug.Assert(token.Payload != null);
                        await TIOAdapter.WriteAsync(InnerStream, new ReadOnlyMemory<byte>(token.Payload!, 0, token.Size), cancellationToken).ConfigureAwait(false);
                        await TIOAdapter.FlushAsync(InnerStream, cancellationToken).ConfigureAwait(false);
                        if (NetEventSource.Log.IsEnabled())
                            NetEventSource.Log.SentFrame(this, token.Payload);
                    }
 
                    token.ReleasePayload();
 
                    if (token.Failed)
                    {
                        // tracing done in NextMessage()
                        throw new AuthenticationException(SR.net_auth_SSPI, token.GetException());
                    }
                    else if (token.Status.ErrorCode == SecurityStatusPalErrorCode.OK)
                    {
                        // We can finish renegotiation without doing any read.
                        handshakeCompleted = true;
                    }
                }
 
                if (!handshakeCompleted)
                {
                    _buffer.EnsureAvailableSpace(InitialHandshakeBufferSize);
                }
 
                while (!handshakeCompleted)
                {
                    int frameSize = await ReceiveHandshakeFrameAsync<TIOAdapter>(cancellationToken).ConfigureAwait(false);
                    token = ProcessTlsFrame(frameSize);
 
                    ReadOnlyMemory<byte> payload = default;
                    if (token.Size > 0)
                    {
                        payload = token.AsMemory();
                    }
                    else if (token.Failed && (_lastFrame.Header.Type == TlsContentType.Handshake || _lastFrame.Header.Type == TlsContentType.ChangeCipherSpec))
                    {
                        // If we failed without OS sending out alert, inject one here to be consistent across platforms.
                        payload = TlsFrameHelper.CreateAlertFrame(_lastFrame.Header.Version, TlsAlertDescription.ProtocolVersion);
                    }
 
                    if (!payload.IsEmpty)
                    {
                        // If there is message send it out even if call failed. It may contain TLS Alert.
                        await TIOAdapter.WriteAsync(InnerStream, payload, cancellationToken).ConfigureAwait(false);
                        await TIOAdapter.FlushAsync(InnerStream, cancellationToken).ConfigureAwait(false);
 
                        if (NetEventSource.Log.IsEnabled())
                            NetEventSource.Log.SentFrame(this, payload.Span);
                    }
 
                    token.ReleasePayload();
 
                    if (token.Failed)
                    {
                        if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, token.Status);
 
                        if (_lastFrame.Header.Type == TlsContentType.Alert && _lastFrame.AlertDescription != TlsAlertDescription.CloseNotify &&
                                 token.Status.ErrorCode == SecurityStatusPalErrorCode.IllegalMessage)
                        {
                            // Improve generic message and show details if we failed because of TLS Alert.
                            throw new AuthenticationException(SR.Format(SR.net_auth_tls_alert, _lastFrame.AlertDescription.ToString()), token.GetException());
                        }
 
                        throw new AuthenticationException(SR.net_auth_SSPI, token.GetException());
                    }
                    else if (token.Status.ErrorCode == SecurityStatusPalErrorCode.OK)
                    {
                        // We can finish renegotiation without doing any read.
                        handshakeCompleted = true;
                    }
                }
 
                CompleteHandshake(_sslAuthenticationOptions);
            }
            finally
            {
                if (reAuthenticationData == null)
                {
                    _nestedAuth = NestedState.StreamNotInUse;
                    _isRenego = false;
                }
 
                token.ReleasePayload();
 
                // reset the cached flag which has potentially outdated value.
                _localClientCertificateUsed = -1;
            }
 
#pragma warning disable SYSLIB0058 // Use NegotiatedCipherSuite.
            if (NetEventSource.Log.IsEnabled())
                NetEventSource.Log.SspiSelectedCipherSuite(nameof(ForceAuthenticationAsync),
                                                                    SslProtocol,
                                                                    CipherAlgorithm,
                                                                    CipherStrength,
                                                                    HashAlgorithm,
                                                                    HashStrength,
                                                                    KeyExchangeAlgorithm,
                                                                    KeyExchangeStrength);
#pragma warning restore SYSLIB0058 // Use NegotiatedCipherSuite.
        }
 
        // This method will make sure we have at least one full TLS frame buffered.
        private async ValueTask<int> ReceiveHandshakeFrameAsync<TIOAdapter>(CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
            int frameSize = await EnsureFullTlsFrameAsync<TIOAdapter>(cancellationToken, InitialHandshakeBufferSize).ConfigureAwait(false);
 
            if (frameSize == 0)
            {
                // We expect to receive at least one frame
                throw new IOException(SR.net_io_eof);
            }
 
            // At this point, we have at least one TLS frame.
            switch (_lastFrame.Header.Type)
            {
                case TlsContentType.Alert:
                    if (TlsFrameHelper.TryGetFrameInfo(_buffer.EncryptedReadOnlySpan, ref _lastFrame))
                    {
                        if (NetEventSource.Log.IsEnabled() && _lastFrame.AlertDescription != TlsAlertDescription.CloseNotify) NetEventSource.Error(this, $"Received TLS alert {_lastFrame.AlertDescription}");
                    }
                    break;
                case TlsContentType.Handshake:
#pragma warning disable CS0618
                    if (!_isRenego && _buffer.EncryptedReadOnlySpan[_lastFrame.Header.Version == SslProtocols.Ssl2 ? HandshakeTypeOffsetSsl2 : HandshakeTypeOffsetTls] == (byte)TlsHandshakeType.ClientHello &&
                        _sslAuthenticationOptions!.IsServer) // guard against malicious endpoints. We should not see ClientHello on client.
#pragma warning restore CS0618
                    {
                        TlsFrameHelper.ProcessingOptions options = NetEventSource.Log.IsEnabled() ?
                                                                    TlsFrameHelper.ProcessingOptions.All :
                                                                    TlsFrameHelper.ProcessingOptions.ServerName;
                        if (OperatingSystem.IsMacOS() && _sslAuthenticationOptions.IsServer)
                        {
                            // macOS cannot process ALPN on server at the moment.
                            // We fallback to our own process similar to SNI bellow.
                            options |= TlsFrameHelper.ProcessingOptions.RawApplicationProtocol;
                        }
 
                        // Process SNI from Client Hello message
                        if (!TlsFrameHelper.TryGetFrameInfo(_buffer.EncryptedReadOnlySpan, ref _lastFrame, options))
                        {
                            if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Failed to parse TLS hello.");
                        }
 
                        if (_lastFrame.HandshakeType == TlsHandshakeType.ClientHello)
                        {
                            // SNI if it exist. Even if we could not parse the hello, we can fall-back to default certificate.
                            if (_lastFrame.TargetName != null)
                            {
                                _sslAuthenticationOptions.TargetHost = _lastFrame.TargetName;
                            }
 
                            if (_sslAuthenticationOptions.ServerOptionDelegate != null)
                            {
                                SslServerAuthenticationOptions userOptions =
                                    await _sslAuthenticationOptions.ServerOptionDelegate(this, new SslClientHelloInfo(_sslAuthenticationOptions.TargetHost, _lastFrame.SupportedVersions),
                                        _sslAuthenticationOptions.UserState, cancellationToken).ConfigureAwait(false);
                                _sslAuthenticationOptions.UpdateOptions(userOptions);
                            }
                        }
 
                        if (NetEventSource.Log.IsEnabled())
                        {
                            NetEventSource.Log.ReceivedFrame(this, _lastFrame);
                        }
                    }
                    break;
                case TlsContentType.AppData:
                    // TLS1.3 it is not possible to distinguish between late Handshake and Application Data
                    if (_isRenego && SslProtocol != SslProtocols.Tls13)
                    {
                        throw new InvalidOperationException(SR.net_ssl_renegotiate_data);
                    }
                    break;
 
            }
 
            return frameSize;
        }
 
        // Calls crypto on received data. No IO inside.
        private ProtocolToken ProcessTlsFrame(int frameSize)
        {
            int chunkSize = frameSize;
 
            ReadOnlySpan<byte> availableData = _buffer.EncryptedReadOnlySpan;
 
            // Often more TLS messages fit into same packet. Get as many complete frames as we can.
            while (_buffer.EncryptedLength - chunkSize > TlsFrameHelper.HeaderSize)
            {
                TlsFrameHeader nextHeader = default;
 
                if (!TlsFrameHelper.TryGetFrameHeader(availableData.Slice(chunkSize), ref nextHeader))
                {
                    break;
                }
 
                frameSize = nextHeader.Length;
 
                // Can process more handshake frames in single step or during TLS1.3 post-handshake auth, but we should
                // avoid processing too much so as to preserve API boundary between handshake and I/O.
                if ((nextHeader.Type != TlsContentType.Handshake && nextHeader.Type != TlsContentType.ChangeCipherSpec) && !_isRenego || frameSize > availableData.Length - chunkSize)
                {
                    // We don't have full frame left or we already have app data which needs to be processed by decrypt.
                    break;
                }
 
                chunkSize += frameSize;
            }
 
            ProtocolToken token = NextMessage(availableData.Slice(0, chunkSize), out int consumed);
            _buffer.DiscardEncrypted(consumed);
            return token;
        }
 
        //
        //  This is to reset auth state on remote side.
        //  If this write succeeds we will allow auth retrying.
        //
        private void SendAuthResetSignal(ReadOnlySpan<byte> alert, ExceptionDispatchInfo exception)
        {
            SetException(exception.SourceException);
 
            if (alert.Length == 0)
            {
                //
                // We don't have an alert to send so cannot retry and fail prematurely.
                //
                exception.Throw();
            }
 
            InnerStream.Write(alert);
 
            exception.Throw();
        }
 
        // - Loads the channel parameters
        // - Optionally verifies the Remote Certificate
        // - Sets HandshakeCompleted flag
        // - Sets the guarding event if other thread is waiting for
        //   handshake completion
        //
        // - Returns false if failed to verify the Remote Cert
        //
        private bool CompleteHandshake(ref ProtocolToken alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus)
        {
            ProcessHandshakeSuccess();
 
            if (_nestedAuth != NestedState.StreamInUse)
            {
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Ignoring unsolicited renegotiated certificate.");
                // ignore certificates received outside of handshake or requested renegotiation.
                sslPolicyErrors = SslPolicyErrors.None;
                chainStatus = X509ChainStatusFlags.NoError;
                return true;
            }
 
#if TARGET_ANDROID
            // On Android, the remote certificate verification can be invoked from Java TrustManager's callback
            // during the handshake process. If that has occurred, we shouldn't run the validation again and
            // return the existing validation result.
            //
            // The Java TrustManager callback is called only when the peer has a certificate. It's possible that
            // the peer didn't provide any certificate (for example when the peer is the client) and the validation
            // result hasn't been set. In that case we still need to run the verification at this point.
            if (TryGetRemoteCertificateValidationResult(out sslPolicyErrors, out chainStatus, ref alertToken, out bool isValid))
            {
                _handshakeCompleted = isValid;
                return isValid;
            }
#endif
 
            if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertValidationDelegate, _sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, out sslPolicyErrors, out chainStatus))
            {
                _handshakeCompleted = false;
                return false;
            }
 
            _handshakeCompleted = true;
            return true;
        }
 
        private void CompleteHandshake(SslAuthenticationOptions sslAuthenticationOptions)
        {
            ProtocolToken alertToken = default;
            if (!CompleteHandshake(ref alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus))
            {
                if (sslAuthenticationOptions!.CertValidationDelegate != null)
                {
                    // there may be some chain errors but the decision was made by custom callback. Details should be tracing if enabled.
                    SendAuthResetSignal(new ReadOnlySpan<byte>(alertToken.Payload), ExceptionDispatchInfo.Capture(new AuthenticationException(SR.net_ssl_io_cert_custom_validation, null)));
                }
                else if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors && chainStatus != X509ChainStatusFlags.NoError)
                {
                    // We failed only because of chain and we have some insight.
                    SendAuthResetSignal(new ReadOnlySpan<byte>(alertToken.Payload), ExceptionDispatchInfo.Capture(new AuthenticationException(SR.Format(SR.net_ssl_io_cert_chain_validation, chainStatus), null)));
                }
                else
                {
                    // Simple add sslPolicyErrors as crude info.
                    SendAuthResetSignal(new ReadOnlySpan<byte>(alertToken.Payload), ExceptionDispatchInfo.Capture(new AuthenticationException(SR.Format(SR.net_ssl_io_cert_validation, sslPolicyErrors), null)));
                }
            }
        }
 
        private async ValueTask WriteAsyncChunked<TIOAdapter>(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
            do
            {
                int chunkBytes = Math.Min(buffer.Length, MaxDataSize);
                await WriteSingleChunk<TIOAdapter>(buffer.Slice(0, chunkBytes), cancellationToken).ConfigureAwait(false);
                buffer = buffer.Slice(chunkBytes);
            } while (buffer.Length != 0);
        }
 
        private ValueTask WriteSingleChunk<TIOAdapter>(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
            ProtocolToken token;
            while (true)
            {
                token = EncryptData(buffer);
                // TryAgain should be rare, when renegotiation happens exactly when we want to write.
                if (token.Status.ErrorCode != SecurityStatusPalErrorCode.TryAgain)
                {
                    break;
                }
 
                // We failed to encrypt because renegotiation is pending.
                TaskCompletionSource<bool>? waiter = _handshakeWaiter;
                if (waiter != null)
                {
                    Task waiterTask = TIOAdapter.WaitAsync(waiter);
                    // We finished synchronously waiting for renegotiation. We can try again immediately.
                    if (waiterTask.IsCompletedSuccessfully)
                    {
                        continue;
                    }
 
                    // We need to wait asynchronously as well as for the write when EncryptData is finished.
                    return WaitAndWriteAsync(buffer, waiterTask, cancellationToken);
                }
            }
 
            if (token.Status.ErrorCode != SecurityStatusPalErrorCode.OK)
            {
                token.ReleasePayload();
                return ValueTask.FromException(ExceptionDispatchInfo.SetCurrentStackTrace(new IOException(SR.net_io_encrypt, SslStreamPal.GetException(token.Status))));
            }
 
            ValueTask t = TIOAdapter.WriteAsync(InnerStream, token.AsMemory(), cancellationToken);
            if (t.IsCompletedSuccessfully)
            {
                token.ReleasePayload();
                return t;
            }
            else
            {
                return CompleteWriteAsync(t, token);
            }
 
            async ValueTask WaitAndWriteAsync(ReadOnlyMemory<byte> buffer, Task waitTask, CancellationToken cancellationToken)
            {
                ProtocolToken token = default;
                try
                {
                    // Wait for renegotiation to finish.
                    await waitTask.ConfigureAwait(false);
 
                    token = EncryptData(buffer);
                    if (token.Status.ErrorCode == SecurityStatusPalErrorCode.TryAgain)
                    {
                        // Call WriteSingleChunk() recursively to avoid code duplication.
                        // This should be extremely rare in cases when second renegotiation happens concurrently with Write.
                        await WriteSingleChunk<TIOAdapter>(buffer, cancellationToken).ConfigureAwait(false);
                    }
                    else if (token.Status.ErrorCode == SecurityStatusPalErrorCode.OK)
                    {
                        await TIOAdapter.WriteAsync(InnerStream, token.AsMemory(), cancellationToken).ConfigureAwait(false);
                    }
                    else
                    {
                        throw new IOException(SR.net_io_encrypt, SslStreamPal.GetException(token.Status));
                    }
                }
                finally
                {
                    token.ReleasePayload();
                }
            }
 
            static async ValueTask CompleteWriteAsync(ValueTask writeTask, ProtocolToken token)
            {
                try
                {
                    await writeTask.ConfigureAwait(false);
                }
                finally
                {
                    token.ReleasePayload();
                }
            }
        }
 
        ~SslStream()
        {
            Dispose(disposing: false);
        }
 
        private void ReturnReadBufferIfEmpty()
        {
            if (_buffer.ActiveLength == 0)
            {
                _buffer.ReturnBuffer();
            }
        }
 
        private bool HaveFullTlsFrame(out int frameSize)
        {
            frameSize = GetFrameSize(_buffer.EncryptedReadOnlySpan);
            return _buffer.EncryptedLength >= frameSize;
        }
 
        [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
        private async ValueTask<int> EnsureFullTlsFrameAsync<TIOAdapter>(CancellationToken cancellationToken, int estimatedSize)
            where TIOAdapter : IReadWriteAdapter
        {
            if (HaveFullTlsFrame(out int frameSize))
            {
                return frameSize;
            }
 
            await TIOAdapter.ReadAsync(InnerStream, Memory<byte>.Empty, cancellationToken).ConfigureAwait(false);
 
            // If we don't have enough data to determine the frame size, use the provided estimate
            // (e.g. a full TLS frame for reads, and a somewhat shorter frame for handshake / renegotiation).
            // If we do know the frame size, ensure we have space for the whole frame.
            _buffer.EnsureAvailableSpace(frameSize == UnknownTlsFrameLength ?
                estimatedSize :
                frameSize - _buffer.EncryptedLength);
 
            while (_buffer.EncryptedLength < frameSize)
            {
                // there should be space left to read into
                Debug.Assert(_buffer.AvailableLength > 0, "_buffer.AvailableBytes > 0");
 
                // We either don't have full frame or we don't have enough data to even determine the size.
                int bytesRead = await TIOAdapter.ReadAsync(InnerStream, _buffer.AvailableMemory, cancellationToken).ConfigureAwait(false);
                if (bytesRead == 0)
                {
                    if (_buffer.EncryptedLength != 0)
                    {
                        // we got EOF in middle of TLS frame. Treat that as error.
                        throw new IOException(SR.net_io_eof);
                    }
 
                    return 0;
                }
 
                _buffer.Commit(bytesRead);
                if (frameSize == int.MaxValue && _buffer.EncryptedLength > TlsFrameHelper.HeaderSize)
                {
                    // recalculate frame size if needed e.g. we could not get it before.
                    frameSize = GetFrameSize(_buffer.EncryptedReadOnlySpan);
                    _buffer.EnsureAvailableSpace(frameSize - _buffer.EncryptedLength);
                }
            }
 
            return frameSize;
        }
 
        private SecurityStatusPal DecryptData(int frameSize)
        {
            SecurityStatusPal status;
 
            lock (_handshakeLock)
            {
                ThrowIfExceptionalOrNotAuthenticated();
 
                // Decrypt will decrypt in-place and modify these to point to the actual decrypted data, which may be smaller.
                status = Decrypt(_buffer.EncryptedSpanSliced(frameSize), out int decryptedOffset, out int decryptedCount);
                _buffer.OnDecrypted(decryptedOffset, decryptedCount, frameSize);
 
                if (status.ErrorCode == SecurityStatusPalErrorCode.Renegotiate)
                {
                    // The status indicates that peer wants to renegotiate. (Windows only)
                    // In practice, there can be some other reasons too - like TLS1.3 session creation
                    // of alert handling. We need to pass the data to lsass and it is not safe to do parallel
                    // write any more as that can change TLS state and the EncryptData() can fail in strange ways.
 
                    // To handle this we call DecryptData() under lock and we create TCS waiter.
                    // EncryptData() checks that under same lock and if it exist it will not call low-level crypto.
                    // Instead it will wait synchronously or asynchronously and it will try again after the wait.
                    // The result will be set when ReplyOnReAuthenticationAsync() is finished e.g. lsass business is over.
                    // If that happen before EncryptData() runs, _handshakeWaiter will be set to null
                    // and EncryptData() will work normally e.g. no waiting, just exclusion with DecryptData()
 
                    if (_sslAuthenticationOptions.AllowRenegotiation || SslProtocol == SslProtocols.Tls13 || _nestedAuth != NestedState.StreamNotInUse)
                    {
                        // create TCS only if we plan to proceed. If not, we will throw later outside of the lock.
                        // Tls1.3 does not have renegotiation. However on Windows this error code is used
                        // for session management e.g. anything lsass needs to see.
                        // We also allow it when explicitly requested using RenegotiateAsync().
                        _handshakeWaiter = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
                    }
                }
            }
 
            return status;
        }
 
        [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
        private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(Memory<byte> buffer, CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
 
            // Throw first if we already have exception.
            // Check for disposal is not atomic so we will check again below.
            ThrowIfExceptionalOrNotAuthenticated();
 
            if (Interlocked.CompareExchange(ref _nestedRead, NestedState.StreamInUse, NestedState.StreamNotInUse) != NestedState.StreamNotInUse)
            {
                ObjectDisposedException.ThrowIf(_nestedRead == NestedState.StreamDisposed, this);
                throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, "read"));
            }
 
            try
            {
                int processedLength = 0;
                int nextTlsFrameLength = UnknownTlsFrameLength;
 
                if (_buffer.DecryptedLength != 0)
                {
                    processedLength = CopyDecryptedData(buffer);
                    if (processedLength == buffer.Length || !HaveFullTlsFrame(out nextTlsFrameLength))
                    {
                        // We either filled whole buffer or used all buffered frames.
                        return processedLength;
                    }
 
                    buffer = buffer.Slice(processedLength);
                }
 
                if (_receivedEOF && nextTlsFrameLength == UnknownTlsFrameLength)
                {
                    // there should be no frames waiting for processing
                    Debug.Assert(_buffer.EncryptedLength == 0);
                    // We received EOF during previous read but had buffered data to return.
                    return 0;
                }
 
                Debug.Assert(_buffer.DecryptedLength == 0);
 
                while (true)
                {
                    int payloadBytes = await EnsureFullTlsFrameAsync<TIOAdapter>(cancellationToken, ReadBufferSize).ConfigureAwait(false);
                    if (payloadBytes == 0)
                    {
                        _receivedEOF = true;
                        break;
                    }
 
                    SecurityStatusPal status = DecryptData(payloadBytes);
                    if (status.ErrorCode != SecurityStatusPalErrorCode.OK)
                    {
                        byte[]? extraBuffer = null;
                        if (_buffer.DecryptedLength != 0)
                        {
                            extraBuffer = new byte[_buffer.DecryptedLength];
                            _buffer.DecryptedSpan.CopyTo(extraBuffer);
 
                            _buffer.Discard(_buffer.DecryptedLength);
                        }
 
                        if (NetEventSource.Log.IsEnabled())
                            NetEventSource.Info(null, $"***Processing an error Status = {status}");
 
                        if (status.ErrorCode == SecurityStatusPalErrorCode.Renegotiate)
                        {
                            // We determined above that we will not process it.
                            if (_handshakeWaiter == null)
                            {
                                throw new IOException(SR.net_ssl_io_renego);
                            }
                            await ReplyOnReAuthenticationAsync<TIOAdapter>(extraBuffer, cancellationToken).ConfigureAwait(false);
                        }
                        else if (status.ErrorCode == SecurityStatusPalErrorCode.ContextExpired)
                        {
                            _receivedEOF = true;
                            break;
                        }
                        else
                        {
                            throw new IOException(SR.net_io_decrypt, SslStreamPal.GetException(status));
                        }
                    }
 
                    if (_buffer.DecryptedLength > 0)
                    {
                        // This will either copy data from rented buffer or adjust final buffer as needed.
                        // In both cases _decryptedBytesOffset and _decryptedBytesCount will be updated as needed.
                        int copyLength = CopyDecryptedData(buffer);
                        processedLength += copyLength;
                        if (copyLength == buffer.Length)
                        {
                            // We have more decrypted data after we filled provided buffer.
                            break;
                        }
 
                        buffer = buffer.Slice(copyLength);
                    }
 
                    if (processedLength == 0)
                    {
                        // We did not get any real data so far.
                        continue;
                    }
 
                    if (!HaveFullTlsFrame(out payloadBytes))
                    {
                        // We don't have another frame to process but we have some data to return to caller.
                        break;
                    }
 
                    TlsFrameHelper.TryGetFrameHeader(_buffer.EncryptedReadOnlySpan, ref _lastFrame.Header);
                    if (_lastFrame.Header.Type != TlsContentType.AppData)
                    {
                        // Alerts, handshake and anything else will be processed separately.
                        // This may not be necessary but it improves compatibility with older versions.
                        break;
                    }
                }
 
                return processedLength;
            }
            catch (Exception e)
            {
                if (e is IOException || (e is OperationCanceledException && cancellationToken.IsCancellationRequested))
                {
                    throw;
                }
 
                throw new IOException(SR.net_io_read, e);
            }
            finally
            {
                ReturnReadBufferIfEmpty();
                _nestedRead = NestedState.StreamNotInUse;
            }
        }
 
        private async ValueTask WriteAsyncInternal<TIOAdapter>(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
            where TIOAdapter : IReadWriteAdapter
        {
            ThrowIfExceptionalOrNotAuthenticatedOrShutdown();
 
            if (buffer.Length == 0 && !SslStreamPal.CanEncryptEmptyMessage)
            {
                // If it's an empty message and the PAL doesn't support that, we're done.
                return;
            }
 
            if (Interlocked.Exchange(ref _nestedWrite, NestedState.StreamInUse) == NestedState.StreamInUse)
            {
                throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, "write"));
            }
 
            try
            {
                ValueTask t = buffer.Length < MaxDataSize ?
                    WriteSingleChunk<TIOAdapter>(buffer, cancellationToken) :
                    WriteAsyncChunked<TIOAdapter>(buffer, cancellationToken);
                await t.ConfigureAwait(false);
            }
            catch (Exception e)
            {
                if (e is IOException || (e is OperationCanceledException && cancellationToken.IsCancellationRequested))
                {
                    throw;
                }
 
                throw new IOException(SR.net_io_write, e);
            }
            finally
            {
                _nestedWrite = NestedState.StreamNotInUse;
            }
        }
 
        private int CopyDecryptedData(Memory<byte> buffer)
        {
            Debug.Assert(_buffer.DecryptedLength > 0);
 
            int copyBytes = Math.Min(_buffer.DecryptedLength, buffer.Length);
            if (copyBytes != 0)
            {
                _buffer.DecryptedReadOnlySpanSliced(copyBytes).CopyTo(buffer.Span);
                _buffer.Discard(copyBytes);
            }
 
            return copyBytes;
        }
 
        // Returns TLS Frame size including header size.
        private int GetFrameSize(ReadOnlySpan<byte> buffer)
        {
            if (buffer.Length < TlsFrameHelper.HeaderSize)
            {
                return UnknownTlsFrameLength;
            }
 
            if (!TlsFrameHelper.TryGetFrameHeader(buffer, ref _lastFrame.Header))
            {
                throw new IOException(SR.net_ssl_io_frame);
            }
 
            if (_lastFrame.Header.Length < 0)
            {
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, "invalid TLS frame size");
                throw new AuthenticationException(SR.net_frame_read_size);
            }
 
            return _lastFrame.Header.Length;
        }
    }
}