Skip to content

fix(rust/ffi): preserve caller's AdbcError private_data on the 1.0.0 path - #2

Closed
fornwall wants to merge 3 commits into
mainfrom
fix-error-compatibility
Closed

fix(rust/ffi): preserve caller's AdbcError private_data on the 1.0.0 path#2
fornwall wants to merge 3 commits into
mainfrom
fix-error-compatibility

Conversation

@fornwall

@fornwall fornwall commented Jul 6, 2026

Copy link
Copy Markdown
Owner

What

The FFI driver exporter wrote the entire FFI_AdbcError struct into the caller's out-pointer on every failed method, unconditionally clobbering private_data (and, at two of the three sites, private_driver).

c/include/arrow-adbc/adbc.h documents that those fields only exist in the ADBC 1.1.0 layout, and that a driver "should read/write these fields if and only if vendor_code is equal to ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA" and "should never touch more than [the 1.0.0-sized] portion of an AdbcError struct" otherwise. Overwriting them corrupts memory an ADBC 1.0.0 caller owns (or does not have at all).

How

All three error-writing sites (check_err!, pointer_as_mut!, and the panic handler in catch_panic) now route through a new set_error_out() that:

  • reads the caller's vendor_code first; when it is not the sentinel, writes only the 1.0.0-sized prefix (message/vendor_code/sqlstate/release) and installs a message-only release, leaving private_data/private_driver exactly as the caller set them;
  • otherwise behaves as before: preserves private_driver and writes the full struct, with structured details carried in private_data.

Test

Makes the C++ validation suite's StatementTest.ErrorCompatibility pass. Adds Rust unit tests for both the opted-in and 1.0.0-compatible paths; the 1.0.0 test fails against the pre-fix behavior (clobbered private_data) and passes after.

Fork-internal PR for review, against main — a single self-contained commit.

Review follow-up (second commit)

Compared set_error_out against the C reference (InternalAdbcSetErrorVariadic, c/driver/common/utils.c) and aligned two behaviors:

  • Sentinel preservation (opted-in path): adbc.h forbids consumers from reading private_data/private_driver unless vendor_code holds ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA, and this crate's own Error::try_from(&FFI_AdbcError) gates on it — but the full-struct write replaced the sentinel with the driver error's own vendor code, making the freshly-stashed details unreachable. The sentinel is now kept, as the C reference does (the driver's own vendor code is not representable on this path — that is the documented 1.1.0 trade-off).
  • Release-on-reuse: any previous error still held in the caller's struct is released before the new one is written, matching the C reference, so reusing an AdbcError across calls no longer leaks the earlier message/details.

Tests: the opted-in test now asserts the sentinel survives; a new test covers the reuse path.

@fornwall
fornwall force-pushed the fix-error-compatibility branch from 6f19405 to 80e4b50 Compare July 6, 2026 22:16
@fornwall
fornwall changed the base branch from fix-execute-query-rows-affected to main July 6, 2026 22:16
@fornwall
fornwall force-pushed the fix-error-compatibility branch 3 times, most recently from 098130a to d5cf695 Compare July 7, 2026 19:42
…path

The FFI driver exporter wrote the entire FFI_AdbcError struct into the
caller's out-pointer on every failed method, unconditionally clobbering the
private_data (and, at two of the three sites, private_driver) fields.

The AdbcError documentation in c/include/arrow-adbc/adbc.h says these fields
exist only in the ADBC 1.1.0 layout and that a driver "should read/write
these fields if and only if vendor_code is equal to
ADBC_ERROR_VENDOR_CODE_PRIVATE_DATA", and "should never touch more than [the
1.0.0-sized] portion of an AdbcError struct" otherwise. Overwriting them
corrupts memory an ADBC 1.0.0 caller owns (or does not have at all).

Route all three error-writing sites (check_err!, pointer_as_mut! and the
panic handler in catch_panic) through a new set_error_out() that:

- reads the caller's vendor_code first; when it is not the sentinel, writes
  only the 1.0.0-sized prefix (message/vendor_code/sqlstate/release) and
  installs a message-only release, leaving private_data/private_driver as the
  caller set them;
- otherwise behaves as before: preserves private_driver and writes the full
  struct, with structured details carried in private_data.

This makes the C++ validation suite's StatementTest.ErrorCompatibility pass.
Adds unit tests for both the opted-in and 1.0.0-compatible paths.

Signed-off-by: Fredrik Fornwall <fredrik@fornwall.net>
@fornwall
fornwall force-pushed the fix-error-compatibility branch from d5cf695 to 75cab7d Compare July 8, 2026 00:22
@fornwall fornwall closed this Jul 13, 2026
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.

2 participants