Skip to content
Open
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
76 changes: 47 additions & 29 deletions lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,34 +120,6 @@ const legacyWrapperList = new SafeSet([
'util',
]);

// The code below assumes that the two lists must not contain any modules
// beginning with "internal/".
// Modules that can only be imported via the node: scheme.
const schemelessBlockList = new SafeSet([
'bench',
'bench/reporters',
'dtls',
'ffi',
'sea',
'sqlite',
'quic',
'test',
'test/reporters',
'vfs',
]);
// Modules that will only be enabled at run time.
const experimentalModuleList = new SafeSet([
'bench',
'bench/reporters',
'dtls',
'ffi',
'quic',
'sqlite',
'stream/iter',
'vfs',
'zlib/iter',
]);

// Set up process.binding() and process._linkedBinding().
{
const bindingObj = { __proto__: null };
Expand Down Expand Up @@ -220,10 +192,41 @@ const getOwn = (target, property, receiver) => {
undefined;
};

// Public builtin exposure policies. Each entry is [id, schemeOnly, option].
// A null option means that the builtin is not gated by a runtime option.
// Do not include internal modules. Runtime option definitions and code-cache
// categories are owned by their respective native subsystems.
const builtinModulePolicies = [
['bench', true, '--experimental-bench'],
['bench/reporters', true, '--experimental-bench'],
['dtls', true, '--experimental-dtls'],
['ffi', true, '--experimental-ffi'],
['sea', true, null],
['sqlite', true, '--experimental-sqlite'],
['quic', true, '--experimental-quic'],
['stream/iter', false, '--experimental-stream-iter'],
['test', true, null],
['test/reporters', true, null],
['vfs', true, '--experimental-vfs'],
['zlib/iter', false, '--experimental-stream-iter'],
];

const schemelessBlockList = new SafeSet();
const optionGatedBuiltinOptions = new SafeMap();
for (let i = 0; i < builtinModulePolicies.length; i++) {
const { 0: id, 1: schemeOnly, 2: option } = builtinModulePolicies[i];
if (schemeOnly) {
schemelessBlockList.add(id);
}
if (option !== null) {
optionGatedBuiltinOptions.set(id, option);
}
}

const publicBuiltinIds = builtinIds
.filter((id) =>
!StringPrototypeStartsWith(id, 'internal/') &&
!experimentalModuleList.has(id),
!optionGatedBuiltinOptions.has(id),
);
// Do not expose the loaders to user land even with --expose-internals.
const internalBuiltinIds = builtinIds
Expand Down Expand Up @@ -284,6 +287,21 @@ class BuiltinModule {
}
}

// Called after runtime options have been initialized, before user modules.
static allowOptionGatedBuiltins(getOptionValue) {
for (const { 0: id, 1: option } of optionGatedBuiltinOptions) {
if (getOptionValue(option)) {
BuiltinModule.allowRequireByUsers(id);
}
}
}

// Return a copy so internal tests cannot mutate the loader's policy.
static getBuiltinModulePolicies() {
return ArrayPrototypeMap(builtinModulePolicies,
(policy) => ArrayPrototypeSlice(policy));
}

static setRealmAllowRequireByUsers(ids) {
canBeRequiredByUsersList =
new SafeSet(ArrayPrototypeFilter(ids, (id) => ArrayPrototypeIncludes(publicBuiltinIds, id)));
Expand Down
72 changes: 3 additions & 69 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,7 @@ function prepareExecution(options) {
setupNetworkInspection();
setupNavigator();
setupWarningHandler();
setupBench();
setupFFI();
setupSQLite();
setupStreamIter();
setupDTLS();
setupVfs();
setupQuic();
setupOptionGatedBuiltins();
setupWebStorage();
removeWebWorkersIfDisabled();
setupEventsource();
Expand Down Expand Up @@ -461,32 +455,9 @@ function setupNavigator() {
defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false);
}

function setupBench() {
if (!getOptionValue('--experimental-bench')) {
return;
}

const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('bench');
BuiltinModule.allowRequireByUsers('bench/reporters');
}

function setupFFI() {
if (!getOptionValue('--experimental-ffi')) {
return;
}

const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('ffi');
}

function setupSQLite() {
if (getOptionValue('--no-experimental-sqlite')) {
return;
}

function setupOptionGatedBuiltins() {
const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('sqlite');
BuiltinModule.allowOptionGatedBuiltins(getOptionValue);
}

function initializeConfigFileSupport() {
Expand All @@ -495,43 +466,6 @@ function initializeConfigFileSupport() {
}
}

function setupStreamIter() {
if (!getOptionValue('--experimental-stream-iter')) {
return;
}

const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('stream/iter');
BuiltinModule.allowRequireByUsers('zlib/iter');
}

function setupDTLS() {
if (!getOptionValue('--experimental-dtls')) {
return;
}

const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('dtls');
}

function setupQuic() {
if (!getOptionValue('--experimental-quic')) {
return;
}

const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('quic');
}

function setupVfs() {
if (!getOptionValue('--experimental-vfs')) {
return;
}

const { BuiltinModule } = require('internal/bootstrap/realm');
BuiltinModule.allowRequireByUsers('vfs');
}

function setupWebStorage() {
if (getEmbedderOptions().noBrowserGlobals ||
!getOptionValue('--experimental-webstorage')) {
Expand Down
8 changes: 7 additions & 1 deletion lib/internal/test/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,10 @@ if (module.isPreloading) {
globalThis.primordials = primordials;
}

module.exports = { internalBinding: filteredInternalBinding, primordials };
module.exports = {
internalBinding: filteredInternalBinding,
primordials,
getBuiltinModulePolicies() {
return require('internal/bootstrap/realm').BuiltinModule.getBuiltinModulePolicies();
},
};
143 changes: 143 additions & 0 deletions test/parallel/test-module-builtin-policies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Flags: --expose-internals
'use strict';

// Run before loading common, whose async-hooks checks need --expose-internals.
// Child processes and Workers exercise only the public loaders.
if (process.argv[2] === 'child') {
const policy = JSON.parse(process.argv[3]);
const enabled = process.argv[4] === 'true';
checkPolicy(policy, enabled).catch((err) => {
console.error(err);
process.exitCode = 1;
});
return;
}

const common = require('../common');
const assert = require('assert');
const { isBuiltin } = require('module');
const { Worker } = require('worker_threads');
const { spawnSyncAndAssert } = require('../common/child_process');
const {
internalBinding,
getBuiltinModulePolicies,
} = require('internal/test/binding');
const { getCLIOptionsInfo } = require('internal/options');

const policies = getBuiltinModulePolicies();
const { builtinIds } = internalBinding('builtins');
const { options } = getCLIOptionsInfo();
const seenIds = new Set();
const moduleAvailability = new Map([
['dtls', common.hasDtls],
['ffi', common.hasFFI],
['quic', common.hasQuic],
['sqlite', common.hasSQLite],
]);

for (const policy of policies) {
assert(Array.isArray(policy));
assert.strictEqual(policy.length, 3);
const [id, schemeOnly, option] = policy;
assert.strictEqual(typeof id, 'string');
assert(!id.startsWith('internal/'), id);
assert(builtinIds.includes(id), `Unknown builtin: ${id}`);
assert(!seenIds.has(id), `Duplicate policy: ${id}`);
seenIds.add(id);
assert.strictEqual(typeof schemeOnly, 'boolean', id);

if (option !== null) {
assert.match(option, /^--experimental-[a-z-]+$/);
// FFI has no CLI option in builds without FFI support.
const missingFFIOption = id === 'ffi' && !common.hasFFI &&
option === '--experimental-ffi';
assert(options.has(option) || missingFFIOption,
`Unknown builtin option: ${option}`);
}
}

// Callers must not be able to change the loader's policy through this API.
const copy = getBuiltinModulePolicies();
copy[0][0] = 'changed';
copy.pop();
assert.deepStrictEqual(getBuiltinModulePolicies(), policies);

// Cover shared flags, both scheme rules, and an option enabled by default.
const workerPolicyIds = new Set([
'bench', 'bench/reporters', 'stream/iter', 'zlib/iter', 'sqlite',
Comment on lines +65 to +67

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This list could become stale as policies change. For example, if these modules no longer need experimental flags while other modules still do, the test could pass without the coverage described above.

]);

for (const policy of policies) {
const [id, , option] = policy;
if (moduleAvailability.get(id) === false) continue;

runPolicyTest(policy, [], option === null || options.get(option).defaultIsTrue);
if (option !== null) {
const disabledOption = option.replace('--', '--no-');
runPolicyTest(policy, [option], true);
runPolicyTest(policy, [disabledOption], false);

if (!process.config.variables.node_without_node_options) {
runPolicyTest(policy, [], true, option);
runPolicyTest(policy, [], false, disabledOption);
// Command-line options take precedence over NODE_OPTIONS.
runPolicyTest(policy, [option], true, disabledOption);
runPolicyTest(policy, [disabledOption], false, option);
}

if (workerPolicyIds.has(id)) {
const parentEnabled = isBuiltin(`node:${id}`);
for (const enabled of [true, false]) {
const worker = new Worker(__filename, {
argv: ['child', JSON.stringify(policy), String(enabled)],
execArgv: [enabled ? option : disabledOption],
env: { ...process.env, NODE_OPTIONS: '' },
});
worker.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
assert.strictEqual(isBuiltin(`node:${id}`), parentEnabled, id);
}));
}
}
}
}

// Run in a fresh process so each case initializes the loader with its flags.
function runPolicyTest(policy, flags, enabled, nodeOptions = '') {
spawnSyncAndAssert(process.execPath, [
...flags,
__filename,
'child',
JSON.stringify(policy),
String(enabled),
], { env: { ...process.env, NODE_OPTIONS: nodeOptions } }, { status: 0 });
}

async function checkPolicy([id, schemeOnly], enabled) {
const assert = require('assert');
const { builtinModules, isBuiltin } = require('module');
const prefixed = `node:${id}`;
assert.strictEqual(builtinModules.includes(id), enabled && !schemeOnly, id);
assert.strictEqual(builtinModules.includes(prefixed),
enabled && schemeOnly, prefixed);

for (const specifier of [id, prefixed]) {
const supported = enabled && (!schemeOnly || specifier === prefixed);
assert.strictEqual(isBuiltin(specifier), supported, specifier);
if (supported) {
const exports = require(specifier);
assert.strictEqual(process.getBuiltinModule(specifier), exports);
assert.strictEqual((await import(specifier)).default, exports);
} else {
assert.strictEqual(process.getBuiltinModule(specifier), undefined);
assert.throws(() => require(specifier), {
code: specifier === prefixed ?
'ERR_UNKNOWN_BUILTIN_MODULE' : 'MODULE_NOT_FOUND',
});
await assert.rejects(import(specifier), {
code: specifier === prefixed ?
'ERR_UNKNOWN_BUILTIN_MODULE' : 'ERR_MODULE_NOT_FOUND',
});
}
}
}
Loading