Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sqlite: support db.loadExtension #53900

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ coverage-report-js: ## Report JavaScript coverage results.
cctest: all ## Run the C++ tests using the built `cctest` executable.
@out/$(BUILDTYPE)/$@ --gtest_filter=$(GTEST_FILTER)
$(NODE) ./test/embedding/test-embedding.js
$(NODE) ./test/sqlite/test-sqlite-extensions.mjs

.PHONY: list-gtests
list-gtests: ## List all available C++ gtests.
Expand Down Expand Up @@ -574,6 +575,7 @@ test-ci: | clear-stalled bench-addons-build build-addons build-js-native-api-tes
--mode=$(BUILDTYPE_LOWER) --flaky-tests=$(FLAKY_TESTS) \
$(TEST_CI_ARGS) $(CI_JS_SUITES) $(CI_NATIVE_SUITES) $(CI_DOC)
$(NODE) ./test/embedding/test-embedding.js
$(NODE) ./test/sqlite/test-sqlite-extensions.mjs
$(info Clean up any leftover processes, error if found.)
ps awwx | grep Release/node | grep -v grep | cat
@PS_OUT=`ps awwx | grep Release/node | grep -v grep | awk '{print $$1}'`; \
Expand Down Expand Up @@ -1432,6 +1434,7 @@ LINT_CPP_FILES = $(filter-out $(LINT_CPP_EXCLUDE), $(wildcard \
test/cctest/*.h \
test/embedding/*.cc \
test/embedding/*.h \
test/sqlite/*.c \
test/fixtures/*.c \
test/js-native-api/*/*.cc \
test/node-api/*/*.cc \
Expand Down
10 changes: 10 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2168,6 +2168,16 @@ added:
An ESM loader hook returned without calling `next()` and without explicitly
signaling a short circuit.

<a id="ERR_LOAD_SQLITE_EXTENSION"></a>

### `ERR_LOAD_SQLITE_EXTENSION`

<!-- YAML
added: REPLACEME
-->

An error occurred while loading a SQLite extension.

<a id="ERR_MEMORY_ALLOCATION_FAILED"></a>

### `ERR_MEMORY_ALLOCATION_FAILED`
Expand Down
2 changes: 2 additions & 0 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ There are constraints you need to know before using this system:
flags that can be set via runtime through `v8.setFlagsFromString`.
* OpenSSL engines cannot be requested at runtime when the Permission
Model is enabled, affecting the built-in crypto, https, and tls modules.
* Run-Time Loadable Extensions cannot be loaded when the Permission Model is
enabled, affecting the sqlite module.
* Using existing file descriptors via the `node:fs` module bypasses the
Permission Model.

Expand Down
29 changes: 29 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ added: v22.5.0
[double-quoted string literals][]. This is not recommended but can be
enabled for compatibility with legacy database schemas.
**Default:** `false`.
* `allowExtension` {boolean} If `true`, the `loadExtension` SQL function
and the `loadExtension()` method are enabled.
You can call `enableLoadExtension(false)` later to disable this feature.
**Default:** `false`.

Constructs a new `DatabaseSync` instance.

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

### `database.loadExtension(path)`

<!-- YAML
added: REPLACEME
-->

* `path` {string} The path to the shared library to load.

Loads a shared library into the database connection. This method is a wrapper
around [`sqlite3_load_extension()`][]. It is required to enable the
`allowExtension` option when constructing the `DatabaseSync` instance.

### `database.enableLoadExtension(allow)`

<!-- YAML
added: REPLACEME
-->

* `allow` {boolean} Whether to allow loading extensions.

Enables or disables the `loadExtension` SQL function, and the `loadExtension()`
method. When `allowExtension` is `false` when constructing, you cannot enable
loading extensions for security reasons.

### `database.exec(sql)`

<!-- YAML
Expand Down Expand Up @@ -467,6 +495,7 @@ The following constants are meant for use with [`database.applyChangeset()`](#da
[`sqlite3_exec()`]: https://www.sqlite.org/c3ref/exec.html
[`sqlite3_expanded_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html
[`sqlite3_load_extension()`]: https://www.sqlite.org/c3ref/load_extension.html
[`sqlite3_prepare_v2()`]: https://www.sqlite.org/c3ref/prepare.html
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
Expand Down
20 changes: 20 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,26 @@
],
}, # embedtest

{
'target_name': 'sqlite_extension',
'type': 'shared_library',
'sources': [
'test/sqlite/extension.c'
],

'include_dirs': [
'test/sqlite',
'deps/sqlite',
],

'cflags': [
'-fPIC',
'-Wall',
'-Wextra',
'-O3',
],
}, # sqlitetest

{
'target_name': 'overlapped-checker',
'type': 'executable',
Expand Down
2 changes: 2 additions & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details);
V(ERR_INVALID_THIS, TypeError) \
V(ERR_INVALID_URL, TypeError) \
V(ERR_INVALID_URL_SCHEME, TypeError) \
V(ERR_LOAD_SQLITE_EXTENSION, Error) \
V(ERR_MEMORY_ALLOCATION_FAILED, Error) \
V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, Error) \
V(ERR_MISSING_ARGS, TypeError) \
Expand Down Expand Up @@ -191,6 +192,7 @@ ERRORS_WITH_CODE(V)
V(ERR_INVALID_STATE, "Invalid state") \
V(ERR_INVALID_THIS, "Value of \"this\" is the wrong type") \
V(ERR_INVALID_URL_SCHEME, "The URL must be of scheme file:") \
V(ERR_LOAD_SQLITE_EXTENSION, "Failed to load SQLite extension") \
V(ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate memory") \
V(ERR_OSSL_EVP_INVALID_DIGEST, "Invalid digest used") \
V(ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE, \
Expand Down
111 changes: 109 additions & 2 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "node_sqlite.h"
#include <path.h>
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
Expand Down Expand Up @@ -114,10 +115,13 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, const char* message) {
DatabaseSync::DatabaseSync(Environment* env,
Local<Object> object,
DatabaseOpenConfiguration&& open_config,
bool open)
bool open,
bool allow_load_extension)
: BaseObject(env, object), open_config_(std::move(open_config)) {
MakeWeak();
connection_ = nullptr;
allow_load_extension_ = allow_load_extension;
enable_load_extension_ = allow_load_extension;
cjihrig marked this conversation as resolved.
Show resolved Hide resolved

if (open) {
Open();
Expand Down Expand Up @@ -182,6 +186,19 @@ bool DatabaseSync::Open() {
CHECK_ERROR_OR_THROW(env()->isolate(), connection_, r, SQLITE_OK, false);
CHECK_EQ(foreign_keys_enabled, open_config_.get_enable_foreign_keys());

if (allow_load_extension_) {
himself65 marked this conversation as resolved.
Show resolved Hide resolved
if (env()->permission()->enabled()) [[unlikely]] {
THROW_ERR_LOAD_SQLITE_EXTENSION(env(),
"Cannot load SQLite extensions when the "
"permission model is enabled.");
return false;
}
const int load_extension_ret = sqlite3_db_config(
connection_, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, nullptr);
CHECK_ERROR_OR_THROW(
env()->isolate(), connection_, load_extension_ret, SQLITE_OK, false);
}

return true;
}

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

bool open = true;
bool allow_load_extension = false;

if (args.Length() > 1) {
if (!args[1]->IsObject()) {
Expand Down Expand Up @@ -302,9 +320,28 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
}
open_config.set_enable_dqs(enable_dqs_v.As<Boolean>()->Value());
}

Local<String> allow_extension_string =
FIXED_ONE_BYTE_STRING(env->isolate(), "allowExtension");
Local<Value> allow_extension_v;
if (!options->Get(env->context(), allow_extension_string)
.ToLocal(&allow_extension_v)) {
return;
}

if (!allow_extension_v->IsUndefined()) {
if (!allow_extension_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.allowExtension\" argument must be a boolean.");
return;
}
allow_load_extension = allow_extension_v.As<Boolean>()->Value();
}
}

new DatabaseSync(env, args.This(), std::move(open_config), open);
new DatabaseSync(
env, args.This(), std::move(open_config), open, allow_load_extension);
}

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

void DatabaseSync::EnableLoadExtension(
const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
if (!args[0]->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"allow\" argument must be a boolean.");
return;
}

const int enable = args[0].As<Boolean>()->Value();
auto isolate = env->isolate();

if (db->allow_load_extension_ == false && enable == true) {
THROW_ERR_INVALID_STATE(
isolate,
"Cannot enable extension loading because it was disabled at database "
"creation.");
return;
}
db->enable_load_extension_ = enable;
const int load_extension_ret = sqlite3_db_config(
db->connection_, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, enable, nullptr);
CHECK_ERROR_OR_THROW(
isolate, db->connection_, load_extension_ret, SQLITE_OK, void());
}

void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(
env, db->connection_ == nullptr, "database is not open");
THROW_AND_RETURN_ON_BAD_STATE(
env, !db->allow_load_extension_, "extension loading is not allowed");
THROW_AND_RETURN_ON_BAD_STATE(
env, !db->enable_load_extension_, "extension loading is not allowed");

if (!args[0]->IsString()) {
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
"The \"path\" argument must be a string.");
return;
}

auto isolate = env->isolate();

BufferValue path(isolate, args[0]);
BufferValue entryPoint(isolate, args[1]);
CHECK_NOT_NULL(*path);
ToNamespacedPath(env, &path);
if (*entryPoint == nullptr) {
ToNamespacedPath(env, &entryPoint);
}
THROW_IF_INSUFFICIENT_PERMISSIONS(
himself65 marked this conversation as resolved.
Show resolved Hide resolved
env, permission::PermissionScope::kFileSystemRead, path.ToStringView());
char* errmsg = nullptr;
const int r =
sqlite3_load_extension(db->connection_, *path, *entryPoint, &errmsg);
if (r != SQLITE_OK) {
isolate->ThrowException(ERR_LOAD_SQLITE_EXTENSION(isolate, errmsg));
}
}

StatementSync::StatementSync(Environment* env,
Local<Object> object,
DatabaseSync* db,
Expand Down Expand Up @@ -1312,6 +1413,12 @@ static void Initialize(Local<Object> target,
isolate, db_tmpl, "createSession", DatabaseSync::CreateSession);
SetProtoMethod(
isolate, db_tmpl, "applyChangeset", DatabaseSync::ApplyChangeset);
SetProtoMethod(isolate,
db_tmpl,
"enableLoadExtension",
himself65 marked this conversation as resolved.
Show resolved Hide resolved
DatabaseSync::EnableLoadExtension);
SetProtoMethod(
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
SetConstructorFunction(context, target, "DatabaseSync", db_tmpl);
SetConstructorFunction(context,
target,
Expand Down
8 changes: 7 additions & 1 deletion src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ class DatabaseSync : public BaseObject {
DatabaseSync(Environment* env,
v8::Local<v8::Object> object,
DatabaseOpenConfiguration&& open_config,
bool open);
bool open,
bool allow_load_extension);
void MemoryInfo(MemoryTracker* tracker) const override;
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Open(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand All @@ -58,6 +59,9 @@ class DatabaseSync : public BaseObject {
static void Exec(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CreateSession(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ApplyChangeset(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableLoadExtension(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
void FinalizeStatements();
void UntrackStatement(StatementSync* statement);
bool IsOpen();
Expand All @@ -72,6 +76,8 @@ class DatabaseSync : public BaseObject {

~DatabaseSync() override;
DatabaseOpenConfiguration open_config_;
bool allow_load_extension_;
bool enable_load_extension_;
sqlite3* connection_;

std::set<sqlite3_session*> sessions_;
Expand Down
18 changes: 18 additions & 0 deletions test/parallel/test-permission-sqlite-load-extension.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';
const common = require('../common');
const assert = require('node:assert');
const childProcess = require('child_process');

const code = `const sqlite = require('node:sqlite');
const db = new sqlite.DatabaseSync(':memory:', { allowExtension: true });
db.loadExtension('nonexistent');`.replace(/\n/g, ' ');

childProcess.exec(
`${process.execPath} --experimental-permission -e "${code}"`,
{},
common.mustCall((err, _, stderr) => {
assert.strictEqual(err.code, 1);
assert.match(stderr, /Error: Cannot load SQLite extensions when the permission model is enabled/);
assert.match(stderr, /code: 'ERR_LOAD_SQLITE_EXTENSION'/);
})
);
Loading
Loading