Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 95 additions & 24 deletions src/codex/app-server-processes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@
*/
import { execFileSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
import { isProcessAlive, waitForExit } from "../lib/process-control";
import {
currentWindowsAccount,
parseWmicCreationDate,
parseWmicListRecords,
resolveWmicExe,
wmicGetOwner,
} from "../lib/windows-wmic";
import { readCodexCatalogPath } from "./catalog/parsing";

export const STALE_CODEX_APP_SERVER_HINT =
Expand Down Expand Up @@ -306,17 +314,61 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {

/**
* Windows snapshots scoped to the invoking user via Win32_Process GetOwner.
* PowerShell is the sole path: WMIC lacks reliable owner data and is disabled on
* many Windows 11 installs; returning unscoped rows would contradict the
* current-user restart contract.
* WMIC is preferred when present (faster cold start, no .NET runtime) and the
* hidden PowerShell CIM enumeration is the fallback for hosts where WMIC is
* absent (a deprecated optional component on modern Windows 11 images). Both
* paths spawn with windowsHide: true — a console-less proxy presenting a child
* console window is the v2.11.0 popup bug this enumeration must not revive.
*
* CIM instance methods must use Invoke-CimMethod (direct .GetOwner() calls fail).
* Candidates are pre-filtered to Codex basename / code-mode-host command lines
* so we do not pay GetOwner per every process on the machine.
* Exported for the Windows integration regression that exercises the real
* PowerShell enumeration.
* so we do not pay GetOwner per every process on the machine. A candidate
* whose owner cannot be verified makes the whole enumeration incomplete; WMIC
* absence falls back rather than failing. Exported for the Windows
* integration regression.
*/
export function listWindowsSnapshots(): ProcessSnapshot[] {
const wmic = resolveWmicExe();
return wmic ? listWindowsSnapshotsViaWmic(wmic) : listWindowsSnapshotsViaPowerShell();
}

function listWindowsSnapshotsViaWmic(wmic: string): ProcessSnapshot[] {
const me = currentWindowsAccount();
if (!me) throw new Error("windows_enum_incomplete");
const wql =
"(Name like 'codex%' or CommandLine like '%codex app-server%' or CommandLine like '%codex-code-mode-host%')";
// Top-level exec failure propagates (see listDarwinSnapshots note).
const output = execFileSync(
wmic,
["process", "where", wql, "get", "ProcessId,CommandLine,CreationDate", "/format:list"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true },
);
const out: ProcessSnapshot[] = [];
for (const record of parseWmicListRecords(output)) {
if (!record.commandLine) continue;
const commandLine = record.commandLine.replace(/\t/g, " ").trim();
// The WMIC helper's own CommandLine embeds the WQL literals ("codex
// app-server", "codex-code-mode-host"), so it matches the candidate
// pre-filter. Skip any process that is the WMIC executable itself.
if (commandLine.toLowerCase().includes(wmic.toLowerCase())) continue;
if (!isWindowsCodexCandidateCommandLine(commandLine)) continue;
const owner = wmicGetOwner(record.processId);
// A candidate whose owner could not be verified makes the whole
// enumeration incomplete — the staleness collector must not read the
// partial result as "nothing running".
if (!owner) throw new Error("windows_enum_incomplete");
const ownerName = `${owner.domain}\\${owner.user}`;
if (ownerName.toLowerCase() !== me.toLowerCase()) continue;
out.push({
pid: record.processId,
commandLine,
owner: ownerName,
startedAtMs: parseWmicCreationDate(record.creationDate) ?? undefined,
});
}
return out;
}

function listWindowsSnapshotsViaPowerShell(): ProcessSnapshot[] {
const out: ProcessSnapshot[] = [];
// Newlines keep -Command as a real script (space-joined statements need ';').
// Double-quoted format string so `t expands to a real tab.
Expand All @@ -326,6 +378,7 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
// path with "opencodex".
const basenameMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_BASENAME_CANDIDATE_RE.source);
const codeModeMatch = powerShellSingleQuotedIgnoreCaseMatch(WINDOWS_CODEX_CODE_MODE_HOST_CANDIDATE_RE.source);
// CIM instance methods must use Invoke-CimMethod (direct .GetOwner() calls fail).
const psCommand = [
"$ErrorActionPreference='SilentlyContinue'",
"$me=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
Expand All @@ -346,7 +399,7 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
"}",
].join("\n");
// Top-level exec failure propagates (see listDarwinSnapshots note).
const output = execFileSync("powershell.exe", [
const output = execFileSync(resolveTrustedWindowsPowerShellExe(), [
"-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
"-Command",
psCommand,
Expand Down Expand Up @@ -456,8 +509,24 @@ function readDarwinProcStartMs(pid: number): number | null {

/** Win32_Process.CreationDate → epoch ms, or null (Windows). */
function readWindowsProcStartMs(pid: number): number | null {
const wmic = resolveWmicExe();
if (!wmic) return readWindowsProcStartMsViaPowerShell(pid);
try {
const out = execFileSync("powershell.exe", [
const out = execFileSync(
wmic,
["process", "where", `ProcessId=${pid}`, "get", "CreationDate", "/format:list"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
);
const match = /^CreationDate=(.*)$/m.exec(out.replace(/\r/g, ""));
return parseWmicCreationDate(match?.[1]);
} catch {
return null;
}
}
Comment on lines +512 to +525

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fall back to PowerShell when the WMIC start-time query fails, not only when WMIC is absent.

Line 513 routes to readWindowsProcStartMsViaPowerShell only when resolveWmicExe() returns null. If WMIC exists but the call fails (timeout on a contended host, WMI repository error, /format:list returning no CreationDate line), the catch at Line 522 returns null and the working PowerShell path is never tried. The caller then treats the process start time as unknown, which weakens the PID-reuse guard that readProcessStartMs exists to support.

Reuse the fallback for both conditions.

🛡️ Proposed fallback on failure
   const match = /^CreationDate=(.*)$/m.exec(out.replace(/\r/g, ""));
-    return parseWmicCreationDate(match?.[1]);
+    return parseWmicCreationDate(match?.[1]) ?? readWindowsProcStartMsViaPowerShell(pid);
   } catch {
-    return null;
+    return readWindowsProcStartMsViaPowerShell(pid);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const wmic = resolveWmicExe();
if (!wmic) return readWindowsProcStartMsViaPowerShell(pid);
try {
const out = execFileSync("powershell.exe", [
const out = execFileSync(
wmic,
["process", "where", `ProcessId=${pid}`, "get", "CreationDate", "/format:list"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
);
const match = /^CreationDate=(.*)$/m.exec(out.replace(/\r/g, ""));
return parseWmicCreationDate(match?.[1]);
} catch {
return null;
}
}
const wmic = resolveWmicExe();
if (!wmic) return readWindowsProcStartMsViaPowerShell(pid);
try {
const out = execFileSync(
wmic,
["process", "where", `ProcessId=${pid}`, "get", "CreationDate", "/format:list"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
);
const match = /^CreationDate=(.*)$/m.exec(out.replace(/\r/g, ""));
return parseWmicCreationDate(match?.[1]) ?? readWindowsProcStartMsViaPowerShell(pid);
} catch {
return readWindowsProcStartMsViaPowerShell(pid);
}
}
🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 520-520: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codex/app-server-processes.ts` around lines 512 - 525, Update the WMIC
query flow in the function containing resolveWmicExe and
readWindowsProcStartMsViaPowerShell so both a missing WMIC executable and any
WMIC query failure fall back to readWindowsProcStartMsViaPowerShell(pid).
Preserve the existing successful WMIC parsing behavior, but replace the failure
path that returns null with the PowerShell fallback.


function readWindowsProcStartMsViaPowerShell(pid: number): number | null {
try {
const out = execFileSync(resolveTrustedWindowsPowerShellExe(), [
"-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
"-Command",
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`,
Expand Down Expand Up @@ -503,30 +572,32 @@ export function readProcessStartMsBatch(
const parsed = Date.parse(match[2]!.trim());
if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
}
for (const pid of pids) out.set(pid, byPid.get(pid) ?? null);
for (const pid of pids) if (!out.has(pid)) out.set(pid, null);
return out;
} catch {
for (const pid of pids) out.set(pid, null);
return out;
}
}
if (platform === "win32") {
const wmic = resolveWmicExe();
if (!wmic) {
for (const pid of pids) out.set(pid, null);
return out;
}
try {
const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
const stdout = execFileSync("powershell.exe", [
"-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
"-Command",
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`,
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true });
const byPid = new Map<number, number>();
for (const line of stdout.split(/\r?\n/)) {
const tab = line.indexOf("\t");
if (tab <= 0) continue;
const pid = Number(line.slice(0, tab));
const parsed = Date.parse(line.slice(tab + 1).trim());
if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
const filter = pids.map(pid => `ProcessId=${pid}`).join(" or ");
const stdout = execFileSync(
wmic,
["process", "where", filter, "get", "ProcessId,CreationDate", "/format:list"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
);
for (const record of parseWmicListRecords(stdout)) {
if (record.creationDate !== undefined) {
out.set(record.processId, parseWmicCreationDate(record.creationDate));
}
}
for (const pid of pids) out.set(pid, byPid.get(pid) ?? null);
for (const pid of pids) if (!out.has(pid)) out.set(pid, null);
Comment on lines 582 to +600

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Chunk the WMIC batch filter and reject unusable PIDs before building the WQL.

Two concrete failure modes exist in this branch:

  1. Line 589 builds one WQL clause per PID with no upper bound. pids is an unbounded readonly number[]. Each term costs about 15 characters, so a few thousand PIDs push the WMIC command line past the Windows limit (about 32767 characters). execFileSync then fails, the catch at Line 602 maps every PID to null, and all start times become unknown in one shot.
  2. No PID validation occurs. A single NaN, negative, or fractional entry produces a term such as ProcessId=NaN, which makes WMIC reject the whole query. One bad input therefore discards the result for every valid PID in the batch.

Chunk the query and filter the PIDs first. The single-PID path at Lines 511-525 has the same lack of validation, but it fails in isolation there.

🛡️ Proposed chunking and validation
     try {
-      const filter = pids.map(pid => `ProcessId=${pid}`).join(" or ");
-      const stdout = execFileSync(
-        wmic,
-        ["process", "where", filter, "get", "ProcessId,CreationDate", "/format:list"],
-        { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
-      );
-      for (const record of parseWmicListRecords(stdout)) {
-        if (record.creationDate !== undefined) {
-          out.set(record.processId, parseWmicCreationDate(record.creationDate));
-        }
-      }
+      const usable = pids.filter(pid => Number.isSafeInteger(pid) && pid > 1);
+      const WMIC_BATCH_SIZE = 200;
+      for (let i = 0; i < usable.length; i += WMIC_BATCH_SIZE) {
+        const chunk = usable.slice(i, i + WMIC_BATCH_SIZE);
+        const filter = chunk.map(pid => `ProcessId=${pid}`).join(" or ");
+        const stdout = execFileSync(
+          wmic,
+          ["process", "where", filter, "get", "ProcessId,CreationDate", "/format:list"],
+          { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
+        );
+        for (const record of parseWmicListRecords(stdout)) {
+          if (record.creationDate !== undefined) {
+            out.set(record.processId, parseWmicCreationDate(record.creationDate));
+          }
+        }
+      }
       for (const pid of pids) if (!out.has(pid)) out.set(pid, null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (platform === "win32") {
const wmic = resolveWmicExe();
if (!wmic) {
for (const pid of pids) out.set(pid, null);
return out;
}
try {
const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
const stdout = execFileSync("powershell.exe", [
"-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
"-Command",
`Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`,
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true });
const byPid = new Map<number, number>();
for (const line of stdout.split(/\r?\n/)) {
const tab = line.indexOf("\t");
if (tab <= 0) continue;
const pid = Number(line.slice(0, tab));
const parsed = Date.parse(line.slice(tab + 1).trim());
if (Number.isSafeInteger(pid) && Number.isFinite(parsed)) byPid.set(pid, parsed);
const filter = pids.map(pid => `ProcessId=${pid}`).join(" or ");
const stdout = execFileSync(
wmic,
["process", "where", filter, "get", "ProcessId,CreationDate", "/format:list"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
);
for (const record of parseWmicListRecords(stdout)) {
if (record.creationDate !== undefined) {
out.set(record.processId, parseWmicCreationDate(record.creationDate));
}
}
for (const pid of pids) out.set(pid, byPid.get(pid) ?? null);
for (const pid of pids) if (!out.has(pid)) out.set(pid, null);
if (platform === "win32") {
const wmic = resolveWmicExe();
if (!wmic) {
for (const pid of pids) out.set(pid, null);
return out;
}
try {
const usable = pids.filter(pid => Number.isSafeInteger(pid) && pid > 1);
const WMIC_BATCH_SIZE = 200;
for (let i = 0; i < usable.length; i += WMIC_BATCH_SIZE) {
const chunk = usable.slice(i, i + WMIC_BATCH_SIZE);
const filter = chunk.map(pid => `ProcessId=${pid}`).join(" or ");
const stdout = execFileSync(
wmic,
["process", "where", filter, "get", "ProcessId,CreationDate", "/format:list"],
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true },
);
for (const record of parseWmicListRecords(stdout)) {
if (record.creationDate !== undefined) {
out.set(record.processId, parseWmicCreationDate(record.creationDate));
}
}
}
for (const pid of pids) if (!out.has(pid)) out.set(pid, null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codex/app-server-processes.ts` around lines 582 - 600, Update the Windows
branch around resolveWmicExe and execFileSync to discard unusable PIDs
(non-finite, non-integer, or non-positive) before constructing WQL, while
preserving null results for rejected inputs. Split the remaining valid PIDs into
bounded chunks, execute and parse each WMIC query independently, merge creation
times into out, and assign null for any valid PID missing from all successful
chunk results so one oversized query or invalid input cannot discard every
result.

return out;
} catch {
for (const pid of pids) out.set(pid, null);
Expand Down
70 changes: 66 additions & 4 deletions src/codex/native-profile-processes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { execFile } from "node:child_process";
import { basename } from "node:path";
import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
import {
resolveTrustedWindowsPowerShellExe,
resolveTrustedWindowsWmicExe,
} from "../lib/windows-elevation";
import { parseWmicListRecords } from "../lib/windows-wmic";

const PROCESS_LIST_MAX_BUFFER = 16 * 1024 * 1024;
const DIRECT_CODEX_BASENAMES = new Set(["codex", "codex.exe"]);
Expand All @@ -13,6 +17,9 @@ const CODEX_ENTRYPOINT_BASENAMES = new Set([
"codex.ts",
]);

const WINDOWS_CODEX_NAME_RE = /^(?:codex|codex\.exe)$/i;
const WINDOWS_CODEX_CMDLINE_RE = /(?:^|[\\/"\s])codex(?:\.exe|\.cmd)?(?:["\s]|$)/i;

export interface NativeProcessExecOptions {
encoding: "utf8";
timeout: number;
Expand Down Expand Up @@ -57,12 +64,67 @@ export const executeNativeProcess: NativeProcessExecutor = (file, args, options)
});
});

async function windowsProcessCount(run: NativeProcessExecutor): Promise<number> {
/**
* Count Codex processes on Windows. WMIC is preferred when present (faster
* cold start, no .NET runtime); the hidden PowerShell CIM query is the
* fallback for hosts without WMIC (a deprecated optional component on modern
* Windows 11 images). Both paths run hidden (windowsHide) so a console-less
* proxy never presents a child console window.
*/
async function windowsProcessCount(run: NativeProcessExecutor, selfPid: number): Promise<number> {
const wmic = resolveTrustedWindowsWmicExe();
return wmic
? windowsProcessCountViaWmic(run, wmic, selfPid)
: windowsProcessCountViaPowerShell(run, selfPid);
}

async function windowsProcessCountViaWmic(
run: NativeProcessExecutor,
wmic: string,
selfPid: number,
): Promise<number> {
const output = await run(wmic, [
"process", "where",
"(Name like 'codex%' or CommandLine like '%codex%')",
"get", "ProcessId,Name,CommandLine", "/format:list",
], {
encoding: "utf8",
timeout: 12_000,
maxBuffer: PROCESS_LIST_MAX_BUFFER,
windowsHide: true,
shell: false,
killSignal: "SIGKILL",
});
const records = parseWmicListRecords(output);
// A valid enumeration always includes at least our own process (the proxy
// command line contains "opencodex"), so an empty result is a failure.
if (records.length === 0) throw new Error("invalid process list");
let count = 0;
for (const record of records) {
if (record.processId === selfPid) continue;
const nameMatch = record.name !== undefined && WINDOWS_CODEX_NAME_RE.test(record.name);
const commandLineMatch = record.commandLine !== undefined
&& WINDOWS_CODEX_CMDLINE_RE.test(record.commandLine);
if (nameMatch || commandLineMatch) count += 1;
}
return count;
Comment on lines +98 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude the WMIC helper process explicitly rather than relying on quote characters.

The WMIC child's own CommandLine contains the WQL literals 'codex%' and '%codex%', so WMIC returns its own process for this query. Today it escapes the count only because WINDOWS_CODEX_CMDLINE_RE requires the character before codex to be one of [\\/"\s], and the actual characters are ' and %. Any later relaxation of that character class silently adds a phantom Codex process and flips a clear probe to busy, which blocks profile writes.

listWindowsSnapshotsViaWmic in src/codex/app-server-processes.ts Lines 349-352 already guards this case explicitly. Mirror that guard here.

🛡️ Proposed explicit exclusion
   for (const record of records) {
     if (record.processId === selfPid) continue;
+    // WMIC's own CommandLine embeds the WQL "codex" literals, so skip it.
+    if (record.commandLine?.toLowerCase().includes(wmic.toLowerCase())) continue;
     const nameMatch = record.name !== undefined && WINDOWS_CODEX_NAME_RE.test(record.name);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const records = parseWmicListRecords(output);
// A valid enumeration always includes at least our own process (the proxy
// command line contains "opencodex"), so an empty result is a failure.
if (records.length === 0) throw new Error("invalid process list");
let count = 0;
for (const record of records) {
if (record.processId === selfPid) continue;
const nameMatch = record.name !== undefined && WINDOWS_CODEX_NAME_RE.test(record.name);
const commandLineMatch = record.commandLine !== undefined
&& WINDOWS_CODEX_CMDLINE_RE.test(record.commandLine);
if (nameMatch || commandLineMatch) count += 1;
}
return count;
const records = parseWmicListRecords(output);
// A valid enumeration always includes at least our own process (the proxy
// command line contains "opencodex"), so an empty result is a failure.
if (records.length === 0) throw new Error("invalid process list");
let count = 0;
for (const record of records) {
if (record.processId === selfPid) continue;
// WMIC's own CommandLine embeds the WQL "codex" literals, so skip it.
if (record.commandLine?.toLowerCase().includes(wmic.toLowerCase())) continue;
const nameMatch = record.name !== undefined && WINDOWS_CODEX_NAME_RE.test(record.name);
const commandLineMatch = record.commandLine !== undefined
&& WINDOWS_CODEX_CMDLINE_RE.test(record.commandLine);
if (nameMatch || commandLineMatch) count += 1;
}
return count;
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFile } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/codex/native-profile-processes.ts` around lines 98 - 110, Update the
record-counting loop in the WMIC process enumeration to explicitly skip the WMIC
helper process, matching the guard used by listWindowsSnapshotsViaWmic. Identify
that helper by its command line before applying WINDOWS_CODEX_NAME_RE or
WINDOWS_CODEX_CMDLINE_RE, while preserving the existing selfPid exclusion and
invalid-empty-list behavior.

}

async function windowsProcessCountViaPowerShell(
run: NativeProcessExecutor,
selfPid: number,
): Promise<number> {
const powershell = resolveTrustedWindowsPowerShellExe();
// The script text itself contains "codex", so the PowerShell host process
// matches the candidate filter and must be excluded ($self). The probing
// proxy (opencodex in its command line) is excluded by pid.
const selfFilter = Number.isSafeInteger(selfPid) && selfPid > 0
? ` $_.ProcessId -ne ${selfPid} -and`
: "";
const script = [
"$ErrorActionPreference='Stop';",
"$self=$PID;",
"$items=Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $self -and ($_.Name -match '^(?i:codex)(?:\\.exe)?$' -or $_.CommandLine -match '(?i)(?:^|[\\\\/\"\\s])codex(?:\\.exe|\\.cmd)?(?:[\"\\s]|$)') };",
`$items=Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $self -and${selfFilter} ($_.Name -match '^(?i:codex)(?:\\.exe)?$' -or $_.CommandLine -match '(?i)(?:^|[\\\\/\"\\s])codex(?:\\.exe|\\.cmd)?(?:[\"\\s]|$)') };`,
"@($items).Count",
].join(" ");
const output = (await run(powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
Expand Down Expand Up @@ -112,7 +174,7 @@ export async function probeNativeCodexProcesses({
}: NativeCodexProcessProbeOptions = {}): Promise<NativeCodexProcessProbe> {
try {
const count = await (platform === "win32"
? windowsProcessCount(run)
? windowsProcessCount(run, pid)
: unixProcessCount(run, pid));
return count > 0 ? { status: "busy", count } : { status: "clear", count: 0 };
} catch {
Expand Down
Loading
Loading