Having a 64 bit x86-64 CPU and a 2 TB drive is a curse, not a blessing, and watching the page file grow to 75 GB because your Perl process went out of control leaking memory at 100% CPU on 1 core and its going to be a few seconds or some dozens of seconds for you to click or type your way to killing that 64 bit Perl PID because it has claimed chattel ownership of your SSD (paging file).
Now, since Windows NT 3.0 is a XOpen Group certified IEEE POSIX 1991 operating system, where on earth is the POSIX 1991 ulimit command in Win2000, WinXP, Win7, and Win10 when you need it?
We know Windows OS has the POSIX ulimit/setrlimit() commands, and that is guaranteed by a very expensive piece of paper from 1991.
After extreme confusion, and reverse engineering and studying other SW on Github, here is a simple script of a Perl on Windows process asking the NT Kernel to enforce POSIX RAM/memory limit controls on itself (its own PID number) using resource limits/quotas/counter values that the Perl process itself decided is correct for itself.
For whatever reason, in Windows (NT Kernel) architectural design, Microsoft decided to organize the POSIX compliant per-process/per-user multi user time sharing/process resource limit API, to live underneath Windows's CRON API/"Scheduled Tasks" API which makes no sense to me.
This script proves you don't need to makes registry changes, or get UAC/Administrator privileges/C debugger privileges, for a process to turn on, on to itself, Windows kernel resource hard limit enforcement.
Make a 50 MB string with a 100 MB process memory limit:
C:\sources>perl w32jobobj.pl 50 100 Opened process PID 14332Success, Perl assembled the 52428812 bytes lon +g string. It will now be printed to STDOUT. 2 kernel handles were leaked in thi +s code. This is a good time to hit Ctrl-C instead of hitting Spacebar or Enter +.Press any key to continue . . .
Trying to make a 110 MB string inside a process limited to 100MB of RAM.
C:\sources>perl w32jobobj.pl 110 100 Out of memory! Opened process PID 7844 C:\sources>
Demo script of how to do it. It needs some polishing, and I am leaking the 2 handle objects I created. But it proves the feature is alive and well and fully functional. No docker or virtual machine nonsense needed to protect yourself from your own runaway memory leaks/infinite loops.
# this script leaks 2 kernel handles, fix that before putting into pro +duction use Win32::API; use warnings; use strict; BEGIN { sub PROCESS_TERMINATE { return 0x0001; } sub PROCESS_DUP_HANDLE {return 0x0040; } sub PROCESS_SET_QUOTA {return 0x0100; } sub FALSE {return 0; } sub INVALID_HANDLE_VALUE { return -1; } sub JobObjectExtendedLimitInformation { return 9;} sub JOB_OBJECT_QUERY { return 0x0004; } sub JOB_OBJECT_LIMIT_WORKINGSET { return 0x00000001; } sub JOB_OBJECT_LIMIT_PROCESS_TIME { return 0x00000002; } sub JOB_OBJECT_LIMIT_JOB_TIME { return 0x00000004; } sub JOB_OBJECT_LIMIT_ACTIVE_PROCESS { return 0x00000008; } sub JOB_OBJECT_LIMIT_AFFINITY { return 0x00000010; } sub JOB_OBJECT_LIMIT_PRIORITY_CLASS { return 0x00000020; } sub JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME { return 0x00000040; } sub JOB_OBJECT_LIMIT_SCHEDULING_CLASS { return 0x00000080; } sub JOB_OBJECT_LIMIT_PROCESS_MEMORY { return 0x00000100; } sub JOB_OBJECT_LIMIT_JOB_MEMORY { return 0x00000200; } sub JOB_OBJECT_LIMIT_JOB_MEMORY_HIGH { return JOB_OBJECT_LIMIT_JOB +_MEMORY; } sub JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION { return 0x0000040 +0; } sub JOB_OBJECT_LIMIT_BREAKAWAY_OK { return 0x00000800; } sub JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK { return 0x00001000; } sub JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE { return 0x00002000; } sub JOB_OBJECT_LIMIT_SUBSET_AFFINITY { return 0x00004000; } sub JOB_OBJECT_LIMIT_JOB_MEMORY_LOW { return 0x00008000; } sub JOB_OBJECT_LIMIT_JOB_READ_BYTES { return 0x00010000; } sub JOB_OBJECT_LIMIT_JOB_WRITE_BYTES { return 0x00020000; } sub JOB_OBJECT_LIMIT_RATE_CONTROL { return 0x00040000; } sub JOB_OBJECT_LIMIT_CPU_RATE_CONTROL{ return JOB_OBJECT_LIMIT_RAT +E_CONTROL; } sub JOB_OBJECT_LIMIT_IO_RATE_CONTROL { return 0x00080000; } sub JOB_OBJECT_LIMIT_NET_RATE_CONTROL { return 0x00100000; } sub JOB_OBJECT_LIMIT_VALID_FLAGS { return 0x0007ffff; } sub DUPLICATE_SAME_ACCESS { return 0x00000002; } } die "Usage: perl win32_memleak_rsrc_limits.pl len_in_MBs_build_tes +t_string RAM_quota_limit_in_MBs\n" if scalar(@ARGV) != 2 || !defined($ARGV[0]) || !defined($ARGV[1 +]); my ($howManyMBsString, $howManyMBsProcessRAMLimit) = ($ARGV[0]+0, +$ARGV[1]+0); Win32::API::Type->typedef( 'LARGE_INTEGER', 'unsigned __int64'); # I have no idea why the ::Type->typedef('ULONG_PTR', 64_bits is r +equired by # uname = Win32 strawberry-perl 5.32.1.1 #1 Sun Jan 24 12:17:47 +2021 i386 # archname = MSWin32-x86-multi-thread-64int # which has 32 bit ptrs, and 'ULONG_PTR' is a size_t/ptr sized typ +e # so its 4 bytes on i386, and 8 bytes on AMD64, but i386 strawberr +y or # the 32-bit edition of kernel32.dll want it as 64b inside a 32 bi +t process Win32::API::Type->typedef('ULONG_PTR', 'unsigned __int64'); # This is the correct typedef for ULONG_PTR, which is defined by M +S as an # unsigned pointer sized integer, aka "void *" aka "size_t". The +HANDLE # typedef works works in AMD64 Win64 Perl as expected: # Win32::API::Type->typedef('ULONG_PTR', 'HANDLE'); # but on Strawberry 5.32 i386 on Win7 AMD64, it fails with # $SetInformationJobObject->Call() GLR=87 at w32jobobj.pl li +ne 167. # so 'ULONG_PTR' == 'unsigned __int64' is needed on i386 for this +demo # script on i386 Perls but it makes no sense, there is a bug somew +here. # ~~~ bulk88 Win32::API::Type->typedef('JOBOBJECTINFOCLASS', 'int'); # C type e +num aka I32 my $OpenProcess = Win32::API::More->new( 'kernel32', 'HANDLE OpenProcess( DWORD dwDesiredAccess, BOOL bInheritHan +dle, DWORD dwProcessId)'); if (!$OpenProcess) { die 'OpenProcess GLR='.Win32::GetLastError(); + } my $dwPID = $$; my $hProcess = $OpenProcess->Call(PROCESS_SET_QUOTA|PROCESS_TERMIN +ATE|PROCESS_DUP_HANDLE, FALSE, $dwPID); if (! $hProcess || $hProcess == INVALID_HANDLE_VALUE) { printf("Could not open process PID %d, GLR=%d\n", $dwPID, Win3 +2::GetLastError()); exit 1; } else { printf("Opened process PID %d", $dwPID); } typedef Win32::API::Struct 'JOBOBJECT_BASIC_LIMIT_INFORMATION' => +qw( LARGE_INTEGER PerProcessUserTimeLimit; LARGE_INTEGER PerJobUserTimeLimit; DWORD LimitFlags; SIZE_T MinimumWorkingSetSize; SIZE_T MaximumWorkingSetSize; DWORD ActiveProcessLimit; ULONG_PTR Affinity; DWORD PriorityClass; DWORD SchedulingClass; ); typedef Win32::API::Struct 'IO_COUNTERS' => qw( ULONGLONG ReadOperationCount; ULONGLONG WriteOperationCount; ULONGLONG OtherOperationCount; ULONGLONG ReadTransferCount; ULONGLONG WriteTransferCount; ULONGLONG OtherTransferCount; ); Win32::API::Struct->typedef( 'JOBOBJECT_EXTENDED_LIMIT_INFORMATIO +N' => qw( JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; IO_COUNTERS IoInfo; SIZE_T ProcessMemoryLimit; SIZE_T JobMemoryLimit; SIZE_T PeakProcessMemoryUsed; SIZE_T PeakJobMemoryUsed; )); my $BasicLimitInformation = Win32::API::Struct->new('JOBOBJECT_BAS +IC_LIMIT_INFORMATION'); $BasicLimitInformation->{PerProcessUserTimeLimit} = 0; $BasicLimitInformation->{PerJobUserTimeLimit} = 0; $BasicLimitInformation->{LimitFlags} = 0; $BasicLimitInformation->{MinimumWorkingSetSize} = 0; $BasicLimitInformation->{MaximumWorkingSetSize} = 0; $BasicLimitInformation->{ActiveProcessLimit} = 0; $BasicLimitInformation->{Affinity} = 0; $BasicLimitInformation->{PriorityClass} = 0; $BasicLimitInformation->{SchedulingClass} = 0; my $IoInfo = Win32::API::Struct->new('IO_COUNTERS'); $IoInfo->{ReadOperationCount} = 0; $IoInfo->{WriteOperationCount} = 0; $IoInfo->{OtherOperationCount} = 0; $IoInfo->{ReadTransferCount} = 0; $IoInfo->{WriteTransferCount} = 0; $IoInfo->{OtherTransferCount} = 0; my $jelInfo = Win32::API::Struct->new('JOBOBJECT_EXTENDED_LIMIT_IN +FORMATION'); $jelInfo->{BasicLimitInformation} = $BasicLimitInformation; $jelInfo->{IoInfo} = $IoInfo; $jelInfo->{ProcessMemoryLimit} = 0; $jelInfo->{JobMemoryLimit} = 0; $jelInfo->{PeakProcessMemoryUsed} = 0; $jelInfo->{PeakJobMemoryUsed} = 0; #my $CreateJobObject = Win32::API::More->new( 'kernel32', # 'HANDLE CreateJobObjectA(LPSECURITY_ATTRIBUTES lpJobAttribute +s, LPCSTR lpName)'); my $CreateJobObject = Win32::API::More->new( 'kernel32', 'HANDLE CreateJobObjectA(HANDLE lpJobAttributes, HANDLE lpName +)'); if(!$CreateJobObject) { die 'CreateJobObject GLR=' . Win32::GetLas +tError();} my $hJob = $CreateJobObject->Call(0,0); # XXX $hJob HANDLE is LEAK +ED XXX my( $dwProcessLimit, $dwJobMemory, $dwProcessMemory, $bKillProcOnJobClose, $bBreakAwayOK, $bSilentBreakAwayOK); $dwJobMemory = $howManyMBsProcessRAMLimit * 1024 * 1024; if ($dwProcessLimit){ $jelInfo->{BasicLimitInformation}->{LimitFlags} |= JOB_OBJECT_ +LIMIT_ACTIVE_PROCESS; $jelInfo->{BasicLimitInformation}->{ActiveProcessLimit} = $dwP +rocessLimit; } if ($dwJobMemory){ $jelInfo->{BasicLimitInformation}->{LimitFlags} |= JOB_OBJECT_ +LIMIT_JOB_MEMORY; $jelInfo->{JobMemoryLimit} = $dwJobMemory; } if ($dwProcessMemory) { $jelInfo->{BasicLimitInformation}->{LimitFlags} |= JOB_OBJECT_ +LIMIT_PROCESS_MEMORY; $jelInfo->{ProcessMemoryLimit} = $dwProcessMemory; } if ($bKillProcOnJobClose) { $jelInfo->{BasicLimitInformation}->{LimitFlags} |= JOB_OBJECT_ +LIMIT_KILL_ON_JOB_CLOSE; } if ($bBreakAwayOK) { $jelInfo->{BasicLimitInformation}->{LimitFlags} |= JOB_OBJECT_ +LIMIT_BREAKAWAY_OK; } if ($bSilentBreakAwayOK) { $jelInfo->{BasicLimitInformation}->{LimitFlags} |= JOB_OBJECT_ +LIMIT_SILENT_BREAKAWAY_OK; } my $SetInformationJobObject = Win32::API::More->new( 'kernel32', 'BOOL SetInformationJobObject(HANDLE hJob,' .' JOBOBJECTINFOCLASS JobObjectInformationClass,' #.' LPVOID lpJobObjectInformation,' .' JOBOBJECT_EXTENDED_LIMIT_INFORMATION * lpJobObjectInfor +mation,' .' DWORD cbJobObjectInformationLength)'); if (!$SetInformationJobObject) { die 'SetInformationJobObject GLR=' . Win32::GetLastError(); } my $b = $SetInformationJobObject->Call($hJob, JobObjectExtendedLim +itInformation, $jelInfo, $jelInfo->sizeof()); if (!$b) { die '$SetInformationJobObject->Call() GLR=' . Win32::GetLastEr +ror(); } my $DuplicateHandle = Win32::API::More->new( 'kernel32', 'BOOL Du +plicateHandle( HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, LPHANDLE lpTargetHandle, DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions )'); if (!$DuplicateHandle) { die '$DuplicateHandle GLR=' . Win32::GetL +astError();} my $GetCurrentProcess = Win32::API::More->new( 'kernel32', 'HANDL +E GetCurrentProcess()'); if (!$GetCurrentProcess) { die '$GetCurrentProcess GLR=' . Win32:: +GetLastError();} my $hCurrentProcess = 0; # XXX this is LEAKED XXX $b = $DuplicateHandle->Call($GetCurrentProcess->Call(), $GetCurrentProcess->Call(), $GetCurrentProcess->Call(), $hCurrentProcess, 0, !!0, DUPLICATE_SAME_ACCESS); if (!$b) { die '$DuplicateHandle->Call() GLR=' . Win32::GetLastErr +or(); } my $AssignProcessToJobObject = Win32::API::More->new( 'kernel32', 'BOOL AssignProcessToJobObject( HANDLE hJob, HANDLE hProcess)' +); if (!$AssignProcessToJobObject) { die '$AssignProcessToJobObject G +LR=' . Win32::GetLastError(); } # passing AssignProcessToJobObject() the psuedo-handle/perma-const + from # GetCurrentProcess() makes it fail with 87 ERROR_INVALID_PARAMETE +R $b = $AssignProcessToJobObject->Call($hJob, $hCurrentProcess); if (!$b) { die '$AssignProcessToJobObject->Call() GLR=' . Win32::G +etLastError(); } my $str = ''; my $tstr; while (length ($str) < ($howManyMBsString * 1024 * 1024)) { $tstr = sprintf('_123456789abcd_%016x',length($str)); $str .= $tstr; #print $tstr; } print "Success, Perl assembled the ".length($str)." bytes long str +ing.\n" ."It will now be printed to STDOUT. " ."2 kernel handles were leaked in this code.\n" ."This is a good time to hit Ctrl-C instead of hitting Spaceba +r or Enter."; system 'pause'; print $str;
|
|---|