From 92e0460dac2cd6340d77e170f6ee7121fce1c9a1 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 7 Aug 2026 12:28:50 -0500 Subject: [PATCH 1/3] Document memory stream buffer contents Clarify that read-only and writable memory streams expose the existing contents of supplied memory, including guidance for rented or reused buffers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/IO/ReadOnlyMemoryStream.cs | 11 ++++++----- .../src/System/IO/WritableMemoryStream.cs | 8 ++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) 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..20f93ea934caa8 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 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/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; From be747db6658b28096b43ecfcdb5da76ccb1255f8 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 7 Aug 2026 12:54:03 -0500 Subject: [PATCH 2/3] Make sequence and string streams non-seekable Prevent backward positioning from repeatedly replaying segmented sequences or encoded text. Keep Length, Position, and Seek consistent with the standard non-seekable Stream contract, and update conformance coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Memory/src/Resources/Strings.resx | 9 +- .../System/Buffers/ReadOnlySequenceStream.cs | 103 +++--------------- ...ReadOnlySequenceStream.ConformanceTests.cs | 24 +--- .../src/System/IO/StringStream.cs | 2 + .../StringStream/StringStreamTests_String.cs | 7 +- 5 files changed, 29 insertions(+), 116 deletions(-) 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..cc1b21a72f2f41 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 over 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,15 @@ 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 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 +72,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 +81,6 @@ public override int Read(Span buffer) remaining.Slice(0, n).CopyTo(buffer); _position = _sequence.GetPosition(n, _position); - _absolutePosition += n; return n; } @@ -159,19 +128,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,7 +153,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.FromCanceled(cancellationToken); } - if (_absolutePosition >= _sequence.Length) + if (_sequence.Slice(_position).IsEmpty) { return Task.CompletedTask; } @@ -202,7 +170,6 @@ private async Task CopyToAsyncCore(Stream destination, CancellationToken cancell } _position = _sequence.End; - _absolutePosition = _sequence.Length; } /// @@ -218,43 +185,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/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs index 36fba65dc37a90..e6a20951349f38 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -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.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] From c43ef25bacac0799fd10c46de722bc1d4242ed70 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Fri, 7 Aug 2026 14:45:04 -0500 Subject: [PATCH 3/3] Address stream wrapper review feedback Reuse the sliced sequence in CopyToAsync and clarify stream wrapper documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Buffers/ReadOnlySequenceStream.cs | 15 ++++++++------- .../src/System/IO/ReadOnlyMemoryStream.cs | 2 +- .../src/System/IO/StringStream.cs | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index cc1b21a72f2f41..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 read-only, non-seekable 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. @@ -49,8 +49,9 @@ public ReadOnlySequenceStream(ReadOnlySequence source) private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(_isDisposed, this); /// - // Keep Length and Position unsupported to match the standard contract for streams where - // CanSeek is false, even though the underlying sequence can provide its length cheaply. + // 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); /// @@ -153,17 +154,17 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.FromCanceled(cancellationToken); } - if (_sequence.Slice(_position).IsEmpty) + 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); 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 20f93ea934caa8..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,7 +7,7 @@ namespace System.IO { /// - /// Provides a seekable, read-only for reading a . + /// Provides a seekable, read-only for reading from a . /// public sealed class ReadOnlyMemoryStream : Stream { 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 e6a20951349f38..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.