Skip to content

Reject empty Slice segment in the middle of an element stream#4756

Merged
pepone merged 4 commits into
icerpc:mainfrom
pepone:fix4755
Jun 17, 2026
Merged

Reject empty Slice segment in the middle of an element stream#4756
pepone merged 4 commits into
icerpc:mainfrom
pepone:fix4755

Conversation

@pepone

@pepone pepone commented Jun 11, 2026

Copy link
Copy Markdown
Member

Fixes #4755.

AsyncStream<T>.EnumerateAsync treated any empty buffer as the end of the stream. A zero-size Slice segment followed by more data also produces an empty buffer with IsCompleted false (see PipeReaderExtensions.IsCompleteSegment): a Debug.Assert fired in debug builds, and in release builds the element stream ended silently, dropping the remaining elements.

The only expected empty segment is the one that ends the stream. EnumerateAsync now throws InvalidDataException when it reads an empty segment in the middle of an element stream.

Changes:

  • AsyncStream.EnumerateAsync: distinguish "empty + completed" (end of stream) from "empty + not completed" (protocol violation, InvalidDataException).
  • New test Zero_size_segment_in_the_middle_of_the_stream_throws_InvalidDataException: written first against the unfixed code, where it failed with Debug.Fail; also checks that elements decoded before the invalid segment are still yielded and that the reader is completed exactly once.
  • New test Zero_size_segment_at_the_end_of_the_stream_completes_iteration: pins the legal case, a trailing zero-size segment ends the stream normally.

The C# encoder (AsyncEnumerablePipeReader) never emits zero-size segments, so this only affects streams received from a peer that does.

Copilot AI review requested due to automatic review settings June 11, 2026 14:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a correctness issue in Slice element-stream decoding where a zero-size Slice segment occurring mid-stream was incorrectly treated as end-of-stream, leading to silent element loss (and a Debug.Assert in debug builds). The stream enumerator now distinguishes a legitimate trailing empty segment (end-of-stream) from an empty segment that is not completed (protocol violation).

Changes:

  • Update AsyncStream<T>.EnumerateAsync to throw InvalidDataException when it encounters an empty segment that does not also mark stream completion.
  • Add a regression test ensuring a mid-stream zero-size segment throws while still yielding already-decoded elements and completing the reader exactly once.
  • Add a test covering the legal case where a trailing zero-size segment ends iteration normally.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/IceRpc.Slice/Operations/Internal/AsyncStream.cs Rejects empty, non-completed segments mid-stream by throwing InvalidDataException instead of silently ending iteration.
tests/IceRpc.Slice.Tests/AsyncStreamTests.cs Adds tests for both the invalid mid-stream zero-size segment case and the valid trailing zero-size segment case; includes a helper to build segmented payloads.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pepone pepone requested a review from bernardnormier June 11, 2026 14:27
@bernardnormier

Copy link
Copy Markdown
Member

I think we should actually reject 0 segment sizes in general, in IsCompleteSegment.

A Slice segment with an encoded 0-size is not correct:

  • if it corresponds to non-streamed args or return value, it must be at least 1 (for the tag end marker)
  • if it corresponds to streamed elements, it must also be at least 1 (the size is the number of bytes in the segment)

The "0" case is the end of stream; we don't encode a size 0.

Note that we currently have various tests that encode a 0 Slice segment. They'll need to be updated.

{
// A zero-size segment is never valid: the end of a stream is signaled by the reader completing with no
// bytes, not by a zero-size segment. See https://github.com/icerpc/icerpc-csharp/issues/4755.
byte[] payload = EncodeInt32Segments([1, 2], [], [3]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like a good test: a stream of int32 is not encoded in segments. It would be better to pick a variable-size element like a string.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude: Done in 415bc1b. Converted both zero-size-segment tests to stream a variable-size element (string) via the segment-based ToAsyncStream overload. You're right that this matters: fixed-size elements like int32 use the elementSize overload, whose ReadAsync calls ReadAtLeastAsync and never reads segments — so a zero-size segment can only occur in a variable-size element stream. The previous int32 + no-elementSize pairing tested a combination that does not happen on the wire. Verified the new string tests fail against the unfixed IsCompleteSegment and the full Slice + IceRpc suites pass.

{
// A zero-size segment is never valid, including at the end of the stream: the end of a stream is signaled
// by the reader completing with no bytes, not by a zero-size segment.
// See https://github.com/icerpc/icerpc-csharp/issues/4755.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Claude: Done in 415bc1b — same change applied to this test (now streams string). See the reply above for the rationale.

Per review feedback: only variable-size element streams are encoded with
segments (fixed-size elements like int32 use the elementSize overload,
which reads fixed chunks without segment framing). The zero-size segment
tests now stream strings so they exercise a scenario that can actually
occur on the wire.
Comment thread tests/IceRpc.Slice.Tests/AsyncStreamTests.cs Outdated
Comment thread tests/IceRpc.Slice.Tests/AsyncStreamTests.cs Outdated
@pepone pepone merged commit 4c09c8f into icerpc:main Jun 17, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Audit-Low] Zero-size Slice segment mid-stream silently ends an element stream

3 participants