Files
ECC/tests/lib/eval-harness/security.test.js
T
Brian 09de2c247a test: skip symlink tests when the OS refuses symlink creation
Adds a trySymlink helper that logs the existing skip notice and returns
false on EPERM/EACCES/ENOSYS, and uses it where tests created symlinks
unguarded. Non-admin Windows accounts without Developer Mode now skip
these cases instead of failing.
2026-10-26 15:08:05 -04:00

191 lines
9.5 KiB
JavaScript

'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { test, tempDir, cleanup, finish } = require('./helpers');
const gate = require('../../../scripts/lib/eval-harness/gate');
const { trySymlink } = require('../helpers/symlink-support');
const library = path.resolve(__dirname, '../../../scripts/lib/eval-harness');
const refused = error => error.code === 'gate.isolation_required';
const invalidVariant = error => error.code === 'gate.variant_invalid';
function setup(fn) {
const root = tempDir('security');
try {
const variant = path.join(root, 'variant');
fs.mkdirSync(variant);
fs.writeFileSync(path.join(variant, 'variant.json'), JSON.stringify({ name: 'candidate', effect_class: 'SE0' }));
fs.writeFileSync(path.join(variant, 'run.js'), 'module.exports={solve:()=>1};');
const taskset = path.join(root, 'answers.json');
fs.writeFileSync(taskset, JSON.stringify({ version: '1', family: 'canary', tasks: [{ id: 't', input: 0, expected: 1 }] }));
fn({ root, variant, taskset, work: path.join(root, 'work') });
} finally {
cleanup(root);
}
}
test('gate refuses all execution modes before creating work, including prior trusted flags', () => setup(c => {
for (const extra of [{}, { trusted_local: true }, { isolation: { verified: true } }, { executor: 'anything' }]) {
assert.throws(() => gate.runGate({ taskset: c.taskset, baseline: c.variant, candidate: c.variant, work_dir: c.work, ...extra }), refused);
assert.ok(!fs.existsSync(c.work));
}
}));
test('refusal happens before reading configuration properties', () => {
const config = new Proxy({}, { get() { throw new Error('configuration was inspected'); } });
assert.throws(() => gate.runGate(config), refused);
assert.throws(() => gate.runVariant(config), refused);
});
test('direct runner refuses caller-supplied trust and isolation claims', () => setup(c => {
const variant = gate.loadVariant(c.variant);
for (const options of [{}, { trusted_local: true }, { isolation: { verified: true } }]) {
assert.throws(() => gate.runVariant(variant, [], c.work, options), refused);
}
assert.ok(!fs.existsSync(c.work));
}));
test('CLI refuses before reading a config or creating a capsule even with trusted-local', () => setup(c => {
const cli = path.resolve(library, '../../eval-harness.js');
const config = path.join(c.root, 'config.json');
fs.writeFileSync(config, JSON.stringify({ taskset: c.taskset, baseline: c.variant, candidate: c.variant }));
const capsule = path.join(c.root, 'capsule');
for (const input of [config, path.join(c.root, 'missing.json')]) {
const result = spawnSync(process.execPath, [cli, 'gate', 'run', input, '--capsule', capsule, '--trusted-local'], { encoding: 'utf8', timeout: 2000 });
assert.strictEqual(result.status, 1);
assert.match(result.stderr, /gate.isolation_required/);
assert.ok(!fs.existsSync(capsule));
}
}));
test('child and retired preload reject before loading an escaping canary payload', () => setup(c => {
const marker = path.join(c.root, 'executed');
const external = path.join(c.root, 'external.js');
fs.writeFileSync(external, `require('fs').writeFileSync(${JSON.stringify(marker)},'bad');module.exports={solve:()=>1};`);
for (const args of [[path.join(library, 'gate-child.js')], ['--require', path.join(library, 'effect-fence.js'), external]]) {
const result = spawnSync(process.execPath, args, {
cwd: c.variant, input: JSON.stringify({ entry: external, tasks: [] }), encoding: 'utf8', timeout: 2000,
env: { ECC_EFFECT_FENCE_ROOT: c.variant, ECC_EFFECT_FENCE_LOG: path.join(c.root, 'log') },
});
assert.notStrictEqual(result.status, 0);
assert.match(result.stderr, /gate.isolation_required/);
assert.ok(!fs.existsSync(marker));
}
}));
test('read, alternate builtin, descriptor and promise escape payloads never load', () => setup(c => {
const marker = path.join(c.root, 'executed');
const payloads = [
`require('fs').readFileSync(${JSON.stringify(c.taskset)});`,
"process.getBuiltinModule('ht'+'tp');", // Acquiring the API only; no request.
`const fs=require('fs');const fd=fs.openSync(${JSON.stringify(marker)},'w');fs.writeSync(fd,'escape');fs.closeSync(fd);`,
`require('fs/promises').writeFile(${JSON.stringify(marker)},'escape');`,
];
for (const source of payloads) {
const entry = path.join(c.variant, 'run.js');
fs.writeFileSync(entry, `require('fs').writeFileSync(${JSON.stringify(marker)},'loaded');${source}`);
const result = spawnSync(process.execPath, ['--require', path.join(library, 'effect-fence.js'), entry], { encoding: 'utf8', timeout: 2000 });
assert.notStrictEqual(result.status, 0);
assert.match(result.stderr, /gate.isolation_required/);
assert.ok(!fs.existsSync(marker), 'payload must not begin executing');
}
}));
test('unsafe names and escaping or undigested entry paths are rejected', () => setup(c => {
const file = path.join(c.variant, 'variant.json');
const cases = [
{ name: 'n/../../escaped' }, { name: '/abs' }, { name: 44 },
{ entry: path.join(c.root, 'external.js') }, { entry: '../run.js' },
{ entry: 'C:\\evil.js' }, { entry: 'node_modules/hidden.js' }, { entry: 42 },
];
for (const extra of cases) {
fs.writeFileSync(file, JSON.stringify({ name: 'candidate', effect_class: 'SE0', ...extra }));
assert.throws(() => gate.loadVariant(c.variant), invalidVariant);
}
}));
test('symlink manifests and symlink trees cannot hide from digest', () => setup(c => {
const file = path.join(c.variant, 'variant.json');
const outside = path.join(c.root, 'manifest.json');
fs.renameSync(file, outside);
if (!trySymlink(outside, file)) return;
assert.throws(() => gate.loadVariant(c.variant), invalidVariant);
fs.unlinkSync(file);
fs.renameSync(outside, file);
if (!trySymlink(c.root, path.join(c.variant, 'link'))) return;
assert.throws(() => gate.loadVariant(c.variant), invalidVariant);
}));
test('valid nested entry is in digest; missing and excluded entries fail closed', () => setup(c => {
fs.mkdirSync(path.join(c.variant, 'nested'));
fs.writeFileSync(path.join(c.variant, 'nested', 'entry.js'), 'module.exports={solve:()=>2};');
const file = path.join(c.variant, 'variant.json');
fs.writeFileSync(file, JSON.stringify({ name: 'candidate', entry: 'nested/entry.js', effect_class: 'SE0' }));
const variant = gate.loadVariant(c.variant);
assert.strictEqual(variant.entry, path.join('nested', 'entry.js'));
assert.match(variant.digest, /^[0-9a-f]{64}$/);
for (const entry of ['missing.js', '.git/hidden.js']) {
fs.writeFileSync(file, JSON.stringify({ name: 'candidate', entry, effect_class: 'SE0' }));
assert.throws(() => gate.loadVariant(c.variant), invalidVariant);
}
}));
test('result parser rejects empty, missing, duplicate, unexpected and ambiguous output', () => {
const tasks = [{ id: 't' }];
const cases = [
{}, null, [], { results: {} }, { results: [] },
{ results: [{ id: 'wrong', output: 1 }] }, { results: [{ id: 't' }] },
{ results: [{ id: 't', output: 1, error: 'bad' }] },
{ results: [{ id: 't', output: 1 }, { id: 't', output: 1 }] }, { fatal: '' },
];
for (const value of cases) {
const result = gate.parseChildResult({ status: 0, stdout: JSON.stringify(value) }, tasks);
assert.ok(result.fatal);
assert.strictEqual(result.outputs.size, 0);
}
const valid = gate.parseChildResult({ status: 0, stdout: JSON.stringify({ results: [{ id: 't', output: 1 }] }) }, tasks);
assert.strictEqual(valid.fatal, null);
assert.strictEqual(valid.outputs.get('t').output, 1);
assert.ok(gate.parseChildResult({ status: 0, stdout: 'x'.repeat(1024 * 1024 + 1) }, tasks).fatal);
assert.ok(gate.parseChildResult(null, tasks).fatal);
});
test('fatal baseline classification rejects timeout, nonzero, signal and protocol failures', () => {
const tasks = [{ id: 't' }];
const cases = [
{ error: { code: 'ETIMEDOUT' } }, { error: { code: 'ENOENT' } },
{ status: 1, stdout: '{}' }, { status: null, signal: 'SIGTERM' },
{ status: 0, stdout: '{broken' }, { status: 0, stdout: JSON.stringify({ fatal: 'cannot load variant' }) },
];
for (const child of cases) {
const run = { ...gate.parseChildResult(child, tasks), exit_code: child.status, marker_intact: true, fence_events: [] };
assert.ok(gate.baselineFailure(run, tasks));
}
});
test('baseline validation requires complete unique error-free results and integrity', () => {
const tasks = [{ id: 't' }];
const run = { outputs: new Map([['t', { id: 't', output: 1 }]]), fatal: null, exit_code: 0, marker_intact: true, fence_events: [] };
assert.strictEqual(gate.baselineFailure(run, tasks), null);
const cases = [
{ outputs: new Map() }, { outputs: new Map([['t', { id: 'wrong', output: 1 }]]) },
{ outputs: new Map([['t', { id: 't', error: 'failure' }]]) }, { fatal: 'bad' },
{ exit_code: 1 }, { marker_intact: false }, { fence_events: [{ kind: 'effect' }] },
];
for (const delta of cases) assert.ok(gate.baselineFailure({ ...run, ...delta }, tasks));
for (const input of [undefined, [], [null], [{ id: 't' }, { id: 't' }]]) {
assert.ok(gate.baselineFailure(run, input));
}
});
test('duplicate task ids cannot erase per-task regression evidence', () => setup(c => {
for (const tasks of [[{ id: 'same', input: 0, expected: 1 }, { id: 'same', input: 1, expected: 2 }], [null]]) {
fs.writeFileSync(c.taskset, JSON.stringify({ version: '1', family: 'canary', tasks }));
assert.throws(() => gate.loadTaskset(c.taskset), error => error.code === 'gate.taskset_invalid');
}
}));
finish('security');