Skip to content

[Security] ProcessKiller.KillProcessTreeAndParents(string) — root process name is not checked against CriticalSystemProcesses safelist #990

@Christophe-Rogiers

Description

@Christophe-Rogiers

Severity: Warning

File: src/Servy.Core/Helpers/ProcessKiller.cs:167-222

Code:

```csharp
public bool KillProcessTreeAndParents(string processName, bool killParents = true)
{
...
var protectedPids = GetAncestorPids();
var byParent = BuildParentChildMapNative();

var allProcesses = Process.GetProcesses();
try
{
    var targetProcesses = allProcesses
        .Where(p => string.Equals(p.ProcessName, processName, StringComparison.OrdinalIgnoreCase))
        .Where(p => !protectedPids.Contains(p.Id))   // <-- only filters by PID
        .ToList();

    ...
    foreach (var proc in targetProcesses)
    {
        KillProcessTree(proc, selfPid, protectedPids, byParent);   // root killed unconditionally
    }
    ...
}
...

}
```

Explanation:

The class maintains `CriticalSystemProcesses` (line 28) — a hardcoded safelist of "never kill" Windows process names (`csrss`, `lsass`, `wininit`, `services`, `svchost`, `explorer`, `MsMpEng`, ...). That safelist is consulted in:

  • `IsProtected` (line 48), used by descendants in `WalkAndKillChildren` (line 140) and parents in `KillParentProcesses` (via `IsProtected` line 385)
  • `KillProcessesUsingFile` (line 308-314)

But `KillProcessTreeAndParents(string processName)` does not consult it. If a caller passes `"svchost"` or `"explorer"` (deliberately or via configuration error), `targetProcesses` is built without the name-based filter, and `KillProcessTree` then calls `process.Kill()` (line 267) on each match without re-checking the name.

`KillProcessTree` only checks:
```csharp
if (protectedPids.Contains(process.Id)) return;
```
which only protects PIDs in the current ancestor chain — it does not protect, e.g., the dozens of unrelated svchost instances on the system.

The actual kill of System (PID 4) and other kernel-protected processes will fail at the Win32 layer, but the attempt is still made and the function will succeed in killing user-mode services that happen to share the name (e.g., killing all `svchost.exe` instances kills most of the OS).

Suggested fix:

Apply the safelist as a third filter in `KillProcessTreeAndParents(string)`, mirroring the guard in `KillProcessesUsingFile`:

```csharp
// Reject critical system process names up-front, before any process is killed
if (CriticalSystemProcesses.Contains(processName))
{
Logger.Warn($"Refused to kill critical system process by name: '{processName}'.");
return false;
}
```

Place this immediately after the `.exe` normalization at line 175. The PID-based overload `KillProcessTreeAndParents(int pid)` should also resolve the process name and apply the same check before walking the tree.

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions