From the documentation on EnterCriticalSection:
This function can raise
EXCEPTION_POSSIBLE_DEADLOCK, also known asSTATUS_POSSIBLE_DEADLOCK, if a wait operation on the critical section times out. The timeout interval is specified by the following registry value:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\CriticalSectionTimeout. Do not handle a possible deadlock exception; instead, debug the application.
I'm trying to reproduce this behavior. To this end, I created a small C# script using Vanara (.NET wrappers over WinAPI functions):
#r "nuget: Vanara.PInvoke.Kernel32, 4.1.6"
#r "nuget: System.CommandLine, 2.0.0-beta5.25306.1"
using System.CommandLine;
using System.Diagnostics;
using System.Threading;
using static Vanara.PInvoke.Kernel32;
var sleepMillisecondsOption = new Option<int>("--sleep-milliseconds", "-s") { DefaultValueFactory = (_) => 5000 };
var rootCommand = new RootCommand("Critical section contention") { sleepMillisecondsOption };
rootCommand.SetAction(parseResult => Runner(parseResult.GetValue(sleepMillisecondsOption)));
return new CommandLineConfiguration(rootCommand).Invoke(Args.ToArray());
void Runner(int sleepMilliseconds)
{
var startTime = Stopwatch.GetTimestamp();
InitializeCriticalSection(out var criticalSection);
WriteLine($"{Stopwatch.GetElapsedTime(startTime)}: Started");
var thread1 = new Thread(Worker);
var thread2 = new Thread(Worker);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
WriteLine($"{Stopwatch.GetElapsedTime(startTime)}: Finished");
void Worker()
{
var threadId = Thread.CurrentThread.ManagedThreadId;
WriteLine($"{Stopwatch.GetElapsedTime(startTime)}: {threadId}: Before EnterCriticalSection");
EnterCriticalSection(ref criticalSection);
WriteLine($"{Stopwatch.GetElapsedTime(startTime)}: {threadId}: After EnterCriticalSection");
Thread.Sleep(sleepMilliseconds);
WriteLine($"{Stopwatch.GetElapsedTime(startTime)}: {threadId}: Before LeaveCriticalSection");
LeaveCriticalSection(ref criticalSection);
WriteLine($"{Stopwatch.GetElapsedTime(startTime)}: {threadId}: After LeaveCriticalSection");
}
}
and compiled it into a self-contained executable on .NET 8:
dotnet script publish -c Release ContendCriticalSection.csx
When I run it on my machine (Windows 11, 26100.4349), it produces the expected output:
00:00:00.0001706: Started
00:00:00.0020008: 4: Before EnterCriticalSection
00:00:00.0021132: 5: Before EnterCriticalSection
00:00:00.0023623: 4: After EnterCriticalSection
00:00:05.0078115: 4: Before LeaveCriticalSection
00:00:05.0082580: 5: After EnterCriticalSection
00:00:05.0084024: 4: After LeaveCriticalSection
00:00:10.0122947: 5: Before LeaveCriticalSection
00:00:10.0126381: 5: After LeaveCriticalSection
00:00:10.0130023: Finished
Then I created a Windows instance on EC2 (Windows Server 2025 Datacenter, 26100.4061), changed the registry value to 2, and rebooted the instance:
PS C:\Users\Administrator> reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager" /v CriticalSectionTimeout
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager
CriticalSectionTimeout REG_DWORD 0x2
I expected the second thread to raise the exception after two seconds, but running the executable on the EC2 instance produces the same output: both threads take turns sleeping for 5 seconds, then return normally. The same happens when I run the executable with larger sleep settings: the critical section doesn't timeout.
I found an old thread on CodeGuru with the following suggestion:
That said, there are 2 ways to set timeouts for critical sections:
Systemwide. The registry value HKLM\SYSTEM\CurrentControlSet\Control\SessionManager\CriticalSectionTimeout
The unit for this value is in seconds. You must set a value <= 3600 for this to take effect. The default value is 30 days, which isn't actually used.
Process specific. Set an option in your executable image file. You need to create a load configuration for your exe. Take a look at the struct IMAGE_LOAD_CONFIG_DIRECTORY32 which is defined in winnt.h. There's a field there CriticalSectionDefaultTimeout. This field is in milliseconds. Again, it must be smaller than an hour to take effect.
The self-contained executable does seem to have a load configuration directory, so I used PETools to change the value of CriticalSectionDefaultTimeout, setting it to 2 as well:

$ cmp ContendCriticalSection.original.exe ContendCriticalSection.patched.exe -b
ContendCriticalSection.original.exe ContendCriticalSection.patched.exe differ: byte 6407525, line 16506 is 0 ^@ 2 ^B
The patched application doesn't seem to look at this value either, and its output is the same: the second thread waits for 5 seconds without throwing any exceptions.
When I run the patched application under WinDbg and set a larger sleep time, its behavior does seem to change compared to the unpatched one. Namely, the debugger emits more debugging messages:
RTL: Enter CriticalSection Timeout (0 secs) 0
RTL: Pid.Tid 00000000000088AC.0000000000000EC8, owner tid 000000000000A818 Critical Section 0000016D92CBCAD8 - ContentionCount == 1
RTL: Re-Waiting
RTL: Enter CriticalSection Timeout (0 secs) 1
RTL: Pid.Tid 00000000000088AC.0000000000000EC8, owner tid 000000000000A818 Critical Section 0000016D92CBCAD8 - ContentionCount == 1
RTL: Re-Waiting
RTL: Enter CriticalSection Timeout (0 secs) 2
RTL: Pid.Tid 00000000000088AC.0000000000000EC8, owner tid 000000000000A818 Critical Section 0000016D92CBCAD8 - ContentionCount == 1
RTL: Re-Waiting
Apparently, the second thread automatically tries to re-acquire the critical section in blocks: 3 times, then 9 times, then 90 times, then 900 times. There seems to be a 2-second delay between the blocks, and a ~10ms delay between the attempts.
On the EC2 instance (with the system-wide setting), the unpatched application does a similar thing, altough it doesn't do blocks and just retries to acquire the critical section every two seconds.
Still, EnterCriticalSection doesn't throw any exceptions in either of these enviroments, and the second thread still waits on the critical section.
How can I make EnterCriticalSection throw an exception on timeout?
ntoskrnl read
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager @ CriticalSectionTimeout
value but only on boot. so when you change this value, it take effect only after system reboot
when new process start, ntoskrnl write this value to PEB::CriticalSectionTimeout
when process itinialized in user mode, ntdll read PEB::CriticalSectionTimeout and write it to RtlpTimeout (global variable in ntdll, not exported)
LARGE_INTEGER RtlpTimeout;
RtlpTimeout.QuadPart = peb->CriticalSectionTimeout.QuadPart;
then ntdll look, are IMAGE_LOAD_CONFIG_DIRECTORY exist in your exe and if yes and CriticalSectionDefaultTimeout not 0, it used
PIMAGE_LOAD_CONFIG_DIRECTORY p;
if (ULONG64 CriticalSectionDefaultTimeout = p->CriticalSectionDefaultTimeout)
{
RtlpTimeout.QuadPart = CriticalSectionDefaultTimeout * -10000;
}
then exist next code
BOOLEAN RtlpTimeoutDisable;
if (RtlpTimeout.QuadPart < -60*60*10000000)
{
RtlpTimeoutDisable = true;
}
so if timeout more(or equal) than 3600 secods (1 hour) it disabled.
exist yet one global variable
BOOLEAN RtlpRaiseExceptionOnPossibleDeadlock;
ntdll first look under HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\ @ RaiseExceptionOnPossibleDeadlock (if not exist - by default FALSE)
and then under HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<your exe name>.exe @ RaiseExceptionOnPossibleDeadlock
when ntdll wait for critical section and wait timeout - it begin loop and print under loop
RTL: Enter CriticalSection Timeout (n secs) #i
RTL: Pid.Tid x.y, owner tid z Critical Section p - ContentionCount == 1
RTL: Re-Waiting
and only at #i >= 3 it call RtlpPossibleDeadlock
(so your timeout by fact multiple on 3)
this api call SendMessageToWERService

and then, if
RaiseExceptionOnPossibleDeadlock is true - STATUS_POSSIBLE_DEADLOCK is raised.
but RtlpPossibleDeadlock use SEH handler - it call UnhandledExceptionFilter
how this function work, depend from, are debugger is attached to process, and if not - RtlKnownExceptionFilter called from ntdll. this exported api have very simply implementation:
NTSTATUS WINAPI RtlKnownExceptionFilter(PEXCEPTION_POINTERS pep)
{
return STATUS_POSSIBLE_DEADLOCK == pep->ExceptionRecord->ExceptionCode
? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
}
if exception code is STATUS_POSSIBLE_DEADLOCK the EXCEPTION_CONTINUE_EXECUTION this mean if you use SEH handle around EnterCriticalSection - it never will be called, if debugger not attached - execution return to RtlpPossibleDeadlock
so if want catch exception - need use VEH handle, which called before SEH.
demo code:
NTSTATUS WINAPI CheckDeadLock(PEXCEPTION_POINTERS pep)
{
if (STATUS_POSSIBLE_DEADLOCK == pep->ExceptionRecord->ExceptionCode)
{
MessageBoxW(0, 0, L"STATUS_POSSIBLE_DEADLOCK", MB_ICONINFORMATION);
ExitProcess(STATUS_POSSIBLE_DEADLOCK);
//TerminateProcess(NtCurrentProcess(), STATUS_POSSIBLE_DEADLOCK);
}
return EXCEPTION_CONTINUE_SEARCH;
}
ULONG WINAPI dfg(CRITICAL_SECTION* pcs)
{
EnterCriticalSection(pcs);
LeaveCriticalSection(pcs);
return 0;
}
if (PVOID pv = AddVectoredExceptionHandler(TRUE, CheckDeadLock))
{
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
if (HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)dfg, &cs, 0, 0))
{
WaitForSingleObject(hThread, INFINITE);
NtClose(hThread);
}
RemoveVectoredExceptionHandler(pv);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With