AI-generated audit finding — this issue was opened from an automated security/correctness audit. It has not been triaged by a human yet; verify the reasoning, reproducibility, and severity before acting on it.
Medium: detached streaming PipeReader leaks when the returned IAsyncEnumerable<T> is ignored — CONFIRMED
Affected code:
Verification:
Confirmed. The leak is real and the ownership transfer is explicit.
DetachPayload removes the payload from the owning frame by replacing incoming.Payload with InvalidPipeReader.Instance. After that point, disposing the IncomingRequest, OutgoingRequest, or IncomingResponse no longer reaches the detached reader. Cleanup is delegated entirely to the compiler-generated async-enumerable state machine in PipeReaderExtensions.ToAsyncEnumerable, and that state machine only enters its try/finally after GetAsyncEnumerator() is called.
As a result, a stream payload is cleaned up in these already-tested cases:
- normal full enumeration,
- partial enumeration with
break,
- iteration canceled via
WithCancellation, and
- decode failures after enumeration has started.
But if a caller simply receives the IAsyncEnumerable<T> and drops it without ever starting enumeration, reader.Complete() is never reached. This can happen on both sides of the stack:
- a server handler validates the request and returns early without reading the streamed argument, or
- a client receives a streamed return value and abandons it before the first
await foreach.
Impact:
- Per-connection resource leak under abandoned streaming payloads.
- Peer-reachable amplification on the server side when handlers legitimately return early without consuming a streamed argument.
Recommendation:
- Replace the compiler-generated
async IAsyncEnumerable<T> wrapper with a custom enumerable/enumerator type that owns the PipeReader and can complete it deterministically even when iteration never starts.
- Or keep ownership on the frame side and expose a higher-level abstraction that does not require detaching the raw
PipeReader.
- Add regression tests for both overloads where the returned enumerable is never enumerated and the underlying reader must still be completed.
Status: Valid, Medium severity.
Source report: src-IceRpc.Slice-audit-2026-04-14.md (finding detached streaming PipeReaderleaks when the returnedIAsyncEnumerable is ignored — **CONFIRMED**)
Severity (auditor-assigned): Medium
Medium: detached streaming
PipeReaderleaks when the returnedIAsyncEnumerable<T>is ignored — CONFIRMEDAffected code:
ToAsyncEnumerabletransfers reader ownershipToAsyncEnumerabledoes the sameasync IAsyncEnumerable<T>only callsreader.Complete()in itsfinallyDetachPayloadtransfers ownership and replacesincoming.PayloadwithInvalidPipeReader.InstanceVerification:
Confirmed. The leak is real and the ownership transfer is explicit.
DetachPayloadremoves the payload from the owning frame by replacingincoming.PayloadwithInvalidPipeReader.Instance. After that point, disposing theIncomingRequest,OutgoingRequest, orIncomingResponseno longer reaches the detached reader. Cleanup is delegated entirely to the compiler-generated async-enumerable state machine inPipeReaderExtensions.ToAsyncEnumerable, and that state machine only enters itstry/finallyafterGetAsyncEnumerator()is called.As a result, a stream payload is cleaned up in these already-tested cases:
break,WithCancellation, andBut if a caller simply receives the
IAsyncEnumerable<T>and drops it without ever starting enumeration,reader.Complete()is never reached. This can happen on both sides of the stack:await foreach.Impact:
Recommendation:
async IAsyncEnumerable<T>wrapper with a custom enumerable/enumerator type that owns thePipeReaderand can complete it deterministically even when iteration never starts.PipeReader.Status: Valid, Medium severity.
Source report: src-IceRpc.Slice-audit-2026-04-14.md (finding
detached streamingPipeReaderleaks when the returnedIAsyncEnumerableis ignored — **CONFIRMED**)Severity (auditor-assigned): Medium