Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package psqlODBC for openSUSE:Factory checked in at 2026-11-06 19:59:03 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/psqlODBC (Old) and /work/SRC/openSUSE:Factory/.psqlODBC.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "psqlODBC" Fri Nov 6 19:59:03 2026 rev:34 rq:1395876 version:18.00.0006 Changes: -------- --- /work/SRC/openSUSE:Factory/psqlODBC/psqlODBC.changes 2026-10-14 18:36:07.639408505 +0200 +++ /work/SRC/openSUSE:Factory/.psqlODBC.new.3759290/psqlODBC.changes 2026-11-06 20:07:29.854182828 +0100 @@ -1,0 +2,41 @@ +Fri Nov 6 10:36:16 UTC 2026 - Reinhard Max <[email protected]> + +- Update to 18.00.0006: + * https://odbc.postgresql.org/docs/release.html + * Large-object read overflows the application buffer + (CVE-2026-120057, bsc#1289252). + * Row cache overflow when a later FETCH changes the column count + (CVE-2026-120058, bsc#1289253). + * Driver uses a server-supplied column name as a format string + (CVE-2026-120059, bsc#1289254). + * Driver does not validate server-supplied counts and lengths + (CVE-2026-120060, bsc#1289255). + * ODBC escape parser stack buffer overflow + (CVE-2026-120061, bsc#1289256). + * SQL injection through SQLSpecialColumns table and schema names + (CVE-2026-120062, bsc#1289257). + * Stack buffer overflow when quoting long schema and table names + (CVE-2026-120063, bsc#1289258). + * Driver does not validate column numbers and row indexes in + binding and descriptor functions + (CVE-2026-120064, bsc#1289259). + * Driver does not escape a server-supplied column name in its + updatable-cursor queries (CVE-2026-120065, bsc#1289260). + * Log files are created with default permissions at a predictable + path and follow symbolic links + (CVE-2026-120066, bsc#1289261). + * Stack buffer overflow when reading the keyset (ctid/oid) + columns of an updatable cursor + (CVE-2026-120067, bsc#1289262). + * Hardening: SQLSetDescRecW stored a freed buffer and the address + of a local variable as deferred bound data. It now stores the + application's pointers, as SQLSetDescRec does. + * Hardening: SQLColumns returned uninitialized relkind and table + OID values when the table query returned no rows. + * Hardening: convert_linefeeds could dereference a NULL pointer + in one build configuration. + * Hardening: secure_sscanf limits an over-wide %Ns width, + msgtowstr checks for a zero buffer length, and the MSDTC + connect-string append is bounded. + +------------------------------------------------------------------- Old: ---- psqlodbc-REL-18_00_0005.tar.gz New: ---- psqlodbc-REL-18_00_0006.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ psqlODBC.spec ++++++ --- /var/tmp/diff_new_pack.OZddxD/_old 2026-11-06 20:07:31.290243009 +0100 +++ /var/tmp/diff_new_pack.OZddxD/_new 2026-11-06 20:07:31.294243177 +0100 @@ -21,8 +21,8 @@ %endif Name: psqlODBC -Version: 18.00.0005 -%define tarv 18_00_0005 +Version: 18.00.0006 +%define tarv 18_00_0006 Release: 0 Summary: ODBC Driver for PostgreSQL License: LGPL-2.1-or-later ++++++ psqlodbc-REL-18_00_0005.tar.gz -> psqlodbc-REL-18_00_0006.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/.github/workflows/docs-deploy.yml new/psqlodbc-REL-18_00_0006/.github/workflows/docs-deploy.yml --- old/psqlodbc-REL-18_00_0005/.github/workflows/docs-deploy.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/psqlodbc-REL-18_00_0006/.github/workflows/docs-deploy.yml 2026-11-02 19:01:24.000000000 +0100 @@ -0,0 +1,16 @@ +name: Deploy Website + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + # Placeholder so that GitHub registers the workflow and it can be started + # on another branch. The real deploy replaces this. + - name: Placeholder + run: "false" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/bind.c new/psqlodbc-REL-18_00_0006/bind.c --- old/psqlodbc-REL-18_00_0005/bind.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/bind.c 2026-11-02 19:01:24.000000000 +0100 @@ -248,6 +248,19 @@ } /* + * The binding counts are SQLSMALLINT. A column number above SHRT_MAX + * would wrap the allocated count negative, and a later extend copy loop + * bounded by that count would run past the array. PGAPI_BindParameter + * has the same guard for parameter numbers. + */ + if (icol > SHRT_MAX) + { + SC_set_error(stmt, STMT_INVALID_COLUMN_NUMBER_ERROR, "Invalid column number for PGAPI_BindCol.", func); + ret = SQL_ERROR; + goto cleanup; + } + + /* * Allocate enough bindings if not already done. Most likely, * execution of a statement would have setup the necessary bindings. * But some apps call BindCol before any statement is executed. @@ -802,7 +815,11 @@ MYLOG(0, "entering ... self=%p, bindings_allocated=%d, icol=%d\n", self, self->allocated, icol); - if (icol > self->allocated) + /* + * icol 0 is the bookmark; a negative value (from a negative + * SQL_DESC_COUNT) would index before the bindings array. + */ + if (icol < 0 || icol > self->allocated) return; /* use zero based col numbers from here out */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/configure.ac new/psqlodbc-REL-18_00_0006/configure.ac --- old/psqlodbc-REL-18_00_0005/configure.ac 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/configure.ac 2026-11-02 19:01:24.000000000 +0100 @@ -1,5 +1,5 @@ # Process this file with autoconf to produce a configure script. -AC_INIT(psqlodbc, 18.00.0005, [[email protected]]) +AC_INIT(psqlodbc, 18.00.0006, [[email protected]]) AC_PREREQ(2.57) AC_CONFIG_AUX_DIR(config) AM_INIT_AUTOMAKE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/connection.c new/psqlodbc-REL-18_00_0006/connection.c --- old/psqlodbc-REL-18_00_0005/connection.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/connection.c 2026-11-02 19:01:24.000000000 +0100 @@ -1708,7 +1708,8 @@ { QLOG(0, "\tGetting result from PGresult failed\n"); success = FALSE; - if (0 >= CC_get_errornumber(conn)) + /* conn is NULL when QR_next_tuple() fetches the next block */ + if (NULL != conn && 0 >= CC_get_errornumber(conn)) { switch (QR_get_rstatus(res)) { @@ -2062,7 +2063,11 @@ int status = PQresultStatus(pgres); if (discardTheRest) + { + PQclear(pgres); + pgres = NULL; continue; + } switch (status) { case PGRES_COMMAND_OK: @@ -2244,6 +2249,12 @@ else { aborted = TRUE; + /* + * In single-row mode the rest of the rows and the + * final PGRES_TUPLES_OK would be read into res again, + * replacing the error with a successful result. + */ + discardTheRest = TRUE; if (QR_command_maybe_successful(res)) retres = NULL; else @@ -2261,6 +2272,7 @@ if (!CC_from_PGresult(res, stmt, NULL, cursor, &pgres)) { retres = NULL; + discardTheRest = TRUE; break; } retres = cmdres; @@ -2472,7 +2484,7 @@ int -CC_send_function(ConnectionClass *self, const char *fn_name, void *result_buf, int *actual_result_len, int result_is_int, LO_ARG *args, int nargs) +CC_send_function(ConnectionClass *self, const char *fn_name, void *result_buf, Int4 result_buf_len, int *actual_result_len, int result_is_int, LO_ARG *args, int nargs) { int i; int ret = FALSE; @@ -2554,6 +2566,15 @@ if (result_is_int == 2) { Int8 int8val; + /* + * The reply length is server-controlled; never read more + * than the server sent, and never write past result_buf. + */ + if (*actual_result_len < (int) sizeof(Int8)) + { + CC_set_errormsg(self, "short int8 result from large_object function"); + goto cleanup; + } memcpy(&int8val, value, sizeof(Int8)); int8val = odbc_ntoh64(int8val); memcpy(result_buf, &int8val, sizeof(Int8)); @@ -2562,12 +2583,28 @@ else if (result_is_int) { Int4 int4val; + if (*actual_result_len < (int) sizeof(Int4)) + { + CC_set_errormsg(self, "short int4 result from large_object function"); + goto cleanup; + } memcpy(&int4val, value, sizeof(Int4)); int4val = ntohl(int4val); memcpy(result_buf, &int4val, sizeof(Int4)); } else - memcpy(result_buf, value, *actual_result_len); + { + /* + * Clamp the copy to the caller's buffer. The server + * controls *actual_result_len, so without this a large + * reply overflows result_buf. The clamped length is what + * the caller sees, so a short read is reported as usual. + */ + if (*actual_result_len > result_buf_len) + *actual_result_len = result_buf_len; + if (*actual_result_len > 0) + memcpy(result_buf, value, *actual_result_len); + } } ret = TRUE; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/connection.h new/psqlodbc-REL-18_00_0006/connection.h --- old/psqlodbc-REL-18_00_0005/connection.h 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/connection.h 2026-11-02 19:01:24.000000000 +0100 @@ -425,7 +425,7 @@ const char *comment, QResultClass *res, BOOL error_not_a_notice); void CC_clear_error(ConnectionClass *self); -int CC_send_function(ConnectionClass *conn, const char *fn_name, void *result_buf, int *actual_result_len, int result_is_int, LO_ARG *argv, int nargs); +int CC_send_function(ConnectionClass *conn, const char *fn_name, void *result_buf, Int4 result_buf_len, int *actual_result_len, int result_is_int, LO_ARG *argv, int nargs); char CC_send_settings(ConnectionClass *self, const char *set_query); void CC_initialize_pg_version(ConnectionClass *conn); void CC_log_error(const char *func, const char *desc, const ConnectionClass *self); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/convert.c new/psqlodbc-REL-18_00_0006/convert.c --- old/psqlodbc-REL-18_00_0005/convert.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/convert.c 2026-11-02 19:01:24.000000000 +0100 @@ -2426,7 +2426,7 @@ size_t c = 0; \ while ((qp)->statement[qp->opos] != '\0' && (qp)->statement[qp->opos] != ch) \ { \ - if (c >= maxsize) \ + if (c + 1 >= maxsize) \ break; \ buf[c++] = (qp)->statement[qp->opos++]; \ } \ @@ -2453,7 +2453,7 @@ Oid *pgType); static int processParameters(QueryParse *qp, QueryBuild *qb, - size_t *output_count, SQLLEN param_pos[][2]); + size_t *output_count, SQLLEN param_pos[][2], size_t param_pos_count); static size_t convert_to_pgbinary(const char *in, char *out, size_t len, QueryBuild *qb); static BOOL convert_special_chars(QueryBuild *qb, const char *si, size_t used); @@ -5606,7 +5606,7 @@ */ static int processParameters(QueryParse *qp, QueryBuild *qb, - size_t *output_count, SQLLEN param_pos[][2]) + size_t *output_count, SQLLEN param_pos[][2], size_t param_pos_count) { int retval, innerParenthesis, param_count; BOOL stop; @@ -5632,6 +5632,12 @@ case ',': if (1 == innerParenthesis) { + if ((size_t) (param_count + 1) >= param_pos_count) + { + qb->errornumber = STMT_EXEC_ERROR; + qb->errormsg = "too many parameters in the escape function call"; + return SQL_ERROR; + } param_pos[param_count][1] = F_NewPos(qb) - 2; param_count++; param_pos[param_count][0] = F_NewPos(qb); @@ -5651,6 +5657,12 @@ innerParenthesis--; if (0 == innerParenthesis) { + if ((size_t) (param_count + 1) >= param_pos_count) + { + qb->errornumber = STMT_EXEC_ERROR; + qb->errormsg = "too many parameters in the escape function call"; + return SQL_ERROR; + } param_pos[param_count][1] = F_NewPos(qb) - 2; param_count++; param_pos[param_count][0] = @@ -5861,7 +5873,14 @@ QB_initialize_copy(&nqb, qb, 1024); nqb_is_valid = TRUE; - if (retval = processParameters(qp, &nqb, ¶m_consumed, param_pos), retval == SQL_ERROR) + /* + * Initialize every row. A '$k' reference in the mapping expression + * for an argument that was not supplied must see -1 here, not a + * stale or uninitialized offset that would copy arbitrary bytes. + */ + for (i = 0; i < (int) (sizeof(param_pos) / sizeof(param_pos[0])); i++) + param_pos[i][0] = param_pos[i][1] = -1; + if (retval = processParameters(qp, &nqb, ¶m_consumed, param_pos, sizeof(param_pos) / sizeof(param_pos[0])), retval == SQL_ERROR) { qb->errornumber = nqb.errornumber; qb->errormsg = nqb.errormsg; @@ -6236,7 +6255,8 @@ if (max == 0) max = 0xffffffff; - *changed = FALSE; + if (changed) /* some callers do not want this */ + *changed = FALSE; for (i = 0; si[i] && out < max - 1; i++) { if (convlf && si[i] == '\n') @@ -6250,7 +6270,8 @@ out++; continue; } - *changed = TRUE; + if (changed) + *changed = TRUE; if (dst) { @@ -6387,6 +6408,14 @@ } else { + /* + * An octal escape needs three digits after the backslash + * (value[i+1..i+3]). A truncated escape at the end of a + * server-supplied bytea would make conv_from_octal read + * past the terminator. + */ + if (i + 3 >= ilen) + break; if (rgbValue) rgbValue[o] = conv_from_octal(&value[i]); o++; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/docs/release.html new/psqlodbc-REL-18_00_0006/docs/release.html --- old/psqlodbc-REL-18_00_0005/docs/release.html 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/docs/release.html 2026-11-02 19:01:24.000000000 +0100 @@ -10,6 +10,56 @@ <h1>psqlODBC release notes</h1> <hr> <ol type="1"> + <h2><a id="18.00.0006">psqlODBC 18.00.0006 Release</a></h2> + This release fixes the following vulnerabilities. All releases before 18.00.0006 are affected.<br /> + Changes:<br /> + <li> + Large-object read overflows the application buffer (CVE-2026-120057, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-qhmr-5339-q33r">GHSA-qhmr-5339-q33r</a>, CVSS 3.1 7.5 High) + </li> + <li> + Row cache overflow when a later FETCH changes the column count (CVE-2026-120058, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-6rmx-9q62-4w6m">GHSA-6rmx-9q62-4w6m</a>, CVSS 3.1 7.5 High) + </li> + <li> + Driver uses a server-supplied column name as a format string (CVE-2026-120059, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-44rq-72jh-x2h8">GHSA-44rq-72jh-x2h8</a>, CVSS 3.1 6.8 Medium) + </li> + <li> + Driver does not validate server-supplied counts and lengths (CVE-2026-120060, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-4mfx-cfxx-c877">GHSA-4mfx-cfxx-c877</a>, CVSS 3.1 5.9 Medium) + </li> + <li> + ODBC escape parser stack buffer overflow (CVE-2026-120061, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-hfm6-qfx8-v4xq">GHSA-hfm6-qfx8-v4xq</a>, CVSS 3.1 7.5 High) + </li> + <li> + SQL injection through SQLSpecialColumns table and schema names (CVE-2026-120062, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-gg8f-fx3w-rj33">GHSA-gg8f-fx3w-rj33</a>, CVSS 3.1 7.5 High) + </li> + <li> + Stack buffer overflow when quoting long schema and table names (CVE-2026-120063, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-285r-84w3-xcxv">GHSA-285r-84w3-xcxv</a>, CVSS 3.1 5.9 Medium) + </li> + <li> + Driver does not validate column numbers and row indexes in binding and descriptor functions (CVE-2026-120064, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-7pq2-q7mj-ph8c">GHSA-7pq2-q7mj-ph8c</a>, CVSS 3.1 7.5 High) + </li> + <li> + Driver does not escape a server-supplied column name in its updatable-cursor queries (CVE-2026-120065, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-9cg8-r57h-h6jj">GHSA-9cg8-r57h-h6jj</a>, CVSS 3.1 7.1 High) + </li> + <li> + Log files are created with default permissions at a predictable path and follow symbolic links (CVE-2026-120066, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-wj7r-6pj3-62g8">GHSA-wj7r-6pj3-62g8</a>, CVSS 3.1 5.3 Medium) + </li> + <li> + Stack buffer overflow when reading the keyset (ctid/oid) columns of an updatable cursor (CVE-2026-120067, <a href="https://github.com/postgresql-interfaces/psqlodbc/security/advisories/GHSA-3r96-h2q4-6pxh">GHSA-3r96-h2q4-6pxh</a>, CVSS 3.1 5.9 Medium) + </li> + The release also contains these hardening changes. They have no demonstrated attack path and no CVE.<br /> + <li> + SQLSetDescRecW stored a freed buffer and the address of a local variable as deferred bound data. ODBC defines no Unicode form of SQLSetDescRec, and driver managers call only SQLSetDescRec, so only an application that calls the driver's SQLSetDescRecW export directly reaches it. It now stores the application's pointers, as SQLSetDescRec does. + </li> + <li> + SQLColumns returned uninitialized relkind and table OID values when the table query returned no rows. + </li> + <li> + convert_linefeeds could dereference a NULL pointer in one build configuration. + </li> + <li> + secure_sscanf limits an over-wide %Ns width, msgtowstr checks for a zero buffer length, and the MSDTC connect-string append is bounded. + </li> + <hr> <h2><a id="18.00.0005">psqlODBC 18.00.0005 Release</a></h2> Changes:<br /> <li> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/info.c new/psqlodbc-REL-18_00_0006/info.c --- old/psqlodbc-REL-18_00_0005/info.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/info.c 2026-11-02 19:01:24.000000000 +0100 @@ -1622,32 +1622,47 @@ dest = buf; else { - bufsize = 2 * srclen + 1; + /* + * Worst case: two output bytes per input byte (a doubled quote or + * escape), the optional opening and closing quote, and the NUL. + */ + bufsize = 2 * (size_t) srclen + 4; dest = malloc(bufsize); } if (!dest) return NULL; encoded_str_constr(&encstr, conn->ccsc, (char *) src); outlen = 0; - if (double_quote) - dest[outlen++] = IDENTIFIER_QUOTE; - for (i = 0, tchar = encoded_nextchar(&encstr); i < srclen && outlen < bufsize - 1; i++, tchar = encoded_nextchar(&encstr)) - { - if (MBCS_NON_ASCII(encstr)) - { - dest[outlen++] = tchar; - continue; - } - if (LITERAL_QUOTE == tchar || - escape_ch == tchar) - dest[outlen++] = tchar; - else if (double_quote && - IDENTIFIER_QUOTE == tchar) + { + /* + * Every write below is bounded by bufsize, including the closing + * quote and the NUL after the loop. With a caller-supplied buffer + * this truncates an over-long identifier instead of overflowing. + */ + size_t reserve = (double_quote ? 1u : 0u) + 1u; /* closing quote + NUL */ + + if (double_quote && (size_t) outlen + reserve < bufsize) + dest[outlen++] = IDENTIFIER_QUOTE; + for (i = 0, tchar = encoded_nextchar(&encstr); + i < srclen && (size_t) outlen + 2 + reserve <= bufsize; + i++, tchar = encoded_nextchar(&encstr)) + { + if (MBCS_NON_ASCII(encstr)) + { + dest[outlen++] = tchar; + continue; + } + if (LITERAL_QUOTE == tchar || + escape_ch == tchar) + dest[outlen++] = tchar; + else if (double_quote && + IDENTIFIER_QUOTE == tchar) + dest[outlen++] = tchar; dest[outlen++] = tchar; - dest[outlen++] = tchar; + } + if (double_quote) + dest[outlen++] = IDENTIFIER_QUOTE; + dest[outlen] = '\0'; } - if (double_quote) - dest[outlen++] = IDENTIFIER_QUOTE; - dest[outlen] = '\0'; MYLOG(0, "leaving output=%s(%d)\n", dest, outlen); return dest; } @@ -2343,9 +2358,15 @@ Int4 mod_length, ordinal, typmod, relhasoids, relhassubclass; - OID field_type, greloid, basetype; + OID field_type, greloid = 0, basetype; char not_null[MAX_INFO_STRING], - relhasrules[MAX_INFO_STRING], relkind[8], attidentity[2]; + /* + * relkind and greloid are filled by a fetch below; keep + * them defined when that fetch returns no row, so the + * relisaview test and any row-versioning tuple do not read + * uninitialized stack. + */ + relhasrules[MAX_INFO_STRING], relkind[8] = "", attidentity[2]; char *escSchemaName = NULL, *escTableName = NULL, *escColumnName = NULL; BOOL search_pattern = TRUE, search_by_ids, relisaview, show_oid_column, row_versioning; ConnInfo *ci; @@ -2923,9 +2944,15 @@ * @return QResultClass */ static QResultClass * -findPrimaryOrUnique(ConnectionClass *conn, StatementClass * stmt, const SQLCHAR * szTableName, const SQLCHAR *szTableQualifier, const SQLCHAR * szSchemaName, bool primaryOrUnique) { +findPrimaryOrUnique(ConnectionClass *conn, StatementClass * stmt, const char *escTableName, const char *escSchemaName, bool primaryOrUnique) { PQExpBufferData columns_query = {0}; + /* + * escTableName and escSchemaName are already literal-escaped by the + * caller (simpleCatalogEscape). Splicing the raw ODBC arguments here + * instead would let an application-supplied name inject SQL. + */ + initPQExpBuffer(&columns_query); printfPQExpBuffer(&columns_query, "select NULL as \"SCOPE\"," "a.attname AS \"COLUMN_NAME\"," @@ -2948,14 +2975,14 @@ "LEFT JOIN pg_index i ON i.indrelid = c.oid " "AND a.attnum = ANY (i.indkey[0:(i.indnkeyatts - 1)]) "); if (primaryOrUnique) { - appendPQExpBuffer(&columns_query, "WHERE i.indisprimary and a.attnum > 0 and c.relname='%s'" , szTableName ); + appendPQExpBuffer(&columns_query, "WHERE i.indisprimary and a.attnum > 0 and c.relname='%s'" , escTableName ? escTableName : "" ); } else { - appendPQExpBuffer(&columns_query, "WHERE i.indisunique and a.attnum > 0 and c.relname ='%s'", szTableName); + appendPQExpBuffer(&columns_query, "WHERE i.indisunique and a.attnum > 0 and c.relname ='%s'", escTableName ? escTableName : ""); } - if (szTableQualifier != NULL) - appendPQExpBuffer(&columns_query, " and n.nspname = '%s'" , szSchemaName); + if (escSchemaName != NULL && escSchemaName[0] != '\0') + appendPQExpBuffer(&columns_query, " and n.nspname = '%s'" , escSchemaName); return CC_send_query(conn, columns_query.data, NULL, READ_ONLY_QUERY, stmt); } @@ -2995,7 +3022,13 @@ PQExpBufferData columns_query = {0}; char *escSchemaName = NULL, *escTableName = NULL; RETCODE ret = SQL_ERROR, result; - char relhasrules[MAX_INFO_STRING], relkind[8], relhasoids[8]; + /* + * relhasoids is only filled from the server on PostgreSQL before 12. + * On 12 and later it stays at this default, so initialize it to "no + * oids" rather than leaving the later test reading an uninitialized + * byte. + */ + char relhasrules[MAX_INFO_STRING], relkind[8], relhasoids[8] = "0"; BOOL relisaview; SQLSMALLINT internal_asis_type = SQL_C_CHAR, cbSchemaName; const SQLCHAR *szSchemaName; @@ -3205,7 +3238,7 @@ set_tuplefield_int2(&tuple[SPECOLS_PSEUDO_COLUMN], SQL_PC_PSEUDO); } else { // look for primary first - res = findPrimaryOrUnique(conn, stmt, szTableName, szTableQualifier, szSchemaName, TRUE); + res = findPrimaryOrUnique(conn, stmt, escTableName, escSchemaName, TRUE); if (!QR_command_maybe_successful(res)) { SC_set_error(stmt, STMT_EXEC_ERROR, "PGAPI_Special query error", func); @@ -3214,7 +3247,7 @@ if (0 == QR_get_num_total_tuples(res)) { // didn't find primary now look for uniaue - res = findPrimaryOrUnique(conn, stmt, szTableName, szTableName, szSchemaName, FALSE); + res = findPrimaryOrUnique(conn, stmt, escTableName, escSchemaName, FALSE); if (!QR_command_maybe_successful(res)) { SC_set_error(stmt, STMT_EXEC_ERROR, "PGAPI_Special query error", func); @@ -3636,6 +3669,13 @@ /* add a row in this table for each field in the index */ colcnt = fields_vector[0]; + /* + * colcnt comes from a server-supplied int2vector. fields_vector + * and indopt_vector hold INDEX_KEYS_STORAGE_COUNT entries (plus + * element 0), so a larger count would read past them. + */ + if (colcnt > INDEX_KEYS_STORAGE_COUNT) + colcnt = INDEX_KEYS_STORAGE_COUNT; for (i = 1; i <= colcnt; i++) { tuple = QR_AddNew(res); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/lobj.c new/psqlodbc-REL-18_00_0006/lobj.c --- old/psqlodbc-REL-18_00_0005/lobj.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/lobj.c 2026-11-02 19:01:24.000000000 +0100 @@ -27,7 +27,7 @@ argv[0].len = 4; argv[0].u.integer = mode; - if (!CC_send_function(conn, "lo_creat", &retval, &result_len, 1, argv, 1)) + if (!CC_send_function(conn, "lo_creat", &retval, sizeof(retval), &result_len, 1, argv, 1)) return 0; /* invalid oid */ else return (OID) retval; @@ -49,7 +49,7 @@ argv[1].len = 4; argv[1].u.integer = mode; - if (!CC_send_function(conn, "lo_open", &fd, &result_len, 1, argv, 2)) + if (!CC_send_function(conn, "lo_open", &fd, sizeof(fd), &result_len, 1, argv, 2)) return -1; if (fd >= 0 && odbc_lo_lseek64(conn, fd, 0L, SEEK_SET) < 0) @@ -70,7 +70,7 @@ argv[0].len = 4; argv[0].u.integer = fd; - if (!CC_send_function(conn, "lo_close", &retval, &result_len, 1, argv, 1)) + if (!CC_send_function(conn, "lo_close", &retval, sizeof(retval), &result_len, 1, argv, 1)) return -1; else return retval; @@ -91,7 +91,7 @@ argv[1].len = 4; argv[1].u.integer = len; - if (!CC_send_function(conn, "loread", (int *) buf, &result_len, 0, argv, 2)) + if (!CC_send_function(conn, "loread", (int *) buf, len, &result_len, 0, argv, 2)) return -1; else return result_len; @@ -116,7 +116,7 @@ argv[1].len = len; argv[1].u.ptr = (char *) buf; - if (!CC_send_function(conn, "lowrite", &retval, &result_len, 1, argv, 2)) + if (!CC_send_function(conn, "lowrite", &retval, sizeof(retval), &result_len, 1, argv, 2)) return -1; else return retval; @@ -143,7 +143,7 @@ argv[2].u.integer = whence; /* We use lo_lseek64 */ - if (!CC_send_function(conn, "lo_lseek", &retval, &result_len, 1, argv, 3)) + if (!CC_send_function(conn, "lo_lseek", &retval, sizeof(retval), &result_len, 1, argv, 3)) return -1; else return retval; @@ -161,7 +161,7 @@ argv[0].u.integer = fd; /* We use lo_tell64 */ - if (!CC_send_function(conn, "lo_tell", &retval, &result_len, 1, argv, 1)) + if (!CC_send_function(conn, "lo_tell", &retval, sizeof(retval), &result_len, 1, argv, 1)) return -1; else return retval; @@ -199,7 +199,7 @@ argv[2].len = 4; argv[2].u.integer = whence; - if (!CC_send_function(conn, "lo_lseek64", &retval, &result_len, 2, argv, 3)) + if (!CC_send_function(conn, "lo_lseek64", &retval, sizeof(retval), &result_len, 2, argv, 3)) return -1; else return retval; @@ -220,7 +220,7 @@ argv[0].len = 4; argv[0].u.integer = fd; - if (!CC_send_function(conn, "lo_tell64", &retval, &result_len, 2, argv, 1)) + if (!CC_send_function(conn, "lo_tell64", &retval, sizeof(retval), &result_len, 2, argv, 1)) return -1; else return retval; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/msdtc_enlist.cpp new/psqlodbc-REL-18_00_0006/msdtc_enlist.cpp --- old/psqlodbc-REL-18_00_0005/msdtc_enlist.cpp 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/msdtc_enlist.cpp 2026-11-02 19:01:24.000000000 +0100 @@ -1151,7 +1151,7 @@ if (!confirmingLink) { confirmingLink = true; - strcat(dtcname, ";" KEYWORD_DTC_CHECK "=0"); + strncat(dtcname, ";" KEYWORD_DTC_CHECK "=0", sizeof(dtcname) - strlen(dtcname) - 1); continue; } default: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/mylog.c new/psqlodbc-REL-18_00_0006/mylog.c --- old/psqlodbc-REL-18_00_0005/mylog.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/mylog.c 2026-11-02 19:01:24.000000000 +0100 @@ -27,6 +27,8 @@ #ifndef WIN32 #include <pwd.h> #include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> #include <unistd.h> #include <errno.h> #define GENERAL_ERRNO (errno) @@ -53,6 +55,43 @@ #endif /* WIN32 */ +/* + * Open a log file for appending. The default log directory is world-writable + * (/tmp) and the file name is predictable, so another local user can create + * the path first. O_NOFOLLOW refuses a symlink. An existing file is used only + * if it is a regular file owned by this user, and its mode is set to 0600, so + * other users cannot read the query text and connection metadata in the log. + * Returning NULL makes the caller fall back to the home directory. + */ +static FILE * +logfile_fopen(const char *filename) +{ +#ifndef WIN32 + struct stat st; + FILE *fp; + int fd = open(filename, + O_WRONLY | O_CREAT | O_APPEND | O_NOFOLLOW, + S_IRUSR | S_IWUSR); + + if (fd < 0) + return NULL; + if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode) || + st.st_uid != geteuid() || + ((st.st_mode & (S_IRWXG | S_IRWXO)) != 0 && + fchmod(fd, S_IRUSR | S_IWUSR) != 0)) + { + close(fd); + return NULL; + } + fp = fdopen(fd, PG_BINARY_A); + if (fp == NULL) + close(fd); + return fp; +#else + return fopen(filename, PG_BINARY_A); +#endif /* WIN32 */ +} + static char *logdir = NULL; void @@ -294,7 +333,7 @@ if (MLOGFP) return; generate_filename(logdir ? logdir : MYLOGDIR, MYLOGFILE, filebuf, sizeof(filebuf)); - MLOGFP = fopen(filebuf, PG_BINARY_A); + MLOGFP = logfile_fopen(filebuf); if (!MLOGFP) { int lasterror = GENERAL_ERRNO; @@ -302,7 +341,7 @@ open_error = TRUE; SPRINTF_FIXED(errbuf, "%s open error %d\n", filebuf, lasterror); generate_homefile(MYLOGFILE, filebuf, sizeof(filebuf)); - MLOGFP = fopen(filebuf, PG_BINARY_A); + MLOGFP = logfile_fopen(filebuf); } if (MLOGFP) { @@ -423,11 +462,11 @@ if (!QLOGFP) { generate_filename(logdir ? logdir : QLOGDIR, QLOGFILE, filebuf, sizeof(filebuf)); - QLOGFP = fopen(filebuf, PG_BINARY_A); + QLOGFP = logfile_fopen(filebuf); if (!QLOGFP) { generate_homefile(QLOGFILE, filebuf, sizeof(filebuf)); - QLOGFP = fopen(filebuf, PG_BINARY_A); + QLOGFP = logfile_fopen(filebuf); } if (!QLOGFP) qlog_on = 0; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/odbcapi30w.c new/psqlodbc-REL-18_00_0006/odbcapi30w.c --- old/psqlodbc-REL-18_00_0005/odbcapi30w.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/odbcapi30w.c 2026-11-02 19:01:24.000000000 +0100 @@ -619,35 +619,23 @@ SQLLEN *Indicator) { RETCODE ret; - SQLLEN vallen; - char *uval = NULL; - BOOL val_alloced = FALSE; MYLOG(0, "Entering h=%p rec=%d type=%d sub=%d len=" FORMAT_LEN " prec=%d scale=%d data=%p\n", DescriptorHandle, RecNumber, Type, SubType, Length, Precision, Scale, Data); MYLOG(0, "str=%p ind=%p\n", StringLength, Indicator); - /* Convert Unicode string to UTF-8 if needed */ - if (Length > 0 || SQL_NTS == Length) - { - uval = ucs2_to_utf8(Data, Length > 0 ? Length / WCLEN : Length, &vallen, FALSE); - val_alloced = TRUE; - } - - /* For non-string data or if conversion wasn't needed, use the original value */ - if (!val_alloced) - { - uval = Data; - vallen = Length; - } - - /* Call the core implementation function */ + /* + * Data is a deferred pointer to a bound buffer owned by the application; + * PGAPI_SetDescRec stores it (and StringLength) to be dereferenced later + * at fetch or execute time. The previous code converted Data into a + * freshly malloc'd buffer and freed it before returning, and passed the + * address of a local for the length, so the stored pointers dangled. + * Pass the application's pointers through unchanged, as the ANSI entry + * point does; the bound buffer's encoding is interpreted by its C type + * at use time, not re-encoded here. + */ ret = PGAPI_SetDescRec(DescriptorHandle, RecNumber, Type, - SubType, Length, Precision, Scale, uval, - &vallen, Indicator); - - /* Free the converted string if we allocated one */ - if (val_alloced) - free(uval); + SubType, Length, Precision, Scale, Data, + StringLength, Indicator); return ret; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/parse.c new/psqlodbc-REL-18_00_0006/parse.c --- old/psqlodbc-REL-18_00_0005/parse.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/parse.c 2026-11-02 19:01:24.000000000 +0100 @@ -381,6 +381,33 @@ } /* + * Copy src to dst, writing each occurrence of dup twice. Return FALSE and + * leave dst empty when the result and its terminator do not fit in dstsize + * bytes. + */ +static BOOL +copy_doubling(char *dst, size_t dstsize, const char *src, char dup) +{ + size_t si, di = 0; + + for (si = 0; src[si] != '\0'; si++) + { + size_t need = (src[si] == dup) ? 2 : 1; + + if (di + need >= dstsize) + { + dst[0] = '\0'; + return FALSE; + } + if (src[si] == dup) + dst[di++] = dup; + dst[di++] = src[si]; + } + dst[di] = '\0'; + return TRUE; +} + +/* * Check relhasoids(before PG12), relhssubclass and get some relevant information. */ BOOL CheckPgClassInfo(StatementClass *stmt) @@ -443,9 +470,33 @@ QR_get_value_backend_int(res, i, COLUMNS_FIELD_TYPE, NULL) == PG_TYPE_INT4) { char query[512]; + char quoted[256]; + char pctsafe[2 * 256]; + const char *colname; - STR_TO_NAME(ti->bestitem, QR_get_value_backend_text(res, i, COLUMNS_COLUMN_NAME)); - SPRINTF_FIXED(query, "\"%s\" = %%d", SAFE_NAME(ti->bestitem)); + /* + * The column name comes from the server. The + * driver places bestitem between double quotes in + * its keyset and positioned-update queries, so + * bestitem holds the name with each '"' doubled. + * bestqual is used as a printf-style format string + * (appendPQExpBuffer(&buf, bestqual, oid)), so its + * copy of the name also has each '%' doubled. A + * name too long for these buffers is not used as + * the row identifier. + */ + colname = QR_get_value_backend_text(res, i, + COLUMNS_COLUMN_NAME); + if (NULL == colname || + !copy_doubling(quoted, sizeof(quoted), + colname, IDENTIFIER_QUOTE) || + !copy_doubling(pctsafe, sizeof(pctsafe), + quoted, '%') || + strlen(pctsafe) + sizeof("\"\" = %d") > + sizeof(query)) + continue; + STR_TO_NAME(ti->bestitem, quoted); + SPRINTF_FIXED(query, "\"%s\" = %%d", pctsafe); STRX_TO_NAME(ti->bestqual, query); break; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/pgapi30.c new/psqlodbc-REL-18_00_0006/pgapi30.c --- old/psqlodbc-REL-18_00_0005/pgapi30.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/pgapi30.c 2026-11-02 19:01:24.000000000 +0100 @@ -502,6 +502,13 @@ { int i; + /* + * A negative count (a negative SQL_DESC_COUNT from the application) + * would make the loop below walk to negative indexes and store a + * negative allocated count. Ignore it. + */ + if (cols < 0) + return; if (cols == opts->allocated) return; if (cols > opts->allocated) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/qresult.c new/psqlodbc-REL-18_00_0006/qresult.c --- old/psqlodbc-REL-18_00_0005/qresult.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/qresult.c 2026-11-02 19:01:24.000000000 +0100 @@ -615,6 +615,22 @@ new_num_fields = PQnfields(*pgres); QLOG(0, "\tnFields: %d\n", new_num_fields); + /* + * When this result object is reused to read a later FETCH of the same + * cursor, the tuple cache was already sized for the column count of the + * first result. A (malicious or buggy) server that answers a later + * FETCH with a different column count would make the tuple writer use a + * mismatched stride and overrun the cache. A cursor's column set does + * not change across fetches, so reject a change here. + */ + if (self->count_backend_allocated > 0 && + new_num_fields != (int) QR_NumResultCols(self)) + { + QR_set_rstatus(self, PORES_FATAL_ERROR); + QR_set_message(self, "backend result column count changed across fetches"); + return FALSE; + } + /* according to that allocate memory */ QR_set_num_fields(self, new_num_fields); if (NULL == QR_get_fields(self)->coli_array) @@ -651,7 +667,20 @@ QR_set_rstatus(self, PORES_FIELDS_OK); self->num_fields = CI_get_num_fields(QR_get_fields(self)); if (QR_haskeyset(self)) + { + /* + * num_fields is unsigned; a keyset reply with fewer columns + * than the key fields would wrap it and later reads would run + * far past the row buffer. + */ + if (self->num_fields < self->num_key_fields) + { + QR_set_rstatus(self, PORES_FATAL_ERROR); + QR_set_message(self, "backend result has fewer columns than key fields"); + return FALSE; + } self->num_fields -= self->num_key_fields; + } if (stmt && conn) { num_io_params = CountParameters(stmt, NULL, &dummy1, &dummy2); @@ -1404,7 +1433,15 @@ len = PQgetlength(*pgres, rowno, field_lf); value = PQgetvalue(*pgres, rowno, field_lf); if (field_lf >= effective_cols) + { + if (len < 0 || len >= (int) sizeof(tidoidbuf)) + { + QR_set_rstatus(self, PORES_FATAL_ERROR); + QR_set_message(self, "Oversized keyset (ctid/oid) field from server"); + return FALSE; + } buffer = tidoidbuf; + } else { QR_MALLOC_return_with_error(buffer, char, len + 1, self, "Out of memory in allocating item buffer.", FALSE); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/results.c new/psqlodbc-REL-18_00_0006/results.c --- old/psqlodbc-REL-18_00_0005/results.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/results.c 2026-11-02 19:01:24.000000000 +0100 @@ -499,7 +499,7 @@ StatementClass *stmt = (StatementClass *) hstmt; IRDFields *irdflds; OID field_type = 0; - Int2 col_idx; + int col_idx; /* must be signed: icol - 1 can be negative */ ConnectionClass *conn; ConnInfo *ci; int column_size, unknown_sizes; @@ -590,7 +590,7 @@ if (SC_parsed_status(stmt) != STMT_PARSE_FATAL && irdflds->fi) { - if (col_idx >= cols) + if (col_idx < 0 || col_idx >= cols) { SC_set_error(stmt, STMT_INVALID_COLUMN_NUMBER_ERROR, "Invalid column number in ColAttributes.", func); return SQL_ERROR; @@ -640,7 +640,7 @@ return SQL_SUCCESS; } - if (col_idx >= cols) + if (col_idx < 0 || col_idx >= cols) { SC_set_error(stmt, STMT_INVALID_COLUMN_NUMBER_ERROR, "Invalid column number in ColAttributes.", func); return SQL_ERROR; @@ -4990,7 +4990,21 @@ } else { - if (SQL_ADD != s.fOption && s.irow > s.stmt->last_fetch_count) + /* + * irow indexes the bound rowset buffers and the row-status array. + * For SQL_ADD it must not exceed the rowset size, and for the other + * options not the fetched row count; otherwise later code reads and + * writes those application arrays out of bounds. + */ + if (SQL_ADD == s.fOption) + { + if (s.irow > rowsetSize) + { + SC_set_error(s.stmt, STMT_ROW_OUT_OF_RANGE, "Row value out of range", func); + return SQL_ERROR; + } + } + else if (s.irow > s.stmt->last_fetch_count) { SC_set_error(s.stmt, STMT_ROW_OUT_OF_RANGE, "Row value out of range", func); return SQL_ERROR; @@ -5247,7 +5261,14 @@ SC_REALLOC_gexit_with_error(res->backend_tuples, TupleField, size_of_rowset * sizeof(TupleField) * num_fields, hstmt, "Couldn't realloc memory for backend.", (ret = SQL_ERROR)); res->count_backend_allocated = size_of_rowset; } - pg_memset(res->backend_tuples + num_fields * cached_rows, 0, (size_of_rowset - cached_rows) * num_fields * sizeof(TupleField)); + /* + * cached_rows comes from the server reply (one row per ctid is + * expected). If it exceeds the rowset, the subtraction wraps and the + * memset runs off the buffer, so only clear the trailing rows when + * there are any. + */ + if (size_of_rowset > cached_rows) + pg_memset(res->backend_tuples + num_fields * cached_rows, 0, (size_of_rowset - cached_rows) * num_fields * sizeof(TupleField)); QR_set_num_cached_rows(res, size_of_rowset); res->num_total_read = size_of_rowset; rowStatusArray = (SC_get_IRDF(stmt))->rowStatusArray; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/secure_sscanf.c new/psqlodbc-REL-18_00_0006/secure_sscanf.c --- old/psqlodbc-REL-18_00_0005/secure_sscanf.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/secure_sscanf.c 2026-11-02 19:01:24.000000000 +0100 @@ -147,6 +147,13 @@ uWidth = lRet; if (uWidth > 0) { useTrim = 1; + /* + * trimCurr is MAX_STRING_OUTPUT bytes and strncpy pads to uWidth, so + * a format width at or above that size overflows the stack buffer + * regardless of the input length. Clamp it. + */ + if (uWidth >= MAX_STRING_OUTPUT) + uWidth = MAX_STRING_OUTPUT - 1; strncpy(trimCurr, pCurrent, uWidth); trimCurr[uWidth] = '\0'; pParse = trimCurr; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/test/expected/keyset-overflow.out new/psqlodbc-REL-18_00_0006/test/expected/keyset-overflow.out --- old/psqlodbc-REL-18_00_0005/test/expected/keyset-overflow.out 1970-01-01 01:00:00.000000000 +0100 +++ new/psqlodbc-REL-18_00_0006/test/expected/keyset-overflow.out 2026-11-02 19:01:24.000000000 +0100 @@ -0,0 +1,14 @@ +connected +real ctid: fetched 3 rows +oversized ctid: execute failed + +HY000=Oversized keyset (ctid/oid) field from server; +Error while executing the query +disconnecting +connected +declare/fetch real ctid: fetched 3 rows +declare/fetch oversized ctid: fetch failed after 1 rows + +HY000=Oversized keyset (ctid/oid) field from server; +Error while fetching the next result +disconnecting diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/test/src/keyset-overflow-test.c new/psqlodbc-REL-18_00_0006/test/src/keyset-overflow-test.c --- old/psqlodbc-REL-18_00_0005/test/src/keyset-overflow-test.c 1970-01-01 01:00:00.000000000 +0100 +++ new/psqlodbc-REL-18_00_0006/test/src/keyset-overflow-test.c 2026-11-02 19:01:24.000000000 +0100 @@ -0,0 +1,100 @@ +/* + * Test that an oversized value in the hidden keyset (ctid) column of an + * updatable cursor is rejected instead of overflowing the driver's buffer. + * + * A view can have a column of its own named ctid. Selecting from it with an + * updatable cursor makes the driver append "ctid" to the query, so the view + * column comes back where the driver expects a tuple identifier. + */ +#include <stdio.h> +#include <stdlib.h> + +#include "common.h" + +static void +run_query(HSTMT hstmt, const char *label, const char *sql) +{ + SQLRETURN rc; + int rows = 0; + + rc = SQLExecDirect(hstmt, (SQLCHAR *) sql, SQL_NTS); + if (!SQL_SUCCEEDED(rc)) + { + printf("%s: execute failed\n", label); + print_diag("", SQL_HANDLE_STMT, hstmt); + SQLFreeStmt(hstmt, SQL_CLOSE); + return; + } + while (SQL_SUCCEEDED(rc = SQLFetch(hstmt))) + rows++; + if (SQL_NO_DATA == rc) + printf("%s: fetched %d rows\n", label, rows); + else + { + printf("%s: fetch failed after %d rows\n", label, rows); + print_diag("", SQL_HANDLE_STMT, hstmt); + } + SQLFreeStmt(hstmt, SQL_CLOSE); +} + +static HSTMT +updatable_stmt(SQLULEN cursor_type) +{ + SQLRETURN rc; + HSTMT hstmt = SQL_NULL_HSTMT; + + rc = SQLAllocHandle(SQL_HANDLE_STMT, conn, &hstmt); + CHECK_CONN_RESULT(rc, "failed to allocate stmt handle", conn); + rc = SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_TYPE, + (SQLPOINTER) cursor_type, 0); + CHECK_STMT_RESULT(rc, "SQLSetStmtAttr CURSOR_TYPE failed", hstmt); + rc = SQLSetStmtAttr(hstmt, SQL_ATTR_CONCURRENCY, + (SQLPOINTER) SQL_CONCUR_ROWVER, 0); + CHECK_STMT_RESULT(rc, "SQLSetStmtAttr CONCURRENCY failed", hstmt); + return hstmt; +} + +static void +create_objects(void) +{ + SQLRETURN rc; + HSTMT hstmt = SQL_NULL_HSTMT; + + rc = SQLAllocHandle(SQL_HANDLE_STMT, conn, &hstmt); + CHECK_CONN_RESULT(rc, "failed to allocate stmt handle", conn); + rc = SQLExecDirect(hstmt, (SQLCHAR *) + "CREATE TEMPORARY TABLE keyset_overflow_tab AS " + "SELECT g AS id FROM generate_series(1, 3) g", SQL_NTS); + CHECK_STMT_RESULT(rc, "CREATE TABLE failed", hstmt); + /* The second row's ctid does not fit in the keyset buffer */ + rc = SQLExecDirect(hstmt, (SQLCHAR *) + "CREATE TEMPORARY VIEW keyset_overflow_view AS " + "SELECT id, CASE id WHEN 2 THEN repeat('x', 200) ELSE '(0,1)' END AS ctid " + "FROM keyset_overflow_tab", SQL_NTS); + CHECK_STMT_RESULT(rc, "CREATE VIEW failed", hstmt); + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); +} + +int main(int argc, char **argv) +{ + HSTMT hstmt; + + test_connect(); + create_objects(); + hstmt = updatable_stmt(SQL_CURSOR_KEYSET_DRIVEN); + run_query(hstmt, "real ctid", "SELECT id FROM keyset_overflow_tab"); + run_query(hstmt, "oversized ctid", "SELECT id FROM keyset_overflow_view"); + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + test_disconnect(); + + /* With one row per fetch, the oversized row is read by a later fetch */ + test_connect_ext("UseDeclareFetch=1;Fetch=1"); + create_objects(); + hstmt = updatable_stmt(SQL_CURSOR_STATIC); + run_query(hstmt, "declare/fetch real ctid", "SELECT id FROM keyset_overflow_tab"); + run_query(hstmt, "declare/fetch oversized ctid", "SELECT id FROM keyset_overflow_view"); + SQLFreeHandle(SQL_HANDLE_STMT, hstmt); + test_disconnect(); + + return 0; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/test/tests new/psqlodbc-REL-18_00_0006/test/tests --- old/psqlodbc-REL-18_00_0005/test/tests 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/test/tests 2026-11-02 19:01:24.000000000 +0100 @@ -73,4 +73,5 @@ exe/connstring-escape-test \ exe/dbms-version-test \ exe/surrogate-pair-test \ - exe/many-params-test + exe/many-params-test \ + exe/keyset-overflow-test diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/version.h new/psqlodbc-REL-18_00_0006/version.h --- old/psqlodbc-REL-18_00_0005/version.h 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/version.h 2026-11-02 19:01:24.000000000 +0100 @@ -14,13 +14,13 @@ * and PG_DRVFILE_VERSION via winbuild/psqlodbc.vcxproj. */ #ifndef POSTGRESDRIVERVERSION -#define POSTGRESDRIVERVERSION "18.00.0005" +#define POSTGRESDRIVERVERSION "18.00.0006" #endif #ifndef POSTGRES_RESOURCE_VERSION #define POSTGRES_RESOURCE_VERSION POSTGRESDRIVERVERSION #endif #ifndef PG_DRVFILE_VERSION -#define PG_DRVFILE_VERSION 18,0,00,05 +#define PG_DRVFILE_VERSION 18,0,00,06 #endif #endif diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/psqlodbc-REL-18_00_0005/win_unicode.c new/psqlodbc-REL-18_00_0006/win_unicode.c --- old/psqlodbc-REL-18_00_0005/win_unicode.c 2026-10-12 19:08:37.000000000 +0200 +++ new/psqlodbc-REL-18_00_0006/win_unicode.c 2026-11-02 19:01:24.000000000 +0100 @@ -833,8 +833,12 @@ outmsg = NULL; outlen = mbstowcs((wchar_t *) outmsg, inmsg, buflen); #endif /* WIN32 */ - if (outmsg && outlen >= buflen) + if (outmsg && buflen > 0 && outlen >= buflen) { + /* + * buflen > 0 guards the index: a caller probing the length with a + * non-NULL buffer and buflen 0 would otherwise write outmsg[-1]. + */ outmsg[buflen - 1] = 0; MYLOG(0, " out=%dchars truncated to %d\n", outlen, buflen - 1); }
