Skip to content

Commit 3bb0e4c

Browse files
committed
Add ReturnValue bounds check tests (oversized + PLP regression)
- Test 8: BatchResponse_ReturnValue_OversizedLength injects an IMAGE RETURNVALUE token with data length -1 (huge when cast to ulong), verifying TryProcessReturnValue rejects non-PLP values > int.MaxValue - Test 9: BatchResponse_ReturnValue_PlpUnknownLength sends a valid PLP VARBINARY(MAX) RETURNVALUE with the unknown-length sentinel (0xFFFFFFFFFFFFFFFE) + immediate terminator, confirming the PLP path is not rejected by the bounds check
1 parent 0f11e87 commit 3bb0e4c

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

src/Microsoft.Data.SqlClient/tests/UnitTests/SimulatedServerTests/TdsTokenBoundsTests.cs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,237 @@ public override void Deflate(Stream destination)
590590
}
591591
}
592592

593+
// ──────────────────────────────────────────────────────────────────────────
594+
// Test 8: Post-login batch response — ReturnValue with oversized data length
595+
// ──────────────────────────────────────────────────────────────────────────
596+
597+
/// <summary>
598+
/// Verifies that <c>TryProcessReturnValue</c> rejects a RETURNVALUE token (0xAC)
599+
/// whose inner data length (for a non-PLP IMAGE type) exceeds int.MaxValue.
600+
/// A malicious server can craft a TEXT/IMAGE return value with a spoofed int32
601+
/// data length that becomes a huge value when cast to ulong, triggering unbounded
602+
/// allocation.
603+
/// </summary>
604+
[Fact]
605+
public void BatchResponse_ReturnValue_OversizedLength_ThrowsParsingError()
606+
{
607+
_server.OnSQLBatchCompleted = responseMessage =>
608+
{
609+
int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken);
610+
if (doneIndex < 0)
611+
{
612+
doneIndex = responseMessage.Count;
613+
}
614+
615+
responseMessage.Insert(doneIndex, new MaliciousReturnValueToken());
616+
};
617+
618+
try
619+
{
620+
using SqlConnection connection = new(_connectionString);
621+
connection.Open();
622+
623+
using SqlCommand command = connection.CreateCommand();
624+
command.CommandText = "SELECT 1";
625+
626+
using (new DebugAssertSuppressor())
627+
{
628+
Exception ex = Assert.ThrowsAny<InvalidOperationException>(
629+
() => command.ExecuteNonQuery());
630+
Assert.Contains("18", ex.Message); // CorruptedTdsStream
631+
}
632+
}
633+
finally
634+
{
635+
_server.OnSQLBatchCompleted = null;
636+
}
637+
}
638+
639+
/// <summary>
640+
/// Writes a RETURNVALUE token (0xAC) with an IMAGE (0x22) type whose data
641+
/// length field is set to -1 (0xFFFFFFFF). When cast to ulong this exceeds
642+
/// int.MaxValue, triggering the bounds check in TryProcessReturnValue.
643+
/// Wire layout:
644+
/// [0xAC] token
645+
/// [uint16] ordinal
646+
/// [byte] param name length (0)
647+
/// [byte] status
648+
/// [uint32] user type
649+
/// [byte] flags1
650+
/// [byte] flags2
651+
/// [byte] tds type = 0x22 (IMAGE)
652+
/// [int32] max length
653+
/// [byte] textPtrLen = 16
654+
/// [16 bytes] textPtr
655+
/// [8 bytes] timestamp
656+
/// [int32] data length = -1 (INVALID)
657+
/// </summary>
658+
private sealed class MaliciousReturnValueToken : TDSPacketToken
659+
{
660+
public override bool Inflate(Stream source) => throw new NotSupportedException();
661+
662+
public override void Deflate(Stream destination)
663+
{
664+
// RETURNVALUE token
665+
destination.WriteByte(0xAC);
666+
667+
// Ordinal (uint16 LE)
668+
destination.WriteByte(0x00);
669+
destination.WriteByte(0x00);
670+
671+
// Param name length (byte) = 0
672+
destination.WriteByte(0x00);
673+
674+
// Status (byte) = 0x01 (output parameter)
675+
destination.WriteByte(0x01);
676+
677+
// UserType (uint32 LE) = 0
678+
destination.Write(new byte[4], 0, 4);
679+
680+
// Flags byte 1 (ignored)
681+
destination.WriteByte(0x00);
682+
683+
// Flags byte 2
684+
destination.WriteByte(0x00);
685+
686+
// TDS type = SQLIMAGE (0x22)
687+
destination.WriteByte(0x22);
688+
689+
// MaxLen (int32 LE) — for IMAGE this is read via TryGetTokenLength
690+
// which for 0x22 reads int32. Value doesn't matter much, just needs
691+
// to be valid for MetaType lookup.
692+
destination.Write(new byte[] { 0x10, 0x00, 0x00, 0x00 }, 0, 4); // 16
693+
694+
// -- TryProcessColumnHeaderNoNBC: IsLong && !IsPlp path --
695+
// TextPtr length (byte) = 16
696+
destination.WriteByte(0x10);
697+
698+
// TextPtr data (16 bytes)
699+
destination.Write(new byte[16], 0, 16);
700+
701+
// Timestamp (8 bytes)
702+
destination.Write(new byte[8], 0, 8);
703+
704+
// Data length (int32 LE) = -1 (0xFFFFFFFF)
705+
// (ulong)(-1) = 0xFFFFFFFFFFFFFFFF > int.MaxValue → triggers bounds check
706+
destination.WriteByte(0xFF);
707+
destination.WriteByte(0xFF);
708+
destination.WriteByte(0xFF);
709+
destination.WriteByte(0xFF);
710+
}
711+
}
712+
713+
// ──────────────────────────────────────────────────────────────────────────
714+
// Test 9: Post-login batch response — PLP ReturnValue (regression guard)
715+
// ──────────────────────────────────────────────────────────────────────────
716+
717+
/// <summary>
718+
/// Verifies that <c>TryProcessReturnValue</c> correctly handles PLP
719+
/// (Partially Length-Prefixed) return values without triggering the bounds
720+
/// check. PLP types use the unknown-length sentinel (0xFFFFFFFFFFFFFFFE)
721+
/// which must NOT be rejected by the non-PLP bounds check.
722+
/// </summary>
723+
[Fact]
724+
public void BatchResponse_ReturnValue_PlpUnknownLength_Succeeds()
725+
{
726+
_server.OnSQLBatchCompleted = responseMessage =>
727+
{
728+
int doneIndex = responseMessage.FindIndex(t => t is TDSDoneToken);
729+
if (doneIndex < 0)
730+
{
731+
doneIndex = responseMessage.Count;
732+
}
733+
734+
responseMessage.Insert(doneIndex, new ValidPlpReturnValueToken());
735+
};
736+
737+
try
738+
{
739+
using SqlConnection connection = new(_connectionString);
740+
connection.Open();
741+
742+
using SqlCommand command = connection.CreateCommand();
743+
command.CommandText = "SELECT 1";
744+
745+
// Should NOT throw — PLP unknown-length sentinel is valid
746+
command.ExecuteNonQuery();
747+
}
748+
finally
749+
{
750+
_server.OnSQLBatchCompleted = null;
751+
}
752+
}
753+
754+
/// <summary>
755+
/// Writes a valid RETURNVALUE token (0xAC) with a PLP VARBINARY(MAX) type
756+
/// using the unknown-length sentinel (0xFFFFFFFFFFFFFFFE) followed by an
757+
/// immediate PLP terminator (chunk length = 0). This exercises the IsPlp
758+
/// branch in TryProcessReturnValue and must NOT trigger the bounds check.
759+
/// Wire layout:
760+
/// [0xAC] token
761+
/// [uint16] ordinal
762+
/// [byte] param name length (0)
763+
/// [byte] status
764+
/// [uint32] user type
765+
/// [byte] flags1
766+
/// [byte] flags2
767+
/// [byte] tds type = 0xA5 (BIGVARBINARY)
768+
/// [uint16] max length = 0xFFFF (PLP marker)
769+
/// [uint64] PLP length = 0xFFFFFFFFFFFFFFFE (unknown)
770+
/// [uint32] chunk length = 0 (terminator)
771+
/// </summary>
772+
private sealed class ValidPlpReturnValueToken : TDSPacketToken
773+
{
774+
public override bool Inflate(Stream source) => throw new NotSupportedException();
775+
776+
public override void Deflate(Stream destination)
777+
{
778+
// RETURNVALUE token
779+
destination.WriteByte(0xAC);
780+
781+
// Ordinal (uint16 LE)
782+
destination.WriteByte(0x00);
783+
destination.WriteByte(0x00);
784+
785+
// Param name length (byte) = 0
786+
destination.WriteByte(0x00);
787+
788+
// Status (byte) = 0x01 (output parameter)
789+
destination.WriteByte(0x01);
790+
791+
// UserType (uint32 LE) = 0
792+
destination.Write(new byte[4], 0, 4);
793+
794+
// Flags byte 1 (ignored)
795+
destination.WriteByte(0x00);
796+
797+
// Flags byte 2
798+
destination.WriteByte(0x00);
799+
800+
// TDS type = SQLBIGVARBINARY (0xA5)
801+
destination.WriteByte(0xA5);
802+
803+
// MaxLen (uint16 LE) = 0xFFFF — PLP marker
804+
destination.WriteByte(0xFF);
805+
destination.WriteByte(0xFF);
806+
807+
// -- TryProcessColumnHeaderNoNBC: non-IsLong path --
808+
// TryGetDataLength → TryReadPlpLength:
809+
// reads uint64 = 0xFFFFFFFFFFFFFFFE (unknown length sentinel)
810+
destination.WriteByte(0xFE);
811+
destination.WriteByte(0xFF);
812+
destination.WriteByte(0xFF);
813+
destination.WriteByte(0xFF);
814+
destination.WriteByte(0xFF);
815+
destination.WriteByte(0xFF);
816+
destination.WriteByte(0xFF);
817+
destination.WriteByte(0xFF);
818+
819+
// PLP chunk terminator (uint32 = 0) — empty data
820+
destination.Write(new byte[4], 0, 4);
821+
}
822+
}
823+
593824
/// <summary>
594825
/// Temporarily suppresses Debug.Assert failures by clearing trace listeners.
595826
/// Used when disposing resources after intentionally corrupting a TDS stream.

0 commit comments

Comments
 (0)