fix(eval): reject resume symlinks without no-follow support

This commit is contained in:
haelyra
2026-10-10 18:38:26 -04:00
parent 1d13881b1e
commit a2dbcdb2e0
2 changed files with 22 additions and 6 deletions
+8 -5
View File
@@ -72,18 +72,21 @@ function parseArgs(argv) {
return options;
}
function readOptionalText(file) {
function readOptionalText(file, io = fs) {
let fd;
try {
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0));
fd = io.openSync(file, io.constants.O_RDONLY | (io.constants.O_NOFOLLOW || 0) | (io.constants.O_NONBLOCK || 0));
} catch (error) {
if (error.code === 'ENOENT') return null;
throw error;
}
try {
if (!fs.fstatSync(fd).isFile()) throw new Error('Resume metadata/results must be regular files');
return fs.readFileSync(fd, 'utf8');
} finally { fs.closeSync(fd); }
if (!io.fstatSync(fd).isFile()
|| (!io.constants.O_NOFOLLOW && io.lstatSync(file).isSymbolicLink())) {
throw new Error('Resume metadata/results must be regular files without symlinks');
}
return io.readFileSync(fd, 'utf8');
} finally { io.closeSync(fd); }
}
function writeMetadata(file, meta) {
+14 -1
View File
@@ -16,7 +16,7 @@ const HOOK = path.join(ROOT, 'scripts', 'hooks', 'gateguard-fact-force.js');
const lib = require(path.join(DIR, 'lib'));
const arms = require(path.join(DIR, 'arms'));
const { applyArm, armQuestionIds, PLACEBO_TEXT } = require(path.join(DIR, 'arm-patch'));
const { parseArgs, writeMetadata, withOutputLock } = require(path.join(DIR, 'run'));
const { parseArgs, readOptionalText, writeMetadata, withOutputLock } = require(path.join(DIR, 'run'));
let passed = 0;
let failed = 0;
@@ -362,6 +362,19 @@ if (gitAvailable()) {
assert.strictEqual(fs.readFileSync(path.join(out, 'results.jsonl'), 'utf8'), results);
assert.deepStrictEqual(JSON.parse(fs.readFileSync(metaPath, 'utf8')), metadata);
if (process.platform !== 'win32') {
const resultsPath = path.join(out, 'results.jsonl');
for (const file of [metaPath, resultsPath]) {
const original = fs.readFileSync(file, 'utf8');
fs.unlinkSync(file);
fs.symlinkSync(victim, file);
const early = run({});
assert.notStrictEqual(early.status, 0, 'early symlink must prevent a trial');
assert.strictEqual(fs.readFileSync(victim, 'utf8'), 'VICTIM_BYTES');
assert.throws(() => readOptionalText(file, { ...fs, constants: { ...fs.constants, O_NOFOLLOW: 0 } }), /without symlinks/);
fs.unlinkSync(file);
fs.writeFileSync(file, original, { flag: 'wx', mode: 0o600 });
}
assert.strictEqual(fs.readFileSync(resultsPath, 'utf8'), results);
const raced = run({ ECC_TEST_META_RACE: '1' });
assert.strictEqual(raced.status, 0, raced.stderr);
assert.strictEqual(fs.readFileSync(victim, 'utf8'), 'VICTIM_BYTES');