Skip to content

Fix: catch C++ exception in duckdb_scalar_function_bind_get_argument to prevent process crash#67

Open
krleonid wants to merge 5 commits into
v1.5-variegatafrom
fix/capi-scalar-bind-subquery-crash
Open

Fix: catch C++ exception in duckdb_scalar_function_bind_get_argument to prevent process crash#67
krleonid wants to merge 5 commits into
v1.5-variegatafrom
fix/capi-scalar-bind-subquery-crash

Conversation

@krleonid

@krleonid krleonid commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • BoundSubqueryExpression::Copy() throws SerializationException when a scalar UDF with a ScalarBinder receives a correlated subquery as an argument
  • The exception escaped the C API boundary, passed through CGo/C frames with no C++ unwind tables, and called std::terminate()abort() — uncatchable from Go
  • Fix wraps the Copy() call in a try/catch inside duckdb_scalar_function_bind_get_argument (src/main/capi/scalar_function-c.cpp), sets the error via duckdb_scalar_function_bind_set_error, and returns nullptr — surfacing it as a normal query error
  • Regression test added in test/api/capi/capi_scalar_functions.cpp verifies the query fails gracefully instead of aborting the process

Test plan

  • test/api/capi/capi_scalar_functions.cpp — new case in Test Scalar Function with Bind Info: passes a correlated subquery to get_connection_id (which uses a ScalarBinder) and asserts REQUIRE_FAIL instead of process abort
  • All existing scalar function bind tests continue to pass

🤖 Generated with Claude Code

krleonid and others added 2 commits July 2, 2026 12:40
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
krleonid added a commit to krleonid/duckdb-go that referenced this pull request Jul 2, 2026
When a scalar UDF has a ScalarBinder and receives a correlated subquery
argument, DuckDB internally calls BoundSubqueryExpression::Copy() which
throws SerializationException. Without a try/catch at the C boundary,
the exception unwinds through Go frames (which have no C++ unwind
tables), reaches std::terminate(), and aborts the process — recover()
cannot help.

The DuckDB-side fix (PR krleonid/duckdb#67, targeting v1.5-variegata)
wraps the Copy() call in duckdb_scalar_function_bind_get_argument in a
try/catch: on exception it sets the bind error via
duckdb_scalar_function_bind_set_error and returns nullptr.

This commit adds the corresponding duckdb-go guard: getScalarUDFArg
now checks for a nil Expression and returns an error instead of
proceeding to call ExpressionIsFoldable on a null pointer. The bind
error set by DuckDB propagates naturally, and QueryContext returns a
normal Go error instead of crashing the process.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
krleonid added a commit to krleonid/duckdb-go that referenced this pull request Jul 2, 2026
When a scalar UDF has a ScalarBinder and receives a correlated subquery
argument, DuckDB internally calls BoundSubqueryExpression::Copy() which
throws SerializationException. Without a try/catch at the C boundary,
the exception unwinds through Go frames (which have no C++ unwind
tables), reaches std::terminate(), and aborts the process — recover()
cannot help.

The DuckDB-side fix (PR krleonid/duckdb#67, targeting v1.5-variegata)
wraps the Copy() call in duckdb_scalar_function_bind_get_argument in a
try/catch: on exception it sets the bind error via
duckdb_scalar_function_bind_set_error and returns nullptr.

This commit adds the corresponding duckdb-go guard: getScalarUDFArg
now checks for a nil Expression and returns an error instead of
proceeding to call ExpressionIsFoldable on a null pointer. The bind
error set by DuckDB propagates naturally, and QueryContext returns a
normal Go error instead of crashing the process.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@krleonid krleonid force-pushed the fix/capi-scalar-bind-subquery-crash branch from 9b241d5 to 98b4262 Compare July 2, 2026 12:43
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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant