-
Notifications
You must be signed in to change notification settings - Fork 877
Enforcing the frame size for read #3782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -468,6 +468,10 @@ internal async Task Open(NpgsqlTimeout timeout, bool async, CancellationToken ca | |
| { | ||
| await Authenticate(username, timeout, async, cancellationToken); | ||
|
|
||
| #if DEBUG | ||
| ReadBuffer.EnableFrameTracking = true; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any particular reason this shouldn't always be on (in DEBUG)? We could also use it to catch bugs in the Startup message and in authentication... |
||
| #endif | ||
|
|
||
| // We treat BackendKeyData as optional because some PostgreSQL-like database | ||
| // don't send it (CockroachDB, CrateDB) | ||
| var msg = await ReadMessage(async); | ||
|
|
@@ -1178,6 +1182,10 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d | |
| return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: readingNotifications); | ||
| } | ||
|
|
||
| #if DEBUG | ||
| ReadBuffer.NextFrame(); | ||
| #endif | ||
|
|
||
| var messageCode = (BackendMessageCode)ReadBuffer.ReadByte(); | ||
| switch (messageCode) | ||
| { | ||
|
|
@@ -1186,6 +1194,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d | |
| case BackendMessageCode.ParameterStatus: | ||
| case BackendMessageCode.ErrorResponse: | ||
| ReadBuffer.ReadPosition--; | ||
| #if DEBUG | ||
| ReadBuffer.ResetFrame(); | ||
| #endif | ||
| return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: false); | ||
| case BackendMessageCode.ReadyForQuery: | ||
| break; | ||
|
|
@@ -1196,6 +1207,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d | |
| if (len > ReadBuffer.ReadBytesLeft) | ||
| { | ||
| ReadBuffer.ReadPosition -= 5; | ||
| #if DEBUG | ||
| ReadBuffer.ResetFrame(); | ||
| #endif | ||
| return ReadMessageLong(this, async, dataRowLoadingMode, readingNotifications: false); | ||
| } | ||
|
|
||
|
|
@@ -1233,6 +1247,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d | |
| while (true) | ||
| { | ||
| await connector.ReadBuffer.Ensure(5, async, readingNotifications); | ||
| #if DEBUG | ||
| connector.ReadBuffer.NextFrame(); | ||
| #endif | ||
| var messageCode = (BackendMessageCode)connector.ReadBuffer.ReadByte(); | ||
| PGUtil.ValidateBackendMessageCode(messageCode); | ||
| var len = connector.ReadBuffer.ReadInt32() - 4; // Transmitted length includes itself | ||
|
|
@@ -1244,6 +1261,9 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d | |
| if (dataRowLoadingMode == DataRowLoadingMode.Skip) | ||
| { | ||
| await connector.ReadBuffer.Skip(len, async); | ||
| #if DEBUG | ||
| connector.ReadBuffer.ResetFrame(); | ||
| #endif | ||
| continue; | ||
| } | ||
| } | ||
|
|
@@ -1289,6 +1309,10 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d | |
| throw connector.Break(error); | ||
| } | ||
|
|
||
| #if DEBUG | ||
| connector.ReadBuffer.ResetFrame(); | ||
| #endif | ||
|
|
||
| continue; | ||
|
|
||
| case BackendMessageCode.ReadyForQuery: | ||
|
|
@@ -1307,7 +1331,12 @@ internal ValueTask<IBackendMessage> ReadMessage(bool async, DataRowLoadingMode d | |
| case BackendMessageCode.ParameterStatus: | ||
| Debug.Assert(msg == null); | ||
| if (!readingNotifications) | ||
| { | ||
| #if DEBUG | ||
| connector.ReadBuffer.ResetFrame(); | ||
| #endif | ||
| continue; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,7 +74,87 @@ internal TimeSpan Timeout | |
| /// </summary> | ||
| internal Encoding RelaxedTextEncoding { get; } | ||
|
|
||
| #if DEBUG | ||
| internal bool EnableFrameTracking { get; set; } | ||
|
|
||
| internal void NextFrame() | ||
| { | ||
| Debug.Assert(_currentFrame.LengthLeft == 0, "Protocol failure"); | ||
| _inFrame = false; | ||
| } | ||
|
|
||
| internal void ResetFrame() | ||
| { | ||
| _inFrame = true; | ||
| if (_previousFrame.HasValue) | ||
| _currentFrame = _previousFrame.Value; | ||
| } | ||
|
|
||
| bool _inFrame; | ||
|
|
||
| bool _bufferReset; | ||
|
|
||
| struct Frame | ||
| { | ||
| public Frame(byte code, int length, int lengthLeft) | ||
| { | ||
| Code = code; | ||
| Length = length; | ||
| LengthLeft = lengthLeft; | ||
| } | ||
|
|
||
| public readonly byte Code; | ||
| public readonly int Length; | ||
| public int LengthLeft; | ||
| } | ||
|
|
||
| Frame _currentFrame; | ||
| Frame? _previousFrame; | ||
|
|
||
| int _readPosition; | ||
|
|
||
| internal int ReadPosition | ||
| { | ||
| get => _readPosition; | ||
| set | ||
| { | ||
| if (EnableFrameTracking) | ||
| { | ||
| var positionDelta = value - _readPosition; | ||
|
|
||
| // Setting the same position as the one we already have | ||
| if (positionDelta == 0) | ||
| return; | ||
|
|
||
| // Buffer reset | ||
| if (!_bufferReset) | ||
| { | ||
| if (!_inFrame) | ||
| { | ||
| Debug.Assert(positionDelta == 1, "Protocol failure"); | ||
| Debug.Assert(ReadBytesLeft >= 5); | ||
| var frameCode = Buffer[_readPosition]; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convert to BackendMessageCode? |
||
| var frameLength = BinaryPrimitives.ReverseEndianness(Unsafe.ReadUnaligned<int>(ref Buffer[_readPosition + 1])) + 1; | ||
| var frameLengthLeft = frameLength - 1; | ||
| _previousFrame = _currentFrame; | ||
| _currentFrame = new Frame(frameCode, frameLength, frameLengthLeft); | ||
| _inFrame = true; | ||
| } | ||
| else | ||
| { | ||
| _currentFrame.LengthLeft -= positionDelta; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe better to have Position (in the frame) - just like the the buffer's ReadPosition - which gets incremented, rather than LengthLeft which gets decremented. |
||
| Debug.Assert(_currentFrame.LengthLeft <= _currentFrame.Length && _currentFrame.LengthLeft >= 0, "Going outside of frame bounds"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| _readPosition = value; | ||
| } | ||
| } | ||
| #else | ||
| internal int ReadPosition { get; set; } | ||
| #endif | ||
|
|
||
| internal int ReadBytesLeft => FilledBytes - ReadPosition; | ||
|
|
||
| internal readonly byte[] Buffer; | ||
|
|
@@ -159,9 +239,17 @@ static async Task EnsureLong( | |
| } | ||
| else if (count > buffer.Size - buffer.FilledBytes) | ||
| { | ||
| #if DEBUG | ||
| buffer._bufferReset = true; | ||
| #endif | ||
|
|
||
| Array.Copy(buffer.Buffer, buffer.ReadPosition, buffer.Buffer, 0, buffer.ReadBytesLeft); | ||
| buffer.FilledBytes = buffer.ReadBytesLeft; | ||
| buffer.ReadPosition = 0; | ||
|
|
||
| #if DEBUG | ||
| buffer._bufferReset = false; | ||
| #endif | ||
| } | ||
|
|
||
| var finalCt = async && buffer.Timeout != TimeSpan.Zero | ||
|
|
@@ -277,6 +365,15 @@ internal NpgsqlReadBuffer AllocateOversize(int count) | |
| tempBuf.Timeout = Timeout; | ||
| CopyTo(tempBuf); | ||
| Clear(); | ||
|
|
||
| #if DEBUG | ||
| tempBuf._currentFrame = _currentFrame; | ||
| tempBuf._previousFrame = _previousFrame; | ||
| tempBuf._inFrame = _inFrame; | ||
| tempBuf.EnableFrameTracking = EnableFrameTracking; | ||
| ResetFrame(); | ||
| #endif | ||
|
|
||
| return tempBuf; | ||
| } | ||
|
|
||
|
|
@@ -637,8 +734,14 @@ public void Dispose() | |
|
|
||
| internal void Clear() | ||
| { | ||
| #if DEBUG | ||
| _bufferReset = true; | ||
| #endif | ||
| ReadPosition = 0; | ||
| FilledBytes = 0; | ||
| #if DEBUG | ||
| _bufferReset = false; | ||
| #endif | ||
| } | ||
|
|
||
| internal void CopyTo(NpgsqlReadBuffer other) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than wrapping each call with
#if DEBUG, the methods themselves can have[Conditional("DEBUG")]. This automatically does what we want, and makes the call sites much nicer.