Files
stack/packages/bus/tests/interpreter.test.mjs

88 lines
5.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, writeFileSync, chmodSync, symlinkSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { inspectInterpreter, recordInterpreterStart } from '../src/interpreter.mjs';
function fixture(change = {}) {
const binary = Buffer.from('\x7fELFbody');
return { realpath: () => '/usr/bin/python3.14',
stat: path => ({ uid: 0, mode: 0o100755, dev: 1, ino: 10, isFile: () => path.endsWith('3.14'), isDirectory: () => !path.endsWith('3.14') }),
readFile: path => path === '/etc/passwd' ? 'root:x:0:0::/root:/bin/sh\nuser:x:1000:1000::/home/user:/bin/sh\n' : binary,
exec: () => '[3,14,0]', ...change };
}
test('system Python is a resolved root-owned ELF, never a shim or home interpreter', () => {
const record = inspectInterpreter({ io: fixture() });
assert.equal(record.path, '/usr/bin/python3.14');
assert.deepEqual(record.version, [3,14,0]);
assert.match(record.sha256, /^[a-f0-9]{64}$/);
const bad = [
{ realpath: () => '/home/user/python3' },
{ realpath: () => '/root/python3' },
{ realpath: () => '/srv/user/python3', readFile: p => p === '/etc/passwd' ? 'u:x:1000:1000::/srv/user:/bin/sh' : Buffer.from('\x7fELF') },
{ readFile: p => p === '/etc/passwd' ? '' : Buffer.from('#!/bin/sh\n') },
{ stat: () => ({ uid: 1000, mode: 0o100755, isFile: () => true, isDirectory: () => true }) },
{ stat: () => ({ uid: 0, mode: 0o100775, isFile: () => true, isDirectory: () => true }) },
{ stat: () => { throw Error('missing'); } },
{ exec: () => '[3,9,9]' },
{ exec: () => 'unexpected' },
];
for (const f of bad) assert.throws(() => inspectInterpreter({ io: fixture(f) }), /interpreter-unsafe/);
});
test('real system interpreter is available and safely inspected', () => {
const actual = inspectInterpreter();
assert.ok(actual.path.startsWith('/usr/'));
assert.ok(actual.version[0] >= 3);
});
test('restart receipt appends actual replacement identity and refuses unsafe files', t => {
const root = mkdtempSync(join(tmpdir(), 'g196-int-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const first = inspectInterpreter({ io: fixture() });
recordInterpreterStart(root, first);
const file = join(root, 'peercred-starts.jsonl');
const old = readFileSync(file, 'utf8');
const second = inspectInterpreter({ io: fixture({ readFile: p => p === '/etc/passwd' ? '' : Buffer.from('\x7fELFnew') }) });
recordInterpreterStart(root, second);
const now = readFileSync(file, 'utf8');
assert.ok(now.startsWith(old));
assert.notEqual(JSON.parse(old).sha256, JSON.parse(now.trim().split('\n')[1]).sha256);
chmodSync(file, 0o644);
assert.throws(() => recordInterpreterStart(root, second), /interpreter-receipt-unsafe/);
rmSync(file);
writeFileSync(join(root,'target'), 'untouched');
symlinkSync(join(root,'target'),file);
assert.throws(() => recordInterpreterStart(root, second), /interpreter-receipt-unsafe/);
assert.equal(readFileSync(join(root,'target'),'utf8'),'untouched');
});
test('activation gate compares fresh startup receipt to the current interpreter identity',async t=>{
const { verifyInterpreterReceipt }=await import('../src/interpreter.mjs');
const root=mkdtempSync(join(tmpdir(),'g196-int-'));t.after(()=>rmSync(root,{recursive:true,force:true}));
const first=inspectInterpreter({io:fixture()}),since=Date.now()-1;
recordInterpreterStart(root,first);
assert.equal(verifyInterpreterReceipt(root,first,since),true);
assert.throws(()=>verifyInterpreterReceipt(root,{...first,sha256:'b'.repeat(64)},since),/interpreter-receipt-mismatch/);
assert.throws(()=>verifyInterpreterReceipt(root,first,Date.now()+1000),/interpreter-receipt-mismatch/);
});
test('non-regular startup receipt refuses promptly without waiting on a FIFO peer',async t=>{
const {execFileSync,spawnSync}=await import('node:child_process');
const root=mkdtempSync(join(tmpdir(),'g196-fifo-'));t.after(()=>rmSync(root,{recursive:true,force:true}));
execFileSync('mkfifo',[join(root,'peercred-starts.jsonl')]);
const url=new URL('../src/interpreter.mjs',import.meta.url).href;
const child=spawnSync(process.execPath,['--input-type=module','-e',`import {recordInterpreterStart} from ${JSON.stringify(url)};try{recordInterpreterStart(${JSON.stringify(root)},{});process.exit(2)}catch(e){process.exit(e.message==='interpreter-receipt-unsafe'?0:3)}`],{timeout:1000});
assert.equal(child.status,0,'unsafe FIFO must refuse without blocking startup');
});
//641: scheduler delay is lack of probe evidence, not evidence of an unsafe executable.
test('interpreter probe timeout refuses distinctly without approving the interpreter', () => {
let calls = 0;
assert.throws(() => inspectInterpreter({ io: fixture({ exec: (_path, _args, options) => {
calls++;
assert.equal(options.timeout, 1000);
throw Object.assign(Error('controlled timeout'), { code: 'ETIMEDOUT' });
} }) }), { message: 'interpreter-probe-timeout' });
assert.equal(calls, 1);
});