sqlite: support db.loadExtension

PR-URL: https://github.com/nodejs/node/pull/53900
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
Alex Yang 2024-12-03 15:15:46 -08:00
parent a1d980c4e0
commit 5c2f599712
13 changed files with 409 additions and 3 deletions

View File

@ -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.
@ -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}'`; \
@ -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 \

View File

@ -2174,6 +2174,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`

View File

@ -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.

View File

@ -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.
@ -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
@ -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

View File

@ -1296,6 +1296,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',

View File

@ -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) \
@ -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, \

View File

@ -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"
@ -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;
if (open) {
Open();
@ -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_) {
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;
}
@ -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()) {
@ -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) {
@ -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(
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,
@ -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",
DatabaseSync::EnableLoadExtension);
SetProtoMethod(
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
SetConstructorFunction(context, target, "DatabaseSync", db_tmpl);
SetConstructorFunction(context,
target,

View File

@ -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);
@ -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();
@ -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_;

View File

@ -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'/);
})
);

94
test/sqlite/extension.c Normal file
View File

@ -0,0 +1,94 @@
/*
** 2020-01-08
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
******************************************************************************
**
** This SQLite extension implements a noop() function used for testing.
**
** Variants:
**
** noop(X) The default. Deterministic.
** noop_i(X) Deterministic and innocuous.
** noop_do(X) Deterministic and direct-only.
** noop_nd(X) Non-deterministic.
*/
#include <assert.h>
#include <sqlite3ext.h>
#include <stdio.h>
#include <string.h>
SQLITE_EXTENSION_INIT1
/*
** Implementation of the noop() function.
**
** The function returns its argument, unchanged.
*/
static void noopfunc(sqlite3_context* context, int argc, sqlite3_value** argv) {
assert(argc == 1);
sqlite3_result_value(context, argv[0]);
}
/*
** Implementation of the multitype_text() function.
**
** The function returns its argument. The result will always have a
** TEXT value. But if the original input is numeric, it will also
** have that numeric value.
*/
static void multitypeTextFunc(sqlite3_context* context,
int argc,
sqlite3_value** argv) {
assert(argc == 1);
(void)argc;
(void)sqlite3_value_text(argv[0]);
sqlite3_result_value(context, argv[0]);
}
#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_extension_init(sqlite3* db,
char** pzErrMsg,
const sqlite3_api_routines* pApi) {
int rc = SQLITE_OK;
SQLITE_EXTENSION_INIT2(pApi);
rc = sqlite3_create_function(
db, "noop", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0, noopfunc, 0, 0);
if (rc) return rc;
rc = sqlite3_create_function(
db,
"noop_i",
1,
SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_INNOCUOUS,
0,
noopfunc,
0,
0);
if (rc) return rc;
rc = sqlite3_create_function(
db,
"noop_do",
1,
SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_DIRECTONLY,
0,
noopfunc,
0,
0);
if (rc) return rc;
rc =
sqlite3_create_function(db, "noop_nd", 1, SQLITE_UTF8, 0, noopfunc, 0, 0);
if (rc) return rc;
rc = sqlite3_create_function(
db, "multitype_text", 1, SQLITE_UTF8, 0, multitypeTextFunc, 0, 0);
return rc;
}

View File

@ -0,0 +1,108 @@
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import path from 'node:path';
import sqlite from 'node:sqlite';
import test from 'node:test';
import fs from 'node:fs';
import childProcess from 'child_process';
// Lib extension binary is named differently on different platforms
function resolveBuiltBinary(binary) {
const targetFile = fs.readdirSync(path.dirname(process.execPath)).find((file) => file.startsWith(binary));
return path.join(path.dirname(process.execPath), targetFile);
}
const binary = resolveBuiltBinary('libsqlite_extension');
test('should load extension successfully', () => {
const db = new sqlite.DatabaseSync(':memory:', {
allowExtension: true,
});
db.loadExtension(binary);
db.exec('SELECT noop(\'Hello, world!\');');
const query = db.prepare('SELECT noop(\'Hello, World!\') AS result');
const { result } = query.get();
assert.strictEqual(result, 'Hello, World!');
});
test('should not load extension', () => {
const db = new sqlite.DatabaseSync(':memory:', {
allowExtension: false,
});
assert.throws(() => {
db.exec('SELECT noop(\'Hello, world!\');');
}, {
message: 'no such function: noop',
code: 'ERR_SQLITE_ERROR',
});
assert.throws(() => {
db.loadExtension(binary);
}, {
message: 'extension loading is not allowed',
code: 'ERR_INVALID_STATE',
});
assert.throws(() => {
const query = db.prepare('SELECT load_extension(?)');
query.run(binary);
}, {
message: 'not authorized',
code: 'ERR_SQLITE_ERROR',
});
assert.throws(() => {
db.enableLoadExtension();
}, {
message: 'The "allow" argument must be a boolean.',
code: 'ERR_INVALID_ARG_TYPE',
});
assert.throws(() => {
db.enableLoadExtension(true);
}, {
message: 'Cannot enable extension loading because it was disabled at database creation.',
});
});
test('should load extension successfully with enableLoadExtension', () => {
const db = new sqlite.DatabaseSync(':memory:', {
allowExtension: true,
});
db.loadExtension(binary);
db.enableLoadExtension(false);
db.exec('SELECT noop(\'Hello, world!\');');
const query = db.prepare('SELECT noop(\'Hello, World!\') AS result');
const { result } = query.get();
assert.strictEqual(result, 'Hello, World!');
});
test('should not load extension with enableLoadExtension', () => {
const db = new sqlite.DatabaseSync(':memory:', {
allowExtension: true,
});
db.enableLoadExtension(false);
assert.throws(() => {
db.loadExtension(binary);
}, {
message: 'extension loading is not allowed',
});
});
test('should throw error if permission is enabled', async () => {
const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" `;
const code = `const sqlite = require('node:sqlite');
const db = new sqlite.DatabaseSync(':memory:', { allowExtension: true });`;
return new Promise((resolve) => {
childProcess.exec(
`${cmd} --experimental-permission -e "${code}"`,
{
...opts,
},
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'/);
resolve();
}),
);
});
});

6
test/sqlite/testcfg.py Normal file
View File

@ -0,0 +1,6 @@
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import testpy
def GetConfiguration(context, root):
return testpy.SimpleTestConfiguration(context, root, 'sqlite')

View File

@ -1584,6 +1584,7 @@ IGNORED_SUITES = [
'js-native-api',
'node-api',
'pummel',
'sqlite',
'tick-processor',
'v8-updates'
]