Files
bot-agents/src/isolation.ts
T

60 lines
2.0 KiB
TypeScript

import { lstat, readdir, readFile } from "node:fs/promises";
import process from "node:process";
import { setTimeout as sleep } from "node:timers/promises";
import { isNotFound } from "./errors.ts";
import type { RuntimeProfile } from "./runtime.ts";
export function isRunnableProfileProcess(status: string, uid: number): boolean {
const owner = /^Uid:\s+(\d+)/mu.exec(status)?.[1];
const state = /^State:\s+(\w)/mu.exec(status)?.[1];
return owner !== undefined && Number(owner) === uid && state !== "Z";
}
async function profileProcessIds(uid: number): Promise<number[]> {
const entries = await readdir("/proc", { withFileTypes: true });
const pids = entries
.filter((entry) => entry.isDirectory() && /^\d+$/u.test(entry.name))
.map((entry) => entry.name);
const statuses = await Promise.all(
pids.map((pid) => readFile(`/proc/${pid}/status`, "utf8").catch(() => null)),
);
return pids
.filter((_pid, index) => {
const status = statuses[index];
return status !== null && status !== undefined && isRunnableProfileProcess(status, uid);
})
.map(Number);
}
// Kills the UID's processes every 10 ms until none is left, for up to 100 rounds.
export async function killProfileProcesses(uid: number, attempt = 0): Promise<void> {
if (attempt === 100) {
throw new Error(`profile UID ${uid} still owns processes after cleanup`);
}
const ids = await profileProcessIds(uid);
if (ids.length === 0) return;
for (const pid of ids) {
try {
process.kill(pid, "SIGKILL");
} catch (error) {
if (!isNotFound(error)) throw error;
}
}
await sleep(10);
await killProfileProcesses(uid, attempt + 1);
}
export async function readProfileFile(profile: RuntimeProfile, path: string): Promise<string> {
const stat = await lstat(path);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.uid !== profile.uid ||
(stat.mode & 0o077) !== 0
) {
throw new Error(`${path}: expected a profile-owned regular private file`);
}
return readFile(path, "utf8");
}