From e4146a56131d8fe390c7059169385f90111df1fe Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Mon, 27 Apr 2026 20:10:30 -0300 Subject: [PATCH] Fix SIGSEGV in SQLCopyDesc when source descriptor is empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OdbcDesc::operator= dereferenced sour.records[n] inside the copy loop without first checking that sour.records was non-NULL. Every other loop in OdbcDesc.cpp guards `if (records)`; this one did not. Reproducer: allocate two statements, fetch their implicit ARDs (no SQLBindCol calls), call SQLCopyDesc on them. Both descriptors have records == NULL and headCount == 0, the loop runs once with n=0, and sour.records[0] crashes with access violation. The Phase 7 regression tests for this bug already shipped (skipped), documenting the exact failure mode at lines 33-34. Un-skip the three tests this fix enables: CopyEmptyARDDoesNotCrash, CopyEmptyToExplicitDescriptor, CopyPopulatedThenEmpty. The four SetDescCount* tests in the same file remain skipped — they exercise a separate Phase 7 fix (SQLSetDescField(SQL_DESC_COUNT) must allocate records) that is not part of this PR. Reported by odbc-crusher v3.5.0-rc1 stress test. --- OdbcDesc.cpp | 3 ++- tests/test_phase7_crusher_fixes.cpp | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/OdbcDesc.cpp b/OdbcDesc.cpp index fcd51e1f..2f264a0d 100644 --- a/OdbcDesc.cpp +++ b/OdbcDesc.cpp @@ -210,7 +210,8 @@ SQLRETURN OdbcDesc::operator =(OdbcDesc &sour) for ( int n = 0 ; n <= headCount ; n++ ) { - DescRecord *srcrec = sour.records[n]; + // sour.records is NULL for an empty source — guard like the other loops in this file. + DescRecord *srcrec = sour.records ? sour.records[n] : NULL; DescRecord &rec = *getDescRecord ( n ); if ( srcrec ) diff --git a/tests/test_phase7_crusher_fixes.cpp b/tests/test_phase7_crusher_fixes.cpp index 2306141e..4ef5151f 100644 --- a/tests/test_phase7_crusher_fixes.cpp +++ b/tests/test_phase7_crusher_fixes.cpp @@ -12,7 +12,6 @@ class CopyDescCrashTest : public OdbcConnectedTest {}; TEST_F(CopyDescCrashTest, CopyEmptyARDDoesNotCrash) { - GTEST_SKIP() << "Requires Phase 7: ODBC Crusher-identified bug fixes (not yet merged)"; // Allocate two statements with no bindings (empty ARDs) SQLHSTMT stmt1 = AllocExtraStmt(); SQLHSTMT stmt2 = AllocExtraStmt(); @@ -49,7 +48,6 @@ TEST_F(CopyDescCrashTest, CopyEmptyARDDoesNotCrash) { } TEST_F(CopyDescCrashTest, CopyEmptyToExplicitDescriptor) { - GTEST_SKIP() << "Requires Phase 7: ODBC Crusher-identified bug fixes (not yet merged)"; // Allocate an explicit descriptor SQLHDESC hExplicit = SQL_NULL_HDESC; SQLRETURN ret = SQLAllocHandle(SQL_HANDLE_DESC, hDbc, &hExplicit); @@ -70,7 +68,6 @@ TEST_F(CopyDescCrashTest, CopyEmptyToExplicitDescriptor) { } TEST_F(CopyDescCrashTest, CopyPopulatedThenEmpty) { - GTEST_SKIP() << "Requires Phase 7: ODBC Crusher-identified bug fixes (not yet merged)"; // First, populate an explicit descriptor by copying a populated ARD SQLINTEGER val = 0; SQLLEN ind = 0;