Skip to content

Commit 92d0e06

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 f9ab994 commit 92d0e06

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
@@ -1738,6 +1738,10 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
17381738
}
17391739
}
17401740

1741+
// Reading the options bag above can run user JavaScript through a property
1742+
// getter, which may have closed the database since it was checked.
1743+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1744+
17411745
Utf8Value sql(env->isolate(), args[0].As<String>());
17421746
sqlite3_stmt* s = nullptr;
17431747

@@ -1925,9 +1929,22 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo<Value>& args) {
19251929
if (!fn->Get(env->context(), env->length_string()).ToLocal(&js_len)) {
19261930
return;
19271931
}
1932+
1933+
if (!js_len->IsInt32()) {
1934+
THROW_ERR_INVALID_ARG_TYPE(
1935+
env->isolate(),
1936+
"The \"function.length\" property must be an integer.");
1937+
return;
1938+
}
1939+
19281940
argc = js_len.As<Int32>()->Value();
19291941
}
19301942

1943+
// Reading the options bag and "function.length" above can run user
1944+
// JavaScript through a property getter, which may have closed the database
1945+
// since it was checked.
1946+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1947+
19311948
UserDefinedFunction* user_data = new UserDefinedFunction(
19321949
env, fn, BaseObjectWeakPtr<DatabaseSync>(db), use_bigint_args);
19331950
int text_rep = SQLITE_UTF8;
@@ -2090,6 +2107,10 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
20902107
}
20912108
}
20922109

2110+
// Reading the options bag above can run user JavaScript through a property
2111+
// getter, which may have closed the database since it was checked.
2112+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2113+
20932114
// sqlite3_malloc64 is required because SQLITE_DESERIALIZE_FREEONCLOSE
20942115
// transfers ownership to SQLite, which calls sqlite3_free() on close.
20952116
// See: https://www.sqlite.org/c3ref/deserialize.html
@@ -2100,7 +2121,16 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
21002121
return;
21012122
}
21022123

2103-
input->CopyContents(buf, byte_length);
2124+
// The same user JavaScript may also have shrunk or detached the backing
2125+
// store, in which case byte_length is stale and CopyContents() leaves the
2126+
// remainder of buf uninitialized. Handing that to SQLite would disclose it
2127+
// through serialize().
2128+
if (input->CopyContents(buf, byte_length) != byte_length) {
2129+
sqlite3_free(buf);
2130+
THROW_ERR_INVALID_STATE(
2131+
env, "The \"buffer\" argument was resized while reading \"options\"");
2132+
return;
2133+
}
21042134

21052135
db->FinalizeStatements();
21062136

@@ -2238,17 +2268,37 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
22382268
return;
22392269
}
22402270

2271+
if (!js_len->IsInt32()) {
2272+
THROW_ERR_INVALID_ARG_TYPE(
2273+
env->isolate(),
2274+
"The \"options.step.length\" property must be an integer.");
2275+
return;
2276+
}
2277+
22412278
// Subtract 1 because the first argument is the aggregate value.
22422279
argc = js_len.As<Int32>()->Value() - 1;
2243-
if (!inverseFunc.IsEmpty() &&
2244-
!inverseFunc->Get(env->context(), env->length_string())
2245-
.ToLocal(&js_len)) {
2246-
return;
2280+
if (!inverseFunc.IsEmpty()) {
2281+
if (!inverseFunc->Get(env->context(), env->length_string())
2282+
.ToLocal(&js_len)) {
2283+
return;
2284+
}
2285+
2286+
if (!js_len->IsInt32()) {
2287+
THROW_ERR_INVALID_ARG_TYPE(
2288+
env->isolate(),
2289+
"The \"options.inverse.length\" property must be an integer.");
2290+
return;
2291+
}
22472292
}
22482293

22492294
argc = std::max({argc, js_len.As<Int32>()->Value() - 1, 0});
22502295
}
22512296

2297+
// Reading the options bag and the step/inverse "length" properties above can
2298+
// run user JavaScript through a property getter, which may have closed the
2299+
// database since it was checked.
2300+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2301+
22522302
int text_rep = SQLITE_UTF8;
22532303
if (direct_only) {
22542304
text_rep |= SQLITE_DIRECTONLY;
@@ -2277,10 +2327,15 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
22772327
}
22782328

22792329
void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
2330+
DatabaseSync* db;
2331+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
2332+
Environment* env = Environment::GetCurrent(args);
2333+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2334+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
2335+
22802336
std::string table;
22812337
std::string db_name = "main";
22822338

2283-
Environment* env = Environment::GetCurrent(args);
22842339
if (args.Length() > 0) {
22852340
if (!args[0]->IsObject()) {
22862341
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -2331,10 +2386,9 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
23312386
}
23322387
}
23332388

2334-
DatabaseSync* db;
2335-
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
2389+
// Reading the options bag above can run user JavaScript through a property
2390+
// getter, which may have closed the database since it was checked.
23362391
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2337-
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
23382392

23392393
sqlite3_session* pSession;
23402394
int r =
@@ -2461,6 +2515,11 @@ void Backup(const FunctionCallbackInfo<Value>& args) {
24612515
}
24622516
}
24632517

2518+
// Reading the destination path and the options bag above can run user
2519+
// JavaScript through a property getter, which may have closed the database
2520+
// since it was checked.
2521+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2522+
24642523
Local<Promise::Resolver> resolver;
24652524
if (!Promise::Resolver::New(env->context()).ToLocal(&resolver)) {
24662525
return;
@@ -2604,6 +2663,10 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
26042663
}
26052664
}
26062665

2666+
// Reading the options bag above can run user JavaScript through a property
2667+
// getter, which may have closed the database since it was checked.
2668+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2669+
26072670
// Keep the database alive in case a callback drops all references to it,
26082671
// which could otherwise let it be garbage-collected mid-callback.
26092672
BaseObjectPtr<DatabaseSync> guard(db);

0 commit comments

Comments
 (0)