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
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
@@ -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 \
10 changes: 10 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
@@ -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`
2 changes: 2 additions & 0 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
@@ -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.

29 changes: 29 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
@@ -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',
2 changes: 2 additions & 0 deletions src/node_errors.h
Original file line number Diff line number Diff line change
@@ -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, \
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"
@@ -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,
8 changes: 7 additions & 1 deletion src/node_sqlite.h
Original file line number Diff line number Diff line change
@@ -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_;
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'/);
})
);
94 changes: 94 additions & 0 deletions test/sqlite/extension.c
Original file line number Diff line number Diff line change
@@ -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;
}
108 changes: 108 additions & 0 deletions test/sqlite/test-sqlite-extensions.mjs
Original file line number Diff line number Diff line change
@@ -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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this doesn't work when node is built with --shared-sqlite


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 changes: 6 additions & 0 deletions test/sqlite/testcfg.py
Original file line number Diff line number Diff line change
@@ -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')
1 change: 1 addition & 0 deletions tools/test.py
Original file line number Diff line number Diff line change
@@ -1584,6 +1584,7 @@ def PrintCrashed(code):
'js-native-api',
'node-api',
'pummel',
'sqlite',
'tick-processor',
'v8-updates'
]
Loading