fix(qa-matrix): resolve Windows taskkill path

This commit is contained in:
Vincent Koc
2026-06-21 11:32:56 +02:00
parent eb7789c8cb
commit 15300291ed
4 changed files with 101 additions and 4 deletions

View File

@@ -77,11 +77,12 @@ describe("Matrix QA CLI runtime", () => {
testing.killMatrixQaCliChild(child, "SIGTERM", runTaskkill);
expect(runTaskkill).toHaveBeenNthCalledWith(1, "taskkill", ["/PID", "12345", "/T"], {
const taskkillPath = path.win32.join("C:\\Windows", "System32", "taskkill.exe");
expect(runTaskkill).toHaveBeenNthCalledWith(1, taskkillPath, ["/PID", "12345", "/T"], {
stdio: "ignore",
windowsHide: true,
});
expect(runTaskkill).toHaveBeenNthCalledWith(2, "taskkill", ["/PID", "12345", "/T", "/F"], {
expect(runTaskkill).toHaveBeenNthCalledWith(2, taskkillPath, ["/PID", "12345", "/T", "/F"], {
stdio: "ignore",
windowsHide: true,
});

View File

@@ -8,6 +8,7 @@ import { setTimeout as sleep } from "node:timers/promises";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { redactSensitiveText } from "openclaw/plugin-sdk/logging-core";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { resolveMatrixQaWindowsSystem32ExePath } from "../../windows-system-tools.js";
export type MatrixQaCliRunResult = {
args: string[];
@@ -111,16 +112,17 @@ function killMatrixQaCliChild(
): void {
if (process.platform === "win32") {
if (child.pid) {
const taskkillPath = resolveMatrixQaWindowsSystem32ExePath("taskkill.exe");
const args = ["/PID", String(child.pid), "/T"];
if (signal === "SIGKILL") {
args.push("/F");
}
const result = runTaskkill("taskkill", args, { stdio: "ignore", windowsHide: true });
const result = runTaskkill(taskkillPath, args, { stdio: "ignore", windowsHide: true });
if (!result.error && result.status === 0) {
return;
}
if (signal !== "SIGKILL") {
const forceResult = runTaskkill("taskkill", [...args, "/F"], {
const forceResult = runTaskkill(taskkillPath, [...args, "/F"], {
stdio: "ignore",
windowsHide: true,
});

View File

@@ -0,0 +1,32 @@
// Qa Matrix tests cover Windows system tool path resolution.
import { describe, expect, it } from "vitest";
import {
resolveMatrixQaWindowsSystem32ExePath,
resolveMatrixQaWindowsSystemRoot,
} from "./windows-system-tools.js";
describe("qa-matrix windows system tools", () => {
it("resolves System32 executables from a trusted SystemRoot", () => {
expect(resolveMatrixQaWindowsSystemRoot({ SystemRoot: "D:\\Windows\\" })).toBe("D:\\Windows");
expect(
resolveMatrixQaWindowsSystem32ExePath("taskkill.exe", { SystemRoot: "D:\\Windows\\" }),
).toBe("D:\\Windows\\System32\\taskkill.exe");
});
it("falls back to the default Windows root when env roots are unsafe", () => {
expect(
resolveMatrixQaWindowsSystem32ExePath("taskkill.exe", {
WINDIR: "\\\\attacker\\share",
}),
).toBe("C:\\Windows\\System32\\taskkill.exe");
});
it("rejects non-basename System32 executable names", () => {
expect(() => resolveMatrixQaWindowsSystem32ExePath("..\\taskkill.exe")).toThrow(
"Invalid Windows System32 executable name",
);
expect(() => resolveMatrixQaWindowsSystem32ExePath("taskkill")).toThrow(
"Invalid Windows System32 executable name",
);
});
});

View File

@@ -0,0 +1,62 @@
// Qa Matrix resolves Windows system tools without trusting PATH.
import path from "node:path";
const DEFAULT_WINDOWS_SYSTEM_ROOT = "C:\\Windows";
function getEnvValueCaseInsensitive(
env: Record<string, string | undefined>,
expectedKey: string,
): string | undefined {
const direct = env[expectedKey];
if (direct !== undefined) {
return direct;
}
const expected = expectedKey.toUpperCase();
const actualKey = Object.keys(env).find((key) => key.toUpperCase() === expected);
return actualKey ? env[actualKey] : undefined;
}
function normalizeWindowsSystemRoot(raw: string | undefined): string | null {
const trimmed = raw?.trim();
if (
!trimmed ||
trimmed.includes("\0") ||
trimmed.includes("\r") ||
trimmed.includes("\n") ||
trimmed.includes(";")
) {
return null;
}
const normalized = path.win32.normalize(trimmed);
if (!path.win32.isAbsolute(normalized) || normalized.startsWith("\\\\")) {
return null;
}
const parsed = path.win32.parse(normalized);
if (!/^[A-Za-z]:\\$/u.test(parsed.root) || normalized.length <= parsed.root.length) {
return null;
}
return normalized.replace(/[\\/]+$/u, "");
}
export function resolveMatrixQaWindowsSystemRoot(
env: Record<string, string | undefined> = process.env,
): string {
return (
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "SystemRoot")) ??
normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "WINDIR")) ??
DEFAULT_WINDOWS_SYSTEM_ROOT
);
}
export function resolveMatrixQaWindowsSystem32ExePath(
executableName: string,
env: Record<string, string | undefined> = process.env,
): string {
if (
path.win32.basename(executableName) !== executableName ||
!/^[A-Za-z0-9_.-]+\.exe$/u.test(executableName)
) {
throw new Error(`Invalid Windows System32 executable name: ${executableName}`);
}
return path.win32.join(resolveMatrixQaWindowsSystemRoot(env), "System32", executableName);
}