Skip to content

Commit d062a39

Browse files
committed
Add test for TryReadSqlValueInternal binary bounds check via sql_variant
The binary bounds check in TryReadSqlValueInternal is only reachable through the sql_variant deserialization path (TryReadSqlVariant). For non-variant binary columns, TryReadSqlValue handles them directly without calling TryReadSqlValueInternal. This test injects a sql_variant column containing a BigVarBinary value whose inner data length (8001) exceeds MAXSIZE (8000), verifying the bounds check fires and prevents unbounded heap allocation. Addresses the 2 uncovered lines flagged by Codecov in PR #4340.
1 parent 54a2817 commit d062a39

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,112 @@ public override void Deflate(Stream destination)
821821
}
822822
}
823823

824+
// ──────────────────────────────────────────────────────────────────────────
825+
// Test 10: Post-login batch response — sql_variant with oversized binary
826+
// ──────────────────────────────────────────────────────────────────────────
827+
828+
/// <summary>
829+
/// Verifies that <c>TryReadSqlValueInternal</c> rejects a binary value inside
830+
/// a sql_variant column whose inner data length exceeds
831+
/// <see cref="TdsEnums.MAXSIZE"/> (8000 bytes). The bounds check in the
832+
/// sql_variant deserialization path prevents unbounded heap allocation.
833+
/// </summary>
834+
[Fact]
835+
public void BatchResponse_SqlVariantBinary_OversizedLength_ThrowsParsingError()
836+
{
837+
_server.OnSQLBatchCompleted = responseMessage =>
838+
{
839+
responseMessage.Clear();
840+
841+
// COLMETADATA: one SSVariant column
842+
var metadata = new TDSColMetadataToken();
843+
var col = new TDSColumnData();
844+
col.DataType = TDSDataType.SSVariant;
845+
col.DataTypeSpecific = (uint)8009; // max length for SSVariant
846+
col.Flags.IsNullable = true;
847+
col.Name = string.Empty;
848+
metadata.Columns.Add(col);
849+
responseMessage.Add(metadata);
850+
851+
// ROW with a sql_variant containing oversized binary data
852+
responseMessage.Add(new MaliciousSqlVariantBinaryRowToken());
853+
854+
// DONE
855+
responseMessage.Add(new TDSDoneToken(TDSDoneTokenStatusType.Final | TDSDoneTokenStatusType.Count, TDSDoneTokenCommandType.Select, 1));
856+
};
857+
858+
try
859+
{
860+
using SqlConnection connection = new(_connectionString);
861+
connection.Open();
862+
863+
using SqlCommand command = connection.CreateCommand();
864+
command.CommandText = "MALICIOUS_QUERY_NOT_RECOGNIZED";
865+
866+
SqlDataReader reader = command.ExecuteReader();
867+
try
868+
{
869+
Assert.True(reader.Read());
870+
Exception ex = Assert.ThrowsAny<InvalidOperationException>(
871+
() => reader.GetValue(0));
872+
Assert.Contains("18", ex.Message); // CorruptedTdsStream
873+
}
874+
finally
875+
{
876+
using (new DebugAssertSuppressor())
877+
{
878+
try { reader.Dispose(); } catch { }
879+
}
880+
}
881+
}
882+
finally
883+
{
884+
_server.OnSQLBatchCompleted = null;
885+
}
886+
}
887+
888+
/// <summary>
889+
/// Writes a ROW token (0xD1) with a single SSVariant column containing a
890+
/// BigVarBinary variant whose inner data length exceeds MAXSIZE (8000).
891+
/// Wire layout for the variant:
892+
/// [int32] total variant length = 8005
893+
/// [byte] inner type = 0xA5 (BigVarBinary)
894+
/// [byte] cbPropBytes = 2
895+
/// [ushort] maxLen (property) = 8001
896+
/// [8001 bytes would be data, but we only write 4 to trigger the check]
897+
/// lenData = 8005 - 2(SQLVARIANT_SIZE) - 2(cbProps) = 8001 > MAXSIZE → throws
898+
/// </summary>
899+
private sealed class MaliciousSqlVariantBinaryRowToken : TDSPacketToken
900+
{
901+
public override bool Inflate(Stream source) => throw new NotSupportedException();
902+
903+
public override void Deflate(Stream destination)
904+
{
905+
// ROW token type
906+
destination.WriteByte(0xD1);
907+
908+
// SSVariant column data: total length (int32 LE)
909+
// lenData = totalLength - SQLVARIANT_SIZE(2) - cbPropBytes(2) = totalLength - 4
910+
// We want lenData = 8001, so totalLength = 8005
911+
int totalLength = 8005;
912+
byte[] lenBytes = BitConverter.GetBytes(totalLength);
913+
destination.Write(lenBytes, 0, 4);
914+
915+
// Inner type: BigVarBinary = 0xA5
916+
destination.WriteByte(0xA5);
917+
918+
// cbPropBytes = 2
919+
destination.WriteByte(0x02);
920+
921+
// Properties: maxLen (ushort) = 8001
922+
destination.WriteByte(0x41); // 8001 & 0xFF = 0x41
923+
destination.WriteByte(0x1F); // 8001 >> 8 = 0x1F
924+
925+
// Write 4 bytes of dummy data (bounds check fires before trying to read 8001)
926+
destination.Write(new byte[4], 0, 4);
927+
}
928+
}
929+
824930
/// <summary>
825931
/// Temporarily suppresses Debug.Assert failures by clearing trace listeners.
826932
/// Used when disposing resources after intentionally corrupting a TDS stream.

0 commit comments

Comments
 (0)