diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts index bfabd142ef0a..884b2432aaae 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.test.ts @@ -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, }); diff --git a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts index 9412d5661bab..278d2853d59f 100644 --- a/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts +++ b/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts @@ -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, }); diff --git a/extensions/qa-matrix/src/windows-system-tools.test.ts b/extensions/qa-matrix/src/windows-system-tools.test.ts new file mode 100644 index 000000000000..3bd857bd6b7b --- /dev/null +++ b/extensions/qa-matrix/src/windows-system-tools.test.ts @@ -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", + ); + }); +}); diff --git a/extensions/qa-matrix/src/windows-system-tools.ts b/extensions/qa-matrix/src/windows-system-tools.ts new file mode 100644 index 000000000000..325f7872a2e1 --- /dev/null +++ b/extensions/qa-matrix/src/windows-system-tools.ts @@ -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, + 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 = process.env, +): string { + return ( + normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "SystemRoot")) ?? + normalizeWindowsSystemRoot(getEnvValueCaseInsensitive(env, "WINDIR")) ?? + DEFAULT_WINDOWS_SYSTEM_ROOT + ); +} + +export function resolveMatrixQaWindowsSystem32ExePath( + executableName: string, + env: Record = 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); +}