July 21, 2022·6 min read·engineeringsqlitesecurityc

SQLite widens counters in its SQL string formatter

SQLite 3.39.2 includes the fix for CVE-2022-35737. Its released source widens the counters used when quoting strings, where output can grow beyond the input length.

Quoting a string for SQL can increase its length. An input containing quotation marks needs extra characters in its output, so a formatter has to count the expanded representation before writing it. SQLite 3.39.2, released on July 21, includes the fix for CVE-2022-35737. Comparing the released formatter with its predecessor shows counters in the SQL-escaping path changing from int to i64. The calculation that chooses a buffer therefore uses a wider representation of the size it is about to produce.

The patch also repairs a query-planner performance regression involving FROM-clause ordering and a LEFT JOIN. The security fix concerns a different part of the library. SQLite includes formatting routines callable by C applications, and their input and memory behavior deserve attention alongside the database engine's SQL execution.

I value SQLite's compact deployment model, but embedding a library also puts its C interfaces inside the host application's process. A formatter's storage calculations are part of that application's memory safety. The relevant question is which API performs the work and how its implementation represents the size of the result.

SQL quoting changes the required allocation

The 3.39.2 formatter source has separate conversion categories for %q, %Q, and %w. Conversion %q escapes single quotation marks. With %Q, a non-NULL argument also receives enclosing single quotes. Conversion %w escapes double quotation marks. The code selects the appropriate quotation character, counts occurrences in the input, and calculates the storage needed for the result.

For each quotation mark requiring escaping, the output writes a second copy. A string with many such characters can therefore require substantially more output bytes than input bytes. The %Q case may add enclosing quotes as well, and a terminating zero also needs storage. A correct input scan alone does not establish that the total allocation calculation fits the type holding it.

The older 3.39.1 implementation declares the scan and count variables as int. In the new implementation, i, j, k, and n are i64, while the boolean state for quoting remains an integer. This is a targeted change to the quantities representing positions and lengths. The distinction matters because a buffer decision depends on arithmetic over those quantities.

The code first counts the additional quotation characters, then adds the number of input bytes scanned and a small allowance. If the required size exceeds the internal buffer, it requests a temporary allocation. Otherwise it writes into the internal buffer. A count with insufficient range can invalidate that decision before the output loop begins. This explains the role of wider counters without requiring an exploit demonstration.

The formatter has a separate allocation check

The same source defines printfTempBuf, whose requested size is a sqlite3_int64. It checks the requested allocation against the output accumulator's available and permitted storage. If the request exceeds the relevant bounds, it sets SQLITE_TOOBIG. An allocation failure instead records SQLITE_NOMEM.

That check illustrates why size types need to remain coherent through the whole calculation. A downstream function can accept a wide integer and compare it correctly, yet still receive a bad value if upstream arithmetic already lost information. The width of the function argument does not retroactively widen calculations performed before the call.

There are consequently two questions in this code. One is whether the calculated size faithfully represents the required output. Another is whether the implementation can or should allocate that amount. Widening the counters addresses the representation used to reach the allocation decision. The allocator's limits address what happens after a valid request size is available.

This distinction is useful in reviewing C interfaces generally, but the mechanism here is concrete. The quoting path counts repeated characters, computes an expanded length, and selects between internal and allocated storage. Every stage must agree on what that length means. A local buffer optimization is safe only if the value choosing it remains valid for the subsequent write loop.

Precision limits input consumption

The source comments specify that precision for these substitutions counts input bytes, or input characters when the alternate ! flag is present. They also state that inserted quoting characters can make output larger than the precision. Precision therefore does not directly state the final number of output bytes.

That detail matters when reasoning about a bounded formatting request. Limiting the input consumed and limiting the output produced are different operations. The quoting routine must still account for expansion within the permitted input. A caller cannot interpret an input precision as an allocation size without applying the conversion's rules.

Character counting adds another distinction. UTF-8 characters can occupy multiple bytes, while the destination buffer is a byte allocation. The routine's handling of the alternate flag skips continuation bytes while scanning characters. It still has to calculate the storage required by the bytes it will copy and the additional quotation characters it will insert.

The implementation comments make these rules observable in the released code. They are more useful for assessing this patch than treating every formatting operation as a direct byte-for-byte copy. The operation's required storage depends on both the input representation and the conversion requested by the format string.

SQLite's formatting API has its own contract

The public C API shipped with 3.39.2 describes these routines as counterparts to the standard C formatting family with extra nonstandard conversions. sqlite3_mprintf and sqlite3_vmprintf allocate their output using SQLite's allocator. The caller releases the resulting string with sqlite3_free, and allocation failure returns a NULL pointer.

The following small example illustrates ownership and the %Q conversion. It is ordinary use of the API, with no oversized input or vulnerability trigger.

char *literal = sqlite3_mprintf("%Q", "O'Brien");
if (literal == NULL) {
    /* Handle allocation failure. */
    return;
}
/* literal contains the SQL text 'O''Brien'. */
sqlite3_free(literal);

Here the result is SQL text representing a string literal. It is not a bound value attached to a prepared statement. Understanding that distinction helps identify formatting calls in host code without confusing them with the engine's ordinary parameter-binding operations.

The same header documents a different ownership model for sqlite3_snprintf, where the caller supplies a buffer and its size. It also records compatibility differences from the standard library. SQLite's argument order places the size before the buffer, and the return value is a pointer to the buffer. A caller should use the contract of the SQLite routine it actually invokes rather than assume the similarly named standard routine's behavior.

The size includes space for the zero terminator, so a buffer of size n can hold at most n-1 content characters in that interface. The library guarantees termination when the supplied size is positive. These details show that formatted output is an API with memory and representation rules, independent of the SQL statements an application normally submits.

The memory allocation documentation also distinguishes sqlite3_malloc, which takes a signed integer size, from sqlite3_malloc64, which takes an unsigned 64-bit size. Their coexistence makes the width of a storage request explicit at the interface. Correct use still requires the arithmetic preparing that request to have sufficient range before the allocation routine receives it.

An embedded dependency can be used beyond queries

Reviewing the C API is relevant even for software that regards SQLite primarily as a database. A program may call utility routines for constructing diagnostic text or SQL fragments. The formatter implementation accepts arguments from C varargs or from SQL function arguments through different paths, then shares conversion logic. The released source makes those entry paths explicit.

This does not establish that every application embedding SQLite reaches the affected calculation. It establishes why exposure depends on the calls the application makes and the strings those calls can receive. A dependency name alone provides less information than an inventory of the interfaces used by the program.

The release notes identify the security correction but do not require an application author to infer an exploit from the identifier. The source provides a narrower, directly inspectable observation. SQL-string quoting needs expanded-length arithmetic, and this release uses wider counters for that calculation. The surrounding allocation checks can then operate on the size represented by those counters.

I consider this a useful reminder to include utility APIs when assessing an embedded database update. SQLite's formatter runs in the same address space as its caller and produces storage that the caller owns under the documented contract. The corrected counting path belongs to that contract, even when the program's main reason for linking SQLite is relational storage.