You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In 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:
expandedSQL and 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 by stmt.expandedSQL aborts.
sourceSQL: the SQL passed to prepare() 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.expandedSQL should throw ERR_STRING_TOO_LONG. Since c5b7a06 (sqlite: throw on oversized string values #66209), column values and user-defined function arguments longer than String::kMaxLength throw ERR_STRING_TOO_LONG, and buffer.toString() throws the same code for the same limit.
A statement traced through sqlite.db.query should complete as it does without a subscriber. If the SQL text cannot be represented as a string, the event can be skipped.
statement.sourceSQL should return the string or throw ERR_STRING_TOO_LONG. On v24.21.0 the sourceSQL repro and the text-parameter expandedSQL variant both return the string.
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:
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() runs CHECK_GE(String::kMaxLength, len) on the strlen() byte count (api.cc#L7470-L7475) and aborts. This CHECK is present from v26.0.0. v25.9.0 (V8 14.1) checks only against kMaxInt.
V8 in v24.x (13.6) and v22.x (12.4): StringLength() checks only against kMaxInt. Factory::NewStringFromUtf8() then sizes the string by its UTF-16 length. If that is over String::kMaxLength, NewRawStringWithMap() throws InvalidStringLength, returns an empty MaybeHandle, and the .ToHandleChecked() in NEW_STRING (v24.21.0 api.cc#L7624) aborts. If it is under, as in the sourceSQL repro, 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');constdc=require('node:diagnostics_channel');const{ constants }=require('node:buffer');dc.subscribe('sqlite.db.query',()=>{});constn=Math.ceil((constants.MAX_STRING_LENGTH+1)/3);constdb=newDatabaseSync(':memory:');conststmt=db.prepare(`SELECT length('${'€'.repeat(n)}') AS a, length(?) AS b`);stmt.get(Buffer.alloc(240_000_000));// aborts
Column 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:
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):
ExpandedSQLGetter and SourceSQLGetter: pass the length, and throw ERR_STRING_TOO_LONG when V8 returns an empty handle. Utf8StringMaybeOneByte(env->isolate(), std::string_view(...)) already does both.
ColumnNameToName: same, but it needs kInternalized, so it would pass the length to NewFromUtf8() directly and throw ERR_STRING_TOO_LONG itself. GetCachedColumnNames() already returns false when it gets an empty handle.
TraceCallback, both calls: pass strlen() as the length. The existing return 0 on 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 inside sqlite3_step(), sqlite3_reset(), or sqlite3_finalize(). Both strings are capped at 1,000,000,000 bytes by default (SQLITE_LIMIT_LENGTH and SQLITE_LIMIT_SQL_LENGTH), so the length fits in an int.
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.
Version
v26.10.0 (all repros below). The
expandedSQLblob repro also aborts on v24.21.0 and v22.23.2.Platform
Subsystem
sqlite
What steps will reproduce the bug?
statement.expandedSQL:sqlite.db.querydiagnostics channel (v26.8.0 and later):statement.sourceSQL:In the first two, the blob is 268,435,445 bytes.
sqlite3_expanded_sql()renders a bound blob asx'<hex>', so the expanded SQL is 536,870,900 bytes, which is more thanString::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::kMaxLengthbytes: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
sourceSQLrepro 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.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?
expandedSQLon v26.10.0 (exit code 133, SIGTRAP):sqlite.db.queryon v26.10.0 (exit code 133):sourceSQLon v26.10.0 (exit code 133):On v24.21.0 and v22.23.2 the blob
expandedSQLrepro fails a different check,Check failed: (location_) != nullptr., with the sameExpandedSQLGetterframe. 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#L3725Each one then checks
ToLocal(), but that code is not reached. In V8'sNEW_STRINGmacro (deps/v8/src/api/api.cc#L7524-L7543), the check that returns an empty handle forlength > String::kMaxLengthruns only whenlength > 0. With the default length of -1, V8 measures the string itself: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.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
expandedSQLrepro aborts on v22.23.2, v24.21.0, and v26.10.0. ThesourceSQLrepro and the text-parameterexpandedSQLvariant abort on v26.10.0 and return the string on v24.21.0.With an explicit length greater than
String::kMaxLength,NEW_STRINGreturns an emptyMaybeLocalwithout throwing and without aborting.TraceCallbackuses the source SQL fallback whensqlite3_expanded_sql()returns NULL. That happens when the expansion would exceedSQLITE_LIMIT_LENGTH(1,000,000,000 bytes by default). This aborts v26.10.0 inTraceCallback(14.5 s, 2.7 GB peak RSS):Column names take the same path.
Statement::ColumnNameToName(src/node_sqlite.cc#L4007) creates row object keys withNewFromUtf8(..., NewStringType::kInternalized)and no length, and SQLite names an unaliased result column after its expression text. This aborts v26.10.0 inStatementSync::GetCachedColumnNames(8.4 s, 3.5 GB peak RSS). On v24.21.0 it returns a row whose key has 178,956,973 characters:These 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 #L296c5b7a06 added the
ERR_STRING_TOO_LONGcheck inUtf8StringMaybeOneByte()(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::kMaxLengthin bytes, not UTF-16 code units. With this fix, thesourceSQLrepro, the text-parameterexpandedSQLvariant, and the column-name repro would throwERR_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 throughnapi_create_string_utf8(..., NAPI_AUTO_LENGTH, ...), which takes the samelength = -1path. Its fix, photostructure/node-sqlite@095efcd, takes the approach above forexpandedSQLand the trace callback: passstrlen(), throwERR_STRING_TOO_LONGfromexpandedSQL, and skip the trace event.