Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is EnterCriticalSection not timing out?

From the documentation on EnterCriticalSection:

This function can raise EXCEPTION_POSSIBLE_DEADLOCK, also known as STATUS_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:

  1. 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.

  2. 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:

PETools

$ 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?

like image 338
Quassnoi Avatar asked Feb 26 '26 04:02

Quassnoi


1 Answers

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
enter image description here
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);
    }
like image 175
RbMm Avatar answered Feb 27 '26 16:02

RbMm



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!