59 lines
3.9 KiB
JavaScript
59 lines
3.9 KiB
JavaScript
// Namespace-local evidence and cleanup. The JSON observation precedes teardown.
|
|
import {readFileSync,writeFileSync,readdirSync,openSync,closeSync} from 'node:fs';
|
|
import {spawnSync} from 'node:child_process';
|
|
const [script,dir,mode]=process.argv.slice(2),records=dir+'/pids',worker=dir+'/worker.mjs';
|
|
const source=readFileSync(script,'utf8');
|
|
let body=source.slice(source.indexOf('# row253-supervisor-begin'),source.indexOf('# row253-supervisor-end'));
|
|
// Adversarial kernel boundary controls keep the real cleanup loop and budget.
|
|
if(mode==='unreadable')body=body.replace('def fields(pid):','def fields(pid):\n raise PermissionError("injected unreadable stat")');
|
|
if(mode==='unkillable')body=body.replace('signal.pidfd_send_signal(fd, signal.SIGKILL)','None # injected successful kill with a surviving member');
|
|
if(mode==='fork')writeFileSync(worker,`
|
|
import {spawn} from 'node:child_process';
|
|
import {appendFileSync,readFileSync} from 'node:fs';
|
|
const record=${JSON.stringify(records)};
|
|
const sid=p=>Number(readFileSync('/proc/'+p+'/stat','utf8').split(') ').at(-1).split(' ')[3]);
|
|
appendFileSync(record,JSON.stringify([process.pid,sid(process.pid)])+'\\n');
|
|
setInterval(()=>{
|
|
const child=spawn('/usr/bin/sleep',['30'],{stdio:'ignore'});
|
|
appendFileSync(record,JSON.stringify([child.pid,sid(child.pid)])+'\\n');
|
|
},4);
|
|
`);
|
|
else writeFileSync(worker,`
|
|
import {spawn} from 'node:child_process';
|
|
import {readFileSync,writeFileSync} from 'node:fs';
|
|
const record=${JSON.stringify(records)};
|
|
const child=spawn('/bin/bash',['-c',${JSON.stringify(mode==='escape'?'exec /usr/bin/setsid /usr/bin/sleep 30':'/usr/bin/sleep 30 & echo $! > "$1"')},'--',record+'.child'],{stdio:'ignore'});
|
|
setTimeout(()=>{
|
|
const pid=${mode==='escape'?'child.pid':"Number(readFileSync(record+'.child','utf8').trim())"};
|
|
const sid=p=>Number(readFileSync('/proc/'+p+'/stat','utf8').split(') ').at(-1).split(' ')[3]);
|
|
writeFileSync(record,JSON.stringify([[process.pid,sid(process.pid)],[pid,sid(pid)]]));
|
|
writeFileSync(record+'.anchor',String(process.ppid));
|
|
writeFileSync(record+'.parent',readFileSync('/proc/'+pid+'/stat','utf8').split(') ').at(-1).split(' ')[1]);
|
|
if(${JSON.stringify(mode)} === "anchor-death")process.kill(process.ppid,"SIGKILL");
|
|
},100);
|
|
setInterval(()=>{},1000);
|
|
`);
|
|
const out=openSync(dir+'/out','w'),err=openSync(dir+'/err','w');
|
|
const result=spawnSync('/bin/bash',['-c',body+'\nexec /bin/bash -c "$TESTER_SUPERVISOR" -- 1 /usr/bin/setsid /usr/bin/sleep "$@"','--','/bin/bash','-c','exec "$@" >/dev/null 2>&1','--',process.execPath,worker],{encoding:'utf8',stdio:['ignore',out,err],timeout:8000,killSignal:'SIGKILL'});
|
|
closeSync(out);closeSync(err);
|
|
result.stdout=readFileSync(dir+'/out','utf8');result.stderr=readFileSync(dir+'/err','utf8');
|
|
let pairs=[];
|
|
try{const text=readFileSync(records,'utf8');pairs=mode==='fork'?text.trim().split('\n').map(JSON.parse):JSON.parse(text);}catch{}
|
|
const live=[];
|
|
for(const [pid] of pairs){try{const state=readFileSync('/proc/'+pid+'/stat','utf8').split(') ').at(-1).split(' ')[0];if(!['Z','X'].includes(state))live.push(pid);}catch{}}
|
|
// Inventory the entire private namespace too: a fork between spawn and its
|
|
// record write must not escape the assertion just because it was unrecorded.
|
|
const sessionLive=[];
|
|
for(const pid of readdirSync('/proc').filter(p=>/^[0-9]+$/.test(p))){
|
|
try{
|
|
const words=readFileSync('/proc/'+pid+'/stat','utf8').split(') ').at(-1).split(' ');
|
|
if(Number(words[3])===pairs[0]?.[1]&&!['Z','X'].includes(words[0]))sessionLive.push(Number(pid));
|
|
}catch{}
|
|
}
|
|
let parent,anchor;
|
|
try{anchor=Number(readFileSync(records+".anchor","utf8"));}catch{}
|
|
try{parent=Number(readFileSync(records+'.parent','utf8'));}catch{}
|
|
process.stdout.write(JSON.stringify({sessionLive,parent,anchor,status:result.status,signal:result.signal,error:result.error?.message,stdout:result.stdout,stderr:result.stderr,pairs,live}));
|
|
// These PIDs exist only in the private namespace. Never signal them on the host.
|
|
for(const [pid] of pairs)try{process.kill(pid,'SIGKILL');}catch{}
|