Skip to content

SslStream.ReadAsyncInternal() throws misleading exception if stream is disposed while reading #78586

@mikeharder

Description

@mikeharder

Description

If SslStream is disposed while there is a concurrent call to ReadAsyncInternal(), a misleading exception may be thrown:

Unhandled exception. System.NotSupportedException:  This method may not be called when another read operation is pending.
   at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.Read(Byte[] buffer, Int32 offset, Int32 count)

The exception message claims that "ReadAsyncInternal() was called when another read operation is pending", but this is misleading, since there were not concurrent calls to ReadAsyncInternal(). This exception message is raised due to the following codepaths:

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 = 1 and _nestedAuth = 1, but that's ok, since
// subsequent operations check the _exception sentinel first
if (Interlocked.Exchange(ref _nestedRead, 1) == 0 &&
Interlocked.Exchange(ref _nestedAuth, 1) == 0)
{
_buffer.ReturnBuffer();
}

private async ValueTask<int> ReadAsyncInternal<TIOAdapter>(Memory<byte> buffer, CancellationToken cancellationToken)
where TIOAdapter : IReadWriteAdapter
{
if (Interlocked.Exchange(ref _nestedRead, 1) == 1)
{
throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, "read"));
}

For customers trying to debug this issue, it would be very helpful if ReadAsyncInternal() could throw ObjectDisposedException in this case. This makes it clear the issue is dispose during read, not concurrent reads.

Maybe a code change like this?

if (Interlocked.Exchange(ref _nestedRead, 1) == 1)
{
    ThrowIfExceptionalOrNotAuthenticated();
    throw new NotSupportedException(SR.Format(SR.net_io_invalidnestedcall, "read"));
}

Reproduction Steps

You should be able to repro this by concurrently calling Read() and Dispose() in a loop until you trigger the race condition. Alternatively, you can use the repro I created for the Azure SDK (which has a bug where it calls Dispose() concurrently during a Read()):

Azure/azure-sdk-for-net#32577

Expected behavior

SslStream throws ObjectDisposedException

Actual behavior

SslStream throws NotSupportedException

Regression?

No

Known Workarounds

No response

Configuration

.NET Runtime 7.0.0
Windows 10
x64

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions