policy: increase tests via permutation matrix

PR-URL: https://github.com/nodejs/node/pull/34404
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Bradley Farias
2020-07-16 16:00:21 -05:00
parent e0d181cf2b
commit b04f2b6618
7 changed files with 437 additions and 438 deletions

View File

@@ -276,18 +276,13 @@ function readPackage(requestPath) {
const existing = packageJsonCache.get(jsonPath);
if (existing !== undefined) return existing;
const result = packageJsonReader.read(path.toNamespacedPath(jsonPath));
const result = packageJsonReader.read(jsonPath);
const json = result.containsKeys === false ? '{}' : result.string;
if (json === undefined) {
packageJsonCache.set(jsonPath, false);
return false;
}
if (manifest) {
const jsonURL = pathToFileURL(jsonPath);
manifest.assertIntegrity(jsonURL, json);
}
try {
const parsed = JSONParse(json);
const filtered = {

View File

@@ -1,5 +1,10 @@
'use strict';
const { getOptionValue } = require('internal/options');
const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
const { Buffer } = require('buffer');
const fs = require('fs');
@@ -15,20 +20,22 @@ const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
async function defaultGetSource(url, { format } = {}, defaultGetSource) {
const parsed = new URL(url);
let source;
if (parsed.protocol === 'file:') {
return {
source: await readFileAsync(parsed)
};
source = await readFileAsync(parsed);
} else if (parsed.protocol === 'data:') {
const match = DATA_URL_PATTERN.exec(parsed.pathname);
if (!match) {
throw new ERR_INVALID_URL(url);
}
const [ , base64, body ] = match;
return {
source: Buffer.from(body, base64 ? 'base64' : 'utf8')
};
source = Buffer.from(body, base64 ? 'base64' : 'utf8');
} else {
throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
}
throw new ERR_INVALID_URL_SCHEME(['file', 'data']);
if (manifest) {
manifest.assertIntegrity(parsed, source);
}
return { source };
}
exports.defaultGetSource = defaultGetSource;

View File

@@ -2,21 +2,35 @@
const { SafeMap } = primordials;
const { internalModuleReadJSON } = internalBinding('fs');
const { pathToFileURL } = require('url');
const { toNamespacedPath } = require('path');
const cache = new SafeMap();
/**
*
* @param {string} path
* @param {string} jsonPath
*/
function read(path) {
if (cache.has(path)) {
return cache.get(path);
function read(jsonPath) {
if (cache.has(jsonPath)) {
return cache.get(jsonPath);
}
const [string, containsKeys] = internalModuleReadJSON(path);
const [string, containsKeys] = internalModuleReadJSON(
toNamespacedPath(jsonPath)
);
const result = { string, containsKeys };
cache.set(path, result);
const { getOptionValue } = require('internal/options');
if (string !== undefined) {
const manifest = getOptionValue('--experimental-policy') ?
require('internal/process/policy').manifest :
null;
if (manifest) {
const jsonURL = pathToFileURL(jsonPath);
manifest.assertIntegrity(jsonURL, string);
}
}
cache.set(jsonPath, result);
return result;
}

View File

@@ -205,6 +205,9 @@ class Worker extends EventEmitter {
cwdCounter: cwdCounter || workerIo.sharedCwdCounter,
workerData: options.workerData,
publicPort: port2,
manifestURL: getOptionValue('--experimental-policy') ?
require('internal/process/policy').url :
null,
manifestSrc: getOptionValue('--experimental-policy') ?
require('internal/process/policy').src :
null,

View File

@@ -1,414 +0,0 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
tmpdir.refresh();
function hash(algo, body) {
const h = crypto.createHash(algo);
h.update(body);
return h.digest('base64');
}
const policyFilepath = path.join(tmpdir.path, 'policy');
const packageFilepath = path.join(tmpdir.path, 'package.json');
const packageURL = pathToFileURL(packageFilepath);
const packageBody = '{"main": "dep.js"}';
const policyToPackageRelativeURLString = `./${
path.relative(path.dirname(policyFilepath), packageFilepath)
}`;
const parentFilepath = path.join(tmpdir.path, 'parent.js');
const parentURL = pathToFileURL(parentFilepath);
const parentBody = 'require(\'./dep.js\')';
const workerSpawningFilepath = path.join(tmpdir.path, 'worker_spawner.js');
const workerSpawningURL = pathToFileURL(workerSpawningFilepath);
const workerSpawningBody = `
const { Worker } = require('worker_threads');
// make sure this is gone to ensure we don't do another fs read of it
// will error out if we do
require('fs').unlinkSync(${JSON.stringify(policyFilepath)});
const w = new Worker(${JSON.stringify(parentFilepath)});
w.on('exit', process.exit);
`;
const depFilepath = path.join(tmpdir.path, 'dep.js');
const depURL = pathToFileURL(depFilepath);
const depBody = '';
const policyToDepRelativeURLString = `./${
path.relative(path.dirname(policyFilepath), depFilepath)
}`;
fs.writeFileSync(parentFilepath, parentBody);
fs.writeFileSync(depFilepath, depBody);
const tmpdirURL = pathToFileURL(tmpdir.path);
if (!tmpdirURL.pathname.endsWith('/')) {
tmpdirURL.pathname += '/';
}
function test({
shouldFail = false,
preload = [],
entry,
onerror = undefined,
resources = {}
}) {
const manifest = {
onerror,
resources: {}
};
for (const [url, { body, match }] of Object.entries(resources)) {
manifest.resources[url] = {
integrity: `sha256-${hash('sha256', match ? body : body + '\n')}`,
dependencies: true
};
fs.writeFileSync(new URL(url, tmpdirURL.href), body);
}
fs.writeFileSync(policyFilepath, JSON.stringify(manifest, null, 2));
const { status } = spawnSync(process.execPath, [
'--experimental-policy', policyFilepath,
...preload.map((m) => ['-r', m]).flat(),
entry
]);
if (shouldFail) {
assert.notStrictEqual(status, 0);
} else {
assert.strictEqual(status, 0);
}
}
{
const { status } = spawnSync(process.execPath, [
'--experimental-policy', policyFilepath,
'--experimental-policy', policyFilepath
], {
stdio: 'pipe'
});
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = path.join(tmpdir.path, 'enoent');
try { fs.unlinkSync(enoentFilepath); } catch {}
const { status } = spawnSync(process.execPath, [
'--experimental-policy', enoentFilepath, '-e', ''
], {
stdio: 'pipe'
});
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
test({
shouldFail: true,
entry: parentFilepath,
resources: {
}
});
test({
shouldFail: false,
entry: parentFilepath,
onerror: 'log',
});
test({
shouldFail: true,
entry: parentFilepath,
onerror: 'exit',
});
test({
shouldFail: true,
entry: parentFilepath,
onerror: 'throw',
});
test({
shouldFail: true,
entry: parentFilepath,
onerror: 'unknown-onerror-value',
});
test({
shouldFail: true,
entry: path.dirname(packageFilepath),
resources: {
}
});
test({
shouldFail: true,
entry: path.dirname(packageFilepath),
resources: {
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: false,
entry: path.dirname(packageFilepath),
onerror: 'log',
resources: {
[packageURL]: {
body: packageBody,
match: false,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: true,
entry: path.dirname(packageFilepath),
resources: {
[packageURL]: {
body: packageBody,
match: false,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: true,
entry: path.dirname(packageFilepath),
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[depURL]: {
body: depBody,
match: false,
}
}
});
test({
shouldFail: false,
entry: path.dirname(packageFilepath),
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: false,
entry: parentFilepath,
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[parentURL]: {
body: parentBody,
match: true,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: false,
preload: [depFilepath],
entry: parentFilepath,
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[parentURL]: {
body: parentBody,
match: true,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: true,
entry: parentFilepath,
resources: {
[parentURL]: {
body: parentBody,
match: false,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: true,
entry: parentFilepath,
resources: {
[parentURL]: {
body: parentBody,
match: true,
},
[depURL]: {
body: depBody,
match: false,
}
}
});
test({
shouldFail: true,
entry: parentFilepath,
resources: {
[parentURL]: {
body: parentBody,
match: true,
}
}
});
test({
shouldFail: false,
entry: depFilepath,
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: false,
entry: depFilepath,
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[policyToDepRelativeURLString]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: true,
entry: depFilepath,
resources: {
[policyToDepRelativeURLString]: {
body: depBody,
match: false,
}
}
});
test({
shouldFail: false,
entry: depFilepath,
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[policyToDepRelativeURLString]: {
body: depBody,
match: true,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: true,
entry: depFilepath,
resources: {
[policyToPackageRelativeURLString]: {
body: packageBody,
match: true,
},
[packageURL]: {
body: packageBody,
match: true,
},
[depURL]: {
body: depBody,
match: false,
}
}
});
test({
shouldFail: true,
entry: workerSpawningFilepath,
resources: {
[workerSpawningURL]: {
body: workerSpawningBody,
match: true,
},
}
});
test({
shouldFail: false,
entry: workerSpawningFilepath,
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[workerSpawningURL]: {
body: workerSpawningBody,
match: true,
},
[parentURL]: {
body: parentBody,
match: true,
},
[depURL]: {
body: depBody,
match: true,
}
}
});
test({
shouldFail: false,
entry: workerSpawningFilepath,
preload: [parentFilepath],
resources: {
[packageURL]: {
body: packageBody,
match: true,
},
[workerSpawningURL]: {
body: workerSpawningBody,
match: true,
},
[parentURL]: {
body: parentBody,
match: true,
},
[depURL]: {
body: depBody,
match: true,
}
}
});

View File

@@ -19,24 +19,28 @@ function hash(algo, body) {
return h.digest('base64');
}
const policyFilepath = path.join(tmpdir.path, 'policy');
const tmpdirPath = path.join(tmpdir.path, 'test-policy-parse-integrity');
fs.rmdirSync(tmpdirPath, { maxRetries: 3, recursive: true });
fs.mkdirSync(tmpdirPath, { recursive: true });
const parentFilepath = path.join(tmpdir.path, 'parent.js');
const policyFilepath = path.join(tmpdirPath, 'policy');
const parentFilepath = path.join(tmpdirPath, 'parent.js');
const parentBody = "require('./dep.js')";
const depFilepath = path.join(tmpdir.path, 'dep.js');
const depFilepath = path.join(tmpdirPath, 'dep.js');
const depURL = pathToFileURL(depFilepath);
const depBody = '';
fs.writeFileSync(parentFilepath, parentBody);
fs.writeFileSync(depFilepath, depBody);
const tmpdirURL = pathToFileURL(tmpdir.path);
const tmpdirURL = pathToFileURL(tmpdirPath);
if (!tmpdirURL.pathname.endsWith('/')) {
tmpdirURL.pathname += '/';
}
const packageFilepath = path.join(tmpdir.path, 'package.json');
const packageFilepath = path.join(tmpdirPath, 'package.json');
const packageURL = pathToFileURL(packageFilepath);
const packageBody = '{"main": "dep.js"}';

View File

@@ -0,0 +1,390 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto) common.skip('missing crypto');
const { debuglog } = require('util');
const debug = debuglog('test');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const { spawnSync, spawn } = require('child_process');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
function hash(algo, body) {
const values = [];
{
const h = crypto.createHash(algo);
h.update(body);
values.push(`${algo}-${h.digest('base64')}`);
}
{
const h = crypto.createHash(algo);
h.update(body.replace('\n', '\r\n'));
values.push(`${algo}-${h.digest('base64')}`);
}
return values;
}
const policyPath = './policy.json';
const parentBody = {
commonjs: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
require(process.env.DEP_FILE)
`,
module: `
if (!process.env.DEP_FILE) {
console.error(
'missing required DEP_FILE env to determine dependency'
);
process.exit(33);
}
import(process.env.DEP_FILE)
`,
};
const workerSpawningBody = `
const path = require('path');
const { Worker } = require('worker_threads');
if (!process.env.PARENT_FILE) {
console.error(
'missing required PARENT_FILE env to determine worker entry point'
);
process.exit(33);
}
if (!process.env.DELETABLE_POLICY_FILE) {
console.error(
'missing required DELETABLE_POLICY_FILE env to check reloading'
);
process.exit(33);
}
const w = new Worker(path.resolve(process.env.PARENT_FILE));
w.on('exit', (status) => process.exit(status === 0 ? 0 : 1));
`;
let nextTestId = 1;
function newTestId() {
return nextTestId++;
}
tmpdir.refresh();
let spawned = 0;
const toSpawn = [];
function queueSpawn(opts) {
toSpawn.push(opts);
drainQueue();
}
function drainQueue() {
if (spawned > 50) {
return;
}
if (toSpawn.length) {
const config = toSpawn.shift();
const {
shouldSucceed, // = (() => { throw new Error('required')})(),
preloads, // = (() =>{ throw new Error('required')})(),
entryPath, // = (() => { throw new Error('required')})(),
willDeletePolicy, // = (() => { throw new Error('required')})(),
onError, // = (() => { throw new Error('required')})(),
resources, // = (() => { throw new Error('required')})(),
parentPath,
depPath,
} = config;
const testId = newTestId();
const configDirPath = path.join(
tmpdir.path,
`test-policy-integrity-permutation-${testId}`
);
const tmpPolicyPath = path.join(
tmpdir.path,
`deletable-policy-${testId}.json`
);
const cliPolicy = willDeletePolicy ? tmpPolicyPath : policyPath;
fs.rmdirSync(configDirPath, { maxRetries: 3, recursive: true });
fs.mkdirSync(configDirPath, { recursive: true });
const manifest = {
onerror: onError,
resources: {},
};
const manifestPath = path.join(configDirPath, policyPath);
for (const [resourcePath, { body, integrities }] of Object.entries(
resources
)) {
const filePath = path.join(configDirPath, resourcePath);
if (integrities !== null) {
manifest.resources[pathToFileURL(filePath).href] = {
integrity: integrities.join(' '),
dependencies: true,
};
}
fs.writeFileSync(filePath, body, 'utf8');
}
const manifestBody = JSON.stringify(manifest);
fs.writeFileSync(manifestPath, manifestBody);
if (cliPolicy === tmpPolicyPath) {
fs.writeFileSync(tmpPolicyPath, manifestBody);
}
const spawnArgs = [
process.execPath,
[
'--unhandled-rejections=strict',
'--experimental-policy',
cliPolicy,
...preloads.flatMap((m) => ['-r', m]),
entryPath,
'--',
testId,
configDirPath,
],
{
env: {
...process.env,
DELETABLE_POLICY_FILE: tmpPolicyPath,
PARENT_FILE: parentPath,
DEP_FILE: depPath,
},
cwd: configDirPath,
stdio: 'pipe',
},
];
spawned++;
const stdout = [];
const stderr = [];
const child = spawn(...spawnArgs);
child.stdout.on('data', (d) => stdout.push(d));
child.stderr.on('data', (d) => stderr.push(d));
child.on('exit', (status, signal) => {
spawned--;
try {
if (shouldSucceed) {
assert.strictEqual(status, 0);
} else {
assert.notStrictEqual(status, 0);
}
} catch (e) {
console.log(
'permutation',
testId,
'failed'
);
console.dir(
{ config, manifest },
{ depth: null }
);
console.log('exit code:', status, 'signal:', signal);
console.log(`stdout: ${Buffer.concat(stdout)}`);
console.log(`stderr: ${Buffer.concat(stderr)}`);
throw e;
}
fs.rmdirSync(configDirPath, { maxRetries: 3, recursive: true });
drainQueue();
});
}
}
{
const { status } = spawnSync(
process.execPath,
['--experimental-policy', policyPath, '--experimental-policy', policyPath],
{
stdio: 'pipe',
}
);
assert.notStrictEqual(status, 0, 'Should not allow multiple policies');
}
{
const enoentFilepath = path.join(tmpdir.path, 'enoent');
try {
fs.unlinkSync(enoentFilepath);
} catch { }
const { status } = spawnSync(
process.execPath,
['--experimental-policy', enoentFilepath, '-e', ''],
{
stdio: 'pipe',
}
);
assert.notStrictEqual(status, 0, 'Should not allow missing policies');
}
/**
* @template {Record<string, Array<string | string[] | boolean>>} T
* @param {T} configurations
* @param {object} path
* @returns {Array<{[key: keyof T]: T[keyof configurations]}>}
*/
function permutations(configurations, path = {}) {
const keys = Object.keys(configurations);
if (keys.length === 0) {
return path;
}
const config = keys[0];
const { [config]: values, ...otherConfigs } = configurations;
return values.flatMap((value) => {
return permutations(otherConfigs, { ...path, [config]: value });
});
}
const tests = new Set();
function fileExtensionFormat(extension, packageType) {
if (extension === '.js') {
return packageType === 'module' ? 'module' : 'commonjs';
} else if (extension === '.mjs') {
return 'module';
} else if (extension === '.cjs') {
return 'commonjs';
}
throw new Error('unknown format ' + extension);
}
for (const permutation of permutations({
entry: ['worker', 'parent', 'dep'],
preloads: [[], ['parent'], ['dep']],
onError: ['log', 'exit'],
parentExtension: ['.js', '.mjs', '.cjs'],
parentIntegrity: ['match', 'invalid', 'missing'],
depExtension: ['.js', '.mjs', '.cjs'],
depIntegrity: ['match', 'invalid', 'missing'],
packageType: ['no-package-json', 'module', 'commonjs'],
packageIntegrity: ['match', 'invalid', 'missing'],
})) {
let shouldSucceed = true;
const parentPath = `./parent${permutation.parentExtension}`;
const effectivePackageType =
permutation.packageType === 'module' ? 'module' : 'commonjs';
const parentFormat = fileExtensionFormat(
permutation.parentExtension,
effectivePackageType
);
const depFormat = fileExtensionFormat(
permutation.depExtension,
effectivePackageType
);
// non-sensical attempt to require ESM
if (depFormat === 'module' && parentFormat === 'commonjs') {
continue;
}
const depPath = `./dep${permutation.depExtension}`;
const workerSpawnerPath = './worker-spawner.cjs';
const entryPath = {
dep: depPath,
parent: parentPath,
worker: workerSpawnerPath,
}[permutation.entry];
const packageJSON = {
main: entryPath,
type: permutation.packageType,
};
if (permutation.packageType === 'no-field') {
delete packageJSON.type;
}
const resources = {
[depPath]: {
body: '',
integrities: hash('sha256', ''),
},
};
if (permutation.depIntegrity === 'invalid') {
resources[depPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.depIntegrity === 'missing') {
resources[depPath].integrities = null;
shouldSucceed = false;
} else if (permutation.depIntegrity === 'match') {
} else {
throw new Error('unreachable');
}
if (parentFormat !== 'commonjs') {
permutation.preloads = permutation.preloads.filter((_) => _ !== 'parent');
}
const hasParent =
permutation.entry !== 'dep' || permutation.preloads.includes('parent');
if (hasParent) {
resources[parentPath] = {
body: parentBody[parentFormat],
integrities: hash('sha256', parentBody[parentFormat]),
};
if (permutation.parentIntegrity === 'invalid') {
resources[parentPath].body += '\n// INVALID INTEGRITY';
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'missing') {
resources[parentPath].integrities = null;
shouldSucceed = false;
} else if (permutation.parentIntegrity === 'match') {
} else {
throw new Error('unreachable');
}
}
if (permutation.entry === 'worker') {
resources[workerSpawnerPath] = {
body: workerSpawningBody,
integrities: hash('sha256', workerSpawningBody),
};
}
if (permutation.packageType !== 'no-package-json') {
let packageBody = JSON.stringify(packageJSON, null, 2);
let packageIntegrities = hash('sha256', packageBody);
if (
permutation.parentExtension !== '.js' ||
permutation.depExtension !== '.js'
) {
// NO PACKAGE LOOKUP
continue;
}
if (permutation.packageIntegrity === 'invalid') {
packageJSON['//'] = 'INVALID INTEGRITY';
packageBody = JSON.stringify(packageJSON, null, 2);
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'missing') {
packageIntegrities = [];
shouldSucceed = false;
} else if (permutation.packageIntegrity === 'match') {
} else {
throw new Error('unreachable');
}
resources['./package.json'] = {
body: packageBody,
integrities: packageIntegrities,
};
}
const willDeletePolicy = permutation.entry === 'worker';
if (permutation.onError === 'log') {
shouldSucceed = true;
}
tests.add(
JSON.stringify({
// hasParent,
// original: permutation,
onError: permutation.onError,
shouldSucceed,
entryPath,
willDeletePolicy,
preloads: permutation.preloads
.map((_) => {
return {
'': '',
'parent': parentFormat === 'commonjs' ? parentPath : '',
'dep': depFormat === 'commonjs' ? depPath : '',
}[_];
})
.filter(Boolean),
parentPath,
depPath,
resources,
})
);
}
debug(`spawning ${tests.size} policy integrity permutations`);
debug(
'use NODE_DEBUG=test:policy-integrity:NUMBER to log a specific permutation'
);
for (const config of tests) {
const parsed = JSON.parse(config);
tests.delete(config);
queueSpawn(parsed);
}