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

policy: cleanup to allow snapshotting #42191

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/internal/bootstrap/node.js
Expand Up @@ -341,6 +341,7 @@ require('url');
require('internal/options');
if (config.hasOpenSSL) {
require('crypto');
require('internal/process/policy');
}

function setupPrepareStackTrace() {
Expand Down
19 changes: 15 additions & 4 deletions lib/internal/bootstrap/pre_execution.js
Expand Up @@ -25,6 +25,8 @@ const {
const { Buffer } = require('buffer');
const { ERR_MANIFEST_ASSERT_INTEGRITY } = require('internal/errors').codes;
const assert = require('internal/assert');
const policy = require('internal/process/policy');
const SRI = require('internal/policy/sri');

function prepareMainThreadExecution(expandArgv1 = false) {
// TODO(joyeecheung): this is also necessary for workers when they deserialize
Expand Down Expand Up @@ -434,7 +436,9 @@ function initializeClusterIPC() {
}

function initializePolicy() {
const experimentalPolicy = getOptionValue('--experimental-policy');
const experimentalPolicy = getOptionValue('[has_experimental_policy_string]') ?
getOptionValue('--experimental-policy') :
null;
if (experimentalPolicy) {
process.emitWarning('Policies are experimental.',
'ExperimentalWarning');
Expand All @@ -453,7 +457,6 @@ function initializePolicy() {
const src = fs.readFileSync(manifestURL, 'utf8');
const experimentalPolicyIntegrity = getOptionValue('--policy-integrity');
if (experimentalPolicyIntegrity) {
const SRI = require('internal/policy/sri');
const { createHash, timingSafeEqual } = require('crypto');
const realIntegrities = new SafeMap();
const integrityEntries = SRI.parse(experimentalPolicyIntegrity);
Expand All @@ -477,9 +480,17 @@ function initializePolicy() {
throw new ERR_MANIFEST_ASSERT_INTEGRITY(manifestURL, realIntegrities);
}
}
require('internal/process/policy')
.setup(src, manifestURL.href);
policy.setup(src, manifestURL.href);
} else {
// This disables the policy TDZ checks so code can just check
// if (!policy.manifest)
// it is important to keep the policy in TDZ until the user supplied
// options are determined at startup
policy.setup(null);
bmeck marked this conversation as resolved.
Show resolved Hide resolved
}
// Ensure that policy no longer has a TDZ by checking
// that we can access .manifest policy.manifest
policy.manifest; // eslint-disable-line no-unused-expressions
}

function initializeWASI() {
Expand Down
5 changes: 4 additions & 1 deletion lib/internal/main/worker_thread.js
Expand Up @@ -63,6 +63,7 @@ let debug = require('internal/util/debuglog').debuglog('worker', (fn) => {
});

const assert = require('internal/assert');
const policy = require('internal/process/policy');

patchProcessObject();
setupInspectorHooks();
Expand Down Expand Up @@ -124,7 +125,9 @@ port.on('message', (message) => {
setupPerfHooks();
initializeReport();
if (manifestSrc) {
require('internal/process/policy').setup(manifestSrc, manifestURL);
policy.setup(manifestSrc, manifestURL);
} else {
policy.setup(null);
}
initializeDeprecations();
initializeWASI();
Expand Down
10 changes: 4 additions & 6 deletions lib/internal/modules/cjs/loader.js
Expand Up @@ -101,9 +101,7 @@ const { getOptionValue } = require('internal/options');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
require('internal/process/policy') :
null;
const policy = require('internal/process/policy');

// Whether any user-provided CJS modules had been loaded (executed).
// Used for internal assertions.
Expand Down Expand Up @@ -1053,7 +1051,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
Module.prototype._compile = function(content, filename) {
let moduleURL;
let redirects;
if (policy?.manifest) {
if (policy.manifest) {
moduleURL = pathToFileURL(filename);
redirects = policy.manifest.getDependencyMapper(moduleURL);
policy.manifest.assertIntegrity(moduleURL, content);
Expand Down Expand Up @@ -1158,7 +1156,7 @@ Module._extensions['.js'] = function(module, filename) {
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');

if (policy?.manifest) {
if (policy.manifest) {
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
}
Expand All @@ -1174,7 +1172,7 @@ Module._extensions['.json'] = function(module, filename) {

// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
if (policy?.manifest) {
if (policy.manifest) {
const content = fs.readFileSync(filename);
const moduleURL = pathToFileURL(filename);
policy.manifest.assertIntegrity(moduleURL, content);
Expand Down
6 changes: 2 additions & 4 deletions lib/internal/modules/esm/get_source.js
Expand Up @@ -9,9 +9,7 @@ const { getOptionValue } = require('internal/options');
const { fetchModule } = require('internal/modules/esm/fetch_module');

// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
require('internal/process/policy') :
null;
const policy = require('internal/process/policy');
const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');

Expand Down Expand Up @@ -52,7 +50,7 @@ async function defaultGetSource(url, context, defaultGetSource) {
experimentalNetworkImports ? ['https', 'http'] : [],
]));
}
if (policy?.manifest) {
if (policy.manifest) {
policy.manifest.assertIntegrity(parsed, source);
}
return source;
Expand Down
52 changes: 28 additions & 24 deletions lib/internal/modules/esm/resolve.js
Expand Up @@ -10,6 +10,7 @@ const {
ObjectFreeze,
ObjectGetOwnPropertyNames,
ObjectPrototypeHasOwnProperty,
ReflectApply,
RegExp,
RegExpPrototypeExec,
RegExpPrototypeSymbolReplace,
Expand All @@ -35,9 +36,7 @@ const {
} = require('fs');
const { getOptionValue } = require('internal/options');
// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
require('internal/process/policy') :
null;
const policy = require('internal/process/policy');
const { sep, relative, resolve } = require('path');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
Expand Down Expand Up @@ -1090,7 +1089,7 @@ function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {

async function defaultResolve(specifier, context = {}, defaultResolveUnused) {
let { parentURL, conditions } = context;
if (parentURL && policy?.manifest) {
if (parentURL && policy.manifest) {
const redirects = policy.manifest.getDependencyMapper(parentURL);
if (redirects) {
const { resolve, reaction } = redirects;
Expand Down Expand Up @@ -1217,9 +1216,33 @@ async function defaultResolve(specifier, context = {}, defaultResolveUnused) {
};
}

function policyWrappingDefaultResolve() {
const $defaultResolve = defaultResolve;
if (policy.manifest) {
module.exports.defaultResolve = async function defaultResolve(
specifier,
context
) {
const ret = await $defaultResolve(specifier, context, $defaultResolve);
// This is a preflight check to avoid data exfiltration by query params etc.
policy.manifest.mightAllow(ret.url, () =>
new ERR_MANIFEST_DEPENDENCY_MISSING(
context.parentURL,
specifier,
context.conditions
)
);
return ret;
};
} else {
module.exports.defaultResolve = $defaultResolve;
}
return ReflectApply(module.exports.defaultResolve, this, arguments);
}

module.exports = {
DEFAULT_CONDITIONS,
defaultResolve,
defaultResolve: policyWrappingDefaultResolve,
encodedSepRegEx,
getPackageScopeConfig,
getPackageType,
Expand All @@ -1231,22 +1254,3 @@ module.exports = {
const {
defaultGetFormatWithoutErrors,
} = require('internal/modules/esm/get_format');

if (policy) {
const $defaultResolve = defaultResolve;
module.exports.defaultResolve = async function defaultResolve(
specifier,
context
) {
const ret = await $defaultResolve(specifier, context, $defaultResolve);
// This is a preflight check to avoid data exfiltration by query params etc.
policy.manifest.mightAllow(ret.url, () =>
new ERR_MANIFEST_DEPENDENCY_MISSING(
context.parentURL,
specifier,
context.conditions
)
);
return ret;
};
}
6 changes: 2 additions & 4 deletions lib/internal/modules/package_json_reader.js
Expand Up @@ -4,6 +4,7 @@ const { SafeMap } = primordials;
const { internalModuleReadJSON } = internalBinding('fs');
const { pathToFileURL } = require('url');
const { toNamespacedPath } = require('path');
const policy = require('internal/process/policy');

const cache = new SafeMap();

Expand All @@ -22,12 +23,9 @@ function read(jsonPath) {
toNamespacedPath(jsonPath)
);
const result = { string, containsKeys };
const { getOptionValue } = require('internal/options');
if (string !== undefined) {
if (manifest === undefined) {
manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
manifest = policy.manifest;
}
if (manifest !== null) {
const jsonURL = pathToFileURL(jsonPath);
Expand Down
13 changes: 10 additions & 3 deletions lib/internal/policy/manifest.js
Expand Up @@ -38,9 +38,7 @@ const HashDigest = uncurryThis(crypto.Hash.prototype.digest);
const BufferToString = uncurryThis(Buffer.prototype.toString);
const kRelativeURLStringPattern = /^\.{0,2}\//;
const { getOptionValue } = require('internal/options');
const shouldAbortOnUncaughtException = getOptionValue(
'--abort-on-uncaught-exception'
);
let shouldAbortOnUncaughtException;
const { abort, exit, _rawDebug } = process;
// #endregion

Expand Down Expand Up @@ -354,6 +352,14 @@ function findScopeHREF(href, scopeStore, allowSame) {
* @typedef {boolean | string | SRI[] | typeof kCascade} Integrity
*/

// To get this in the snapshot we can't have things that use getOptionsValue or
// expose C++ functions so we do these lazily
bmeck marked this conversation as resolved.
Show resolved Hide resolved
function lazyBits() {
shouldAbortOnUncaughtException = getOptionValue(
'--abort-on-uncaught-exception'
);
}

class Manifest {
#defaultDependencies;
/**
Expand Down Expand Up @@ -423,6 +429,7 @@ class Manifest {
* @param {string} manifestHREF
*/
constructor(obj, manifestHREF) {
lazyBits();
this.href = manifestHREF;
const scopes = this.#scopeDependencies;
const integrities = this.#resourceIntegrities;
Expand Down
6 changes: 5 additions & 1 deletion lib/internal/policy/sri.js
Expand Up @@ -33,7 +33,11 @@ ObjectSeal(kAllWSP);

const BufferFrom = require('buffer').Buffer.from;

// Returns {algorithm, value (in base64 string), options,}[]
/**
* Strictly parses an SRI string and returns all found entries
* @param {string} str
* @returns {Array<{algorithm:string, value:string, options: string | null}>}
*/
const parse = (str) => {
let prevIndex = 0;
let match;
Expand Down
16 changes: 16 additions & 0 deletions lib/internal/process/policy.js
Expand Up @@ -10,17 +10,33 @@ const {
ERR_MANIFEST_TDZ,
} = require('internal/errors').codes;
const { Manifest } = require('internal/policy/manifest');
/**
* @type {InstanceType<Manifest>}
*/
let manifest;
/**
* @type {string}
*/
let manifestSrc;
/**
* @type {string}
*/
let manifestURL;

module.exports = ObjectFreeze({
__proto__: null,
/**
* @param {string | null} src null to not use a policy
* @param {string} url
* @returns {void}
*/
setup(src, url) {
manifestSrc = src;
manifestURL = url;
if (src === null) {
manifest = null;
manifestSrc = null;
manifestURL = null;
return;
}

Expand Down
10 changes: 3 additions & 7 deletions lib/internal/worker.js
Expand Up @@ -38,7 +38,7 @@ const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
} = errorCodes;
const { getOptionValue } = require('internal/options');
const policy = require('internal/process/policy');

const workerIo = require('internal/worker/io');
const {
Expand Down Expand Up @@ -241,12 +241,8 @@ class Worker extends EventEmitter {
workerData: options.workerData,
environmentData,
publicPort: port2,
manifestURL: getOptionValue('--experimental-policy') ?
require('internal/process/policy').url :
null,
manifestSrc: getOptionValue('--experimental-policy') ?
require('internal/process/policy').src :
null,
manifestURL: policy.url,
manifestSrc: policy.src,
hasStdin: !!options.stdin
}, transferList);
// Use this to cache the Worker's loopStart value once available.
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Expand Up @@ -344,11 +344,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"experimental ES Module import.meta.resolve() support",
&EnvironmentOptions::experimental_import_meta_resolve,
kAllowedInEnvironment);
AddOption("[has_experimental_policy_string]",
"",
&EnvironmentOptions::has_experimental_policy_string);
AddOption("--experimental-policy",
"use the specified file as a "
"security policy",
&EnvironmentOptions::experimental_policy,
kAllowedInEnvironment);
Implies("--experimental-policy", "[has_experimental_policy_string]");
AddOption("[has_policy_integrity_string]",
"",
&EnvironmentOptions::has_policy_integrity_string);
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Expand Up @@ -114,6 +114,7 @@ class EnvironmentOptions : public Options {
bool experimental_wasm_modules = false;
bool experimental_import_meta_resolve = false;
std::string module_type;
bool has_experimental_policy_string = false;
std::string experimental_policy;
std::string experimental_policy_integrity;
bool has_policy_integrity_string = false;
Expand Down
3 changes: 3 additions & 0 deletions test/parallel/test-bootstrap-modules.js
Expand Up @@ -99,6 +99,9 @@ const expectedModules = new Set([
'NativeModule internal/perf/timerify',
'NativeModule internal/perf/usertiming',
'NativeModule internal/perf/utils',
'NativeModule internal/policy/manifest',
'NativeModule internal/policy/sri',
'NativeModule internal/process/policy',
'NativeModule internal/priority_queue',
'NativeModule internal/process/esm_loader',
'NativeModule internal/process/execution',
Expand Down