Skip to content

FIX: Validate caller-controlled native buffer sizes - #802

Open
gargsaumya wants to merge 5 commits into
mainfrom
saumya/native-size-validation
Open

gargsaumya wants to merge 5 commits into
mainfrom
saumya/native-size-validation

Conversation

@gargsaumya

@gargsaumya gargsaumya commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

ADO Work Item: AB#47843


Summary

  • Reject invalid Python row counts before crossing native boundaries.
  • Check parameter allocation arithmetic and metadata cardinality.
  • Tighten binary and wide-character input validation.

No tracking issue exists for this security hardening work.

Copilot AI lite review requested due to automatic review settings September 21, 2026 06:53
@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown

PR Performance Report

No consistent slowdowns detected across all 2 environments.

Coverage: 2 of 2 environments completed. Advisory result; does not block merging.

Environment Status
Unix / SQL Server 2022 Completed
Unix / SQL Server 2025 Completed
Affected phases and call counts

Phase times are inclusive diagnostics and must not be added together. They identify where measured time changed, not why it changed.

No affected phases or call-count changes were recorded.

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 10.962 ms 10.952 ms +0.9% no signal
SELECT queries 1.157 ms 1.132 ms -2.5% no signal
Row insertion 31.946 ms 31.141 ms -0.9% no signal
Executemany inserts 137.226 ms 134.863 ms -0.4% no signal
Fetch-all queries 165.791 ms 165.848 ms -0.5% no signal
Row-by-row fetching 52.389 ms 51.720 ms -1.0% no signal
Batched row fetching 154.494 ms 160.505 ms +4.1% no signal
Transaction commit and rollback 98.012 ms 98.307 ms +0.5% no signal
Arrow row fetching 91.409 ms 91.418 ms +0.0% no signal
100,000-row insertion 401.425 ms 411.200 ms +1.3% no signal
Row fetching in batches of 100 199.944 ms 200.469 ms -0.6% no signal
Row fetching in batches of 10,000 172.389 ms 170.059 ms -1.6% no signal
Repeated positional queries 38.128 ms 38.103 ms -0.5% no signal
Repeated named-parameter queries 40.871 ms 40.511 ms -0.9% no signal
Legacy 100,000-row insertion 321.292 ms 309.530 ms -2.8% no signal
Insertion with explicit input sizes 2171.115 ms 2200.389 ms +1.9% no signal
Joined aggregation queries 188.055 ms 186.298 ms -0.6% no signal
Large joined-result fetching 205.068 ms 203.912 ms +0.2% no signal
1.2-million-row fetching 5194.218 ms 5250.734 ms +0.0% no signal
Common table expression queries 5.653 ms 5.539 ms -2.3% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 96.951 ms 96.502 ms -0.5% no signal
SELECT queries 1.187 ms 1.228 ms +0.2% no signal
Row insertion 34.537 ms 34.781 ms +0.7% no signal
Executemany inserts 149.395 ms 154.801 ms +2.1% no signal
Fetch-all queries 173.371 ms 180.978 ms +3.8% no signal
Row-by-row fetching 62.061 ms 64.964 ms +2.1% no signal
Batched row fetching 167.085 ms 169.527 ms -1.2% no signal
Transaction commit and rollback 118.757 ms 116.835 ms -1.6% no signal
Arrow row fetching 96.413 ms 94.816 ms -1.7% no signal
100,000-row insertion 456.611 ms 453.387 ms -0.3% no signal
Row fetching in batches of 100 225.604 ms 228.093 ms +1.3% no signal
Row fetching in batches of 10,000 185.495 ms 190.696 ms +1.5% no signal
Repeated positional queries 42.661 ms 42.590 ms +0.1% no signal
Repeated named-parameter queries 44.838 ms 45.046 ms -0.0% no signal
Legacy 100,000-row insertion 352.975 ms 353.886 ms -1.6% no signal
Insertion with explicit input sizes 2389.779 ms 2453.337 ms +2.2% no signal
Joined aggregation queries 160.445 ms 160.368 ms +0.9% no signal
Large joined-result fetching 208.785 ms 206.527 ms -0.9% no signal
1.2-million-row fetching 5156.186 ms 5080.881 ms -1.4% no signal
Common table expression queries 5.262 ms 5.146 ms -1.6% no signal
Build, commits and measurement details

ADO build 176912

PR head: 44982a9c08c2e1e3a381b351f637ee4ae5558d51
Base: c963ee1ebf11a96b2dc5275b465d2a36f3f55d77
Measured merge: 9e0d866dafee16c66dd23c8b2b3bdcc47acc9a83

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4295.3; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5005.3; 5 paired comparisons and 1 warmup.

A consistent change requires more than 20% median paired movement, at least 1 ms between the median runtimes, and at least 80% of pairs exceeding the relative threshold in the same direction. A slowdown without enough pair agreement is reported as inconsistent.

The displayed change is the median of paired before-and-after ratios. It is not recalculated from the two displayed median runtimes.

Both revisions use profiling-enabled builds on the same agent and database, with alternating order and discarded warmups. Results are diagnostic and do not represent production-wheel latency.

Raw samples and logs are attached to the ADO run as profiler-* artifacts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved row-count and binary validation gaps remain, and an existing Arrow batch-size test requires reconciliation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Hardens Python-to-native boundary validation for row counts, allocations, metadata, and binary inputs.

Changes:

  • Adds row-count, boolean, and input-size validation.
  • Adds native allocation overflow and metadata checks.
  • Tightens binary and wide-character handling with regression tests.
File Description
tests/​test_024_bulkcopy_arrow.py Tests boolean batch-size rejection.
tests/​test_004_cursor.py Tests cursor size and metadata validation.
mssql_python/​pybind/​ddbc_bindings.cpp Adds native allocation and parameter validation.
mssql_python/​cursor.py Validates row counts and input sizes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread mssql_python/cursor.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp
Copilot AI review requested due to automatic review settings September 21, 2026 07:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Native paths still permit dangerous negative or extremely large allocations, and arrow_reader() does not reject invalid sizes synchronously.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 4 High severity · 1 Medium severity

Open (5)

Comment thread mssql_python/cursor.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Comment thread mssql_python/pybind/ddbc_bindings.cpp
@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

84%


🎯 Overall Coverage

84%


📈 Total Lines Covered: 8850 out of 10497
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/cursor.py (100%)
  • mssql_python/pybind/ddbc_bindings.cpp (82.8%): Missing lines 326-327,336-337,343-344,362-363,406,2277-2278,2348-2349,2382,2453-2454,2471-2472,2520,4159,4171,4193,4213,4248,4251,4891,4927-4928,4989-4991,5087,5102-5103,5125,5148,5170-5171,5211

Summary

  • Total: 252 lines
  • Missing: 39 lines
  • Coverage: 84%

mssql_python/pybind/ddbc_bindings.cpp

Lines 322-331

  322 
  323 template <typename ParamType>
  324 ParamType* AllocateParamBufferArray(std::vector<std::shared_ptr<void>>& paramBuffers,
  325                                     size_t count) {
! 326     if (count > std::numeric_limits<size_t>::max() / sizeof(ParamType)) {
! 327         ThrowStdException("Parameter buffer size is too large");
  328     }
  329     std::shared_ptr<ParamType> buffer(new ParamType[count], std::default_delete<ParamType[]>());
  330     ParamType* raw = buffer.get();
  331     paramBuffers.push_back(buffer);

Lines 332-341

  332     return raw;
  333 }
  334 
  335 size_t CheckedAddSize(size_t left, size_t right, const char* errorMessage) {
! 336     if (left > std::numeric_limits<size_t>::max() - right) {
! 337         ThrowStdException(errorMessage);
  338     }
  339     return left + right;
  340 }

Lines 339-348

  339     return left + right;
  340 }
  341 
  342 size_t CheckedMultiplySize(size_t left, size_t right, const char* errorMessage) {
! 343     if (left != 0 && right > std::numeric_limits<size_t>::max() / left) {
! 344         ThrowStdException(errorMessage);
  345     }
  346     return left * right;
  347 }

Lines 358-367

  358 }
  359 
  360 template <typename ElementType>
  361 std::unique_ptr<ElementType[]> AllocateUniqueArray(size_t count, const char* errorMessage) {
! 362     if (count > std::numeric_limits<size_t>::max() / sizeof(ElementType)) {
! 363         ThrowStdException(errorMessage);
  364     }
  365     return std::make_unique<ElementType[]>(count);
  366 }

Lines 402-410

  402                                                   const char* errorMessage) {
  403     ReserveNativeFetchBytes(reservedBytes, count, sizeof(ElementType));
  404     return AllocateUniqueArray<ElementType>(count, errorMessage);
  405 }
! 406 
  407 std::string DescribeChar(unsigned char ch) {
  408     if (ch >= 32 && ch <= 126) {
  409         return std::string("'") + static_cast<char>(ch) + "'";
  410     } else {

Lines 2273-2282

  2273     LOG("BindParameterArray: Starting column-wise array binding - "
  2274         "param_count=%zu, param_set_size=%zu",
  2275         columnwise_params.size(), paramSetSize);
  2276     if (columnwise_params.size() != paramInfos.size()) {
! 2277         ThrowStdException("Parameter count does not match parameter metadata count");
! 2278     }
  2279 
  2280     std::vector<std::shared_ptr<void>> tempBuffers;
  2281 
  2282     try {

Lines 2344-2353

  2344                         "param_index=%d, count=%zu, column_size=%zu",
  2345                         paramIndex, paramSetSize, info.columnSize);
  2346                     const size_t elementWidth = CheckedAddSize(
  2347                         info.columnSize, 1, "Wide-character parameter size is too large");
! 2348                     const size_t bufferBytes = CheckedMultiplySize(
! 2349                         elementWidth, sizeof(SQLWCHAR),
  2350                         "Wide-character parameter length is too large");
  2351                     if (bufferBytes > static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
  2352                         ThrowStdException("Wide-character parameter length is too large");
  2353                     }

Lines 2378-2386

  2378                     LOG("BindParameterArray: SQL_C_WCHAR bound - "
  2379                         "param_index=%d",
  2380                         paramIndex);
  2381                     dataPtr = wcharArray;
! 2382                     bufferLength = static_cast<SQLLEN>(bufferBytes);
  2383                     break;
  2384                 }
  2385                 case SQL_C_TINYINT:
  2386                 case SQL_C_UTINYINT: {

Lines 2449-2458

  2449                 case SQL_C_BINARY: {
  2450                     LOG("BindParameterArray: Binding SQL_C_CHAR/BINARY array - "
  2451                         "param_index=%d, count=%zu, column_size=%zu, encoding='%s'",
  2452                         paramIndex, paramSetSize, info.columnSize, charEncoding.c_str());
! 2453                     const size_t elementWidth = CheckedAddSize(
! 2454                         info.columnSize, 1, "Character parameter size is too large");
  2455                     if (elementWidth > static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
  2456                         ThrowStdException("Character parameter length is too large");
  2457                     }
  2458                     char* charArray = AllocateParamBufferArray<char>(

Lines 2467-2476

  2467                                         info.columnSize + 1);
  2468                         } else {
  2469                             if (info.paramCType == SQL_C_BINARY &&
  2470                                 !py::isinstance<py::bytes>(columnValues[i]) &&
! 2471                                 !py::isinstance<py::bytearray>(columnValues[i])) {
! 2472                                 ThrowStdException(MakeParamMismatchErrorStr(info.paramCType,
  2473                                                                             paramIndex));
  2474                             }
  2475                             std::string encodedStr;

Lines 2516-2524

  2516                     LOG("BindParameterArray: SQL_C_CHAR/BINARY bound - "
  2517                         "param_index=%d",
  2518                         paramIndex);
  2519                     dataPtr = charArray;
! 2520                     bufferLength = static_cast<SQLLEN>(elementWidth);
  2521                     break;
  2522                 }
  2523                 case SQL_C_BIT: {
  2524                     LOG("BindParameterArray: Binding SQL_C_BIT array - "

Lines 4155-4163

  4155                 uint64_t fetchBufferSize = columnSize + 1 /*null-terminator*/;
  4156                 ResizeNativeFetchBuffer(buffers.wcharBuffers[col - 1],
  4157                                         CheckedMultiplySize(fetchSize, fetchBufferSize,
  4158                                                             "Native fetch buffer is too large"),
! 4159                                         reservedBytes);
  4160                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_WCHAR, buffers.wcharBuffers[col - 1].data(),
  4161                                      fetchBufferSize * sizeof(SQLWCHAR),
  4162                                      buffers.indicators[col - 1].data());
  4163                 break;

Lines 4167-4175

  4167                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_SLONG, buffers.intBuffers[col - 1].data(),
  4168                                      sizeof(SQLINTEGER), buffers.indicators[col - 1].data());
  4169                 break;
  4170             case SQL_SMALLINT:
! 4171                 ResizeNativeFetchBuffer(buffers.smallIntBuffers[col - 1], fetchSize, reservedBytes);
  4172                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_SSHORT,
  4173                                      buffers.smallIntBuffers[col - 1].data(), sizeof(SQLSMALLINT),
  4174                                      buffers.indicators[col - 1].data());
  4175                 break;

Lines 4189-4197

  4189                                      sizeof(SQLREAL), buffers.indicators[col - 1].data());
  4190                 break;
  4191             case SQL_DECIMAL:
  4192             case SQL_NUMERIC:
! 4193                 ResizeNativeFetchBuffer(
  4194                     buffers.charBuffers[col - 1],
  4195                     CheckedMultiplySize(fetchSize, MAX_DIGITS_IN_NUMERIC,
  4196                                         "Native fetch buffer is too large"),
  4197                     reservedBytes);

Lines 4209-4217

  4209             case SQL_TIMESTAMP:
  4210             case SQL_TYPE_TIMESTAMP:
  4211             case SQL_DATETIME:
  4212                 ResizeNativeFetchBuffer(buffers.timestampBuffers[col - 1], fetchSize,
! 4213                                         reservedBytes);
  4214                 ret = SQLBindCol_ptr(
  4215                     hStmt, col, SQL_C_TYPE_TIMESTAMP, buffers.timestampBuffers[col - 1].data(),
  4216                     sizeof(SQL_TIMESTAMP_STRUCT), buffers.indicators[col - 1].data());
  4217                 break;

Lines 4244-4255

  4244             case SQL_LONGVARBINARY:
  4245                 // TODO: handle variable length data correctly. This logic wont
  4246                 // suffice
  4247                 HandleZeroColumnSizeAtFetch(columnSize);
! 4248                 ResizeNativeFetchBuffer(buffers.charBuffers[col - 1],
  4249                                         CheckedMultiplySize(fetchSize, columnSize,
  4250                                                             "Native fetch buffer is too large"),
! 4251                                         reservedBytes);
  4252                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_BINARY, buffers.charBuffers[col - 1].data(),
  4253                                      columnSize, buffers.indicators[col - 1].data());
  4254                 break;
  4255             case SQL_SS_TIMESTAMPOFFSET:

Lines 4887-4895

  4887     }
  4888 
  4889     // Ensure initial buffer has space for at least the null terminator
  4890     if (dataVec.size() < sizeNullTerminator) {
! 4891         ResizeNativeFetchBuffer(dataVec, sizeNullTerminator, reservedBytes);
  4892     }
  4893 
  4894     while (true) {
  4895         SQLLEN localInd = 0;

Lines 4923-4932

  4923         if (ret == SQL_SUCCESS_WITH_INFO) {
  4924             // Determine how much more space we need
  4925             if (localInd < 0) {
  4926                 // SQL_NO_TOTAL: driver doesn't know total size, double the buffer
! 4927                 end = CheckedMultiplySize(dataVec.size(), 2,
! 4928                                           "Native fetch buffer size is too large");
  4929             } else {
  4930                 // Driver returned total size: allocate exactly what we need
  4931                 assert(localInd % sizeof(T) == 0);
  4932                 end = CheckedAddSize(

Lines 4985-4995

  4985                                int arrowBatchSize,
  4986                                int charCtype) {
  4987     PERF_TIMER("FetchArrowBatch_wrap");
  4988     ValidateNativeRowCount(arrowBatchSize, "Arrow batch size", true);
! 4989     const size_t batchSize = static_cast<size_t>(arrowBatchSize);
! 4990     const size_t offsetCount = CheckedAddSize(batchSize, 1, "Arrow batch size is too large");
! 4991     const size_t initialVarDataSize =
  4992         CheckedMultiplySize(batchSize, 42, "Arrow batch size is too large");
  4993     const size_t bitmapSize =
  4994         CheckedAddSize(batchSize, 7, "Arrow batch size is too large") / 8;
  4995     // Fetch narrow char data as SQL_C_CHAR if on Linux/macOS and configured by the user

Lines 5083-5091

  5083                 arrowColumnProducer->varVal =
  5084                     AllocateArrowArray<uint64_t>(offsetCount, reservedBytes,
  5085                                                  "Arrow offset buffer is too large");
  5086                 ResizeNativeFetchBuffer(arrowColumnProducer->varData, initialVarDataSize,
! 5087                                         reservedBytes);
  5088                 columnVarLen[i] = true;
  5089                 // start at offset 0
  5090                 arrowColumnProducer->varVal[0] = 0;
  5091                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->varVal.get();

Lines 5098-5107

  5098                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->uint8Val.get();
  5099                 break;
  5100             case SQL_SMALLINT:
  5101                 format = "s";
! 5102                 arrowColumnProducer->int16Val =
! 5103                     AllocateArrowArray<int16_t>(batchSize, reservedBytes,
  5104                                                 "Arrow value buffer is too large");
  5105                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->int16Val.get();
  5106                 break;
  5107             case SQL_INTEGER:

Lines 5121-5129

  5121             case SQL_REAL:
  5122                 format = "f";
  5123                 arrowColumnProducer->float32Val =
  5124                     AllocateArrowArray<float>(batchSize, reservedBytes,
! 5125                                               "Arrow value buffer is too large");
  5126                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->float32Val.get();
  5127                 break;
  5128             case SQL_FLOAT:
  5129             case SQL_DOUBLE:

Lines 5144-5152

  5144                 std::memcpy(arrowSchemaPrivateData[i]->format.get(), formatStr.c_str(), formatLen);
  5145                 format = arrowSchemaPrivateData[i]->format.get();
  5146                 arrowColumnProducer->decimalVal =
  5147                     AllocateArrowArray<Int128_t>(batchSize, reservedBytes,
! 5148                                                  "Arrow value buffer is too large");
  5149                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->decimalVal.get();
  5150                 break;
  5151             }
  5152             case SQL_TIMESTAMP:

Lines 5166-5175

  5166                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->tsMicroVal.get();
  5167                 break;
  5168             case SQL_TYPE_DATE:
  5169                 format = "tdD";
! 5170                 arrowColumnProducer->dateVal =
! 5171                     AllocateArrowArray<int32_t>(batchSize, reservedBytes,
  5172                                                 "Arrow value buffer is too large");
  5173                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->dateVal.get();
  5174                 break;
  5175             case SQL_SS_TIME2:

Lines 5207-5215

  5207 
  5208         arrowColumnProducer->valid =
  5209             AllocateArrowArray<uint8_t>(bitmapSize, reservedBytes, "Arrow bitmap is too large");
  5210         // Initialize validity bitmap to all valid
! 5211         std::memset(arrowColumnProducer->valid.get(), 0xFF, bitmapSize);
  5212     }
  5213 
  5214     // Initialize column buffers
  5215     ReserveNativeFetchBytes(


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 64.1%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 79.2%
mssql_python.pybind.connection.connection_pool.cpp: 82.3%
mssql_python.pybind.connection.connection.cpp: 82.5%
mssql_python.row.py: 83.4%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot AI review requested due to automatic review settings September 21, 2026 08:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Binary input validation must also cover array bindings consistently.

Review effort: Lite
Findings: 4 High severity · 1 Medium severity

Open (5)

@github-actions github-actions Bot added the pr-size: medium Moderate update size label Sep 21, 2026
Copilot AI review requested due to automatic review settings September 21, 2026 09:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

arrow_reader() should validate batch_size before fetching the schema and performing native work.

Review effort: Lite
Findings: None

Resolved since last review (5)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: medium Moderate update size

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants