Repository navigation
sqlite: expandedSQL, sourceSQL, column names, and sqlite.db.query tracing abort the process on text longer than String::kMaxLength #66487
Description
Version
v26.10.0 (all repros below). The expandedSQL blob repro also aborts on v24.21.0 and v22.23.2.
Platform
Linux hostname 7.0.0-34-generic #34-Ubuntu SMP PREEMPT_DYNAMIC Wed Sep 2 14:29:37 UTC 2026 x86_64 GNU/Linux
Subsystem
sqlite
What steps will reproduce the bug?
statement.expandedSQL:
const { DatabaseSync } = require('node:sqlite');
const { constants } = require('node:buffer');
const db = new DatabaseSync(':memory:');
const stmt = db.prepare('SELECT ?');
stmt.run(Buffer.alloc((constants.MAX_STRING_LENGTH >>> 1) + 1));
stmt.expandedSQL; // abortssqlite.db.query diagnostics channel (v26.8.0 and later):
const { DatabaseSync } = require('node:sqlite');
const dc = require('node:diagnostics_channel');
const { constants } = require('node:buffer');
dc.subscribe('sqlite.db.query', () => {});
const db = new DatabaseSync(':memory:');
db.prepare('SELECT length(?)').get(
Buffer.alloc((constants.MAX_STRING_LENGTH >>> 1) + 1)); // abortsstatement.sourceSQL:
const { DatabaseSync } = require('node:sqlite');
const { constants } = require('node:buffer');
const n = Math.ceil((constants.MAX_STRING_LENGTH + 1) / 3);
const db = new DatabaseSync(':memory:');
const stmt = db.prepare(`SELECT '${'€'.repeat(n)}'`);
stmt.sourceSQL; // abortsIn the first two, the blob is 268,435,445 bytes. sqlite3_expanded_sql() renders a bound blob as x'<hex>', so the expanded SQL is 536,870,900 bytes, which is more than String::kMaxLength (536,870,888 on 64-bit).
In the third, the SQL string has 178,956,972 characters, which is under String::kMaxLength, but its UTF-8 encoding is 536,870,898 bytes, which is over it.
How often does it reproduce? Is there a required condition?
Every run. The condition is SQL text whose UTF-8 encoding is longer than String::kMaxLength bytes:
expandedSQLand the trace callback: a bound parameter makes the expanded SQL that long. A blob does it at about 256 MiB. On v26.10.0, a text parameter of multi-byte characters also does it:stmt.run('€'.repeat(178956963))followed bystmt.expandedSQLaborts.sourceSQL: the SQL passed toprepare()is that long in UTF-8.
The two blob repros take about 8 s and reach about 830 MB peak RSS. The sourceSQL repro takes 7.4 s and reaches 3.0 GB. All were measured with /usr/bin/time.
What is the expected behavior? Why is that the expected behavior?
The process should not abort.
statement.expandedSQLshould throwERR_STRING_TOO_LONG. Since c5b7a06 (sqlite: throw on oversized string values #66209), column values and user-defined function arguments longer thanString::kMaxLengththrowERR_STRING_TOO_LONG, andbuffer.toString()throws the same code for the same limit.- A statement traced through
sqlite.db.queryshould complete as it does without a subscriber. If the SQL text cannot be represented as a string, the event can be skipped. statement.sourceSQLshould return the string or throwERR_STRING_TOO_LONG. On v24.21.0 thesourceSQLrepro and the text-parameterexpandedSQLvariant both return the string.
What do you see instead?
expandedSQL on v26.10.0 (exit code 133, SIGTRAP):
#
# Fatal error in , line 0
# Check failed: String::kMaxLength >= len.
#
#
#
#FailureMessage Object: 0x7ffdf8743130
----- Native stack trace -----
1: 0xaa1c41 [node]
2: 0x28ff6b1 V8_Fatal(char const*, ...) [node]
3: 0xcf20be [node]
4: 0xbb1ff5 node::sqlite::StatementSync::ExpandedSQLGetter(v8::FunctionCallbackInfo<v8::Value> const&) [node]
5: 0x73297fdcfae9
Trace/breakpoint trap (core dumped)
sqlite.db.query on v26.10.0 (exit code 133):
#
# Fatal error in , line 0
# Check failed: String::kMaxLength >= len.
#
#
#
#FailureMessage Object: 0x7ffd1b3ad090
----- Native stack trace -----
1: 0xaa1c41 [node]
2: 0x28ff6b1 V8_Fatal(char const*, ...) [node]
3: 0xcf20be [node]
4: 0xba679c node::sqlite::DatabaseSync::TraceCallback(unsigned int, void*, void*, void*) [node]
5: 0x22f1a46 [node]
6: 0x22eda9d [node]
7: 0xbb07ac node::sqlite::StatementExecutionHelper::Get(node::Environment*, node::sqlite::StatementSync*) [node]
8: 0xbb1368 node::sqlite::StatementSync::Get(v8::FunctionCallbackInfo<v8::Value> const&) [node]
9: 0x746b6f58fae9
Trace/breakpoint trap (core dumped)
sourceSQL on v26.10.0 (exit code 133):
#
# Fatal error in , line 0
# Check failed: String::kMaxLength >= len.
#
#
#
#FailureMessage Object: 0x7ffe08b2f620
----- Native stack trace -----
1: 0xaa1c41 [node]
2: 0x28ff6b1 V8_Fatal(char const*, ...) [node]
3: 0xcf20be [node]
4: 0xbb1ea6 node::sqlite::StatementSync::SourceSQLGetter(v8::FunctionCallbackInfo<v8::Value> const&) [node]
Trace/breakpoint trap (core dumped)
On v24.21.0 and v22.23.2 the blob expandedSQL repro fails a different check, Check failed: (location_) != nullptr., with the same ExpandedSQLGetter frame. The reason is below.
Additional information
This was discovered and repro'ed by claude opus 5.5.
These call sites pass SQLite's text to String::NewFromUtf8() without a length:
Statement::ExpandedSQLGetter: src/node_sqlite.cc#L4437Statement::SourceSQLGetter: src/node_sqlite.cc#L4417Database::TraceCallback, for the expanded SQL: src/node_sqlite.cc#L3716 (added in c7b962c, sqlite: add diagnostic channel #62241)Database::TraceCallback, for the source SQL fallback: src/node_sqlite.cc#L3725
Each one then checks ToLocal(), but that code is not reached. In V8's NEW_STRING macro (deps/v8/src/api/api.cc#L7524-L7543), the check that returns an empty handle for length > String::kMaxLength runs only when length > 0. With the default length of -1, V8 measures the string itself:
- V8 in v26.x and main (14.6):
StringLength()runsCHECK_GE(String::kMaxLength, len)on thestrlen()byte count (api.cc#L7470-L7475) and aborts. This CHECK is present from v26.0.0. v25.9.0 (V8 14.1) checks only againstkMaxInt. - V8 in v24.x (13.6) and v22.x (12.4):
StringLength()checks only againstkMaxInt.Factory::NewStringFromUtf8()then sizes the string by its UTF-16 length. If that is overString::kMaxLength,NewRawStringWithMap()throwsInvalidStringLength, returns an emptyMaybeHandle, and the.ToHandleChecked()inNEW_STRING(v24.21.0 api.cc#L7624) aborts. If it is under, as in thesourceSQLrepro, the string is created.
So the blob expandedSQL repro aborts on v22.23.2, v24.21.0, and v26.10.0. The sourceSQL repro and the text-parameter expandedSQL variant abort on v26.10.0 and return the string on v24.21.0.
With an explicit length greater than String::kMaxLength, NEW_STRING returns an empty MaybeLocal without throwing and without aborting.
TraceCallback uses the source SQL fallback when sqlite3_expanded_sql() returns NULL. That happens when the expansion would exceed SQLITE_LIMIT_LENGTH (1,000,000,000 bytes by default). This aborts v26.10.0 in TraceCallback (14.5 s, 2.7 GB peak RSS):
const { DatabaseSync } = require('node:sqlite');
const dc = require('node:diagnostics_channel');
const { constants } = require('node:buffer');
dc.subscribe('sqlite.db.query', () => {});
const n = Math.ceil((constants.MAX_STRING_LENGTH + 1) / 3);
const db = new DatabaseSync(':memory:');
const stmt = db.prepare(
`SELECT length('${'€'.repeat(n)}') AS a, length(?) AS b`);
stmt.get(Buffer.alloc(240_000_000)); // abortsColumn names take the same path. Statement::ColumnNameToName (src/node_sqlite.cc#L4007) creates row object keys with NewFromUtf8(..., NewStringType::kInternalized) and no length, and SQLite names an unaliased result column after its expression text. This aborts v26.10.0 in StatementSync::GetCachedColumnNames (8.4 s, 3.5 GB peak RSS). On v24.21.0 it returns a row whose key has 178,956,973 characters:
const { DatabaseSync } = require('node:sqlite');
const { constants } = require('node:buffer');
const n = Math.ceil((constants.MAX_STRING_LENGTH + 1) / 3);
const db = new DatabaseSync(':memory:');
db.prepare(`SELECT length('${'€'.repeat(n)}')`).get(); // abortsThese calls also pass no length. I have not tested whether SQLite can produce text that long for them:
NullableSQLiteStringToValue(), used bystatement.columns()and the authorizer callback: src/node_sqlite.cc#L402CreateSQLiteErrorImpl(), for the error message anderrstr: src/node_sqlite.cc#L287 and #L296
c5b7a06 added the ERR_STRING_TOO_LONG check in Utf8StringMaybeOneByte() (src/node_sqlite.cc#L74-L93) for column values and function arguments. It did not change these call sites.
Suggested fix (not built or tested against Node.js core):
ExpandedSQLGetterandSourceSQLGetter: pass the length, and throwERR_STRING_TOO_LONGwhen V8 returns an empty handle.Utf8StringMaybeOneByte(env->isolate(), std::string_view(...))already does both.ColumnNameToName: same, but it needskInternalized, so it would pass the length toNewFromUtf8()directly and throwERR_STRING_TOO_LONGitself.GetCachedColumnNames()already returnsfalsewhen it gets an empty handle.TraceCallback, both calls: passstrlen()as the length. The existingreturn 0on an empty handle then skips publishing and the traced statement completes, so subscribing to the channel does not change whether a statement succeeds. SQLite calls this callback from insidesqlite3_step(),sqlite3_reset(), orsqlite3_finalize(). Both strings are capped at 1,000,000,000 bytes by default (SQLITE_LIMIT_LENGTHandSQLITE_LIMIT_SQL_LENGTH), so the length fits in anint.
V8 compares an explicit length against String::kMaxLength in bytes, not UTF-16 code units. With this fix, the sourceSQL repro, the text-parameter expandedSQL variant, and the column-name repro would throw ERR_STRING_TOO_LONG, where v24.21.0 returns the string or row. Utf8StringMaybeOneByte() already rejects column values by byte length the same way. Returning those strings would require computing the UTF-16 length and transcoding before creating the string.
@photostructure/sqlite, a Node-API port of node:sqlite, hit the same abort through napi_create_string_utf8(..., NAPI_AUTO_LENGTH, ...), which takes the same length = -1 path. Its fix, photostructure/node-sqlite@095efcd, takes the approach above for expandedSQL and the trace callback: pass strlen(), throw ERR_STRING_TOO_LONG from expandedSQL, and skip the trace event.
Activity
mceachen commentedon Oct 7, 2026
The calls listed above as untested are reachable, and are fixed separately: #66520 (
CreateSQLiteErrorImpl(), for error messages) and #66522 (NullableSQLiteStringToValue(), for authorizer arguments andcolumns()metadata) were fixed by #66521, which landed in aa1bf2c. Theerrstrcall at L296 needs no change, because it always receives the short fixed text fromsqlite3_errstr(). #66498 covers the remaining call sites in this issue.@lazerg, this covers the follow-up mentioned in the #66498 description.