@@ -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