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

src: add option to disable loading native addons #39977

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/api/cli.md
Expand Up @@ -598,6 +598,15 @@ added: v7.10.0

This option is a no-op. It is kept for compatibility.

### `--no-addons`
<!-- YAML
added: REPLACEME
-->

Enable a `no-addons` resolution condition as well as disable loading native
addons. When `--no-addons` is specified, calling `process.dlopen` or requiring
a native C++ addon will fail and throw an exception.

### `--no-deprecation`
<!-- YAML
added: v0.8.0
Expand Down Expand Up @@ -1421,6 +1430,7 @@ Node.js options that are allowed are:
* `--inspect`
* `--max-http-header-size`
* `--napi-modules`
* `--no-addons`
* `--no-deprecation`
* `--no-experimental-repl-await`
* `--no-force-async-hooks-checks`
Expand Down
9 changes: 9 additions & 0 deletions doc/api/errors.md
Expand Up @@ -1027,6 +1027,14 @@ added:

The [debugger][] timed out waiting for the required host/port to be free.

<a id="ERR_DLOPEN_DISABLED"></a>
### `ERR_DLOPEN_DISABLED`
<!-- YAML
added: REPLACEME
-->

Loading native addons has been disabled using [`--no-addons`][].

<a id="ERR_DLOPEN_FAILED"></a>
### `ERR_DLOPEN_FAILED`
<!-- YAML
Expand Down Expand Up @@ -2879,6 +2887,7 @@ The native call from `process.cpuUsage` could not be processed.
[`'uncaughtException'`]: process.md#event-uncaughtexception
[`--disable-proto=throw`]: cli.md#--disable-protomode
[`--force-fips`]: cli.md#--force-fips
[`--no-addons`]: cli.md#--no-addons
[`Class: assert.AssertionError`]: assert.md#class-assertassertionerror
[`ERR_INVALID_ARG_TYPE`]: #err_invalid_arg_type
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: #err_missing_message_port_in_transfer_list
Expand Down
4 changes: 4 additions & 0 deletions doc/node.1
Expand Up @@ -277,6 +277,10 @@ Silence deprecation warnings.
Disable runtime checks for `async_hooks`.
These will still be enabled dynamically when `async_hooks` is enabled.
.
.It Fl -no-addons
Enable a `no-addons` resolution condition as well as disable loading native addons. When `--no-addons` is specified,
calling `process.dlopen` or requiring a native C++ addon will fail and throw an exception.
.
.It Fl -no-warnings
Silence all process warnings (including deprecations).
.
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/modules/cjs/helpers.js
Expand Up @@ -33,6 +33,10 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
const cjsConditions = new SafeSet(['require', 'node', ...userConditions]);

if (getOptionValue('--no-addons')) {
cjsConditions.add('no-addons');
}

function loadNativeModule(filename, request) {
const mod = NativeModule.map.get(filename);
if (mod?.canBeRequiredByUsers) {
Expand Down
10 changes: 9 additions & 1 deletion lib/internal/modules/esm/resolve.js
Expand Up @@ -58,7 +58,15 @@ const { Module: CJSModule } = require('internal/modules/cjs/loader');

const packageJsonReader = require('internal/modules/package_json_reader');
const userConditions = getOptionValue('--conditions');
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
const noAddons = getOptionValue('--no-addons');

const DEFAULT_CONDITIONS = ObjectFreeze([
'node',
'import',
...(noAddons ? ['no-addons'] : []),
...userConditions,
]);

const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);

/**
Expand Down
5 changes: 5 additions & 0 deletions src/env-inl.h
Expand Up @@ -861,6 +861,11 @@ inline bool Environment::is_main_thread() const {
return worker_context() == nullptr;
}

inline bool Environment::no_native_addons() const {
return (flags_ & EnvironmentFlags::kNoNativeAddons) ||
!options_->allow_native_addons;
}

inline bool Environment::should_not_register_esm_loader() const {
return flags_ & EnvironmentFlags::kNoRegisterESMLoader;
}
Expand Down
1 change: 1 addition & 0 deletions src/env.h
Expand Up @@ -1197,6 +1197,7 @@ class Environment : public MemoryRetainer {
inline void set_has_serialized_options(bool has_serialized_options);

inline bool is_main_thread() const;
inline bool no_native_addons() const;
inline bool should_not_register_esm_loader() const;
inline bool owns_process_state() const;
inline bool owns_inspector() const;
Expand Down
8 changes: 7 additions & 1 deletion src/node.h
Expand Up @@ -407,7 +407,13 @@ enum Flags : uint64_t {
// Set this flag to force hiding console windows when spawning child
// processes. This is usually used when embedding Node.js in GUI programs on
// Windows.
kHideConsoleWindows = 1 << 5
kHideConsoleWindows = 1 << 5,
// Set this flag to disable loading native addons via `process.dlopen`.
// This environment flag is especially important for worker threads
// so that a worker thread can't load a native addon even if `execArgv`
// is overwritten and `--no-addons` is not specified but was specified
// for this Environment instance.
kNoNativeAddons = 1 << 6
};
} // namespace EnvironmentFlags

Expand Down
6 changes: 6 additions & 0 deletions src/node_binding.cc
Expand Up @@ -415,6 +415,12 @@ inline napi_addon_register_func GetNapiInitializerCallback(DLib* dlib) {
// cache that's a plain C list or hash table that's shared across contexts?
void DLOpen(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

if (env->no_native_addons()) {
return THROW_ERR_DLOPEN_DISABLED(
env, "Cannot load native addon because loading addons is disabled.");
}

auto context = env->context();

CHECK_NULL(thread_local_modpending);
Expand Down
1 change: 1 addition & 0 deletions src/node_errors.h
Expand Up @@ -57,6 +57,7 @@ void OnFatalError(const char* location, const char* message);
V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \
V(ERR_CRYPTO_UNSUPPORTED_OPERATION, Error) \
V(ERR_CRYPTO_JOB_INIT_FAILED, Error) \
V(ERR_DLOPEN_DISABLED, Error) \
V(ERR_DLOPEN_FAILED, Error) \
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
V(ERR_INVALID_ADDRESS, Error) \
Expand Down
5 changes: 5 additions & 0 deletions src/node_options.cc
Expand Up @@ -402,6 +402,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::force_async_hooks_checks,
kAllowedInEnvironment,
true);
AddOption("--addons",
bmeck marked this conversation as resolved.
Show resolved Hide resolved
"disable loading native addons",
&EnvironmentOptions::allow_native_addons,
kAllowedInEnvironment,
true);
AddOption("--warnings",
"silence all process warnings",
&EnvironmentOptions::warnings,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Expand Up @@ -121,6 +121,7 @@ class EnvironmentOptions : public Options {
uint64_t max_http_header_size = 16 * 1024;
bool deprecation = true;
bool force_async_hooks_checks = true;
bool allow_native_addons = true;
bool warnings = true;
bool force_context_aware = false;
bool pending_deprecation = false;
Expand Down
2 changes: 2 additions & 0 deletions src/node_worker.cc
Expand Up @@ -560,6 +560,8 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
worker->environment_flags_ |= EnvironmentFlags::kTrackUnmanagedFds;
if (env->hide_console_windows())
worker->environment_flags_ |= EnvironmentFlags::kHideConsoleWindows;
if (env->no_native_addons())
worker->environment_flags_ |= EnvironmentFlags::kNoNativeAddons;
}

void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
Expand Down
9 changes: 9 additions & 0 deletions test/addons/no-addons/binding.gyp
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ '../hello-world/binding.cc' ],
'includes': ['../common.gypi'],
}
]
}
59 changes: 59 additions & 0 deletions test/addons/no-addons/test-worker.js
@@ -0,0 +1,59 @@
// Flags: --no-addons

'use strict';

const common = require('../../common');
const assert = require('assert');
const path = require('path');
const { Worker } = require('worker_threads');

const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);

const assertError = (error) => {
assert.strictEqual(error.code, 'ERR_DLOPEN_DISABLED');
assert.strictEqual(
error.message,
'Cannot load native addon because loading addons is disabled.'
);
};

{
// Flags should be inherited
const worker = new Worker(`require(${JSON.stringify(binding)})`, {
eval: true,
});

worker.on('error', common.mustCall(assertError));
}

{
// Should throw when using `process.dlopen` directly
const worker = new Worker(
`process.dlopen({ exports: {} }, ${JSON.stringify(binding)});`,
{
eval: true,
}
);

worker.on('error', common.mustCall(assertError));
}

{
// Explicitly pass `--no-addons`
const worker = new Worker(`require(${JSON.stringify(binding)})`, {
eval: true,
execArgv: ['--no-addons'],
});

worker.on('error', common.mustCall(assertError));
}

{
// If `execArgv` is overwritten it should still fail to load addons
const worker = new Worker(`require(${JSON.stringify(binding)})`, {
eval: true,
execArgv: [],
});

worker.on('error', common.mustCall(assertError));
}
43 changes: 43 additions & 0 deletions test/addons/no-addons/test.js
@@ -0,0 +1,43 @@
// Flags: --no-addons

'use strict';

const common = require('../../common');
const assert = require('assert');

const bindingPath = require.resolve(`./build/${common.buildType}/binding`);

const assertError = (error) => {
assert(error instanceof Error);
assert.strictEqual(error.code, 'ERR_DLOPEN_DISABLED');
assert.strictEqual(
error.message,
'Cannot load native addon because loading addons is disabled.'
);
};

{
let threw = false;

try {
require(bindingPath);
} catch (error) {
assertError(error);
threw = true;
}

assert(threw);
}

{
let threw = false;

try {
process.dlopen({ exports: {} }, bindingPath);
} catch (error) {
assertError(error);
threw = true;
}

assert(threw);
}
27 changes: 27 additions & 0 deletions test/es-module/test-esm-no-addons.mjs
@@ -0,0 +1,27 @@
import { mustCall } from '../common/index.mjs';
import { Worker, isMainThread } from 'worker_threads';
import assert from 'assert';
import { fileURLToPath } from 'url';
import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';

if (isMainThread) {
const tests = [[], ['--no-addons']];

for (const execArgv of tests) {
new Worker(fileURLToPath(import.meta.url), { execArgv });
}
} else {
[requireFixture, importFixture].forEach((loadFixture) => {
loadFixture('pkgexports/no-addons').then(
mustCall((module) => {
const message = module.default;

if (process.execArgv.length === 0) {
assert.strictEqual(message, 'using native addons');
} else {
assert.strictEqual(message, 'not using native addons');
}
})
);
});
}
3 changes: 3 additions & 0 deletions test/fixtures/node_modules/pkgexports/addons-entry.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions test/fixtures/node_modules/pkgexports/no-addons-entry.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions test/fixtures/node_modules/pkgexports/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions test/parallel/test-no-addons-resolution-condition.js
@@ -0,0 +1,29 @@
'use strict';

const common = require('../common');
const fixtures = require('../common/fixtures');
const { Worker, isMainThread, parentPort } = require('worker_threads');
const assert = require('assert');
const { createRequire } = require('module');

const loadFixture = createRequire(fixtures.path('node_modules'));

if (isMainThread) {
const tests = [[], ['--no-addons']];

for (const execArgv of tests) {
const worker = new Worker(__filename, { execArgv });

worker.on('message', common.mustCall((message) => {
if (execArgv.length === 0) {
assert.strictEqual(message, 'using native addons');
} else {
assert.strictEqual(message, 'not using native addons');
}
}));
}

} else {
const message = loadFixture('pkgexports/no-addons');
parentPort.postMessage(message);
}