Skip to content

Commit 53cc0cc

Browse files
himself65targos
authored andcommitted
sqlite: support db.loadExtension
PR-URL: #53900 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 6f49c80 commit 53cc0cc

13 files changed

+409
-3
lines changed

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ coverage-report-js: ## Report JavaScript coverage results.
294294
cctest: all ## Run the C++ tests using the built `cctest` executable.
295295
@out/$(BUILDTYPE)/$@ --gtest_filter=$(GTEST_FILTER)
296296
$(NODE) ./test/embedding/test-embedding.js
297+
$(NODE) ./test/sqlite/test-sqlite-extensions.mjs
297298

298299
.PHONY: list-gtests
299300
list-gtests: ## List all available C++ gtests.
@@ -574,6 +575,7 @@ test-ci: | clear-stalled bench-addons-build build-addons build-js-native-api-tes
574575
--mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \
575576
$(TEST_CI_ARGS) $(CI_JS_SUITES) $(CI_NATIVE_SUITES) $(CI_DOC)
576577
$(NODE) ./test/embedding/test-embedding.js
578+
$(NODE) ./test/sqlite/test-sqlite-extensions.mjs
577579
$(info Clean up any leftover processes, error if found.)
578580
ps awwx | grep Release/node | grep -v grep | cat
579581
@PS_OUT=`ps awwx | grep Release/node | grep -v grep | awk '{print $$1}'`; \
@@ -1432,6 +1434,7 @@ LINT_CPP_FILES = $(filter-out $(LINT_CPP_EXCLUDE), $(wildcard \
14321434
test/cctest/*.h \
14331435
test/embedding/*.cc \
14341436
test/embedding/*.h \
1437+
test/sqlite/*.c \
14351438
test/fixtures/*.c \
14361439
test/js-native-api/*/*.cc \
14371440
test/node-api/*/*.cc \

doc/api/errors.md

+10
Original file line numberDiff line numberDiff line change
@@ -2172,6 +2172,16 @@ added:
21722172
An ESM loader hook returned without calling `next()` and without explicitly
21732173
signaling a short circuit.
21742174

2175+
<a id="ERR_LOAD_SQLITE_EXTENSION"></a>
2176+
2177+
### `ERR_LOAD_SQLITE_EXTENSION`
2178+
2179+
<!-- YAML
2180+
added: REPLACEME
2181+
-->
2182+
2183+
An error occurred while loading a SQLite extension.
2184+
21752185
<a id="ERR_MEMORY_ALLOCATION_FAILED"></a>
21762186

21772187
### `ERR_MEMORY_ALLOCATION_FAILED`

doc/api/permissions.md

+2
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ There are constraints you need to know before using this system:
147147
flags that can be set via runtime through `v8.setFlagsFromString`.
148148
* OpenSSL engines cannot be requested at runtime when the Permission
149149
Model is enabled, affecting the built-in crypto, https, and tls modules.
150+
* Run-Time Loadable Extensions cannot be loaded when the Permission Model is
151+
enabled, affecting the sqlite module.
150152
* Using existing file descriptors via the `node:fs` module bypasses the
151153
Permission Model.
152154

doc/api/sqlite.md

+29
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ added: v22.5.0
108108
[double-quoted string literals][]. This is not recommended but can be
109109
enabled for compatibility with legacy database schemas.
110110
**Default:** `false`.
111+
* `allowExtension` {boolean} If `true`, the `loadExtension` SQL function
112+
and the `loadExtension()` method are enabled.
113+
You can call `enableLoadExtension(false)` later to disable this feature.
114+
**Default:** `false`.
111115

112116
Constructs a new `DatabaseSync` instance.
113117

@@ -120,6 +124,30 @@ added: v22.5.0
120124
Closes the database connection. An exception is thrown if the database is not
121125
open. This method is a wrapper around [`sqlite3_close_v2()`][].
122126

127+
### `database.loadExtension(path)`
128+
129+
<!-- YAML
130+
added: REPLACEME
131+
-->
132+
133+
* `path` {string} The path to the shared library to load.
134+
135+
Loads a shared library into the database connection. This method is a wrapper
136+
around [`sqlite3_load_extension()`][]. It is required to enable the
137+
`allowExtension` option when constructing the `DatabaseSync` instance.
138+
139+
### `database.enableLoadExtension(allow)`
140+
141+
<!-- YAML
142+
added: REPLACEME
143+
-->
144+
145+
* `allow` {boolean} Whether to allow loading extensions.
146+
147+
Enables or disables the `loadExtension` SQL function, and the `loadExtension()`
148+
method. When `allowExtension` is `false` when constructing, you cannot enable
149+
loading extensions for security reasons.
150+
123151
### `database.exec(sql)`
124152

125153
<!-- YAML
@@ -457,6 +485,7 @@ The following constants are meant for use with [`database.applyChangeset()`](#da
457485
[`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html
458486
[`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
459487
[`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html
488+
[`sqlite3_load_extension()`]: https://www.sqlite.org/c3ref/load_extension.html
460489
[`sqlite3_prepare_v2()`]: https://www.sqlite.org/c3ref/prepare.html
461490
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
462491
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html

node.gyp

+20
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,26 @@
12961296
],
12971297
}, # embedtest
12981298

1299+
{
1300+
'target_name': 'sqlite_extension',
1301+
'type': 'shared_library',
1302+
'sources': [
1303+
'test/sqlite/extension.c'
1304+
],
1305+
1306+
'include_dirs': [
1307+
'test/sqlite',
1308+
'deps/sqlite',
1309+
],
1310+
1311+
'cflags': [
1312+
'-fPIC',
1313+
'-Wall',
1314+
'-Wextra',
1315+
'-O3',
1316+
],
1317+
}, # sqlitetest
1318+
12991319
{
13001320
'target_name': 'overlapped-checker',
13011321
'type': 'executable',

src/node_errors.h

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
9191
V(ERR_INVALID_THIS, TypeError) \
9292
V(ERR_INVALID_URL, TypeError) \
9393
V(ERR_INVALID_URL_SCHEME, TypeError) \
94+
V(ERR_LOAD_SQLITE_EXTENSION, Error) \
9495
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
9596
V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, Error) \
9697
V(ERR_MISSING_ARGS, TypeError) \
@@ -191,6 +192,7 @@ ERRORS_WITH_CODE(V)
191192
V(ERR_INVALID_STATE, "Invalid state") \
192193
V(ERR_INVALID_THIS, "Value of \"this\" is the wrong type") \
193194
V(ERR_INVALID_URL_SCHEME, "The URL must be of scheme file:") \
195+
V(ERR_LOAD_SQLITE_EXTENSION, "Failed to load SQLite extension") \
194196
V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \
195197
V(ERR_OSSL_EVP_INVALID_DIGEST, "Invalid digest used") \
196198
V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, \

src/node_sqlite.cc

+109-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "node_sqlite.h"
2+
#include <path.h>
23
#include "base_object-inl.h"
34
#include "debug_utils-inl.h"
45
#include "env-inl.h"
@@ -114,10 +115,13 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, const char* message) {
114115
DatabaseSync::DatabaseSync(Environment* env,
115116
Local<Object> object,
116117
DatabaseOpenConfiguration&& open_config,
117-
bool open)
118+
bool open,
119+
bool allow_load_extension)
118120
: BaseObject(env, object), open_config_(std::move(open_config)) {
119121
MakeWeak();
120122
connection_ = nullptr;
123+
allow_load_extension_ = allow_load_extension;
124+
enable_load_extension_ = allow_load_extension;
121125

122126
if (open) {
123127
Open();
@@ -182,6 +186,19 @@ bool DatabaseSync::Open() {
182186
CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false);
183187
CHECK_EQ(foreign_keys_enabled, open_config_.get_enable_foreign_keys());
184188

189+
if (allow_load_extension_) {
190+
if (env()->permission()->enabled()) [[unlikely]] {
191+
THROW_ERR_LOAD_SQLITE_EXTENSION(env(),
192+
"Cannot load SQLite extensions when the "
193+
"permission model is enabled.");
194+
return false;
195+
}
196+
const int load_extension_ret = sqlite3_db_config(
197+
connection_, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, nullptr);
198+
CHECK_ERROR_OR_THROW(
199+
env()->isolate(), connection_, load_extension_ret, SQLITE_OK, false);
200+
}
201+
185202
return true;
186203
}
187204

@@ -227,6 +244,7 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
227244
DatabaseOpenConfiguration open_config(std::move(location));
228245

229246
bool open = true;
247+
bool allow_load_extension = false;
230248

231249
if (args.Length() > 1) {
232250
if (!args[1]->IsObject()) {
@@ -302,9 +320,28 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
302320
}
303321
open_config.set_enable_dqs(enable_dqs_v.As<Boolean>()->Value());
304322
}
323+
324+
Local<String> allow_extension_string =
325+
FIXED_ONE_BYTE_STRING(env->isolate(), "allowExtension");
326+
Local<Value> allow_extension_v;
327+
if (!options->Get(env->context(), allow_extension_string)
328+
.ToLocal(&allow_extension_v)) {
329+
return;
330+
}
331+
332+
if (!allow_extension_v->IsUndefined()) {
333+
if (!allow_extension_v->IsBoolean()) {
334+
THROW_ERR_INVALID_ARG_TYPE(
335+
env->isolate(),
336+
"The \"options.allowExtension\" argument must be a boolean.");
337+
return;
338+
}
339+
allow_load_extension = allow_extension_v.As<Boolean>()->Value();
340+
}
305341
}
306342

307-
new DatabaseSync(env, args.This(), std::move(open_config), open);
343+
new DatabaseSync(
344+
env, args.This(), std::move(open_config), open, allow_load_extension);
308345
}
309346

310347
void DatabaseSync::Open(const FunctionCallbackInfo<Value>& args) {
@@ -526,6 +563,70 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
526563
args.GetReturnValue().Set(true);
527564
}
528565

566+
void DatabaseSync::EnableLoadExtension(
567+
const FunctionCallbackInfo<Value>& args) {
568+
DatabaseSync* db;
569+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
570+
Environment* env = Environment::GetCurrent(args);
571+
if (!args[0]->IsBoolean()) {
572+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
573+
"The \"allow\" argument must be a boolean.");
574+
return;
575+
}
576+
577+
const int enable = args[0].As<Boolean>()->Value();
578+
auto isolate = env->isolate();
579+
580+
if (db->allow_load_extension_ == false && enable == true) {
581+
THROW_ERR_INVALID_STATE(
582+
isolate,
583+
"Cannot enable extension loading because it was disabled at database "
584+
"creation.");
585+
return;
586+
}
587+
db->enable_load_extension_ = enable;
588+
const int load_extension_ret = sqlite3_db_config(
589+
db->connection_, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, enable, nullptr);
590+
CHECK_ERROR_OR_THROW(
591+
isolate, db->connection_, load_extension_ret, SQLITE_OK, void());
592+
}
593+
594+
void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
595+
DatabaseSync* db;
596+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
597+
Environment* env = Environment::GetCurrent(args);
598+
THROW_AND_RETURN_ON_BAD_STATE(
599+
env, db->connection_ == nullptr, "database is not open");
600+
THROW_AND_RETURN_ON_BAD_STATE(
601+
env, !db->allow_load_extension_, "extension loading is not allowed");
602+
THROW_AND_RETURN_ON_BAD_STATE(
603+
env, !db->enable_load_extension_, "extension loading is not allowed");
604+
605+
if (!args[0]->IsString()) {
606+
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
607+
"The \"path\" argument must be a string.");
608+
return;
609+
}
610+
611+
auto isolate = env->isolate();
612+
613+
BufferValue path(isolate, args[0]);
614+
BufferValue entryPoint(isolate, args[1]);
615+
CHECK_NOT_NULL(*path);
616+
ToNamespacedPath(env, &path);
617+
if (*entryPoint == nullptr) {
618+
ToNamespacedPath(env, &entryPoint);
619+
}
620+
THROW_IF_INSUFFICIENT_PERMISSIONS(
621+
env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
622+
char* errmsg = nullptr;
623+
const int r =
624+
sqlite3_load_extension(db->connection_, *path, *entryPoint, &errmsg);
625+
if (r != SQLITE_OK) {
626+
isolate->ThrowException(ERR_LOAD_SQLITE_EXTENSION(isolate, errmsg));
627+
}
628+
}
629+
529630
StatementSync::StatementSync(Environment* env,
530631
Local<Object> object,
531632
DatabaseSync* db,
@@ -1312,6 +1413,12 @@ static void Initialize(Local<Object> target,
13121413
isolate, db_tmpl, "createSession", DatabaseSync::CreateSession);
13131414
SetProtoMethod(
13141415
isolate, db_tmpl, "applyChangeset", DatabaseSync::ApplyChangeset);
1416+
SetProtoMethod(isolate,
1417+
db_tmpl,
1418+
"enableLoadExtension",
1419+
DatabaseSync::EnableLoadExtension);
1420+
SetProtoMethod(
1421+
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
13151422
SetConstructorFunction(context, target, "DatabaseSync", db_tmpl);
13161423
SetConstructorFunction(context,
13171424
target,

src/node_sqlite.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ class DatabaseSync : public BaseObject {
4949
DatabaseSync(Environment* env,
5050
v8::Local<v8::Object> object,
5151
DatabaseOpenConfiguration&& open_config,
52-
bool open);
52+
bool open,
53+
bool allow_load_extension);
5354
void MemoryInfo(MemoryTracker* tracker) const override;
5455
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
5556
static void Open(const v8::FunctionCallbackInfo<v8::Value>& args);
@@ -58,6 +59,9 @@ class DatabaseSync : public BaseObject {
5859
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
5960
static void CreateSession(const v8::FunctionCallbackInfo<v8::Value>& args);
6061
static void ApplyChangeset(const v8::FunctionCallbackInfo<v8::Value>& args);
62+
static void EnableLoadExtension(
63+
const v8::FunctionCallbackInfo<v8::Value>& args);
64+
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
6165
void FinalizeStatements();
6266
void UntrackStatement(StatementSync* statement);
6367
bool IsOpen();
@@ -72,6 +76,8 @@ class DatabaseSync : public BaseObject {
7276

7377
~DatabaseSync() override;
7478
DatabaseOpenConfiguration open_config_;
79+
bool allow_load_extension_;
80+
bool enable_load_extension_;
7581
sqlite3* connection_;
7682

7783
std::set<sqlite3_session*> sessions_;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('node:assert');
4+
const childProcess = require('child_process');
5+
6+
const code = `const sqlite = require('node:sqlite');
7+
const db = new sqlite.DatabaseSync(':memory:', { allowExtension: true });
8+
db.loadExtension('nonexistent');`.replace(/\n/g, ' ');
9+
10+
childProcess.exec(
11+
`${process.execPath} --experimental-permission -e "${code}"`,
12+
{},
13+
common.mustCall((err, _, stderr) => {
14+
assert.strictEqual(err.code, 1);
15+
assert.match(stderr, /Error: Cannot load SQLite extensions when the permission model is enabled/);
16+
assert.match(stderr, /code: 'ERR_LOAD_SQLITE_EXTENSION'/);
17+
})
18+
);

0 commit comments

Comments
 (0)