HexBrain

MariaDB · Report #3920633

Connector/ODBC: SqlwcsOctetLen sizes UTF-16 with a byte-oriented helper, reading past the caller's buffer and sending client memory to the server

Validstatus
mediumseverity
7upvotes
4 Nov 2026disclosed

How it was found

SqlwcsOctetLen (driver/ma_platform_posix.cpp:121) sizes a UTF-16 string by calling Connector/C's mb_charlen. That helper is byte-oriented: its UTF16_HIGH_HEAD(x) macro casts the argument to uchar, so it inspects only the low byte. SqlwcsOctetLen hands it a whole SQLWCHAR, so every character whose low byte falls in 0xD8..0xDB is mistaken for a UTF-16 surrogate lead and the cursor advances two code units while the character counter drops by one. The scan runs past the end of the caller's buffer and returns an inflated octet count, which MADB_ConvertFromWChar passes straight to iconv, so memory the application never supplied is converted and sent to the server as SQL text.

That set is 4 of every 256 code points and it includes very ordinary characters: U+8FD9 (one of the most frequent Chinese characters), U+4ED8, U+52D9, and U+00D8, U+00D9, U+00DA, U+00DB.

Scope limits stated up front: this is the POSIX build only, since the Windows platform file has a separate MADB_ConvertFromWChar that does not use this function; the memory goes to the database server rather than directly to an attacker; and the volume transmitted per statement is capped by the conversion output buffer, measured at 12 to 16 bytes, though the out-of-bounds read itself grows without bound. Reproduced on the driver as shipped in odbc-mariadb 3.2.6-1.1, with no custom build and no sanitizer required.

Root cause

driver/ma_platform_posix.cpp:121:

/* CharLen < 0 - treat as NTS */
SQLINTEGER SqlwcsOctetLen(const SQLWCHAR *str, SQLINTEGER *CharLen)
{
  SQLINTEGER result= 0, inChars= *CharLen;

  if (str)
  {
    while (inChars > 0 || (inChars < 0 && *str))
    {
      result+= DmUnicodeCs->mb_charlen(*str);
      --inChars;
      str+= DmUnicodeCs->mb_charlen(*str)/sizeof(SQLWCHAR);
    }
  }

DmUnicodeCs is set at driver/ma_environment.cpp:140 to utf16le or utf16 when sizeof(SQLWCHAR) == 2, which is the case on Linux with unixODBC. Both entries in Connector/C's charset table use mysql_mbcharlen_utf16:

#define UTF16_HIGH_HEAD(x)  ((((uchar) (x)) & 0xFC) == 0xD8)

static uint mysql_mbcharlen_utf16(unsigned int utf16)
{
  return UTF16_HIGH_HEAD(utf16) ? 4 : 2;
}

The cast to uchar truncates to the low 8 bits. That is correct for the byte-wise scanner the macro was written for, check_mb_utf16 at ma_charset.c:465, which passes *start from a char*. It is wrong here because SqlwcsOctetLen passes a whole code unit. For *str == 0x00D8 the cast yields 0xD8, 0xD8 & 0xFC == 0xD8, and the helper reports 4 octets, so the cursor advances 4/sizeof(SQLWCHAR) == 2 units for one counted character. The test is also inverted for genuine surrogates: U+D83D, a real high surrogate, has low byte 0x3D and is treated as a 2-octet BMP character.

Any code point of the form 0xNND8, 0xNND9, 0xNNDA or 0xNNDB triggers it. Everyday examples: U+8FD9 (Chinese "this", among the most common characters in the language), U+4ED8 ("pay", as in payment text), U+52D9 (Japanese "service", as in business terminology), and U+00D8, U+00D9, U+00DA, U+00DB in Nordic and Romance names and in any upper-cased text field.

With an explicit count of N characters the loop iterates N times and the cursor reaches index 2N-8 for a string of 8 ASCII characters plus N-8 triggers, so a buffer holding exactly N code units is read well past its end. The inflated return value is used directly as the iconv input length:

    /* PtrLength is in characters. MADB_ConvertString(iconv) needs bytes */
    PtrOctetLen= SqlwcsOctetLen(Ptr, &PtrLength);
    AscLen= (PtrLength + (mustBeNullTerminated ? 1 : 0))*cc->cs_info->char_maxlen;
  }

  if (!(AscStr = (char *)MADB_CALLOC(AscLen)))
    return NULL;

  AscLen= MADB_ConvertString((char*)Ptr, &PtrOctetLen, DmUnicodeCs, AscStr, &AscLen, cc->cs_info, Error);

The output buffer AscLen is sized from the honest character count while the input length PtrOctetLen is the inflated one, which is why the amount transmitted per call is bounded even though the read is not.

Under SQL_NTS the same defect has an extra consequence. A trigger character sitting immediately before the terminator makes the cursor step from the trigger to the unit after the NUL, so the loop condition *str never sees the terminator and the scan continues into whatever follows. This is demonstrated below, not merely inferred.

MADB_ConvertFromWChar is the conversion used by every wide entry point, so this reaches the server through SQLPrepareW, SQLExecDirectW (driver/ma_api_internal.cpp:2057 and siblings), SQLDriverConnectW and the wide catalog functions.

The intended shape is visible elsewhere in the same driver: SqlwcsCharLen (driver/ma_string.cpp:571) walks a wide string with an explicit end pointer and breaks when it passes it. SqlwcsOctetLen has no end pointer at all.

Steps to reproduce

  1. Install the driver as shipped, odbc-mariadb 3.2.6-1.1, or build from 4e175d9. No sanitizer and no special build flags are needed.
  2. Compile the attached reproducer: gcc -O0 -g -o poc_h1_repro poc_h1_repro.c -lodbc.
  3. Run it against any MariaDB instance: ./poc_h1_repro "DRIVER=/usr/lib/x86_64-linux-gnu/odbc/libmaodbc.so;SERVER=127.0.0.1;PORT=3306;UID=user;PWD=pass;DATABASE=db;"
  4. Case B is the clearest. The application builds a NUL-terminated wide string of 10 characters, SELECT 'x followed by U+00D8, and calls SQLExecDirectW with SQL_NTS. It writes the marker LEAKED_PAST_NUL into memory after the terminator, which it never asks the driver to send.
  5. Observe the row the server returns, which is 19 bytes: x\xC3\x98\0LEAKED_PAST_NUL. The embedded NUL is the application's string terminator, and everything after it is client memory that the driver read past the end of the string, converted, and sent to the server, which then returned it.
  6. Case A does the same with an explicit length of 32 characters and ordinary Chinese text. The server's own error quotes back the marker, showing it crossed the wire. Confirm independently in the server's general query log: SET GLOBAL general_log=1; then read the logged statement, which contains bytes the application never supplied.
  7. For the memory-safety view, build with -fsanitize=address and run the attached poc/poc_wcs_octetlen.c, which allocates exactly 32 SQLWCHAR and calls SQLPrepareW(stmt, buf, 32). AddressSanitizer reports heap-buffer-overflow READ of size 2 in SqlwcsOctetLen at driver/ma_platform_posix.cpp:129, located 0 bytes after the 64-byte allocation. The identical call with ASCII content reports nothing.
  8. Fixed control: replace the mb_charlen calls with a direct test of the code unit, unit >= 0xD800 && unit <= 0xDBFF ? 4 : 2. Rebuild and rerun. The AddressSanitizer report disappears and the server receives only the characters the application declared. poc/run_poc.sh performs the whole sequence including this step.

Affected versions and suggested fix

mariadb-connector-odbc POSIX build, master 4e175d9 (VERSION 03.02.0010) and released 3.2.x including odbc-mariadb 3.2.6-1.1. SqlwcsOctetLen is defined and used only in driver/ma_platform_posix.cpp; driver/ma_platform_win32.cpp implements its own MADB_ConvertFromWChar and does not call it, so Windows is not affected by this particular defect.

In SqlwcsOctetLen, test the code unit for a high surrogate directly rather than delegating to the byte-oriented mb_charlen, and give the loop an end pointer as SqlwcsCharLen already has.

This is a client library with no MariaDB-operated instance, so no production confirmation is possible and none is claimed.

All poc/... paths above are inside the attached mariadb_connector_odbc_wcs_overread_poc_by_fg0x0.zip.

Impact

An application's process memory is read out of bounds and the contents are transmitted to the database server inside the SQL statement.

Confidentiality is rated Low because each statement discloses a bounded amount, 12 to 16 bytes in the measured configuration, but the leak repeats on every statement containing a trigger character, so an application handling Chinese, Japanese or Nordic text discloses a continuous trickle of adjacent heap for as long as it runs. In a database client that memory commonly holds other statement text, previously fetched row data, or credentials held for reconnection.

Integrity is rated Low because the statement the server executes is provably not the statement the application composed: in Case B the application asked for a 10-character query and the server returned a 19-byte value.

Availability is rated None because no crash was demonstrated against the released build, though the unbounded out-of-bounds read makes one plausible on an unlucky heap layout.

No hostile server, no elevated privilege and no unusual configuration is required. The trigger is ordinary user content. U+8FD9 alone appears in a large share of Chinese-language text, so an application storing Chinese user input through the wide ODBC API leaks on routine traffic rather than on a crafted payload. An attacker who can place any of the 1024 affected code points into a string the application sends, for example through a name, address, comment or search field, both decides that the over-read happens and controls how far the cursor runs, since the distance grows by one code unit per trigger character.

Where the database is operated by a party other than the application owner, such as a hosted or multi-tenant service, this crosses a trust boundary directly. The disclosed bytes are also written to the server's general query log and appear in server error messages, which applications frequently record or surface to users.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original