Skip to content

Commit d3e10a8

Browse files
committed
sqlite: re-validate database state after reading options
prepare(), function(), aggregate(), deserialize(), applyChangeset() and backup() validated the connection, then read their options bag with Object::Get(). A property getter runs arbitrary JavaScript at that point, so a getter calling close() invalidates what was just checked. Five of the six then passed a null sqlite3* to SQLite and crashed; prepare() reported a spurious "out of memory". Re-check IsOpen() after option parsing, immediately before the SQLite call, keeping the early check so invalid calls still fail before any user code runs. IsOpen() is the only condition a getter can change: authorizer and callback depths are RAII-managed. createSession() already parsed options first, so it only gains the early check. deserialize() also latched the buffer length before reading options.dbName. A getter that shrank the backing store left the length too large; CopyContents() then handed the uninitialized remainder to SQLite, from where serialize() returned it to JavaScript. Check the CopyContents() result instead of discarding it. function() and aggregate() cast the callback's length property with As<Int32>() and no IsInt32() guard. length is configurable, so any type reached the cast and produced a silently wrong arity. Fixes: #65586 Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
1 parent 29c517f commit d3e10a8

3 files changed

Lines changed: 378 additions & 11 deletions

File tree

doc/api/sqlite.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ Registers a new aggregate function with the SQLite database. This method is a wr
243243
JavaScript numbers. **Default:** `false`.
244244
* `varargs` {boolean} If `true`, `options.step` and `options.inverse` may be invoked with any number of
245245
arguments (between zero and [`SQLITE_MAX_FUNCTION_ARG`][]). If `false`,
246-
`inverse` and `step` must be invoked with exactly `length` arguments.
246+
`inverse` and `step` must be invoked with exactly `length` arguments, and
247+
their `length` properties must be integers.
247248
**Default:** `false`.
248249
* `start` {number | string | null | Array | Object | Function} The identity
249250
value for the aggregation function. This value is used when the aggregation
@@ -430,7 +431,8 @@ added:
430431
JavaScript numbers. **Default:** `false`.
431432
* `varargs` {boolean} If `true`, `function` may be invoked with any number of
432433
arguments (between zero and [`SQLITE_MAX_FUNCTION_ARG`][]). If `false`,
433-
`function` must be invoked with exactly `function.length` arguments.
434+
`function` must be invoked with exactly `function.length` arguments, which
435+
must be an integer.
434436
**Default:** `false`.
435437
* `fn` {Function} The JavaScript function to call when the SQLite function is
436438
invoked. The return value of this function should be a valid SQLite data type:

src/node_sqlite.cc

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,6 +1718,10 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
17181718
}
17191719
}
17201720

1721+
// Reading the options bag above can run user JavaScript through a property
1722+
// getter, which may have closed the database since it was checked.
1723+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1724+
17211725
Utf8Value sql(env->isolate(), args[0].As<String>());
17221726
sqlite3_stmt* s = nullptr;
17231727

@@ -1905,9 +1909,22 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo<Value>& args) {
19051909
if (!fn->Get(env->context(), env->length_string()).ToLocal(&js_len)) {
19061910
return;
19071911
}
1912+
1913+
if (!js_len->IsInt32()) {
1914+
THROW_ERR_INVALID_ARG_TYPE(
1915+
env->isolate(),
1916+
"The \"function.length\" property must be an integer.");
1917+
return;
1918+
}
1919+
19081920
argc = js_len.As<Int32>()->Value();
19091921
}
19101922

1923+
// Reading the options bag and "function.length" above can run user
1924+
// JavaScript through a property getter, which may have closed the database
1925+
// since it was checked.
1926+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1927+
19111928
UserDefinedFunction* user_data = new UserDefinedFunction(
19121929
env, fn, BaseObjectWeakPtr<DatabaseSync>(db), use_bigint_args);
19131930
int text_rep = SQLITE_UTF8;
@@ -2070,6 +2087,10 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
20702087
}
20712088
}
20722089

2090+
// Reading the options bag above can run user JavaScript through a property
2091+
// getter, which may have closed the database since it was checked.
2092+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2093+
20732094
// sqlite3_malloc64 is required because SQLITE_DESERIALIZE_FREEONCLOSE
20742095
// transfers ownership to SQLite, which calls sqlite3_free() on close.
20752096
// See: https://www.sqlite.org/c3ref/deserialize.html
@@ -2080,7 +2101,16 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
20802101
return;
20812102
}
20822103

2083-
input->CopyContents(buf, byte_length);
2104+
// The same user JavaScript may also have shrunk or detached the backing
2105+
// store, in which case byte_length is stale and CopyContents() leaves the
2106+
// remainder of buf uninitialized. Handing that to SQLite would disclose it
2107+
// through serialize().
2108+
if (input->CopyContents(buf, byte_length) != byte_length) {
2109+
sqlite3_free(buf);
2110+
THROW_ERR_INVALID_STATE(
2111+
env, "The \"buffer\" argument was resized while reading \"options\"");
2112+
return;
2113+
}
20842114

20852115
db->FinalizeStatements();
20862116

@@ -2218,17 +2248,37 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
22182248
return;
22192249
}
22202250

2251+
if (!js_len->IsInt32()) {
2252+
THROW_ERR_INVALID_ARG_TYPE(
2253+
env->isolate(),
2254+
"The \"options.step.length\" property must be an integer.");
2255+
return;
2256+
}
2257+
22212258
// Subtract 1 because the first argument is the aggregate value.
22222259
argc = js_len.As<Int32>()->Value() - 1;
2223-
if (!inverseFunc.IsEmpty() &&
2224-
!inverseFunc->Get(env->context(), env->length_string())
2225-
.ToLocal(&js_len)) {
2226-
return;
2260+
if (!inverseFunc.IsEmpty()) {
2261+
if (!inverseFunc->Get(env->context(), env->length_string())
2262+
.ToLocal(&js_len)) {
2263+
return;
2264+
}
2265+
2266+
if (!js_len->IsInt32()) {
2267+
THROW_ERR_INVALID_ARG_TYPE(
2268+
env->isolate(),
2269+
"The \"options.inverse.length\" property must be an integer.");
2270+
return;
2271+
}
22272272
}
22282273

22292274
argc = std::max({argc, js_len.As<Int32>()->Value() - 1, 0});
22302275
}
22312276

2277+
// Reading the options bag and the step/inverse "length" properties above can
2278+
// run user JavaScript through a property getter, which may have closed the
2279+
// database since it was checked.
2280+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2281+
22322282
int text_rep = SQLITE_UTF8;
22332283
if (direct_only) {
22342284
text_rep |= SQLITE_DIRECTONLY;
@@ -2257,10 +2307,15 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
22572307
}
22582308

22592309
void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
2310+
DatabaseSync* db;
2311+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
2312+
Environment* env = Environment::GetCurrent(args);
2313+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2314+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
2315+
22602316
std::string table;
22612317
std::string db_name = "main";
22622318

2263-
Environment* env = Environment::GetCurrent(args);
22642319
if (args.Length() > 0) {
22652320
if (!args[0]->IsObject()) {
22662321
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -2311,10 +2366,9 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
23112366
}
23122367
}
23132368

2314-
DatabaseSync* db;
2315-
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
2369+
// Reading the options bag above can run user JavaScript through a property
2370+
// getter, which may have closed the database since it was checked.
23162371
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2317-
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
23182372

23192373
sqlite3_session* pSession;
23202374
int r =
@@ -2441,6 +2495,11 @@ void Backup(const FunctionCallbackInfo<Value>& args) {
24412495
}
24422496
}
24432497

2498+
// Reading the destination path and the options bag above can run user
2499+
// JavaScript through a property getter, which may have closed the database
2500+
// since it was checked.
2501+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2502+
24442503
Local<Promise::Resolver> resolver;
24452504
if (!Promise::Resolver::New(env->context()).ToLocal(&resolver)) {
24462505
return;
@@ -2584,6 +2643,10 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
25842643
}
25852644
}
25862645

2646+
// Reading the options bag above can run user JavaScript through a property
2647+
// getter, which may have closed the database since it was checked.
2648+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2649+
25872650
// Keep the database alive during sqlite3changeset_apply(), which may
25882651
// call conflict or filter callbacks that trigger JavaScript execution.
25892652
// If the JavaScript callback drops all references to the database,

0 commit comments

Comments
 (0)