Skip to content

Commit

Permalink
src: add option to disable loading native addons
Browse files Browse the repository at this point in the history
PR-URL: #39977
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
  • Loading branch information
d3lm authored and BethGriggs committed Sep 21, 2021
1 parent d657ae6 commit 9f3a015
Show file tree
Hide file tree
Showing 23 changed files with 272 additions and 9 deletions.
10 changes: 10 additions & 0 deletions doc/api/cli.md
Expand Up @@ -595,6 +595,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 @@ -1418,6 +1427,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 @@ -1019,6 +1019,14 @@ added: v16.4.0

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 @@ -2871,6 +2879,7 @@ The native call from `process.cpuUsage` could not be processed.
[`'uncaughtException'`]: process.md#process_event_uncaughtexception
[`--disable-proto=throw`]: cli.md#cli_disable_proto_mode
[`--force-fips`]: cli.md#cli_force_fips
[`--no-addons`]: cli.md#cli_no_addons
[`Class: assert.AssertionError`]: assert.md#assert_class_assert_assertionerror
[`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
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](#packages_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.
[`"name"`]: #packages_name
[`"packageManager"`]: #packages_packagemanager
[`"type"`]: #packages_type
[`--no-addons` flag]: cli.md#cli_no_addons
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#errors_err_package_path_not_exported
[`esm`]: https://github.com/standard-things/esm#readme
[`package.json`]: #packages_node_js_package_json_field_definitions
Expand Down
5 changes: 5 additions & 0 deletions doc/node.1
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
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
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
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 @@ -406,7 +406,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",
"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');
}
})
);
});
}
10 changes: 8 additions & 2 deletions test/fixtures/es-module-loaders/loader-with-custom-condition.mjs
@@ -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.

0 comments on commit 9f3a015

Please sign in to comment.