Skip to content

Commit

Permalink
Added deep-freeze experimental support
Browse files Browse the repository at this point in the history
  • Loading branch information
lowleveldesign committed Feb 6, 2025
1 parent 5dc5ae0 commit 923ab38
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 270 deletions.
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ This application allows you to set constraints on Windows processes. It uses [a
- [Set the priority class](#set-the-priority-class)
- [Set additional environment variables for a process](#set-additional-environment-variables-for-a-process)
- [Enable process privileges](#enable-process-privileges)
- [Deep-freeze processes \(experimental\)](#deep-freeze-processes-experimental)
- [Contributions](#contributions)
- [Links](#links)

<!-- /MarkdownTOC -->

## Installation
Installation
------------

You may download the latest version binaries from the [release page](https://github.com/lowleveldesign/process-governor/releases) or install it with [Chocolatey](https://chocolatey.org/) or [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/):

Expand All @@ -44,7 +46,8 @@ choco install procgov
winget install procgov
```

## Understanding procgov run modes
Understanding procgov run modes
-------------------------------

### The command-line application mode (default)

Expand All @@ -64,7 +67,8 @@ The ProcessGovernor service monitors starting processes and applies limits prede

To uninstall the service, use the **--uninstall** switch. The service will be removed when you remove the last saved configuration. If you want to remove all saved procgov data, along with the service, use the **--uninstall-all** switch.

## Applying limits on processes
Applying limits on processes
----------------------------

### Setting limits on a single process

Expand Down Expand Up @@ -102,7 +106,8 @@ Then we run procgov again with the new CPU limit - procgov will update the exist
procgov.exe --nowait -c 4 -p 1234
```

## Available process constraints
Available process constraints
-----------------------------

### Limit memory of a process

Expand Down Expand Up @@ -201,7 +206,8 @@ With the **--timeout** option you may define the maximum time (clock time) the p
The **--process-utime** and **--job-utime** options allow you to set a limit on the maximum user-mode execution time for a process (with the **--recursive** option also all its children) or a job. The latter case will make sense with the **--recursive** option as it will set a limit on the total user-mode execution time for the process and its children.
## Other options
Other options
-------------
### Set the priority class
Expand Down Expand Up @@ -234,15 +240,21 @@ procgov.exe --enable-privilege=SeDebugPrivilege --enable-privilege=SeShutdownPri
Keep in mind that in Windows, you can't add new privileges to the process token. You may only enable existing ones. You may check the available process privileges in Process Hacker or Process Explorer. Check the documentation for a given privilege to learn how to make it available for a given user (for example, you may need to update group policies).

## Contributions
### Deep-freeze processes (experimental)

You may use the --freeze and --thaw options to control execution of processes managed by procgov jobs. When launching a new process with the --freeze option, it will remain in a suspended state that occurs even earlier than when using the [CREATE_SUSPENDED flag](https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags). This means that even if there are missing imports, the process won't fail - instead, the main image will be loaded into memory, allowing for any necessary fixes to be made.
Contributions
-------------
Below you may find a list of people who contributed to this project. Thank you!
- @rowandh - an issue with the WS limit not being set
- @beevvy - an issue report and a fix for a bug with the environment variables
- @weidingerhp - an idea of environment variables for a process and CLR profiler setup
## Links
Links
-----
- **2013.11.21** - [Set process memory limit with Process Governor](http://lowleveldesign.wordpress.com/2013/11/21/set-process-memory-limit-with-process-governor)
- **2016.10.21** - [Releasing wtrace 1.0 and procgov 2.0](https://lowleveldesign.wordpress.com/2016/10/21/releasing-wtrace-1-0-and-procgov-2-0/)
Expand Down
39 changes: 34 additions & 5 deletions procgov-tests/Code/ProgramTests_CmdApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Win32;
using Windows.Win32.Foundation;

namespace ProcessGovernor.Tests.Code;

Expand All @@ -24,7 +26,8 @@ public static async Task CmdAppProcessStartFailure()
var exception = Assert.CatchAsync<Win32Exception>(async () =>
{
await Program.Execute(new RunAsCmdApp(new JobSettings(), new LaunchProcess(
["____wrong-executable.exe"], false), [], [], LaunchConfig.Quiet, ExitBehavior.WaitForJobCompletion), CancellationToken.None);
["____wrong-executable.exe"], false), [], [], LaunchConfig.Quiet, StartBehavior.None, ExitBehavior.WaitForJobCompletion),
CancellationToken.None);
});
Assert.That(exception?.NativeErrorCode, Is.EqualTo(2));
}
Expand All @@ -37,7 +40,7 @@ public static async Task CmdAppLaunchProcessExitCodeForwarding()
using var pipe = await StartMonitor(cts.Token);

var exitCode = await Program.Execute(new RunAsCmdApp(new JobSettings(), new LaunchProcess(
["cmd.exe", "/c", "exit 5"], false), [], [], LaunchConfig.Quiet, ExitBehavior.WaitForJobCompletion), cts.Token);
["cmd.exe", "/c", "exit 5"], false), [], [], LaunchConfig.Quiet, StartBehavior.None, ExitBehavior.WaitForJobCompletion), cts.Token);
Assert.That(exitCode, Is.EqualTo(5));
}

Expand All @@ -50,7 +53,8 @@ public static async Task CmdAppAttachProcessExitCodeForwarding()
var (cmd2, cmd2MainThreadHandle) = ProcessModule.CreateSuspendedProcess(["cmd.exe", "/c", "exit 6"], false, []);

var runTask = Task.Run(() => Program.Execute(new RunAsCmdApp(new JobSettings(), new AttachToProcess(
[cmd1.Id, cmd2.Id]), [], [], LaunchConfig.Quiet | LaunchConfig.NoMonitor, ExitBehavior.WaitForJobCompletion), cts.Token));
[cmd1.Id, cmd2.Id]), [], [], LaunchConfig.Quiet | LaunchConfig.NoMonitor, StartBehavior.None, ExitBehavior.WaitForJobCompletion),
cts.Token));

// give time to start for the job
await Task.Delay(1000);
Expand Down Expand Up @@ -99,7 +103,7 @@ public static async Task CmdAppAttachProcessAndUpdateJob()
JobSettings jobSettings = new(maxProcessMemory: 1024 * 1024 * 1024);

await Program.Execute(new RunAsCmdApp(jobSettings, new AttachToProcess([cmd.Id]),
[], [], LaunchConfig.Quiet, ExitBehavior.DontWaitForJobCompletion), cts.Token);
[], [], LaunchConfig.Quiet, StartBehavior.None, ExitBehavior.DontWaitForJobCompletion), cts.Token);

// let's give the IOCP some time to arrive
await Task.Delay(500);
Expand All @@ -108,7 +112,7 @@ public static async Task CmdAppAttachProcessAndUpdateJob()

jobSettings = new(maxProcessMemory: jobSettings.MaxProcessMemory, cpuMaxRate: 50);
await Program.Execute(new RunAsCmdApp(jobSettings, new AttachToProcess([cmd.Id]),
[], [], LaunchConfig.Quiet, ExitBehavior.DontWaitForJobCompletion), cts.Token);
[], [], LaunchConfig.Quiet, StartBehavior.None, ExitBehavior.DontWaitForJobCompletion), cts.Token);

Assert.That(await SharedApi.TryGetJobSettingsFromMonitor(cmd.Id, cts.Token), Is.EqualTo(jobSettings));

Expand Down Expand Up @@ -176,4 +180,29 @@ public static async Task CmdAppUpdateProcessEnvironmentVariablesWow64()

UpdateProcessEnvironmentVariables(proc);
}

[Test]
public static async Task CmdAppLaunchProcessFreezeAndThaw()
{
using var cts = new CancellationTokenSource(10000);

using var pipe = await StartMonitor(cts.Token);

var startTime = DateTime.Now;
var exitCode = await Program.Execute(new RunAsCmdApp(new JobSettings(), new LaunchProcess(
["cmd.exe", "/c", "exit 5"], false), [], [], LaunchConfig.Quiet,
StartBehavior.Freeze, ExitBehavior.DontWaitForJobCompletion), cts.Token);
Assert.That(exitCode, Is.EqualTo(NTSTATUS.STILL_ACTIVE.Value));

var cmd = Process.GetProcessesByName("cmd").FirstOrDefault(p => p.StartTime > startTime);
while (!cts.IsCancellationRequested && cmd == null)
{
cmd = Process.GetProcessesByName("cmd").FirstOrDefault(p => p.StartTime > startTime);
}
Debug.Assert(cmd is not null);

exitCode = await Program.Execute(new RunAsCmdApp(new JobSettings(), new AttachToProcess([(uint)cmd.Id]),
[], [], LaunchConfig.Quiet, StartBehavior.Thaw, ExitBehavior.WaitForJobCompletion), cts.Token);
Assert.That(exitCode, Is.EqualTo(0));
}
}
6 changes: 4 additions & 2 deletions procgov-tests/Code/ProgramTests_Monitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Windows.Win32.Foundation;
using Windows.Win32.System.Threading;

using static ProcessGovernor.Win32.Helpers;

namespace ProcessGovernor.Tests.Code;

public static partial class ProgramTests
Expand Down Expand Up @@ -325,7 +327,7 @@ static async Task RunAndMonitorProcessUnderJob(Win32Job job, JobSettings jobSett
// job settings check
Assert.That(await SharedApi.TryGetJobSettingsFromMonitor(pid, ct), Is.EqualTo(jobSettings));

NtApi.CheckWin32Result(PInvoke.ResumeThread(threadHandle));
CheckWin32Result(PInvoke.ResumeThread(threadHandle));

// notification listener should finish as the pipe gets diconnected
await notificationListenerTask;
Expand Down Expand Up @@ -363,7 +365,7 @@ async Task NotificationListener()

unsafe
{
NtApi.CheckWin32Result(PInvoke.CreateProcess(null, (char*)processArgsPtr, null, null,
CheckWin32Result(PInvoke.CreateProcess(null, (char*)processArgsPtr, null, null,
false, processCreationFlags, null, null, &si, &pi));
}

Expand Down
22 changes: 18 additions & 4 deletions procgov-tests/Code/ProgramTests_ParseArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,7 @@ public static void ParseArgsPriorityClass()
}

[Test]
public static void ParseEmptyArgs()
public static void ParseArgsEmptyArgs()
{
Assert.That(Program.ParseArgs(RealSystemInfo, []) is ShowSystemInfoAndExit);
}
Expand All @@ -737,18 +737,27 @@ public static void ParseArgsUnknownArgument()
}

[Test]
public static void ParseArgsExitBehaviorArguments()
public static void ParseArgsBehavioralArguments()
{
Assert.That(Program.ParseArgs(RealSystemInfo, ["-m=10M", "test.exe"]) is RunAsCmdApp
{
StartBehavior: StartBehavior.None,
ExitBehavior: ExitBehavior.WaitForJobCompletion,
LaunchConfig: LaunchConfig.Default
});

Assert.That(Program.ParseArgs(RealSystemInfo, ["-m=10M", "-q", "--nowait", "test.exe"]) is RunAsCmdApp
Assert.That(Program.ParseArgs(RealSystemInfo, ["-m=10M", "--freeze", "test.exe"]) is RunAsCmdApp
{
StartBehavior: StartBehavior.Freeze,
ExitBehavior: ExitBehavior.WaitForJobCompletion,
LaunchConfig: LaunchConfig.Default
});

Assert.That(Program.ParseArgs(RealSystemInfo, ["-m=10M", "-q", "--nowait", "--thaw", "test.exe"]) is RunAsCmdApp
{
StartBehavior: StartBehavior.Thaw,
ExitBehavior: ExitBehavior.DontWaitForJobCompletion,
LaunchConfig: LaunchConfig.Quiet
LaunchConfig: LaunchConfig.Quiet,
});


Expand All @@ -758,6 +767,11 @@ public static void ParseArgsExitBehaviorArguments()
LaunchConfig: LaunchConfig.NoGui | LaunchConfig.NoMonitor
});

Assert.That(Program.ParseArgs(RealSystemInfo, ["-m=10M", "--freeze", "--thaw", "test.exe"]) is ShowHelpAndExit
{
ErrorMessage: "--thaw and --freeze cannot be set at the same time"
});

Assert.That(Program.ParseArgs(RealSystemInfo, ["-m=10M", "--terminate-job-on-exit", "--nowait", "test.exe"]) is ShowHelpAndExit
{
ErrorMessage: "--terminate-job-on-exit and --nowait cannot be used together"
Expand Down
2 changes: 1 addition & 1 deletion procgov/AccountPrivilegeModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
using static ProcessGovernor.NtApi;
using static ProcessGovernor.Win32.Helpers;

namespace ProcessGovernor;

Expand Down
3 changes: 3 additions & 0 deletions procgov/ExecutionModes.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace ProcessGovernor;

enum StartBehavior { Freeze, Thaw, None };

enum ExitBehavior { WaitForJobCompletion, DontWaitForJobCompletion, TerminateJobOnExit };

[Flags]
Expand All @@ -23,6 +25,7 @@ record RunAsCmdApp(
Dictionary<string, string> Environment,
List<string> Privileges,
LaunchConfig LaunchConfig,
StartBehavior StartBehavior,
ExitBehavior ExitBehavior) : IExecutionMode;

record RunAsMonitor(TimeSpan MaxMonitorIdleTime, bool NoGui) : IExecutionMode;
Expand Down
1 change: 1 addition & 0 deletions procgov/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ SC_MANAGER_CREATE_SERVICE
SERVICE_ALL_ACCESS
SERVICE_QUERY_STATUS
SERVICE_START_TYPE
STILL_ACTIVE
WIN32_ERROR
WAIT_EVENT
PROCESS_CREATION_FLAGS
Expand Down
Loading

0 comments on commit 923ab38

Please sign in to comment.