From bd0185633903969fce64efa096d67a04b056162e Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Wed, 2 Sep 2026 19:28:26 +0200 Subject: [PATCH 1/2] avoid memoryPool rent with 0 length --- .../src/Internal/Http/Http1OutputProducer.cs | 14 ++++++++ .../test/Http1/Http1OutputProducerTests.cs | 34 +++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs index 9ea1c7b118cf..583c605a304a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs @@ -668,6 +668,13 @@ private void WriteCurrentChunkMemoryToPipeWriter(ref BufferWriter wr internal Memory GetFakeMemory(int minSize) { + if (minSize == 0) + { + // MemoryPool.Rent(0) may return empty memory; + // use Kestrel's minimum segment size to satisfy the IBufferWriter contract. + minSize = _memoryPool.GetMinimumSegmentSize(); + } + // Try to reuse _fakeMemoryOwner if (_fakeMemoryOwner != null) { @@ -735,6 +742,13 @@ private void EnsureCapacity(int sizeHint) private void AddSegment(int sizeHint = 0) { + if (sizeHint == 0) + { + // MemoryPool.Rent(0) may return empty memory; + // use Kestrel's minimum segment size to satisfy the IBufferWriter contract. + sizeHint = _memoryPool.GetMinimumSegmentSize(); + } + if (_currentSegment.Length != 0) { // We're adding a segment to the list diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs index 0683b9ea6c84..a3e55a5c4420 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs @@ -137,6 +137,35 @@ public void AbortsTransportEvenAfterDispose() Assert.Equal(ConnectionEndReason.AbortedByApp, metricsContext.ConnectionEndReason); } + [Fact] + public void GetMemoryAndGetSpanWithZeroSizeHintReturnNonEmptyBuffers() + { + var memoryPool = new Mock>(); + memoryPool.SetupGet(pool => pool.MaxBufferSize).Returns(MemoryPool.Shared.MaxBufferSize); + memoryPool.Setup(pool => pool.Rent(0)).Returns(Mock.Of>()); + memoryPool.Setup(pool => pool.Rent(It.Is(size => size > 0))) + .Returns((int size) => MemoryPool.Shared.Rent(size)); + + using var output = CreateOutputProducer(memoryPool: memoryPool.Object); + + var beforeStartMemoryLength = output.GetMemory(0).Length; + var beforeStartSpanLength = output.GetSpan(0).Length; + + output.Dispose(); + + var completedMemoryLength = output.GetMemory(0).Length; + var completedSpanLength = output.GetSpan(0).Length; + + Assert.All( + [ + (Operation: "GetMemory before response start", Length: beforeStartMemoryLength), + (Operation: "GetSpan before response start", Length: beforeStartSpanLength), + (Operation: "GetMemory after completion", Length: completedMemoryLength), + (Operation: "GetSpan after completion", Length: completedSpanLength), + ], + result => Assert.True(result.Length > 0, $"{result.Operation} returned an empty buffer.")); + } + [Fact] public void AllocatesFakeMemorySmallerThanMaxBufferSize() { @@ -223,7 +252,8 @@ public void ReusesFakeMemory() private TestHttpOutputProducer CreateOutputProducer( PipeOptions pipeOptions = null, ConnectionContext connectionContext = null, - ConnectionMetricsContext metricsContext = null) + ConnectionMetricsContext metricsContext = null, + MemoryPool memoryPool = null) { pipeOptions = pipeOptions ?? new PipeOptions(); connectionContext = connectionContext ?? Mock.Of(); @@ -234,7 +264,7 @@ private TestHttpOutputProducer CreateOutputProducer( pipe, "0", connectionContext, - _memoryPool, + memoryPool ?? _memoryPool, serviceContext.Log, Mock.Of(), Mock.Of(), From d01224037be8b3a06c1f12ce937ecf3ebc43779c Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Wed, 2 Sep 2026 19:45:02 +0200 Subject: [PATCH 2/2] nit --- src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs index a3e55a5c4420..72f46b77d640 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs @@ -151,7 +151,7 @@ public void GetMemoryAndGetSpanWithZeroSizeHintReturnNonEmptyBuffers() var beforeStartMemoryLength = output.GetMemory(0).Length; var beforeStartSpanLength = output.GetSpan(0).Length; - output.Dispose(); + output.Stop(); var completedMemoryLength = output.GetMemory(0).Length; var completedSpanLength = output.GetSpan(0).Length;