Repository navigation
Validate client split message count before reserving buffer - #1088
Open
jimdroberts wants to merge 1 commit into
Open
jimdroberts wants to merge 1 commit into
jimdroberts wants to merge 1 commit into
Conversation
The split count is read from the wire before the connection is authenticated and was used directly to size the SplitReader buffer. Kick connections sending a count outside what a client can produce and never reserve more than MaximumClientPacketSize for client senders.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The server sizes the buffer for an incoming split message from the message count in the split header, and it
does this before the connection has authenticated. The count is not validated, so a client can make the server
reserve far more memory than
TransportManager.MaximumClientPacketSize(20480 bytes by default) permits a clientto send in total: up to roughly 2 GB for a single split header.
Since split readers became per-connection and pooled, the reserved buffer also outlives the message: the
SplitReaderis returned toResettableObjectCaches<SplitReader>with its writer at the enlarged capacity(
Writer.Clear()does not shrink), so the memory is retained for the life of the process.Found while reviewing the receive path.
Cause
ServerManager.ParseReceived(Runtime/Managing/Server/ServerManager.cs:767) readsexpectedMessageswithreader.ReadInt32()and passes it straight toconnection.TryGetSplitReader(expectedMessages, ...)(line 769).This runs before the authentication check, which only happens per packet id inside the parse loop (line 810).
NetworkConnection.TryGetSplitReader(Runtime/Connection/NetworkConnection.Buffer.cs:59-60) retrieves apooled
SplitReaderand callsInitialize(..., expectedMessages).SplitReader.Initialize(Runtime/Managing/Transporting/SplitReader.cs:50-52) computesexpectedMessages * 1500and callsEnsureBufferCapacity, which is anArray.Resizeto that size.SplitReader.Write(line 64), after the buffer has alreadybeen resized.
A legitimate client never sends more than
MaximumClientPacketSizebytes in a split:TransportManager.SendSplitMessage(TransportManager.cs:605) refuses to send whenmessageCount * maximumSegmentLength > _maximumClientPacketSize. So the largest count a client produces isfloor(MaximumClientPacketSize / maximumSegmentLength), and it is always at least 1.Fix
TransportManager.GetMaximumClientSplitMessageCount()returnsceil(MaximumClientPacketSize / maximumSegmentLength), using the same_maximumSplitPacketSegmentLengththesender uses. Rounding up leaves one message of headroom over what the sender can produce.
ServerManager.ParseReceivedkicks withKickReason.UnusualActivitywhen the count is below 1 or above thatbound, logging at normal level like the neighbouring split checks (to avoid log spam).
SplitReader.Initializenever reserves more thanmaximumClientByteswhen the sender is a client, so thereservation stays bounded even if another caller passes an unchecked count.
The server-to-client direction (
ClientManager) is unchanged; the client already trusts the server.Evidence
Verified with a small arithmetic model of
SplitReader.InitializeandSendSplitMessage(defaultMaximumClientPacketSize20480, Tugboat MTU 1282 minus the default reserves, so a segment length of 1270):count * 1500bytes before any byte limit is checked. Forcounts in the high hundreds of thousands, that is hundreds of MB to about 2 GB. With this change such
counts are kicked before a reader is retrieved.
Behavior change
ceil(MaximumClientPacketSize / segment length)is kicked(
UnusualActivity) instead of being accepted. A stock FishNet client cannot produce such a count, providedclient and server report the same reliable-channel MTU and MTU reserve. The existing per-packet MTU kick in the
same method already assumes this.
MaximumClientPacketSize.