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();
},
};
171 changes: 171 additions & 0 deletions test/parallel/test-module-builtin-policies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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 policies = JSON.parse(process.argv[3]);
const enabled = process.argv[4] === 'true';
Promise.all(policies.map((policy) => 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 moduleAvailability = new Map([
['dtls', common.hasDtls],
['ffi', common.hasFFI],
['quic', common.hasQuic],
['sqlite', common.hasSQLite],
]);

// Validate every declared policy before testing its behavior.
{
const seenIds = new Set();
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) continue;
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);
}

// Test defaults, command-line flags, NODE_OPTIONS, and their precedence in
// fresh processes.
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);
}
}
}

// Test option-gated builtins in Workers without changing the parent state.
{
const policiesByOption = new Map();
for (const policy of policies) {
const [id, , option] = policy;
if (option === null || moduleAvailability.get(id) === false) continue;

const optionPolicies = policiesByOption.get(option);
if (optionPolicies === undefined) {
policiesByOption.set(option, [policy]);
} else {
optionPolicies.push(policy);
}
}

// Test each option once, together with all builtins it controls.
for (const [option, optionPolicies] of policiesByOption) {
const parentState = optionPolicies.map(([id]) => [
id,
isBuiltin(`node:${id}`),
]);
const disabledOption = option.replace('--', '--no-');

for (const enabled of [true, false]) {
const worker = new Worker(__filename, {
argv: ['child', JSON.stringify(optionPolicies), String(enabled)],
execArgv: [enabled ? option : disabledOption],
env: { ...process.env, NODE_OPTIONS: '' },
});

worker.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
for (const [id, parentEnabled] of parentState) {
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