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, &param_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, &param_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);
        }

Reply via email to