This commit is contained in:
Oleg Maslov
2026-09-02 10:14:22 +02:00
parent 0c3e2ead3b
commit b20b138fe4
771 changed files with 161561 additions and 9027 deletions

View File

@@ -0,0 +1,136 @@
import { execFileSync, spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { spawnSidecarOwnedProcess } from '../src/sidecar-owned-process.js';
const TARGET_SOURCE = String.raw`
const { spawn } = require('node:child_process');
const fs = require('node:fs');
const marker = process.argv[1];
const descendant = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
detached: true,
windowsHide: true,
shell: false,
stdio: 'ignore',
});
descendant.unref();
fs.writeFileSync(marker, JSON.stringify({ targetPid: process.pid, descendantPid: descendant.pid }));
setInterval(() => {}, 1000);
`;
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
});
function isAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
async function waitFor(predicate: () => boolean, timeoutMs = 10_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (!predicate()) {
if (Date.now() >= deadline) throw new Error(`Condition not met within ${timeoutMs}ms`);
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
function forceKillTree(pid: number): void {
if (!Number.isSafeInteger(pid) || pid <= 0) return;
if (!isAlive(pid)) return;
const windowsRoot = process.env.SystemRoot ?? process.env.WINDIR ?? 'C:\\Windows';
try {
execFileSync(path.join(windowsRoot, 'System32', 'taskkill.exe'), [
'/PID', String(pid), '/T', '/F',
], { stdio: 'ignore', windowsHide: true });
} catch { /* best-effort fixture cleanup */ }
}
describe('sidecar-owned process supervision', () => {
it.runIf(process.platform === 'win32')(
'reproduces the detached descendant left by a root-only sidecar kill',
async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-orphan-control-'));
tempDirs.push(dir);
const marker = path.join(dir, 'pids.json');
const target = spawn(process.execPath, ['-e', TARGET_SOURCE, marker], {
stdio: 'ignore',
windowsHide: true,
});
let targetPid = 0;
let descendantPid = 0;
try {
await waitFor(() => fs.existsSync(marker));
({ targetPid, descendantPid } = JSON.parse(fs.readFileSync(marker, 'utf8')) as {
targetPid: number;
descendantPid: number;
});
expect(isAlive(descendantPid)).toBe(true);
target.kill();
await waitFor(() => !isAlive(targetPid));
expect(isAlive(descendantPid)).toBe(true);
} finally {
forceKillTree(target.pid ?? 0);
forceKillTree(targetPid);
forceKillTree(descendantPid);
}
},
20_000,
);
it.runIf(process.platform === 'win32')(
'kills the target and its descendant when the sidecar IPC owner disappears',
async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-owned-process-'));
tempDirs.push(dir);
const marker = path.join(dir, 'pids.json');
const supervisor = spawnSidecarOwnedProcess(process.execPath, [
'-e', TARGET_SOURCE, marker,
], {
env: {
...process.env,
SystemRoot: path.join(dir, 'attacker-controlled-system-root'),
WINDIR: path.join(dir, 'attacker-controlled-windir'),
},
stdio: ['ignore', 'ignore', 'pipe'],
windowsHide: true,
});
let targetPid = 0;
let descendantPid = 0;
try {
await waitFor(() => fs.existsSync(marker));
({ targetPid, descendantPid } = JSON.parse(fs.readFileSync(marker, 'utf8')) as {
targetPid: number;
descendantPid: number;
});
expect(isAlive(targetPid)).toBe(true);
expect(isAlive(descendantPid)).toBe(true);
supervisor.disconnect();
await waitFor(() => !isAlive(targetPid) && !isAlive(descendantPid));
await waitFor(() => supervisor.exitCode !== null);
expect(isAlive(targetPid)).toBe(false);
expect(isAlive(descendantPid)).toBe(false);
expect(supervisor.exitCode).toBe(0);
} finally {
forceKillTree(supervisor.pid ?? 0);
forceKillTree(targetPid);
forceKillTree(descendantPid);
}
},
20_000,
);
});