dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.82k stars 4.61k forks source link

NotSupportedException when writing to a closed SSL stream #29006

Closed karmeli87 closed 4 years ago

karmeli87 commented 5 years ago

NotSupportedException: The WriteAsync method cannot be called when another write operation is pending while disposing after writing to closed stream.

Failed both on Ubuntu 18.04 and Windows 10 with dotnet runtime 2.2.3 and sdk 2.2.105 with the exception:

System.NotSupportedException:  The WriteAsync method cannot be called when another write operation is pending.
   at System.Net.Security.SslStreamInternal.WriteAsyncInternal[TWriteAdapter](TWriteAdapter writeAdapter, ReadOnlyMemory`1 buffer)
   at System.Net.Security.SslStreamInternal.Write(Byte[] buffer, Int32 offset, Int32 count)
   at System.Net.Security.SslStream.Write(Byte[] buffer, Int32 offset, Int32 count)
   at System.IO.StreamWriter.Flush(Boolean flushStream, Boolean flushEncoder)
   at System.IO.StreamWriter.Dispose(Boolean disposing)
   at System.IO.TextWriter.Dispose()
   at Tryouts.Program.Main(String[] args) in C:\Work\RavenDB-4.1\test\Tryouts\Program.cs:line 48

This is the code to repo the issue:

using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Tryouts
{
    public static class Program
    {
        public static void Main(string[] args)
        {
            var mre = new ManualResetEvent(false);
            var listener = new TcpListener(IPAddress.Any, 8888);
            listener.Start();

            var cert = new X509Certificate(@"C:\Work\server.pfx"); // self signed cert
            Task.Run(() =>
            {
                var client = new TcpClient();

                client.Connect("localhost", 8888);
                var stream = new SslStream(client.GetStream(), false, (sender, actualCert, chain, errors) => true);
                stream.AuthenticateAsClient("karmel-pc", new X509CertificateCollection { cert }, SslProtocols.Tls12, false);

                client.Dispose();

                mre.Set();
            });

            var serverConn = listener.AcceptTcpClient();

            using (var stream = new SslStream(serverConn.GetStream(), false, (sender, actualCert, chain, errors) => true))
            {
                stream.AuthenticateAsServer(cert, true, SslProtocols.Tls12, false);

                using (var writer = new StreamWriter(stream,Encoding.UTF8)) 
                {
                    mre.WaitOne();
                    try
                    {
                        writer.Write(42);
                        writer.Flush();
                    }
                    catch
                    {
                        writer.Write(42);
                    }
                }
            }
        }
    }
}

There are few interesting things here:

  1. I would expect to have an IOException (To me this is the actual bug)
  2. This doesn't seems to appear when the StreamWriter uses the StreamWriter(Stream) constructor. In that case there is no exception at all (not sure if IOException would be also expect).
ayende commented 5 years ago

@karmeli87 There might be a race here between server & client. Better to add some explicit waits to ensure that the timing is consistent.

stephentoub commented 5 years ago

The first write call often results in this exception, which I believe you expect:

Unhandled Exception: System.IO.IOException: Unable to write data to the transport connection: An established connection was aborted by the software in your host machine.. ---> System.Net.Sockets.SocketException: An established connection was aborted by the software in your host machine.

since you're forcefully closing the connection on one end and then having the other end write to it.

You're then catching that exception and calling write again. There's a bug in SslStream where if an exception occurs while writing to the stream, it leaves the SslStream "locked", such that you get the error you mentioned. That was fixed for .NET Core 3.0 in https://github.com/dotnet/corefx/pull/36106. This really only affects the exception you get, though; you'll still get an exception from the secondary write on the failed stream, just not this NotSupportedException.