using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using log4net;
using WinSW.Extensions;
using WinSW.Util;
using static WinSW.Plugins.RunawayProcessKiller.RunawayProcessKillerExtension.Native;
namespace WinSW.Plugins.RunawayProcessKiller
{
public partial class RunawayProcessKillerExtension : AbstractWinSWExtension
{
///
/// Absolute path to the PID file, which stores ID of the previously launched process.
///
public string Pidfile { get; private set; }
///
/// Defines the process termination timeout in milliseconds.
/// This timeout will be applied multiple times for each child process.
///
public TimeSpan StopTimeout { get; private set; }
///
/// If true, the runaway process will be checked for the WinSW environment variable before termination.
/// This option is not documented AND not supposed to be used by users.
///
public bool CheckWinSWEnvironmentVariable { get; private set; }
public override string DisplayName => "Runaway Process Killer";
private string ServiceId { get; set; }
private static readonly ILog Logger = LogManager.GetLogger(typeof(RunawayProcessKillerExtension));
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
public RunawayProcessKillerExtension()
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
{
// Default initializer
}
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
public RunawayProcessKillerExtension(string pidfile, int stopTimeoutMs = 5000, bool checkWinSWEnvironmentVariable = true)
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
{
this.Pidfile = pidfile;
this.StopTimeout = TimeSpan.FromMilliseconds(stopTimeoutMs);
this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable;
}
private static unsafe string? ReadEnvironmentVariable(IntPtr processHandle, string variable)
{
if (IntPtr.Size == sizeof(long))
{
return SearchEnvironmentVariable(
processHandle,
variable,
GetEnvironmentAddress64(processHandle).ToInt64(),
(handle, address, buffer, size) => NtReadVirtualMemory(handle, new IntPtr(address), buffer, new IntPtr(size)));
}
if (Is64BitOSWhen32BitProcess(Process.GetCurrentProcess().Handle) && !Is64BitOSWhen32BitProcess(processHandle))
{
return SearchEnvironmentVariable(
processHandle,
variable,
GetEnvironmentAddressWow64(processHandle),
(handle, address, buffer, size) => NtWow64ReadVirtualMemory64(handle, address, buffer, size));
}
return SearchEnvironmentVariable(
processHandle,
variable,
GetEnvironmentAddress32(processHandle).ToInt64(),
(handle, address, buffer, size) => NtReadVirtualMemory(handle, new IntPtr(address), buffer, new IntPtr(size)));
}
private static bool Is64BitOSWhen32BitProcess(IntPtr processHandle) =>
IsWow64Process(processHandle, out int isWow64) != 0 && isWow64 != 0;
private unsafe delegate int ReadMemoryCallback(IntPtr processHandle, long baseAddress, void* buffer, int bufferSize);
private static unsafe string? SearchEnvironmentVariable(IntPtr processHandle, string variable, long address, ReadMemoryCallback reader)
{
const int BaseBufferSize = 0x1000;
string variableKey = '\0' + variable + '=';
string buffer = new string('\0', BaseBufferSize + variableKey.Length);
fixed (char* bufferPtr = buffer)
{
long startAddress = address;
for (; ; )
{
int status = reader(processHandle, address, bufferPtr, buffer.Length * sizeof(char));
int index = buffer.IndexOf("\0\0");
if (index >= 0)
{
break;
}
address += BaseBufferSize * sizeof(char);
}
for (; ; )
{
int variableIndex = buffer.IndexOf(variableKey);
if (variableIndex >= 0)
{
int valueStartIndex = variableIndex + variableKey.Length;
int valueEndIndex = buffer.IndexOf('\0', valueStartIndex);
string value = buffer.Substring(valueStartIndex, valueEndIndex - valueStartIndex);
return value;
}
address -= BaseBufferSize * sizeof(char);
if (address < startAddress)
{
break;
}
int status = reader(processHandle, address, bufferPtr, buffer.Length * sizeof(char));
}
}
return null;
}
private static unsafe IntPtr GetEnvironmentAddress32(IntPtr processHandle)
{
_ = NtQueryInformationProcess(
processHandle,
PROCESSINFOCLASS.ProcessBasicInformation,
out PROCESS_BASIC_INFORMATION32 information,
sizeof(PROCESS_BASIC_INFORMATION32));
PEB32 peb;
_ = NtReadVirtualMemory(processHandle, new IntPtr(information.PebBaseAddress), &peb, new IntPtr(sizeof(PEB32)));
RTL_USER_PROCESS_PARAMETERS32 parameters;
_ = NtReadVirtualMemory(processHandle, new IntPtr(peb.ProcessParameters), ¶meters, new IntPtr(sizeof(RTL_USER_PROCESS_PARAMETERS32)));
return new IntPtr(parameters.Environment);
}
private static unsafe IntPtr GetEnvironmentAddress64(IntPtr processHandle)
{
_ = NtQueryInformationProcess(
processHandle,
PROCESSINFOCLASS.ProcessBasicInformation,
out PROCESS_BASIC_INFORMATION64 information,
sizeof(PROCESS_BASIC_INFORMATION64));
PEB64 peb;
_ = NtReadVirtualMemory(processHandle, new IntPtr(information.PebBaseAddress), &peb, new IntPtr(sizeof(PEB64)));
RTL_USER_PROCESS_PARAMETERS64 parameters;
_ = NtReadVirtualMemory(processHandle, new IntPtr(peb.ProcessParameters), ¶meters, new IntPtr(sizeof(RTL_USER_PROCESS_PARAMETERS64)));
return new IntPtr(parameters.Environment);
}
private static unsafe long GetEnvironmentAddressWow64(IntPtr processHandle)
{
_ = NtWow64QueryInformationProcess64(
processHandle,
PROCESSINFOCLASS.ProcessBasicInformation,
out PROCESS_BASIC_INFORMATION64 information,
sizeof(PROCESS_BASIC_INFORMATION64));
PEB64 peb;
_ = NtWow64ReadVirtualMemory64(processHandle, information.PebBaseAddress, &peb, sizeof(PEB64));
RTL_USER_PROCESS_PARAMETERS64 parameters;
_ = NtWow64ReadVirtualMemory64(processHandle, peb.ProcessParameters, ¶meters, sizeof(RTL_USER_PROCESS_PARAMETERS64));
return parameters.Environment;
}
public override void Configure(XmlServiceConfig config, XmlNode node)
{
// We expect the upper logic to process any errors
// TODO: a better parser API for types would be useful
this.Pidfile = XmlHelper.SingleElement(node, "pidfile", false)!;
this.StopTimeout = TimeSpan.FromMilliseconds(int.Parse(XmlHelper.SingleElement(node, "stopTimeout", false)!));
this.ServiceId = config.Id;
// TODO: Consider making it documented
var checkWinSWEnvironmentVariable = XmlHelper.SingleElement(node, "checkWinSWEnvironmentVariable", true);
this.CheckWinSWEnvironmentVariable = checkWinSWEnvironmentVariable is null ? true : bool.Parse(checkWinSWEnvironmentVariable);
}
///
/// This method checks if the PID file is stored on the disk and then terminates runaway processes if they exist.
///
/// Unused logger
public override void OnWrapperStarted()
{
// Read PID file from the disk
int pid;
if (File.Exists(this.Pidfile))
{
string pidstring;
try
{
pidstring = File.ReadAllText(this.Pidfile);
}
catch (Exception ex)
{
Logger.Error("Cannot read PID file from " + this.Pidfile, ex);
return;
}
try
{
pid = int.Parse(pidstring);
}
catch (FormatException e)
{
Logger.Error("Invalid PID file number in '" + this.Pidfile + "'. The runaway process won't be checked", e);
return;
}
}
else
{
Logger.Warn("The requested PID file '" + this.Pidfile + "' does not exist. The runaway process won't be checked");
return;
}
// Now check the process
Logger.DebugFormat("Checking the potentially runaway process with PID={0}", pid);
Process proc;
try
{
proc = Process.GetProcessById(pid);
}
catch (ArgumentException)
{
Logger.Debug("No runaway process with PID=" + pid + ". The process has been already stopped.");
return;
}
// Ensure the process references the service
string expectedEnvVarName = WinSWSystem.EnvVarNameServiceId;
string? affiliatedServiceId = ReadEnvironmentVariable(proc.Handle, expectedEnvVarName);
if (affiliatedServiceId is null && this.CheckWinSWEnvironmentVariable)
{
Logger.Warn("The process " + pid + " has no " + expectedEnvVarName + " environment variable defined. "
+ "The process has not been started by WinSW, hence it won't be terminated.");
return;
}
// Check the service ID value
if (this.CheckWinSWEnvironmentVariable && !this.ServiceId.Equals(affiliatedServiceId))
{
Logger.Warn("The process " + pid + " has been started by Windows service with ID='" + affiliatedServiceId + "'. "
+ "It is another service (current service id is '" + this.ServiceId + "'), hence the process won't be terminated.");
return;
}
// Kill the runaway process
StringBuilder bldr = new StringBuilder("Stopping the runaway process (pid=");
bldr.Append(pid);
bldr.Append(") and its children. Environment was ");
if (!this.CheckWinSWEnvironmentVariable)
{
bldr.Append("not ");
}
bldr.Append("checked, affiliated service ID: ");
bldr.Append(affiliatedServiceId ?? "undefined");
bldr.Append(", process to kill: ");
bldr.Append(proc);
Logger.Warn(bldr.ToString());
proc.StopTree(this.StopTimeout);
}
///
/// Records the started process PID for the future use in OnStart() after the restart.
///
public override void OnProcessStarted(Process process)
{
Logger.Info("Recording PID of the started process:" + process.Id + ". PID file destination is " + this.Pidfile);
try
{
File.WriteAllText(this.Pidfile, process.Id.ToString());
}
catch (Exception ex)
{
Logger.Error("Cannot update the PID file " + this.Pidfile, ex);
}
}
internal static class Native
{
private const string Kernel32 = "kernel32.dll";
private const string NTDll = "ntdll.dll";
[DllImport(Kernel32)]
internal static extern int IsWow64Process(IntPtr hProcess, out int Wow64Process);
[DllImport(NTDll)]
internal static extern int NtQueryInformationProcess(
IntPtr ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
out PROCESS_BASIC_INFORMATION32 ProcessInformation,
int ProcessInformationLength,
IntPtr ReturnLength = default);
[DllImport(NTDll)]
internal static extern int NtQueryInformationProcess(
IntPtr ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
out PROCESS_BASIC_INFORMATION64 ProcessInformation,
int ProcessInformationLength,
IntPtr ReturnLength = default);
[DllImport(NTDll)]
internal static extern unsafe int NtReadVirtualMemory(
IntPtr ProcessHandle,
IntPtr BaseAddress,
void* Buffer,
IntPtr BufferSize,
IntPtr NumberOfBytesRead = default);
[DllImport(NTDll)]
internal static extern int NtWow64QueryInformationProcess64(
IntPtr ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
out PROCESS_BASIC_INFORMATION64 ProcessInformation,
int ProcessInformationLength,
IntPtr ReturnLength = default);
[DllImport(NTDll)]
internal static extern unsafe int NtWow64ReadVirtualMemory64(
IntPtr ProcessHandle,
long BaseAddress,
void* Buffer,
long BufferSize,
long NumberOfBytesRead = default);
internal enum PROCESSINFOCLASS
{
ProcessBasicInformation = 0,
}
[StructLayout(LayoutKind.Sequential)]
internal readonly struct MEMORY_BASIC_INFORMATION
{
public readonly IntPtr BaseAddress;
private readonly IntPtr AllocationBase;
private readonly uint AllocationProtect;
public readonly IntPtr RegionSize;
private readonly uint State;
private readonly uint Protect;
private readonly uint Type;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct PROCESS_BASIC_INFORMATION32
{
private readonly int Reserved1;
public readonly int PebBaseAddress;
private fixed int Reserved2[2];
private readonly uint UniqueProcessId;
private readonly int Reserved3;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct PROCESS_BASIC_INFORMATION64
{
private readonly long Reserved1;
public readonly long PebBaseAddress;
private fixed long Reserved2[2];
private readonly ulong UniqueProcessId;
private readonly long Reserved3;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct PEB32
{
private fixed byte Reserved1[2];
private readonly byte BeingDebugged;
private fixed byte Reserved2[1];
private fixed int Reserved3[2];
private readonly int Ldr;
public readonly int ProcessParameters;
private fixed int Reserved4[3];
private readonly int AtlThunkSListPtr;
private readonly int Reserved5;
private readonly uint Reserved6;
private readonly int Reserved7;
private readonly uint Reserved8;
private readonly uint AtlThunkSListPtr32;
private fixed int Reserved9[45];
private fixed byte Reserved10[96];
private readonly int PostProcessInitRoutine;
private fixed byte Reserved11[128];
private fixed int Reserved12[1];
private readonly uint SessionId;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct PEB64
{
private fixed byte Reserved1[2];
private readonly byte BeingDebugged;
private fixed byte Reserved2[1];
private fixed long Reserved3[2];
private readonly long Ldr;
public readonly long ProcessParameters;
private fixed long Reserved4[3];
private readonly long AtlThunkSListPtr;
private readonly long Reserved5;
private readonly uint Reserved6;
private readonly long Reserved7;
private readonly uint Reserved8;
private readonly uint AtlThunkSListPtr32;
private fixed long Reserved9[45];
private fixed byte Reserved10[96];
private readonly long PostProcessInitRoutine;
private fixed byte Reserved11[128];
private fixed long Reserved12[1];
private readonly uint SessionId;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct RTL_USER_PROCESS_PARAMETERS32
{
private fixed byte Reserved1[16];
private fixed int Reserved2[10];
private readonly UNICODE_STRING32 ImagePathName;
private readonly UNICODE_STRING32 CommandLine;
internal readonly int Environment;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct RTL_USER_PROCESS_PARAMETERS64
{
private fixed byte Reserved1[16];
private fixed long Reserved2[10];
private readonly UNICODE_STRING64 ImagePathName;
private readonly UNICODE_STRING64 CommandLine;
internal readonly long Environment;
}
[StructLayout(LayoutKind.Sequential)]
internal readonly struct UNICODE_STRING32
{
private readonly ushort Length;
private readonly ushort MaximumLength;
private readonly int Buffer;
}
[StructLayout(LayoutKind.Sequential)]
internal readonly struct UNICODE_STRING64
{
private readonly ushort Length;
private readonly ushort MaximumLength;
private readonly long Buffer;
}
}
}
}