Skip to content

Standard stream argument validation #44405

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 2 additions & 26 deletions src/Components/Shared/src/ArrayBuilderMemoryStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
ValidateArguments(buffer, offset, count);
ValidateBufferArguments(buffer, offset, count);

ArrayBuilder.Append(buffer, offset, count);
}
Expand All @@ -76,7 +76,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory<byte> memory, CancellationTo
/// <inheritdoc />
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ValidateArguments(buffer, offset, count);
ValidateBufferArguments(buffer, offset, count);

ArrayBuilder.Append(buffer, offset, count);
return Task.CompletedTask;
Expand All @@ -103,30 +103,6 @@ protected override void Dispose(bool disposing)
/// <inheritdoc />
public override ValueTask DisposeAsync() => default;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateArguments(byte[] buffer, int offset, int count)
{
if (buffer == null)
{
ThrowHelper.ThrowArgumentNullException(nameof(buffer));
}

if (offset < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(offset));
}

if (count < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(count));
}

if (buffer.Length - offset < count)
{
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(offset));
}
}

private static class ThrowHelper
{
public static void ThrowArgumentNullException(string name)
Expand Down
18 changes: 0 additions & 18 deletions src/Hosting/TestHost/src/ResponseBodyReaderStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ public override int Read(byte[] buffer, int offset, int count)

public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
VerifyBuffer(buffer, offset, count);

return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}

Expand Down Expand Up @@ -105,22 +103,6 @@ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, Cancellation
return (int)actual;
}

private static void VerifyBuffer(byte[] buffer, int offset, int count)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (offset < 0 || offset > buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if (count <= 0 || count > buffer.Length - offset)
{
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
}
}

internal void Cancel()
{
Abort(new OperationCanceledException());
Expand Down
35 changes: 35 additions & 0 deletions src/Hosting/TestHost/test/ResponseBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore.TestHost.Tests;
Expand Down Expand Up @@ -119,6 +120,40 @@ public async Task BodyStream_SyncEnabled_FlushSucceeds()
Assert.Equal(contentBytes, responseBytes);
}

[Fact]
public async Task BodyStream_ZeroByteRead_Success()
{
var emptyReadStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

var contentBytes = new byte[] { 32 };
using var host = await CreateHost(async httpContext =>
{
await httpContext.Response.Body.WriteAsync(contentBytes);
await emptyReadStarted.Task;
await httpContext.Response.Body.WriteAsync(contentBytes);
});

var client = host.GetTestServer().CreateClient();
var response = await client.GetAsync("/", HttpCompletionOption.ResponseHeadersRead);
var stream = await response.Content.ReadAsStreamAsync();
var bytes = new byte[5];
var read = await stream.ReadAsync(bytes);
Assert.Equal(1, read);
Assert.Equal(contentBytes[0], bytes[0]);

// This will chain to the Memory overload, but that does less restrictive validation on the input.
// https://github.com/dotnet/aspnetcore/issues/41692#issuecomment-1248714684
var zeroByteRead = stream.ReadAsync(Array.Empty<byte>(), 0, 0);
Assert.False(zeroByteRead.IsCompleted);
emptyReadStarted.SetResult();
read = await zeroByteRead.DefaultTimeout();
Assert.Equal(0, read);

read = await stream.ReadAsync(bytes);
Assert.Equal(1, read);
Assert.Equal(contentBytes[0], bytes[0]);
}

private Task<IHost> CreateHost(RequestDelegate appDelegate)
{
return new HostBuilder()
Expand Down
13 changes: 1 addition & 12 deletions src/Http/WebUtilities/src/BufferedReadStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
ValidateBuffer(buffer, offset, count);
ValidateBufferArguments(buffer, offset, count);

// Drain buffer
if (_bufferCount > 0)
Expand All @@ -222,7 +222,6 @@ public override int Read(byte[] buffer, int offset, int count)
/// <inheritdoc/>
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ValidateBuffer(buffer, offset, count);
return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}

Expand Down Expand Up @@ -421,14 +420,4 @@ private void CheckDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, nameof(BufferedReadStream));
}

private static void ValidateBuffer(byte[] buffer, int offset, int count)
{
// Delegate most of our validation.
_ = new ArraySegment<byte>(buffer, offset, count);
if (count == 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero.");
}
}
}
23 changes: 1 addition & 22 deletions src/Http/WebUtilities/src/FileBufferingWriteStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
ThrowArgumentException(buffer, offset, count);
ValidateBufferArguments(buffer, offset, count);
ThrowIfDisposed();

if (_bufferLimit.HasValue && _bufferLimit - Length < count)
Expand Down Expand Up @@ -141,7 +141,6 @@ public override void Write(byte[] buffer, int offset, int count)
/// <inheritdoc />
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ThrowArgumentException(buffer, offset, count);
await WriteAsync(buffer.AsMemory(offset, count), cancellationToken);
}

Expand Down Expand Up @@ -285,24 +284,4 @@ private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(Disposed, nameof(FileBufferingWriteStream));
}

private static void ThrowArgumentException(byte[] buffer, int offset, int count)
{
ArgumentNullException.ThrowIfNull(buffer);

if (offset < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}

if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}

if (buffer.Length - offset < count)
{
throw new ArgumentOutOfRangeException(nameof(offset));
}
}
}
15 changes: 1 addition & 14 deletions src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,7 @@ public override void SetLength(long value)

public override void Write(byte[] buffer, int offset, int count)
{
ArgumentNullException.ThrowIfNull(buffer);

if (offset < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required.");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), count, "Non-negative number required.");
}
if (count > buffer.Length - offset)
{
throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.");
}
ValidateBufferArguments(buffer, offset, count);
if (!CanWrite)
{
throw new ObjectDisposedException("The stream has been closed for writing.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,22 +122,7 @@ public override void SetLength(long value)

public override void Write(byte[] buffer, int offset, int count)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (offset < 0)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required.");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), count, "Non-negative number required.");
}
if (count > buffer.Length - offset)
{
throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.");
}
ValidateBufferArguments(buffer, offset, count);
if (!CanWrite)
{
throw new ObjectDisposedException("The stream has been closed for writing.");
Expand Down
17 changes: 2 additions & 15 deletions src/Servers/HttpSys/src/RequestProcessing/RequestStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,27 +94,14 @@ internal void Abort()
_requestContext.Abort();
}

private static void ValidateReadBuffer(byte[] buffer, int offset, int size)
{
ArgumentNullException.ThrowIfNull(buffer);
if ((uint)offset > (uint)buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if ((uint)size > (uint)(buffer.Length - offset))
{
throw new ArgumentOutOfRangeException(nameof(size), size, string.Empty);
}
}

public override unsafe int Read([In, Out] byte[] buffer, int offset, int size)
{
if (!RequestContext.AllowSynchronousIO)
{
throw new InvalidOperationException("Synchronous IO APIs are disabled, see AllowSynchronousIO.");
}

ValidateReadBuffer(buffer, offset, size);
ValidateBufferArguments(buffer, offset, size);
CheckSizeLimit();
if (_closed)
{
Expand Down Expand Up @@ -200,7 +187,7 @@ public override int EndRead(IAsyncResult asyncResult)

public override unsafe Task<int> ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken)
{
ValidateReadBuffer(buffer, offset, size);
ValidateBufferArguments(buffer, offset, size);
CheckSizeLimit();
if (_closed)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ public async Task RequestBody_InvalidBuffer_ArgumentException()
byte[] input = new byte[100];
Assert.Throws<ArgumentNullException>("buffer", () => httpContext.Request.Body.Read(null, 0, 1));
Assert.Throws<ArgumentOutOfRangeException>("offset", () => httpContext.Request.Body.Read(input, -1, 1));
Assert.Throws<ArgumentOutOfRangeException>("offset", () => httpContext.Request.Body.Read(input, input.Length + 1, 1));
Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 10, -1));
Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 1, input.Length));
Assert.Throws<ArgumentOutOfRangeException>("size", () => httpContext.Request.Body.Read(input, 0, input.Length + 1));
Assert.Throws<ArgumentOutOfRangeException>("count", () => httpContext.Request.Body.Read(input, input.Length + 1, 1));
Assert.Throws<ArgumentOutOfRangeException>("count", () => httpContext.Request.Body.Read(input, 10, -1));
Assert.Throws<ArgumentOutOfRangeException>("count", () => httpContext.Request.Body.Read(input, 1, input.Length));
Assert.Throws<ArgumentOutOfRangeException>("count", () => httpContext.Request.Body.Read(input, 0, input.Length + 1));
return Task.FromResult(0);
}))
{
Expand Down