diff --git a/docker/gateguard-effectiveness/run.js b/docker/gateguard-effectiveness/run.js index 1ee28c97d..8824b4a5c 100644 --- a/docker/gateguard-effectiveness/run.js +++ b/docker/gateguard-effectiveness/run.js @@ -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) { diff --git a/tests/docker/gateguard-effectiveness.test.js b/tests/docker/gateguard-effectiveness.test.js index 06cbf9238..143844b0e 100644 --- a/tests/docker/gateguard-effectiveness.test.js +++ b/tests/docker/gateguard-effectiveness.test.js @@ -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');