Skip to content

Commit 7800803

Browse files
fix: fastbufferreader string deserialization int overflow (#4052)
* fix This fixes an edge case scenario with string reading where if user code has caused the character count to have been already read prior to attempting to read a string value (safely or unsafely) and then reading the string can result in the signed integer size to roll over to a negative value and thus causing the reader to attempt to read into restricted memory outside of the application domain which results in the editor crashing. The fix catches this scenario and throws an overflow exception prior to attempting to read into negative memory space relative to the application domain. * test This test validates the fix. * update Adding changelog entry. * update Making the reader use the already calculated readSize. * test Updating test to unsafely read a string under the same condition. * fix Fixing an issue where the unsafe string read needs to create an empty string based on the character count and not the byte count. * refactor Based on Paolo's suggestion on combining the string size, in bytes, validation script to a single in-lined method shared between the safe and unsafe string read methods. Also combined the actual reading of the string data into a single in-lined method. * style Removing the auto-added (and not used) UnityEngine.UIElements using directive. * style updated comments and renamed CheckIfValidStringLength to ValidateStringByteCount. * test Removing the added 3 bytes used for earlier debugging purposes. * style spelling/typo fix. whitespace after sentence in comment fix. * refactor Based on Emma's suggestion, removing the `SizeOfLengthField` method and `TryBeginReadInternal` check within the ReadValueSafe (string) method and replacing that with `ReadLengthSafe`. * Update com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs Co-authored-by: Emma <emma.mcmillan@unity3d.com> * update Based on suggestion from Emma, removing InBitwiseContext check since this is done in both ReadLengthSafe and TryBeginReadInternal. --------- Co-authored-by: Emma <emma.mcmillan@unity3d.com>
1 parent e6ecb65 commit 7800803

3 files changed

Lines changed: 108 additions & 46 deletions

File tree

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
2121

2222

2323
### Fixed
24+
25+
- Issue when FastBufferReader is attempting to read a string and all or a portion of the character count has already been read by user script, it could read a character length that results in a negative byte length which could result in an editor crash. (#4052)
2426
- Issue where NetworkRigidbodyBase was not applying rotation correctly when using Rigidbody2D. (#4012)
2527
- Issue where NetworkRigidbodyBase was always checking the 3D rigid body's interpolation mode when determining if it is kinematic and needs to put the rigid body to sleep and then switch to interpolation. (#4012)
2628

com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -564,14 +564,36 @@ public void ReadNetworkSerializableInPlace<T>(ref T value) where T : INetworkSer
564564
}
565565

566566
/// <summary>
567-
/// Reads a string
568-
/// NOTE: ALLOCATES
567+
/// Validates the string's total byte count based on whether we are
568+
/// using one or two byte characters.
569+
/// </summary>
570+
/// <remarks>
571+
/// Will throw an overflow exception if the size is greater than <see cref="int.MaxValue"/>.
572+
/// </remarks>
573+
/// <param name="length">Character count</param>
574+
/// <param name="oneByteChars">If false(default) 2 byte characters and if true 1 byte characters</param>
575+
/// <returns>total size in bytes to read</returns>
576+
/// <exception cref="OverflowException"></exception>
577+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
578+
private int ValidateStringByteCount(int length, bool oneByteChars)
579+
{
580+
var readSize = oneByteChars ? length : length * sizeof(char);
581+
if (int.MaxValue < (uint)readSize)
582+
{
583+
throw new OverflowException($"Invalid reader position detected when trying to read a string of size {(uint)readSize}! This can result from an error in the serialization. Ensure deserialization exactly matches what was serialized!");
584+
}
585+
return readSize;
586+
}
587+
588+
/// <summary>
589+
/// Commonly shared string read method between <see cref="ReadValue"/>.
569590
/// </summary>
570-
/// <param name="s">Stores the read string</param>
571-
/// <param name="oneByteChars">Whether or not to use one byte per character. This will only allow ASCII</param>
572-
public unsafe void ReadValue(out string s, bool oneByteChars = false)
591+
/// <param name="s">The output of the string read.</param>
592+
/// <param name="length">The number of characters in the string.</param>
593+
/// <param name="oneByteChars">If false(default) 2 byte characters and if true 1 byte characters.</param>
594+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
595+
private unsafe void ReadString(out string s, int length, bool oneByteChars)
573596
{
574-
ReadLength(out int length);
575597
s = "".PadRight(length);
576598
int target = s.Length;
577599
fixed (char* native = s)
@@ -592,56 +614,50 @@ public unsafe void ReadValue(out string s, bool oneByteChars = false)
592614
}
593615

594616
/// <summary>
595-
/// Reads a string.
596-
/// NOTE: ALLOCATES
597-
///
598-
/// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking
599-
/// for multiple reads at once by calling TryBeginRead.
617+
/// Reads a string without bounds checking.
618+
/// NOTE: This method ALLOCATES memory.
600619
/// </summary>
620+
/// <remarks>
621+
/// This is the un-safe string read which requires invoking <see cref="TryBeginRead(int)"/> prior to invoking this method.<br />
622+
/// Using one byte characters only allows ASCII characters.
623+
/// </remarks>
601624
/// <param name="s">Stores the read string</param>
602-
/// <param name="oneByteChars">Whether or not to use one byte per character. This will only allow ASCII</param>
603-
public unsafe void ReadValueSafe(out string s, bool oneByteChars = false)
625+
/// <param name="oneByteChars">If false(default) 2 byte characters and if true 1 byte characters.</param>
626+
public unsafe void ReadValue(out string s, bool oneByteChars = false)
604627
{
605-
#if DEBUG
606-
if (Handle->InBitwiseContext)
607-
{
608-
throw new InvalidOperationException(
609-
"Cannot use BufferReader in bytewise mode while in a bitwise context.");
610-
}
611-
#endif
628+
ReadLength(out int length);
612629

613-
if (!TryBeginReadInternal(SizeOfLengthField()))
614-
{
615-
throw new OverflowException("Reading past the end of the buffer");
616-
}
630+
// Validate the string's byte count based on the character count.
631+
ValidateStringByteCount(length, oneByteChars);
617632

618-
ReadLength(out int length);
633+
// Read the string
634+
ReadString(out s, length, oneByteChars);
635+
}
636+
637+
/// <summary>
638+
/// Reads a string after it performs bounds checking automatically.
639+
/// NOTE: This method ALLOCATES memory.
640+
/// </summary>
641+
/// <remarks>
642+
/// This is the safe string read which invokes <see cref = "TryBeginReadInternal(int)"/> prior to reading the string.<br />
643+
/// Using one byte characters only allows ASCII characters.
644+
/// </remarks>
645+
/// <param name="s">The string re the read string</param>
646+
/// <param name="oneByteChars">If false(default) 2 byte characters and if true 1 byte characters.</param>
647+
public unsafe void ReadValueSafe(out string s, bool oneByteChars = false)
648+
{
649+
ReadLengthSafe(out int length);
619650

620-
if (!TryBeginReadInternal(length * (oneByteChars ? 1 : sizeof(char))))
651+
// Validate the string's byte count based on the character count and if it is valid begin reading based on the returned
652+
// byte count.
653+
if (!TryBeginReadInternal(ValidateStringByteCount(length, oneByteChars)))
621654
{
622655
throw new OverflowException("Reading past the end of the buffer");
623656
}
624-
s = "".PadRight(length);
625-
int target = s.Length;
626-
fixed (char* native = s)
627-
{
628-
if (oneByteChars)
629-
{
630-
for (int i = 0; i < target; ++i)
631-
{
632-
ReadByte(out byte b);
633-
native[i] = (char)b;
634-
}
635-
}
636-
else
637-
{
638-
ReadBytes((byte*)native, target * sizeof(char));
639-
}
640-
}
641-
}
642657

643-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
644-
private static int SizeOfLengthField() => sizeof(uint);
658+
// Read the string
659+
ReadString(out s, length, oneByteChars);
660+
}
645661

646662
[MethodImpl(MethodImplOptions.AggressiveInlining)]
647663
private void ReadLengthSafe(out uint length) => ReadUnmanagedSafe(out length);

com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,50 @@ public void GivenFastBufferWriterContainingValue_WhenReadingString_ValueMatchesW
924924
}
925925
}
926926

927+
/// <summary>
928+
/// This validates that <see cref="FastBufferReader"/> catches a potential
929+
/// scenario where the string's character count value has already been read
930+
/// due to an error within user script and the resultant character count
931+
/// multiplied times 2 (when using 2 bytes vs 1) causes the length to roll over
932+
/// to a negative value which, in turn, causes the reader to attempt to read
933+
/// into restricted memory and causes the editor to crash.
934+
/// </summary>
935+
[Test]
936+
public void ReadingStringAfterStringLengthHasAlreadyBeenRead([Values] bool isSafeRead)
937+
{
938+
// This was an issue uncovered in UUM-145752 that resulted
939+
// in the below text to result in a length that when using
940+
// 2 bytes per character would cause the skewed size to roll
941+
// over into a negative value causing the editor to crash
942+
// when it attempted to read a large negative offset value.
943+
string valueToTest = "true";
944+
945+
var serializedValueSize = FastBufferWriter.GetWriteSize(valueToTest);
946+
947+
using var writer = new FastBufferWriter(serializedValueSize, Allocator.Temp);
948+
writer.WriteValueSafe(valueToTest);
949+
950+
using var reader = new FastBufferReader(writer, Allocator.Temp);
951+
952+
// Read the value of the character count before trying to read the string
953+
// This mocks user code having read too far into a stream causing the position to be skewed such that
954+
// the string reader reads the some of the bytes for the actual text as the length.
955+
reader.ReadByteSafe(out byte count);
956+
Assert.True(count == valueToTest.Length, $"Count ({count}) is not the expected size of {valueToTest.Length}!");
957+
if (isSafeRead)
958+
{
959+
// This should throw an overflow exception but should not crash the editor.
960+
Assert.Throws<OverflowException>(() => reader.ReadValueSafe(out string valueRead));
961+
}
962+
else
963+
{
964+
// Assume user does a pre-calculation of the size to be read:
965+
Assert.IsTrue(reader.TryBeginRead(count), "Reader denied read permission");
966+
967+
// This should throw an overflow exception but should not crash the editor.
968+
Assert.Throws<OverflowException>(() => reader.ReadValue(out string valueRead));
969+
}
970+
}
927971

928972
[TestCase(1, 0)]
929973
[TestCase(2, 0)]

0 commit comments

Comments
 (0)