Handle the case when zero byte reads are cancelled with an empty buffer (#53456)
authorDavid Fowler <davidfowl@gmail.com>
Tue, 1 Jun 2021 17:55:38 +0000 (10:55 -0700)
committerGitHub <noreply@github.com>
Tue, 1 Jun 2021 17:55:38 +0000 (10:55 -0700)
- When there's no buffer allocated and the zero byte reads is cancelled, it'll try to make a read only sequence with the current buffer, if that buffer is null because it wasn't yet allocated, it'll end up throwing a null reference exception. This adds a check to make sure were return an empty ReadOnlySequence if the backing buffer is null.
- Added tests

src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/StreamPipeReader.cs
src/libraries/System.IO.Pipelines/tests/StreamPipeReaderTests.cs

index dde8775..ccafce2 100644 (file)
@@ -513,7 +513,7 @@ namespace System.IO.Pipelines
                     ClearCancellationToken();
                 }
 
-                ReadOnlySequence<byte> buffer = _readHead == null ? default : GetCurrentReadOnlySequence();
+                ReadOnlySequence<byte> buffer = GetCurrentReadOnlySequence();
 
                 result = new ReadResult(buffer, isCancellationRequested, _isStreamCompleted);
                 return true;
@@ -525,8 +525,8 @@ namespace System.IO.Pipelines
 
         private ReadOnlySequence<byte> GetCurrentReadOnlySequence()
         {
-            Debug.Assert(_readHead != null && _readTail != null);
-            return new ReadOnlySequence<byte>(_readHead, _readIndex, _readTail, _readTail.End);
+            // If _readHead is null then _readTail is also null
+            return _readHead is null ? default : new ReadOnlySequence<byte>(_readHead, _readIndex, _readTail!, _readTail!.End);
         }
 
         private void AllocateReadTail(int? minimumSize = null)
index 69fb704..a9cb0d3 100644 (file)
@@ -267,11 +267,13 @@ namespace System.IO.Pipelines.Tests
             reader.Complete();
         }
 
-        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
-        public async Task ReadCanBeCanceledViaCancelPendingReadWhenReadIsAsync()
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task ReadCanBeCanceledViaCancelPendingReadWhenReadAsync(bool useZeroByteReads)
         {
             var stream = new CancelledReadsStream();
-            PipeReader reader = PipeReader.Create(stream);
+            PipeReader reader = PipeReader.Create(stream, new StreamPipeReaderOptions(useZeroByteReads: useZeroByteReads));
 
             ValueTask<ReadResult> task = reader.ReadAsync();
 
@@ -284,6 +286,25 @@ namespace System.IO.Pipelines.Tests
             reader.Complete();
         }
 
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task ReadCanBeCanceledViaCancelPendingReadWhenReadAtLeastAsync(bool useZeroByteReads)
+        {
+            var stream = new CancelledReadsStream();
+            PipeReader reader = PipeReader.Create(stream, new StreamPipeReaderOptions(useZeroByteReads: useZeroByteReads));
+
+            ValueTask<ReadResult> task = reader.ReadAtLeastAsync(1);
+
+            reader.CancelPendingRead();
+
+            stream.WaitForReadTask.TrySetResult(null);
+
+            ReadResult readResult = await task;
+            Assert.True(readResult.IsCanceled);
+            reader.Complete();
+        }
+
         [Fact]
         public async Task ReadAsyncReturnsCanceledIfCanceledBeforeRead()
         {