Skip to content

Commit b102e07

Browse files
d3lmBethGriggs
authored andcommitted
src: add option to disable loading native addons
PR-URL: #39977 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Michael Dawson <[email protected]>
1 parent d657ae6 commit b102e07

23 files changed

+273
-10
lines changed

doc/api/cli.md

+10
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,15 @@ added: v7.10.0
595595

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

598+
### `--no-addons`
599+
<!-- YAML
600+
added: REPLACEME
601+
-->
602+
603+
Disable the `node-addons` exports condition as well as disable loading
604+
native addons. When `--no-addons` is specified, calling `process.dlopen` or
605+
requiring a native C++ addon will fail and throw an exception.
606+
598607
### `--no-deprecation`
599608
<!-- YAML
600609
added: v0.8.0
@@ -1418,6 +1427,7 @@ Node.js options that are allowed are:
14181427
* `--inspect`
14191428
* `--max-http-header-size`
14201429
* `--napi-modules`
1430+
* `--no-addons`
14211431
* `--no-deprecation`
14221432
* `--no-experimental-repl-await`
14231433
* `--no-force-async-hooks-checks`

doc/api/errors.md

+9
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,14 @@ added: v16.4.0
10191019

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

1022+
<a id="ERR_DLOPEN_DISABLED"></a>
1023+
### `ERR_DLOPEN_DISABLED`
1024+
<!-- YAML
1025+
added: REPLACEME
1026+
-->
1027+
1028+
Loading native addons has been disabled using [`--no-addons`][].
1029+
10221030
<a id="ERR_DLOPEN_FAILED"></a>
10231031
### `ERR_DLOPEN_FAILED`
10241032
<!-- YAML
@@ -2871,6 +2879,7 @@ The native call from `process.cpuUsage` could not be processed.
28712879
[`'uncaughtException'`]: process.md#process_event_uncaughtexception
28722880
[`--disable-proto=throw`]: cli.md#cli_disable_proto_mode
28732881
[`--force-fips`]: cli.md#cli_force_fips
2882+
[`--no-addons`]: cli.md#--no-addons
28742883
[`Class: assert.AssertionError`]: assert.md#assert_class_assert_assertionerror
28752884
[`ERR_INVALID_ARG_TYPE`]: #ERR_INVALID_ARG_TYPE
28762885
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: #ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST

doc/api/packages.md

+17-5
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,11 @@ Node.js implements the following conditions:
537537
* `"node"` - matches for any Node.js environment. Can be a CommonJS or ES
538538
module file. _This condition should always come after `"import"` or
539539
`"require"`._
540+
* `"node-addons"` - similar to `"node"` and matches for any Node.js environment.
541+
This condition can be used to provide an entry point which uses native C++
542+
addons as opposed to an entry point which is more universal and doesn't rely
543+
on native addons. This condition can be disabled via the
544+
[`--no-addons` flag][].
540545
* `"default"` - the generic fallback that always matches. Can be a CommonJS
541546
or ES module file. _This condition should always come last._
542547

@@ -615,16 +620,22 @@ node --conditions=development main.js
615620
```
616621

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

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

623628
### Conditions Definitions
624629

625-
The `"import"`, `"require"`, `"node"` and `"default"` conditions are defined
626-
and implemented in Node.js core,
627-
[as specified above](#packages_conditional_exports).
630+
The `"import"` , `"require"` , `"node"` , `"node-addons"` and `"default"`
631+
conditions are defined and implemented in Node.js core,
632+
[as specified above](#conditional-exports).
633+
634+
The `"node-addons"` condition can be used to provide an entry point which
635+
uses native C++ addons. However, this condition can be disabled via the
636+
[`--no-addons` flag][]. When using `"node-addons"`, it's recommended to treat
637+
`"default"` as an enhancement that provides a more universal entry point, e.g.
638+
using WebAssembly instead of a native addon.
628639

629640
Other condition strings are unknown to Node.js and thus ignored by default.
630641
Runtimes or tools other than Node.js can use them at their discretion.
@@ -1249,6 +1260,7 @@ This field defines [subpath imports][] for the current package.
12491260
[`"name"`]: #packages_name
12501261
[`"packageManager"`]: #packages_packagemanager
12511262
[`"type"`]: #packages_type
1263+
[`--no-addons` flag]: cli.md#--no-addons
12521264
[`ERR_PACKAGE_PATH_NOT_EXPORTED`]: errors.md#errors_err_package_path_not_exported
12531265
[`esm`]: https://github.com/standard-things/esm#readme
12541266
[`package.json`]: #packages_node_js_package_json_field_definitions

doc/node.1

+5
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ Silence deprecation warnings.
277277
Disable runtime checks for `async_hooks`.
278278
These will still be enabled dynamically when `async_hooks` is enabled.
279279
.
280+
.It Fl -no-addons
281+
Disable the `node-addons` exports condition as well as disable loading native
282+
addons. When `--no-addons` is specified, calling `process.dlopen` or requiring
283+
a native C++ addon will fail and throw an exception.
284+
.
280285
.It Fl -no-warnings
281286
Silence all process warnings (including deprecations).
282287
.

lib/internal/modules/cjs/helpers.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,16 @@ let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
3030
debug = fn;
3131
});
3232

33+
const noAddons = getOptionValue('--no-addons');
34+
const addonConditions = noAddons ? [] : ['node-addons'];
35+
3336
// TODO: Use this set when resolving pkg#exports conditions in loader.js.
34-
const cjsConditions = new SafeSet(['require', 'node', ...userConditions]);
37+
const cjsConditions = new SafeSet([
38+
'require',
39+
'node',
40+
...addonConditions,
41+
...userConditions,
42+
]);
3543

3644
function loadNativeModule(filename, request) {
3745
const mod = NativeModule.map.get(filename);

lib/internal/modules/esm/resolve.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,16 @@ const { Module: CJSModule } = require('internal/modules/cjs/loader');
5858

5959
const packageJsonReader = require('internal/modules/package_json_reader');
6060
const userConditions = getOptionValue('--conditions');
61-
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import', ...userConditions]);
61+
const noAddons = getOptionValue('--no-addons');
62+
const addonConditions = noAddons ? [] : ['node-addons'];
63+
64+
const DEFAULT_CONDITIONS = ObjectFreeze([
65+
'node',
66+
'import',
67+
...addonConditions,
68+
...userConditions,
69+
]);
70+
6271
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);
6372

6473
/**

src/env-inl.h

+5
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,11 @@ inline bool Environment::is_main_thread() const {
861861
return worker_context() == nullptr;
862862
}
863863

864+
inline bool Environment::no_native_addons() const {
865+
return (flags_ & EnvironmentFlags::kNoNativeAddons) ||
866+
!options_->allow_native_addons;
867+
}
868+
864869
inline bool Environment::should_not_register_esm_loader() const {
865870
return flags_ & EnvironmentFlags::kNoRegisterESMLoader;
866871
}

src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,7 @@ class Environment : public MemoryRetainer {
11971197
inline void set_has_serialized_options(bool has_serialized_options);
11981198

11991199
inline bool is_main_thread() const;
1200+
inline bool no_native_addons() const;
12001201
inline bool should_not_register_esm_loader() const;
12011202
inline bool owns_process_state() const;
12021203
inline bool owns_inspector() const;

src/node.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,13 @@ enum Flags : uint64_t {
406406
// Set this flag to force hiding console windows when spawning child
407407
// processes. This is usually used when embedding Node.js in GUI programs on
408408
// Windows.
409-
kHideConsoleWindows = 1 << 5
409+
kHideConsoleWindows = 1 << 5,
410+
// Set this flag to disable loading native addons via `process.dlopen`.
411+
// This environment flag is especially important for worker threads
412+
// so that a worker thread can't load a native addon even if `execArgv`
413+
// is overwritten and `--no-addons` is not specified but was specified
414+
// for this Environment instance.
415+
kNoNativeAddons = 1 << 6
410416
};
411417
} // namespace EnvironmentFlags
412418

src/node_binding.cc

+6
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,12 @@ inline napi_addon_register_func GetNapiInitializerCallback(DLib* dlib) {
415415
// cache that's a plain C list or hash table that's shared across contexts?
416416
void DLOpen(const FunctionCallbackInfo<Value>& args) {
417417
Environment* env = Environment::GetCurrent(args);
418+
419+
if (env->no_native_addons()) {
420+
return THROW_ERR_DLOPEN_DISABLED(
421+
env, "Cannot load native addon because loading addons is disabled.");
422+
}
423+
418424
auto context = env->context();
419425

420426
CHECK_NULL(thread_local_modpending);

src/node_errors.h

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ void OnFatalError(const char* location, const char* message);
5757
V(ERR_CRYPTO_UNKNOWN_DH_GROUP, Error) \
5858
V(ERR_CRYPTO_UNSUPPORTED_OPERATION, Error) \
5959
V(ERR_CRYPTO_JOB_INIT_FAILED, Error) \
60+
V(ERR_DLOPEN_DISABLED, Error) \
6061
V(ERR_DLOPEN_FAILED, Error) \
6162
V(ERR_EXECUTION_ENVIRONMENT_NOT_AVAILABLE, Error) \
6263
V(ERR_INVALID_ADDRESS, Error) \

src/node_options.cc

+5
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
402402
&EnvironmentOptions::force_async_hooks_checks,
403403
kAllowedInEnvironment,
404404
true);
405+
AddOption("--addons",
406+
"disable loading native addons",
407+
&EnvironmentOptions::allow_native_addons,
408+
kAllowedInEnvironment,
409+
true);
405410
AddOption("--warnings",
406411
"silence all process warnings",
407412
&EnvironmentOptions::warnings,

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class EnvironmentOptions : public Options {
121121
uint64_t max_http_header_size = 16 * 1024;
122122
bool deprecation = true;
123123
bool force_async_hooks_checks = true;
124+
bool allow_native_addons = true;
124125
bool warnings = true;
125126
bool force_context_aware = false;
126127
bool pending_deprecation = false;

src/node_worker.cc

+2
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,8 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
560560
worker->environment_flags_ |= EnvironmentFlags::kTrackUnmanagedFds;
561561
if (env->hide_console_windows())
562562
worker->environment_flags_ |= EnvironmentFlags::kHideConsoleWindows;
563+
if (env->no_native_addons())
564+
worker->environment_flags_ |= EnvironmentFlags::kNoNativeAddons;
563565
}
564566

565567
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {

test/addons/no-addons/binding.gyp

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': [ '../hello-world/binding.cc' ],
6+
'includes': ['../common.gypi'],
7+
}
8+
]
9+
}

test/addons/no-addons/test-worker.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Flags: --no-addons
2+
3+
'use strict';
4+
5+
const common = require('../../common');
6+
const assert = require('assert');
7+
const path = require('path');
8+
const { Worker } = require('worker_threads');
9+
10+
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
11+
12+
const assertError = (error) => {
13+
assert.strictEqual(error.code, 'ERR_DLOPEN_DISABLED');
14+
assert.strictEqual(
15+
error.message,
16+
'Cannot load native addon because loading addons is disabled.'
17+
);
18+
};
19+
20+
{
21+
// Flags should be inherited
22+
const worker = new Worker(`require(${JSON.stringify(binding)})`, {
23+
eval: true,
24+
});
25+
26+
worker.on('error', common.mustCall(assertError));
27+
}
28+
29+
{
30+
// Should throw when using `process.dlopen` directly
31+
const worker = new Worker(
32+
`process.dlopen({ exports: {} }, ${JSON.stringify(binding)});`,
33+
{
34+
eval: true,
35+
}
36+
);
37+
38+
worker.on('error', common.mustCall(assertError));
39+
}
40+
41+
{
42+
// Explicitly pass `--no-addons`
43+
const worker = new Worker(`require(${JSON.stringify(binding)})`, {
44+
eval: true,
45+
execArgv: ['--no-addons'],
46+
});
47+
48+
worker.on('error', common.mustCall(assertError));
49+
}
50+
51+
{
52+
// If `execArgv` is overwritten it should still fail to load addons
53+
const worker = new Worker(`require(${JSON.stringify(binding)})`, {
54+
eval: true,
55+
execArgv: [],
56+
});
57+
58+
worker.on('error', common.mustCall(assertError));
59+
}

test/addons/no-addons/test.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Flags: --no-addons
2+
3+
'use strict';
4+
5+
const common = require('../../common');
6+
const assert = require('assert');
7+
8+
const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
9+
10+
const assertError = (error) => {
11+
assert(error instanceof Error);
12+
assert.strictEqual(error.code, 'ERR_DLOPEN_DISABLED');
13+
assert.strictEqual(
14+
error.message,
15+
'Cannot load native addon because loading addons is disabled.'
16+
);
17+
};
18+
19+
{
20+
let threw = false;
21+
22+
try {
23+
require(bindingPath);
24+
} catch (error) {
25+
assertError(error);
26+
threw = true;
27+
}
28+
29+
assert(threw);
30+
}
31+
32+
{
33+
let threw = false;
34+
35+
try {
36+
process.dlopen({ exports: {} }, bindingPath);
37+
} catch (error) {
38+
assertError(error);
39+
threw = true;
40+
}
41+
42+
assert(threw);
43+
}

test/es-module/test-esm-no-addons.mjs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { mustCall } from '../common/index.mjs';
2+
import { Worker, isMainThread } from 'worker_threads';
3+
import assert from 'assert';
4+
import { fileURLToPath } from 'url';
5+
import { requireFixture, importFixture } from '../fixtures/pkgexports.mjs';
6+
7+
if (isMainThread) {
8+
const tests = [[], ['--no-addons']];
9+
10+
for (const execArgv of tests) {
11+
new Worker(fileURLToPath(import.meta.url), { execArgv });
12+
}
13+
} else {
14+
[requireFixture, importFixture].forEach((loadFixture) => {
15+
loadFixture('pkgexports/no-addons').then(
16+
mustCall((module) => {
17+
const message = module.default;
18+
19+
if (process.execArgv.length === 0) {
20+
assert.strictEqual(message, 'using native addons');
21+
} else {
22+
assert.strictEqual(message, 'not using native addons');
23+
}
24+
})
25+
);
26+
});
27+
}

test/fixtures/es-module-loaders/loader-with-custom-condition.mjs

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import {ok, deepStrictEqual} from 'assert';
1+
import { ok, deepStrictEqual } from 'assert';
22

33
export async function resolve(specifier, context, defaultResolve) {
44
ok(Array.isArray(context.conditions), 'loader receives conditions array');
5-
deepStrictEqual([...context.conditions].sort(), ['import', 'node']);
5+
6+
deepStrictEqual([...context.conditions].sort(), [
7+
'import',
8+
'node',
9+
'node-addons',
10+
]);
11+
612
return defaultResolve(specifier, {
713
...context,
814
conditions: ['custom-condition', ...context.conditions],

test/fixtures/node_modules/pkgexports/addons-entry.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/node_modules/pkgexports/no-addons-entry.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)