From ef3d75e7b92a09913fca07c9e6e2111500cdd4ab Mon Sep 17 00:00:00 2001 From: Leonid Krugliak Date: Thu, 2 Jul 2026 12:40:48 +0300 Subject: [PATCH 1/5] Add C API test: scalar bind with correlated subquery does not crash BoundSubqueryExpression::Copy() throws SerializationException when a scalar UDF with a ScalarBinder receives a correlated subquery argument. Before the fix in duckdb_scalar_function_bind_get_argument, this exception escaped the C API boundary and called std::terminate() because CGo frames have no C++ unwind tables. Co-Authored-By: Claude Sonnet 4.6 --- test/api/capi/capi_scalar_functions.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/api/capi/capi_scalar_functions.cpp b/test/api/capi/capi_scalar_functions.cpp index 60476f6f1121..3116a6d2bc35 100644 --- a/test/api/capi/capi_scalar_functions.cpp +++ b/test/api/capi/capi_scalar_functions.cpp @@ -558,6 +558,14 @@ TEST_CASE("Test Scalar Function with Bind Info", "[capi]") { result = tester.Query("SELECT get_connection_id(200::UTINYINT + 200::UTINYINT)"); REQUIRE_FAIL(result); REQUIRE(StringUtil::Contains(result->ErrorMessage(), "Overflow in addition of")); + + // Correlated subquery as argument: BoundSubqueryExpression::Copy() throws SerializationException. + // Before the fix, this exception escaped the C API boundary and called std::terminate() because + // Go's CGo frames (and plain C callers) have no C++ unwind tables. The fix catches at the C + // boundary and surfaces it as a normal query error. + result = tester.Query( + "SELECT get_connection_id((SELECT 42::UBIGINT)) FROM (VALUES(1)) t(x)"); + REQUIRE_FAIL(result); } TEST_CASE("Test volatile scalar function with bind in WHERE clause", "[capi]") { From a907366b1ab8146edc60731e7c0ea9b7ac681c4e Mon Sep 17 00:00:00 2001 From: Leonid Krugliak Date: Thu, 2 Jul 2026 12:43:06 +0300 Subject: [PATCH 2/5] Fix: catch C++ exception in duckdb_scalar_function_bind_get_argument BoundSubqueryExpression::Copy() throws SerializationException when a scalar UDF with a ScalarBinder receives a correlated subquery argument. The exception escaped the C API boundary through CGo/C frames that have no C++ unwind tables, reaching std::terminate() -> abort(). Catch at the C boundary, surface via duckdb_scalar_function_bind_set_error, and return nullptr so callers see a normal query error instead of a crash. Co-Authored-By: Claude Sonnet 4.6 --- src/main/capi/scalar_function-c.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/capi/scalar_function-c.cpp b/src/main/capi/scalar_function-c.cpp index 1653b19450e4..cef1edc36085 100644 --- a/src/main/capi/scalar_function-c.cpp +++ b/src/main/capi/scalar_function-c.cpp @@ -354,9 +354,14 @@ duckdb_expression duckdb_scalar_function_bind_get_argument(duckdb_bind_info info return nullptr; } auto &bind_info = GetCScalarFunctionBindInfo(info); - auto wrapper = new ExpressionWrapper(); - wrapper->expr = bind_info.arguments[index]->Copy(); - return reinterpret_cast(wrapper); + try { + auto wrapper = new ExpressionWrapper(); + wrapper->expr = bind_info.arguments[index]->Copy(); + return reinterpret_cast(wrapper); + } catch (std::exception &e) { + duckdb_scalar_function_bind_set_error(info, e.what()); + return nullptr; + } } void duckdb_scalar_function_set_extra_info(duckdb_scalar_function function, void *extra_info, From abcb0c5026a0948b8c084da0ae4e4ac37d4089c0 Mon Sep 17 00:00:00 2001 From: Leonid Krugliak Date: Thu, 2 Jul 2026 12:56:54 +0300 Subject: [PATCH 3/5] Fix format: join query string onto single line Co-Authored-By: Claude Sonnet 4.6 --- test/api/capi/capi_scalar_functions.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/api/capi/capi_scalar_functions.cpp b/test/api/capi/capi_scalar_functions.cpp index 3116a6d2bc35..401bd75b6b5f 100644 --- a/test/api/capi/capi_scalar_functions.cpp +++ b/test/api/capi/capi_scalar_functions.cpp @@ -563,8 +563,7 @@ TEST_CASE("Test Scalar Function with Bind Info", "[capi]") { // Before the fix, this exception escaped the C API boundary and called std::terminate() because // Go's CGo frames (and plain C callers) have no C++ unwind tables. The fix catches at the C // boundary and surfaces it as a normal query error. - result = tester.Query( - "SELECT get_connection_id((SELECT 42::UBIGINT)) FROM (VALUES(1)) t(x)"); + result = tester.Query("SELECT get_connection_id((SELECT 42::UBIGINT)) FROM (VALUES(1)) t(x)"); REQUIRE_FAIL(result); } From 98b42621fabb3d02dec52495316fc0698663bae1 Mon Sep 17 00:00:00 2001 From: Leonid Krugliak Date: Thu, 2 Jul 2026 15:39:17 +0300 Subject: [PATCH 4/5] Fix memory leak in duckdb_scalar_function_bind_get_argument catch block ExpressionWrapper was allocated before Copy() threw, but the catch block returned nullptr without freeing it. LeakSanitizer caught this on the upstream CI run. Co-Authored-By: Claude Sonnet 4.6 --- src/main/capi/scalar_function-c.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/capi/scalar_function-c.cpp b/src/main/capi/scalar_function-c.cpp index cef1edc36085..64fac529b62e 100644 --- a/src/main/capi/scalar_function-c.cpp +++ b/src/main/capi/scalar_function-c.cpp @@ -354,11 +354,13 @@ duckdb_expression duckdb_scalar_function_bind_get_argument(duckdb_bind_info info return nullptr; } auto &bind_info = GetCScalarFunctionBindInfo(info); + ExpressionWrapper *wrapper = nullptr; try { - auto wrapper = new ExpressionWrapper(); + wrapper = new ExpressionWrapper(); wrapper->expr = bind_info.arguments[index]->Copy(); return reinterpret_cast(wrapper); } catch (std::exception &e) { + delete wrapper; duckdb_scalar_function_bind_set_error(info, e.what()); return nullptr; } From 248d152e45d969c8eb59c900a8b7852600d91e65 Mon Sep 17 00:00:00 2001 From: Leonid Krugliak Date: Thu, 2 Jul 2026 15:56:58 +0300 Subject: [PATCH 5/5] Use make_uniq+release in duckdb_scalar_function_bind_get_argument Replaces manual new/delete with make_uniq so the wrapper is automatically freed if Copy() throws, without needing an explicit delete in the catch block. Co-Authored-By: Claude Sonnet 4.6 --- src/main/capi/scalar_function-c.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/capi/scalar_function-c.cpp b/src/main/capi/scalar_function-c.cpp index 64fac529b62e..89625b28765e 100644 --- a/src/main/capi/scalar_function-c.cpp +++ b/src/main/capi/scalar_function-c.cpp @@ -354,13 +354,11 @@ duckdb_expression duckdb_scalar_function_bind_get_argument(duckdb_bind_info info return nullptr; } auto &bind_info = GetCScalarFunctionBindInfo(info); - ExpressionWrapper *wrapper = nullptr; try { - wrapper = new ExpressionWrapper(); + auto wrapper = duckdb::make_uniq(); wrapper->expr = bind_info.arguments[index]->Copy(); - return reinterpret_cast(wrapper); + return reinterpret_cast(wrapper.release()); } catch (std::exception &e) { - delete wrapper; duckdb_scalar_function_bind_set_error(info, e.what()); return nullptr; }