opensearch-project / opensearch-project/sql-odbc

SQLError/SQLErrorW returns the same connection error on every call and never SQL_NO_DATA, so unixODBC's SQLDriverConnect(W) spins forever on any failed connect (closed port, useSSL mismatch, bad auth=…)

Open
#102 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

untriaged
Dominant language
C++
Stars
10
Forks
25
PR merge metrics
No merged PRs in 30d

Description

What happens

Through unixODBC, a SQLDriverConnect/SQLDriverConnectW that cannot succeed never returns: a closed port, useSSL=1 against a plaintext cluster, an unknown auth= value — the call sits at 100 % CPU until the process is killed (I gave up at 11 minutes on an unresolvable host). responseTimeout= makes no difference, because the network part does finish (the refused connect() is diagnosed within about two seconds); it is what happens afterwards that never ends.

Why

After a failed connect, unixODBC's SQLDriverConnectW collects the driver's diagnostics before dropping the connection. If the driver exports SQLErrorW, the driver manager prefers it over SQLGetDiagRecW (DriverManager/SQLDriverConnectW.c, 2.3.12, the CHECK_SQLERRORW( connection ) branch) and calls it in a do … while ( SQL_SUCCEEDED( ret )) loop — SQLError has no record number, so by the ODBC 2 contract each call returns the next error and the driver clears it; the loop ends at SQL_NO_DATA.

This driver exports SQLErrorW, and its OPENSEARCHAPI_ConnectError (src/sqlodbc/environ.c) returns the connection's error on every call without ever clearing it: CC_get_error() is read, the sqlstate mapped, and the function returns SQL_SUCCESS — nothing resets conn->__error_number/message. So the driver manager loops forever. The driver's own log shows it: strace of the hung process is 31,948 write()s in 20 seconds to the mylog file, all of them

environ.c[OPENSEARCHAPI_ConnectError]212: entering hdbc=0x… <513>
environ.c[OPENSEARCHAPI_ConnectError]229: CC_get_error: status = 202, msg = #Connection error: [OpenSearch][SQL ODBC Driver][SQL Plugin] Connection error: SQL plugin is not available at url: http://127.0.0.1:19999, please install the SQL plugin to use this driver.#

(15,974 pairs), and a gdb sample of the process has only libodbc.so.2 frames under SQLDriverConnectW — the driver manager is the one looping. SQLGetDiagRecW is fine (it checks RecNumber and answers SQL_NO_DATA for record 2), which is why the same failure returns promptly through a driver manager that uses SQLGetDiagRec, and why an application that reaches the driver directly sees a normal 08001.

Environment

  • Driver built from main at dcd08dc (2026-08-07), Linux x86_64, SQL_DRIVER_VER 1.6.0.0; unixODBC 2.3.12
  • Any unreachable target; OpenSearch 3.8.0 was on 19200, the closed port used below is 19999

Reproduction 1 — the hang, through unixODBC (gcc -O1 -Wall connect_hang.c -lodbc):

/* OpenSearch SQL ODBC: a connect that cannot succeed never returns.
 * Build: gcc -O1 -Wall connect_hang.c -lodbc     Run: ./connect_hang '<connection string>'   (wrap in `timeout 60`) */
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc, char** argv) {
  setvbuf(stdout, NULL, _IONBF, 0);
  SQLHENV env; SQLHDBC dbc; SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
  SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
  const char* c = argv[1]; size_t n = strlen(c); SQLWCHAR w[1024]; for (size_t i = 0; i <= n; i++) w[i] = (SQLWCHAR)(unsigned char)c[i];
  time_t t0 = time(NULL); printf("SQLDriverConnectW ... ");
  SQLRETURN rc = SQLDriverConnectW(dbc, NULL, w, (SQLSMALLINT)n, NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
  printf("returned rc=%d after %ld s\n", (int)rc, (long)(time(NULL) - t0));
  SQLCHAR st[6], msg[1024]; SQLINTEGER nat; SQLSMALLINT len, i = 1, k = 0;
  while (SQLGetDiagRec(SQL_HANDLE_DBC, dbc, i++, st, &nat, msg, sizeof msg, &len) == SQL_SUCCESS) { k++; printf("  %s (%d): %s\n", st, (int)nat, msg); }
  printf("  diagnostic records: %d\n", k); return 0;
}
$ timeout 45 ./connect_hang 'Driver=…/libsqlodbc.so;host=127.0.0.1;port=19999;auth=NONE;useSSL=0;'
SQLDriverConnectW ... exit=124
$ timeout 45 ./connect_hang 'Driver=…/libsqlodbc.so;host=127.0.0.1;port=19999;auth=NONE;useSSL=0;responseTimeout=3;'
SQLDriverConnectW ... exit=124
$ timeout 45 ./connect_hang 'Driver=…/libsqlodbc.so;host=127.0.0.1;port=19200;auth=NONE;useSSL=1;responseTimeout=3;'
SQLDriverConnectW ... exit=124

(exit=124 is timeout killing the process after 45 s; SQLDriverConnectW never returned.)

Reproduction 2 — the driver called directly, no driver manager (gcc -O1 -Wall diagrec_loop.c -ldl): after the same failed connect, SQLGetDiagRecW answers records 1 then SQL_NO_DATA, while SQLErrorW answers the same record on call 1, 2 and 3:

/* Calls the OpenSearch SQL ODBC driver directly (dlopen, no driver manager) and asks
 * SQLGetDiagRecW for records 1, 2, 3 after a failed connect.
 * Build: gcc -O1 -Wall diagrec_loop.c -ldl -o diagrec_loop   Run: ./diagrec_loop /path/libsqlodbc.so */
#include <sql.h>
#include <sqlext.h>
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char** argv) {
  void* so = dlopen(argv[1], RTLD_NOW); if (!so) { printf("dlopen: %s\n", dlerror()); return 1; }
  SQLRETURN (*AllocHandle)(SQLSMALLINT, SQLHANDLE, SQLHANDLE*) = dlsym(so, "SQLAllocHandle");
  SQLRETURN (*SetEnvAttr)(SQLHENV, SQLINTEGER, SQLPOINTER, SQLINTEGER) = dlsym(so, "SQLSetEnvAttr");
  SQLRETURN (*DriverConnectW)(SQLHDBC, SQLHWND, SQLWCHAR*, SQLSMALLINT, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*, SQLUSMALLINT) = dlsym(so, "SQLDriverConnectW");
  SQLRETURN (*GetDiagRecW)(SQLSMALLINT, SQLHANDLE, SQLSMALLINT, SQLWCHAR*, SQLINTEGER*, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*) = dlsym(so, "SQLGetDiagRecW");
  SQLHENV env; SQLHDBC dbc; AllocHandle(SQL_HANDLE_ENV, NULL, &env);
  SetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); AllocHandle(SQL_HANDLE_DBC, env, &dbc);
  const char* c = "host=127.0.0.1;port=19999;auth=NONE;useSSL=0;"; size_t n = strlen(c); SQLWCHAR w[256]; for (size_t i = 0; i <= n; i++) w[i] = (SQLWCHAR)c[i];
  SQLRETURN rc = DriverConnectW(dbc, NULL, w, (SQLSMALLINT)n, NULL, 0, NULL, SQL_DRIVER_NOPROMPT);
  printf("driver SQLDriverConnectW -> rc=%d\n", (int)rc);
  for (SQLSMALLINT rec = 1; rec <= 4; rec++) {
    SQLWCHAR st[6] = {0}, msg[512] = {0}; SQLINTEGER nat = 0; SQLSMALLINT len = 0;
    rc = GetDiagRecW(SQL_HANDLE_DBC, dbc, rec, st, &nat, msg, 512, &len);
    char s[6], m[120]; for (int i = 0; i < 5; i++) s[i] = (char)st[i]; s[5] = 0; for (int i = 0; i < 119; i++) m[i] = (char)msg[i]; m[119] = 0;
    printf("SQLGetDiagRecW(RecNumber=%d) -> rc=%d%s state=%s native=%d msg=%.100s\n", rec, (int)rc, rc == SQL_NO_DATA ? " (SQL_NO_DATA)" : "", s, (int)nat, m);
  }
  SQLRETURN (*ErrorW)(SQLHENV, SQLHDBC, SQLHSTMT, SQLWCHAR*, SQLINTEGER*, SQLWCHAR*, SQLSMALLINT, SQLSMALLINT*) = dlsym(so, "SQLErrorW");
  printf("driver exports SQLErrorW: %s\n", ErrorW ? "yes" : "no");
  for (int k = 1; k <= 3 && ErrorW; k++) {
    SQLWCHAR st[6] = {0}, msg[512] = {0}; SQLINTEGER nat = 0; SQLSMALLINT len = 0;
    rc = ErrorW(SQL_NULL_HENV, dbc, SQL_NULL_HSTMT, st, &nat, msg, 512, &len);
    char s[6]; for (int i = 0; i < 5; i++) s[i] = (char)st[i]; s[5] = 0;
    printf("SQLErrorW call %d -> rc=%d%s state=%s len=%d\n", k, (int)rc, rc == SQL_NO_DATA ? " (SQL_NO_DATA)" : "", s, (int)len);
  }
  return 0;
}
$ ./diagrec_loop …/libsqlodbc.so
driver SQLDriverConnectW -> rc=-1
SQLGetDiagRecW(RecNumber=1) -> rc=0 state=08001 native=202 msg=Connection error: [OpenSearch][SQL ODBC Driver][SQL Plugin] Connection error: SQL plugin is not avai
SQLGetDiagRecW(RecNumber=2) -> rc=100 (SQL_NO_DATA) state= native=0 msg=
SQLGetDiagRecW(RecNumber=3) -> rc=100 (SQL_NO_DATA) state= native=0 msg=
SQLGetDiagRecW(RecNumber=4) -> rc=100 (SQL_NO_DATA) state= native=0 msg=
driver exports SQLErrorW: yes
SQLErrorW call 1 -> rc=0 state=08001 len=187
SQLErrorW call 2 -> rc=0 state=08001 len=187
SQLErrorW call 3 -> rc=0 state=08001 len=187

Suggested fix

Stop exporting SQLError/SQLErrorW so that driver managers use SQLGetDiagRec(W), which is already correct here — that is what psqlodbc, which this code descends from, does: nm -D psqlodbcw.so (16.00) exports neither symbol, and its PGAPI_ConnectError has the same no-clear tail as this one, so the ODBC 2 entry points are what makes the difference. (Alternatively, clear the connection error once SQLError has returned it.) The same pattern presumably applies to the statement- and environment-level SQLError paths, though the connect one is the one that hangs every application at its first mistake.

Two side observations from the same trace, not bugs in themselves: the connect first tries 169.254.169.254:80 (the EC2 instance-metadata endpoint — the AWS SDK credential chain) even with auth=NONE, which costs about two seconds on a non-EC2 host before the real connect is attempted; and an empty host= fails with SQL_ERROR and zero diagnostic records on both entry points.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/sqlodbc/environ.c at OPENSEARCHAPI_ConnectError, then compare its SQLErrorW behavior with SQLGetDiagRecW and the unixODBC DriverManager/SQLDriverConnectW.c CHECK_SQLERRORW branch. Reproduce with the provided connect_hang.c or diagrec_loop.c examples; done means a failed SQLDriverConnectW returns promptly and repeated error retrieval reaches SQL_NO_DATA.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, sql
Domain
backend, databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
75/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.