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 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
10 changes: 10 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
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
-->

Disable the `node-addons` exports 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
Original file line number Diff line number Diff line change
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
20 changes: 16 additions & 4 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ Node.js implements the following conditions:
* `"node"` - matches for any Node.js environment. Can be a CommonJS or ES
module file. _This condition should always come after `"import"` or
`"require"`._
* `"node-addons"` - similar to `"node"` and matches for any Node.js environment.
This condition can be used to provide an entry point which uses native C++
addons as opposed to an entry point which is more universal and doesn't rely
on native addons. This condition can be disabled via the
[`--no-addons` flag][].
* `"default"` - the generic fallback that always matches. Can be a CommonJS
or ES module file. _This condition should always come last._

Expand Down Expand Up @@ -615,17 +620,23 @@ node --conditions=development main.js
```

which would then resolve the `"development"` condition in package imports and
exports, while resolving the existing `"node"`, `"default"`, `"import"`, and
`"require"` conditions as appropriate.
exports, while resolving the existing `"node"`, `"node-addons"`, `"default"`,
`"import"`, and `"require"` conditions as appropriate.

Any number of custom conditions can be set with repeat flags.

### Conditions Definitions

The `"import"`, `"require"`, `"node"` and `"default"` conditions are defined
and implemented in Node.js core,
The `"import"`, `"require"`, `"node"`, `"node-addons"` and `"default"`
conditions are defined and implemented in Node.js core,
[as specified above](#conditional-exports).

The `"node-addons"` condition can be used to provide an entry point which
uses native C++ addons. However, this condition can be disabled via the
[`--no-addons` flag][]. When using `"node-addons"`, it's recommended to treat
`"default"` as an enhancement that provides a more universal entry point, e.g.
using WebAssembly instead of a native addon.

Other condition strings are unknown to Node.js and thus ignored by default.
Runtimes or tools other than Node.js can use them at their discretion.

Expand Down Expand Up @@ -1249,6 +1260,7 @@ This field defines [subpath imports][] for the current package.
[`"packageManager"`]: #packagemanager
[`"type"`]: #type
[`--conditions` flag]: #resolving-user-conditions
[`--no-addons` flag]: cli.md#--no-addons
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#err_package_path_not_exported
[`esm`]: https://github.com/standard-things/esm#readme
[`package.json`]: #nodejs-packagejson-field-definitions
Expand Down
5 changes: 5 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@ Silence deprecation warnings.
Disable runtime checks for `async_hooks`.
These will still be enabled dynamically when `async_hooks` is enabled.
.
.It Fl -no-addons
Disable the `node-addons` exports 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
10 changes: 9 additions & 1 deletion lib/internal/modules/cjs/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
debug = fn;
});

const noAddons = getOptionValue('--no-addons');
const addonConditions = noAddons ? [] : ['node-addons'];

// TODO: Use this set when resolving pkg#exports conditions in loader.js.
const cjsConditions = new SafeSet(['require', 'node', ...userConditions]);
const cjsConditions = new SafeSet([
'require',
'node',
...addonConditions,
...userConditions,
]);

function loadNativeModule(filename, request) {
const mod = NativeModule.map.get(filename);
Expand Down
11 changes: 10 additions & 1 deletion lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,16 @@ 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 addonConditions = noAddons ? [] : ['node-addons'];

const DEFAULT_CONDITIONS = ObjectFreeze([
'node',
'import',
...addonConditions,
...userConditions,
]);

const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);

/**
Expand Down
5 changes: 5 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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');
}
})
);
});
}
10 changes: 8 additions & 2 deletions test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import {ok, deepStrictEqual} from 'assert';
import { ok, deepStrictEqual } from 'assert';

export async function resolve(specifier, context, defaultResolve) {
ok(Array.isArray(context.conditions), 'loader receives conditions array');
deepStrictEqual([...context.conditions].sort(), ['import', 'node']);

deepStrictEqual([...context.conditions].sort(), [
'import',
'node',
'node-addons',
]);

return defaultResolve(specifier, {
...context,
conditions: ['custom-condition', ...context.conditions],
Expand Down
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.

Loading