diff --git a/src/libraries/System.Memory/src/Resources/Strings.resx b/src/libraries/System.Memory/src/Resources/Strings.resx
index 90489cb74c5898..0c13320a1e0e01 100644
--- a/src/libraries/System.Memory/src/Resources/Strings.resx
+++ b/src/libraries/System.Memory/src/Resources/Strings.resx
@@ -147,13 +147,10 @@
Cannot allocate a buffer of size {0}.
+
+ Stream does not support seeking.
+
Stream does not support writing.
-
- An attempt was made to move the position before the beginning of the stream.
-
-
- Invalid seek origin.
-
\ No newline at end of file
diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs
index 9367aea898540a..0cdd942de199c8 100644
--- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs
+++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs
@@ -8,7 +8,7 @@
namespace System.Buffers
{
///
- /// Provides a seekable, read-only over a .
+ /// Provides a read-only, non-seekable for reading from a .
///
///
/// The underlying sequence is not copied; reads are served directly from its segments.
@@ -18,7 +18,6 @@ public sealed class ReadOnlySequenceStream : Stream
{
private ReadOnlySequence _sequence;
private SequencePosition _position;
- private long _absolutePosition;
private bool _isDisposed;
private CachedCompletedInt32Task _lastReadTask;
@@ -30,15 +29,19 @@ public ReadOnlySequenceStream(ReadOnlySequence source)
{
_sequence = source;
_position = source.Start;
- _absolutePosition = 0;
- _isDisposed = false;
}
///
public override bool CanRead => !_isDisposed;
///
- public override bool CanSeek => !_isDisposed;
+ // Keep this intentionally non-seekable: backward positioning requires traversing segments
+ // again from the beginning, making repeated seeks worst-case O(N). ReadOnlySequence
+ // segment boundaries may be indirectly controlled by an untrusted network client through
+ // packet framing, so even correct stitching logic can produce adversarial fragmentation.
+ // Consumers must remain resilient against the worst technically compliant implementation
+ // rather than assuming ASP.NET-like segmentation.
+ public override bool CanSeek => false;
///
public override bool CanWrite => false;
@@ -46,43 +49,16 @@ public ReadOnlySequenceStream(ReadOnlySequence source)
private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(_isDisposed, this);
///
- public override long Length
- {
- get
- {
- EnsureNotDisposed();
- return _sequence.Length;
- }
- }
+ // Keep Length and Position unsupported to match the standard contract encoded by the
+ // stream conformance tests for streams where CanSeek is false, even though the underlying
+ // sequence can provide its length cheaply.
+ public override long Length => throw new NotSupportedException(SR.NotSupported_UnseekableStream);
///
public override long Position
{
- get
- {
- EnsureNotDisposed();
- return _absolutePosition;
- }
- set
- {
- EnsureNotDisposed();
- ArgumentOutOfRangeException.ThrowIfNegative(value);
-
- if (value >= _sequence.Length)
- {
- _position = _sequence.End;
- }
- else if (value >= _absolutePosition)
- {
- _position = _sequence.GetPosition(value - _absolutePosition, _position);
- }
- else
- {
- _position = _sequence.GetPosition(value, _sequence.Start);
- }
-
- _absolutePosition = value;
- }
+ get => throw new NotSupportedException(SR.NotSupported_UnseekableStream);
+ set => throw new NotSupportedException(SR.NotSupported_UnseekableStream);
}
///
@@ -97,11 +73,6 @@ public override int Read(Span buffer)
{
EnsureNotDisposed();
- if (_absolutePosition >= _sequence.Length)
- {
- return 0;
- }
-
ReadOnlySequence remaining = _sequence.Slice(_position);
int n = (int)Math.Min(remaining.Length, buffer.Length);
if (n <= 0)
@@ -111,7 +82,6 @@ public override int Read(Span buffer)
remaining.Slice(0, n).CopyTo(buffer);
_position = _sequence.GetPosition(n, _position);
- _absolutePosition += n;
return n;
}
@@ -159,19 +129,18 @@ public override void CopyTo(Stream destination, int bufferSize)
ValidateCopyToArguments(destination, bufferSize);
EnsureNotDisposed();
- if (_absolutePosition >= _sequence.Length)
+ ReadOnlySequence remaining = _sequence.Slice(_position);
+ if (remaining.IsEmpty)
{
return;
}
- ReadOnlySequence remaining = _sequence.Slice(_position);
foreach (ReadOnlyMemory segment in remaining)
{
destination.Write(segment.Span);
}
_position = _sequence.End;
- _absolutePosition = _sequence.Length;
}
///
@@ -185,24 +154,23 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio
return Task.FromCanceled(cancellationToken);
}
- if (_absolutePosition >= _sequence.Length)
+ ReadOnlySequence remaining = _sequence.Slice(_position);
+ if (remaining.IsEmpty)
{
return Task.CompletedTask;
}
- return CopyToAsyncCore(destination, cancellationToken);
+ return CopyToAsyncCore(remaining, destination, cancellationToken);
}
- private async Task CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
+ private async Task CopyToAsyncCore(ReadOnlySequence remaining, Stream destination, CancellationToken cancellationToken)
{
- ReadOnlySequence remaining = _sequence.Slice(_position);
foreach (ReadOnlyMemory segment in remaining)
{
await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false);
}
_position = _sequence.End;
- _absolutePosition = _sequence.Length;
}
///
@@ -218,43 +186,7 @@ private async Task CopyToAsyncCore(Stream destination, CancellationToken cancell
public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);
///
- public override long Seek(long offset, SeekOrigin origin)
- {
- EnsureNotDisposed();
-
- long basePosition = origin switch
- {
- SeekOrigin.Begin => 0L,
- SeekOrigin.Current => _absolutePosition,
- SeekOrigin.End => _sequence.Length,
- _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin))
- };
-
- ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, long.MaxValue - basePosition);
-
- long absolutePosition = basePosition + offset;
-
- if (absolutePosition < 0)
- {
- throw new IOException(SR.IO_SeekBeforeBegin);
- }
-
- if (absolutePosition >= _sequence.Length)
- {
- _position = _sequence.End;
- }
- else if (absolutePosition >= _absolutePosition)
- {
- _position = _sequence.GetPosition(absolutePosition - _absolutePosition, _position);
- }
- else
- {
- _position = _sequence.GetPosition(absolutePosition, _sequence.Start);
- }
-
- _absolutePosition = absolutePosition;
- return absolutePosition;
- }
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.NotSupported_UnseekableStream);
///
public override void Flush() { }
diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs
index 33f32fc05d662b..6e3dc733f4c28d 100644
--- a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs
+++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs
@@ -4,13 +4,12 @@
using System.Buffers;
using System.IO.Tests;
using System.Threading.Tasks;
-using Xunit;
namespace System.Memory.Tests
{
public class ROSequenceStreamConformanceTests : StandaloneStreamConformanceTests
{
- protected override bool CanSeek => true;
+ protected override bool CanSeek => false;
protected override bool CanSetLength => false;
protected override bool NopFlushCompletesSynchronously => true;
@@ -34,27 +33,6 @@ protected virtual ReadOnlySequence CreateSequence(byte[] data)
protected override Task CreateReadWriteStreamCore(byte[]? initialData)
=> Task.FromResult(null);
-
- public override async Task Seek_PastEnd_ReadReturns0(SeekMode mode)
- {
- await base.Seek_PastEnd_ReadReturns0(mode);
-
- // ReadOnlySequenceStream-specific: seeking past end against an empty sequence
- // is allowed, Position reflects the requested offset, and reads stay at 0.
- var stream = new ReadOnlySequenceStream(ReadOnlySequence.Empty);
- Assert.Equal(0, stream.Length);
- Assert.Equal(0, stream.Position);
-
- byte[] buffer = new byte[10];
- Assert.Equal(0, stream.Read(buffer, 0, 10));
-
- stream.Seek(0, SeekOrigin.Begin);
- Assert.Equal(0, stream.Position);
-
- long newPosition = stream.Seek(1, SeekOrigin.Begin);
- Assert.Equal(1, newPosition);
- Assert.Equal(1, stream.Position);
- }
}
///
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs
index 739a3391ff872c..82d05825286fda 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs
@@ -7,11 +7,8 @@
namespace System.IO
{
///
- /// Provides a seekable, read-only over a .
+ /// Provides a seekable, read-only for reading from a .
///
- ///
- /// The stream cannot be written to. always returns .
- ///
public sealed class ReadOnlyMemoryStream : Stream
{
private ReadOnlyMemory _memory;
@@ -22,7 +19,11 @@ public sealed class ReadOnlyMemoryStream : Stream
///
/// Initializes a new instance of the class over the specified .
///
- /// The to wrap.
+ /// The memory region from which to create the stream.
+ ///
+ /// The existing contents of are immediately readable.
+ /// Clear rented or reused memory before constructing the stream if its existing contents should not be exposed.
+ ///
public ReadOnlyMemoryStream(ReadOnlyMemory source)
{
_memory = source;
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
index 36fba65dc37a90..c7074545db9a3f 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs
@@ -9,7 +9,7 @@ namespace System.IO
{
///
/// Provides a read-only, non-seekable that encodes a or
- /// into bytes on-the-fly using a specified .
+ /// into bytes on-the-fly using a specified .
///
///
/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves.
@@ -74,6 +74,8 @@ public StringStream(ReadOnlyMemory text, Encoding encoding)
public override bool CanRead => !_disposed;
///
+ // Keep this intentionally non-seekable: backward positioning requires re-running the
+ // encoder from the beginning, making repeated seeks worst-case O(N).
public override bool CanSeek => false;
///
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs
index 802c6191c7501c..4f681578c36d57 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs
@@ -7,7 +7,7 @@
namespace System.IO
{
///
- /// Provides a seekable, writable over a .
+ /// Provides a seekable for reading from and writing to a .
///
public sealed class WritableMemoryStream : Stream
{
@@ -20,7 +20,11 @@ public sealed class WritableMemoryStream : Stream
///
/// Initializes a new instance of the class over the specified .
///
- /// The to wrap.
+ /// The memory region from which to create the stream.
+ ///
+ /// The existing contents of are immediately readable.
+ /// Clear rented or reused memory before constructing the stream if its existing contents should not be exposed.
+ ///
public WritableMemoryStream(Memory buffer)
{
_memory = buffer;
diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs
index b123499b6132ca..28b5a2d4077e40 100644
--- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs
@@ -70,10 +70,15 @@ public void ThrowsOnNullEncoding()
[Fact]
public void StreamCapabilities()
{
- var stream = new StringStream("test", Encoding.UTF8);
+ using var stream = new StringStream("test", Encoding.UTF8);
+
Assert.True(stream.CanRead);
Assert.False(stream.CanSeek);
Assert.False(stream.CanWrite);
+ Assert.Throws(() => stream.Length);
+ Assert.Throws(() => stream.Position);
+ Assert.Throws(() => stream.Position = 0);
+ Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin));
}
[Fact]