diff --git a/Projects/Server.Tests/Tests/Network/NetworkCompressionBoundsTests.cs b/Projects/Server.Tests/Tests/Network/NetworkCompressionBoundsTests.cs new file mode 100644 index 000000000..c1bc6a800 --- /dev/null +++ b/Projects/Server.Tests/Tests/Network/NetworkCompressionBoundsTests.cs @@ -0,0 +1,86 @@ +using System; +using Server.Network; +using Xunit; + +namespace Server.Tests.Network; + +/// +/// Bounds behaviour of the Huffman compressor when the destination is too small. +/// +/// This is reachable in production: NetState only checks that the send buffer has *some* writable +/// space before handing the remainder to Compress, so a nearly-full buffer can offer a span of one +/// to three bytes. The internal guard is computed as an unsigned output.Length - 4, which +/// underflows for those sizes and stops bounding the writes at all. +/// +public class NetworkCompressionBoundsTests +{ + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void RefusesOutputTooSmallToBound(int outputSize) + { + var input = new byte[64]; + Array.Fill(input, (byte)'A'); + + // Sentinel-filled backing array; only the middle window is offered to the compressor, so + // any write past the span shows up as a modified sentinel rather than silent corruption. + var backing = new byte[256]; + Array.Fill(backing, (byte)0xCC); + + const int windowStart = 64; + var output = backing.AsSpan(windowStart, outputSize); + + var written = NetworkCompression.Compress(input, output); + + Assert.Equal(0, written); + + for (var i = 0; i < backing.Length; i++) + { + Assert.Equal(0xCC, backing[i]); + } + } + + [Fact] + public void StillCompressesWhenOutputIsLargeEnough() + { + var input = new byte[64]; + Array.Fill(input, (byte)'A'); + + var output = new byte[256]; + + var written = NetworkCompression.Compress(input, output); + + Assert.True(written > 0); + Assert.True(written <= output.Length); + } + + [Fact] + public void ReportsFailureRatherThanOverrunningATightOutput() + { + // Large input against a small-but-bounded output: the guard is well-defined here, so this + // must fail cleanly rather than write past the end. + var input = new byte[4096]; + Array.Fill(input, (byte)'A'); + + var backing = new byte[256]; + Array.Fill(backing, (byte)0xCC); + + const int windowStart = 64; + const int windowSize = 16; + var output = backing.AsSpan(windowStart, windowSize); + + NetworkCompression.Compress(input, output); + + for (var i = 0; i < windowStart; i++) + { + Assert.Equal(0xCC, backing[i]); + } + + for (var i = windowStart + windowSize; i < backing.Length; i++) + { + Assert.Equal(0xCC, backing[i]); + } + } +} diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index 6c6262ae4..05d8a8f16 100755 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -444,17 +444,36 @@ public partial class NetState : IComparable, IValueLinkListNode buffer.Length) + { + SendBufferExhausted(span.Length, buffer.Length); + return; } else { @@ -484,6 +503,31 @@ public partial class NetState : IComparable, IValueLinkListNode + /// Handles a packet that cannot be placed in the send buffer. + /// + /// + /// High unacked means a slow client holding the buffer; needed approaching capacity means the + /// buffer is too small for this shard and network.sendBufferSize should be raised. + /// + private void SendBufferExhausted(int needed, int writable) + { + var sendBuffer = _socket?.SendBuffer; + var unacked = sendBuffer?.InFlightBytes ?? 0; + var capacity = sendBuffer?.PhysicalSize ?? 0; + + logger.Warning( + "{NetState}: send buffer exhausted - needed {Needed} bytes, {Writable} writable, {Unacked} awaiting acknowledgement, {Capacity} capacity. Raise network.sendBufferSize (power of two) if this recurs on healthy connections.", + this, + needed, + writable, + unacked, + capacity + ); + + Disconnect($"Send buffer exhausted (needed {needed}, writable {writable}, unacked {unacked}, capacity {capacity})"); + } + private void StartPacketLog() { try diff --git a/Projects/Server/Network/NetworkCompression.cs b/Projects/Server/Network/NetworkCompression.cs index 1a0154d40..12b4108da 100644 --- a/Projects/Server/Network/NetworkCompression.cs +++ b/Projects/Server/Network/NetworkCompression.cs @@ -73,7 +73,9 @@ public static class NetworkCompression public static int Compress(ReadOnlySpan input, Span output) { - if (input.Length > DefiniteOverflow) + // output.Length < 4 underflows safeOutputLength below (nuint), defeating the hot loop's + // bounds check. Reachable whenever the send buffer is nearly full. + if (input.Length > DefiniteOverflow || output.Length < 4) { return 0; }