Skip to content

ProcessRunner::stop() termination signal lost on Windows due to NamedEvent race #5199

@aleks-f

Description

@aleks-f

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

  1. Use ProcessRunner to launch a ServerApplication-based process without a PID file (no --pidfile argument)
  2. ProcessRunner::start() returns as soon as CreateProcess completes and _pid is set -- before the child finishes loading DLLs and static initialization
  3. Call ProcessRunner::stop() shortly after start() returns
  4. requestTermination(pid) fires before the child's Util DLL creates ServerApplication::_terminate
  5. The child reaches waitForTerminationRequest() but never receives the signal
  6. 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.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions