Skip to content

Fix SQL_C_GUID parameter binding sending corrupted data on the wire (#295)#296

Open
fdcastel wants to merge 1 commit into
FirebirdSQL:masterfrom
fdcastel:fix/issue-295-sql-c-guid-param-binding
Open

Fix SQL_C_GUID parameter binding sending corrupted data on the wire (#295)#296
fdcastel wants to merge 1 commit into
FirebirdSQL:masterfrom
fdcastel:fix/issue-295-sql-c-guid-param-binding

Conversation

@fdcastel
Copy link
Copy Markdown
Member

@fdcastel fdcastel commented Apr 27, 2026

What this fixes

When an ODBC application calls SQLBindParameter(SQL_C_GUID, SQL_GUID, …, ptr, 16, &len) and hands the driver a 16-byte UUID, the driver returns SQL_SUCCESS but Firebird never sees the 16 UUID bytes:

  • BINARY(16) / CHAR(16) CHARACTER SET OCTETS target — Firebird stores the ASCII bytes of the first 16 chars of the canonical UUID string (30 33 30 36 43 31 37 36 2D 45 34 30 31 2D 31 31 instead of the 16 raw UUID bytes). The driver was stringifying to 36 chars and then truncating to fit the column.
  • VARCHAR target (e.g. CHAR_TO_UUID(?)) — Firebird returns expression evaluation not supported — Human readable UUID argument for CHAR_TO_UUID must have hex digit at position 2 instead of "". The driver was writing a wide-char (UTF-16) buffer into a narrow-char wire slot; the embedded NULs made the string look empty.

Reproducers, both from issue #295:

-- Reproducer A: parameter inferred as BINARY(16)
SELECT UUID_TO_CHAR(?) FROM rdb$database
--   Expected: 'A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'
--   Actual:   '30333036-4331-3736-2D45-3430312D3131' (ASCII of truncated string)

-- Reproducer B: parameter inferred as VARCHAR
SELECT UUID_TO_CHAR(CHAR_TO_UUID(?)) FROM rdb$database
--   Expected: 'A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'
--   Actual:   HY000(-833) "must have hex digit at position 2 instead of ''"

Real-world impact: duckdb/odbc-scanner binds DuckDB ::UUID values exactly this way (see src/types/uuid_type.cpp L33-44). Their Firebird UUID round-trip test (duckdb/odbc-scanner#169) is parked on this.

Closes #295.

How the fix works

Two new wire-only conversion functions, picked up by OdbcConvert::getAdressFunction when binding an SQL_C_GUID input parameter:

  • convGuidToBinary — writes 16 raw bytes in canonical UUID byte order (Data1/Data2/Data3 swapped from x86 little-endian to big-endian, Data4 unchanged). Used when the IPD describes the slot as BINARY(16) / CHAR(16) CHARACTER SET OCTETS (sqlsubtype == 1 && sqllen == 16) or as FB4+ BINARY/VARBINARY (SQL_C_BINARY).
  • convGuidToVarString — stages the 36-char canonical UUID in the DescRecord's local buffer and redirects the wire's sqldata via setSqlData(), mirroring the idiom transferStringToAllowedType uses. Necessary because an untyped ? placeholder is described by Firebird as VARCHAR(0): its wire buffer only holds the 2-byte length prefix — nowhere near enough for 36 chars without the redirection. setTypeText() converts the wire from SQL_VARYING to SQL_TEXT so there's no length prefix to maintain.

The pre-existing convGuidToString / convGuidToStringW are intentionally untouched. They handle the app-side fetch direction (SQLGetData(…, SQL_C_CHAR/WCHAR, …) from a column described as SQL_GUID). That path is currently dead code because the column-side SQL_GUID mapping is open task T5-5 (#287), but the functions remain correct for when T5-5 lands.

getSqlSubtype() / getSqlLen() on HeadSqlVar are pure read-only accessors so the dispatch can distinguish OCTETS-subtype + sqllen 16 from any other text charset.

Tests

Three new GuidParamBindingTest cases in tests/test_guid_and_binary.cpp cover each reproducer:

  • BindGuidToCharOctets16 — SQL_C_GUID into CHAR(16) CHARACTER SET OCTETS, round-tripped via UUID_TO_CHAR.
  • BindGuidToVarcharViaCharToUuid — SQL_C_GUID into VARCHAR via CHAR_TO_UUID(?) (reproducer B).
  • BindGuidToUuidToCharRoundtrip — SQL_C_GUID into BINARY(16) via UUID_TO_CHAR(?) (reproducer A).

All three pass on the full FB-master matrix (windows-x64/x86/ARM64, ubuntu-x64/ARM64). Existing suite shows zero regressions locally (202 passed, 0 failed, same skip count as before this PR).

CI status

All five master (Firebird 6) matrix jobs pass with this change. The still-failing 5.0.3 jobs error out at the database-provisioning step with Could not find Firebird release for version 5.0.3 on GitHub from PSFirebird/1.2.2/Get-FirebirdReleaseUrl.ps1 — a pre-existing infra issue that surfaces before any code from this branch runs, unrelated to this PR.

Test plan

  • BindGuidToCharOctets16, BindGuidToVarcharViaCharToUuid, BindGuidToUuidToCharRoundtrip pass on the master CI matrix.
  • No regressions in the existing test suite locally (202 passed, 184 skipped, 0 failed).
  • Once PSFirebird is updated, re-run to confirm 5.0.3 jobs pass.

@irodushka
Copy link
Copy Markdown
Contributor

Honestly, I studied this PR and didn't understand a thing. Could you describe it more clearly—where the error was, how to reproduce it, and what the purpose of these changes was?..

@irodushka
Copy link
Copy Markdown
Contributor

I understand the code switching for string/binary, it's okay. But what's the purpose of total rewriting of the convGuidToString() foo? And why the convGuidToStringW() is left untouched?..

@fdcastel fdcastel force-pushed the fix/issue-295-sql-c-guid-param-binding branch from a32a022 to 538a720 Compare May 15, 2026 01:22
When an ODBC application called SQLBindParameter with C type SQL_C_GUID
and SQL type SQL_GUID, the driver accepted the call but did not convert
the 16-byte UUID into Firebird's wire format. Symptoms reported in FirebirdSQL#295:

  * BINARY(16) / CHAR(16) CHARACTER SET OCTETS targets received the
    ASCII bytes of the canonical UUID string truncated to 16 chars.
  * Untyped VARCHAR parameters (e.g. inside CHAR_TO_UUID(?)) received
    a wide-char buffer interpreted as narrow text, so Firebird raised
    "Human readable UUID argument for CHAR_TO_UUID must have hex digit
    at position 2 instead of ''".

Add two wire-only conversion functions and route SQL_C_GUID input
parameters to them:

  * convGuidToBinary writes 16 raw bytes in canonical UUID byte order
    (Data1/2/3 swapped from x86 little-endian to big-endian, Data4 as
    is). Used for BINARY(16) / CHAR(16) OCTETS / FB4+ BINARY targets.

  * convGuidToVarString stages the 36-char canonical UUID in the
    DescRecord local buffer and redirects the wire's sqldata via
    setSqlData(), matching the idiom transferStringToAllowedType
    already uses. This sidesteps the SQLDA-allocated buffer being
    only 2 bytes wide for an untyped `?` (VARCHAR(0)) placeholder.

The pre-existing convGuidToString / convGuidToStringW remain unchanged;
they handle the (currently unreachable) app-side fetch path that will
become live with the column-side SQL_GUID mapping work tracked under
FirebirdSQL#287 T5-5.

Expose getSqlSubtype() and getSqlLen() as read-only accessors on
HeadSqlVar so the dispatch in getAdressFunction can distinguish
sqlsubtype == 1 + sqllen == 16 (BINARY/CHAR(16) OCTETS, raw bytes)
from text wires of any other charset (canonical UUID string).

Add three GuidParamBindingTest acceptance tests covering the issue's
exact reproducers: SQL_C_GUID into CHAR(16) OCTETS, into VARCHAR via
CHAR_TO_UUID(?), and into BINARY(16) via UUID_TO_CHAR(?).

Closes FirebirdSQL#295.
@fdcastel
Copy link
Copy Markdown
Member Author

fdcastel commented May 15, 2026

Thanks for the review — fair feedback. I rewrote the commit so the diff is easier to read; here's the walkthrough.

The bug. An ODBC app calls SQLBindParameter(SQL_C_GUID, SQL_GUID, …, ptr, 16, &len) and hands the driver a 16-byte UUID. The driver returns SQL_SUCCESS but Firebird never sees the 16 UUID bytes:

  • BINARY(16) / CHAR(16) CHARACTER SET OCTETS target: the bytes Firebird stores are 30 33 30 36 43 31 37 36 2D 45 34 30 31 2D 31 31 — the ASCII characters of the first 16 chars of the canonical UUID string. The driver was stringifying the GUID to 36 chars and then truncating to 16 to fit the column.
  • VARCHAR target (e.g. CHAR_TO_UUID(?)): Firebird returns expression evaluation not supported — Human readable UUID argument for CHAR_TO_UUID must have hex digit at position 2 instead of "". The driver was writing a wide-char (UTF-16) buffer into a narrow-char wire slot, so the embedded NULs made the string look empty.

Issue #295 has the full empirical trace including the two reproducers above (SELECT UUID_TO_CHAR(?) and SELECT UUID_TO_CHAR(CHAR_TO_UUID(?))). Real-world impact: duckdb/odbc-scanner binds DuckDB ::UUID values exactly this way, so their Firebird UUID round-trip test (their PR #169) is parked on this.

The fix shape, after the rewrite. convGuidToString and convGuidToStringW are now unchanged — they remain the original short snprintf/swprintf helpers for the app-side fetch path (SQLGetData(…, SQL_C_CHAR/WCHAR, …) from a column described as SQL_GUID). That path is currently dead code because the column-side SQL_GUID mapping is open T5-5, but there's no reason to disturb them.

The wire-side fix lives in two new functions:

  • convGuidToBinary — writes 16 bytes in canonical UUID byte order (Data1/2/3 swapped from x86 little-endian to big-endian, Data4 unchanged). Used when the wire is BINARY(16) / CHAR(16) OCTETS, or FB4+ BINARY/VARBINARY.
  • convGuidToVarString — stages the 36-char canonical UUID in the DescRecord local buffer and points the wire's sqldata there via setSqlData(). This mirrors the idiom transferStringToAllowedType already uses, and sidesteps the fact that an untyped ? placeholder is described by Firebird as VARCHAR(0) — its wire buffer only has room for the 2-byte length prefix, nowhere near enough for 36 chars.

The dispatch in getAdressFunction routes to the new functions on the wire side (to->isIndicatorSqlDa) and leaves the existing SQL_C_CHAR / SQL_C_WCHAR switch arms alone for the app-output direction.

getSqlSubtype() and getSqlLen() on HeadSqlVar are pure read-only accessors that let the dispatch distinguish sqlsubtype == 1 + sqllen == 16 (= BINARY/CHAR(16) OCTETS, must be raw bytes) from a regular text charset (= must be canonical UUID string).

History is squashed to one commit; force-pushed with --force-with-lease. Happy to split it back out if you'd rather see step-by-step commits.

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.

SQL_C_GUID parameter binding sends corrupted data on the wire (Phase 8 / T5-5)

2 participants