Summary
On Windows, ProcessRunner::stop() calls Process::requestTermination(pid) which creates a temporary NamedEvent, signals it via SetEvent(), and immediately destroys it. If the child process has not yet created its own NamedEvent handle (the static ServerApplication::_terminate member, initialized during DLL static initialization), the kernel event object is destroyed when the parent's handle closes (refcount drops to zero). When the child later creates its NamedEvent with the same name, it gets a new, unsignaled kernel event object, and the termination signal is lost. The child blocks forever in waitForTerminationRequest() until ProcessRunner times out and force-kills it.
Reproduction
- Use
ProcessRunner to launch a ServerApplication-based process without a PID file (no --pidfile argument)
ProcessRunner::start() returns as soon as CreateProcess completes and _pid is set -- before the child finishes loading DLLs and static initialization
- Call
ProcessRunner::stop() shortly after start() returns
requestTermination(pid) fires before the child's Util DLL creates ServerApplication::_terminate
- The child reaches
waitForTerminationRequest() but never receives the signal
ProcessRunner::stop() polls isRunning() for the full timeout (default 10s, can be up to 60s), then force-kills
The race is timing-dependent. With a PID file, start() waits for the child to write the file (ensuring full initialization), so the child's event handle exists before stop() can be called.
Root cause
In Foundation/src/Process_WIN32U.cpp:
void ProcessImpl::requestTerminationImpl(PIDImpl pid)
{
NamedEvent ev(terminationEventName(pid)); // CreateEventW
ev.set(); // SetEvent
} // ~NamedEvent calls CloseHandle -- kernel object destroyed if child hasn't opened it
The NamedEvent is a local variable. Its destructor closes the handle immediately after signaling. If the child's static _terminate member hasn't been constructed yet (DLL init hasn't run), no other handle to this kernel event exists, so the kernel destroys the object. The child later creates a fresh, unsignaled event with the same name.
Fix
In ProcessRunner::stop(), create the NamedEvent directly and keep it alive for the duration of the polling loop. This ensures the kernel event object persists until the child creates its own handle and receives the already-signaled event:
#if defined(POCO_OS_FAMILY_WINDOWS)
// Keep the NamedEvent alive so the kernel object persists until the child
// opens its own handle and receives the signal.
NamedEvent terminateEvent(Process::terminationEventName(pid));
terminateEvent.set();
#else
Process::requestTermination(pid);
#endif
while (Process::isRunning(pid))
{
// ... polling loop (unchanged) ...
}
// terminateEvent destroyed here, after child has exited
This works because:
- Auto-reset events stay signaled if no thread is waiting when
SetEvent is called
- When the child's
CreateEventW opens a handle to the existing (already-signaled) kernel object, the child's WaitForSingleObject returns immediately
- On Unix,
requestTermination() sends SIGINT -- a direct kernel operation with no equivalent race
Affected files
platform/Foundation/src/ProcessRunner.cpp - stop() method
platform/Foundation/src/ProcessRunner.cpp - add #include "Poco/NamedEvent.h" (Windows only)
Note
This issue does not affect Unix/Linux. On Unix, Process::requestTermination() sends SIGINT via kill(), which is a direct kernel operation delivered to the process regardless of initialization state.
Summary
On Windows,
ProcessRunner::stop()callsProcess::requestTermination(pid)which creates a temporaryNamedEvent, signals it viaSetEvent(), and immediately destroys it. If the child process has not yet created its ownNamedEventhandle (the staticServerApplication::_terminatemember, initialized during DLL static initialization), the kernel event object is destroyed when the parent's handle closes (refcount drops to zero). When the child later creates itsNamedEventwith the same name, it gets a new, unsignaled kernel event object, and the termination signal is lost. The child blocks forever inwaitForTerminationRequest()untilProcessRunnertimes out and force-kills it.Reproduction
ProcessRunnerto launch aServerApplication-based process without a PID file (no--pidfileargument)ProcessRunner::start()returns as soon asCreateProcesscompletes and_pidis set -- before the child finishes loading DLLs and static initializationProcessRunner::stop()shortly afterstart()returnsrequestTermination(pid)fires before the child's Util DLL createsServerApplication::_terminatewaitForTerminationRequest()but never receives the signalProcessRunner::stop()pollsisRunning()for the full timeout (default 10s, can be up to 60s), then force-killsThe race is timing-dependent. With a PID file,
start()waits for the child to write the file (ensuring full initialization), so the child's event handle exists beforestop()can be called.Root cause
In
Foundation/src/Process_WIN32U.cpp:The
NamedEventis a local variable. Its destructor closes the handle immediately after signaling. If the child's static_terminatemember hasn't been constructed yet (DLL init hasn't run), no other handle to this kernel event exists, so the kernel destroys the object. The child later creates a fresh, unsignaled event with the same name.Fix
In
ProcessRunner::stop(), create theNamedEventdirectly and keep it alive for the duration of the polling loop. This ensures the kernel event object persists until the child creates its own handle and receives the already-signaled event:This works because:
SetEventis calledCreateEventWopens a handle to the existing (already-signaled) kernel object, the child'sWaitForSingleObjectreturns immediatelyrequestTermination()sendsSIGINT-- a direct kernel operation with no equivalent raceAffected files
platform/Foundation/src/ProcessRunner.cpp-stop()methodplatform/Foundation/src/ProcessRunner.cpp- add#include "Poco/NamedEvent.h"(Windows only)Note
This issue does not affect Unix/Linux. On Unix,
Process::requestTermination()sendsSIGINTviakill(), which is a direct kernel operation delivered to the process regardless of initialization state.