68 lines
2.6 KiB
JavaScript
68 lines
2.6 KiB
JavaScript
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { readdir, stat } from "node:fs/promises";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
/**
|
|
* Observe leftover demo processes on a dedicated clean Linux runner, including
|
|
* groups reparented after their launcher exits. This is not ownership proof and
|
|
* never authorizes signaling; unrelated runtime processes also block this lane.
|
|
*/
|
|
export const assertCleanLinuxHopperRuntime = async (
|
|
launcherPaths = [
|
|
"/opt/hopper/bin/Hopper",
|
|
...(process.env.HOPPER_LAUNCHER_PATH === undefined
|
|
? []
|
|
: [process.env.HOPPER_LAUNCHER_PATH]),
|
|
],
|
|
procRoot = "/proc",
|
|
) => {
|
|
const { stdout } = await execFileAsync("ps", ["-ax", "-o", "command="]);
|
|
if (/\/opt\/hopper\/bin\/Hopper|\bXvfb\b|hopper-demo-x11\.py/u.test(stdout))
|
|
throw new Error("Clean Linux runner retained a Hopper demo process");
|
|
const launchers = [];
|
|
for (const path of launcherPaths) {
|
|
try {
|
|
launchers.push(await stat(path, { bigint: true }));
|
|
} catch (error) {
|
|
if (error?.code !== "ENOENT") throw error;
|
|
}
|
|
}
|
|
if (launchers.length === 0) return;
|
|
// /proc exposes the executed file even when argv[0] names a removed symlink.
|
|
// Compare file identity, not a display path or a process name.
|
|
let inspectionError;
|
|
for (const entry of await readdir(procRoot, { withFileTypes: true })) {
|
|
if (!entry.isDirectory() || !/^\d+$/u.test(entry.name)) continue;
|
|
let executable;
|
|
try {
|
|
executable = await stat(`${procRoot}/${entry.name}/exe`, {
|
|
bigint: true,
|
|
});
|
|
} catch (error) {
|
|
if (error?.code === "ENOENT" || error?.code === "ESRCH") continue;
|
|
// Other users' executables may be unreadable. The setup lane launches
|
|
// providers as the caller; unreadable caller-owned processes fail closed.
|
|
if (error?.code === "EACCES") {
|
|
const owner = await stat(`${procRoot}/${entry.name}`).catch((cause) => {
|
|
if (cause?.code === "ENOENT") return undefined;
|
|
throw cause;
|
|
});
|
|
if (owner === undefined || owner.uid !== process.getuid()) continue;
|
|
}
|
|
// Finish the inventory so an unrelated restricted process cannot mask a
|
|
// positively identified leak. Unknown caller processes still fail closed.
|
|
inspectionError ??= error;
|
|
continue;
|
|
}
|
|
if (
|
|
launchers.some(
|
|
({ dev, ino }) => dev === executable.dev && ino === executable.ino,
|
|
)
|
|
)
|
|
throw new Error("Clean Linux runner retained a Hopper demo process");
|
|
}
|
|
if (inspectionError !== undefined) throw inspectionError;
|
|
};
|