PRB: ServiceMain thread May Get Terminated in DllMain() when a Service Process Exits Normally

ID: Q201349


The information in this article applies to:


SYMPTOMS

If a service implicitly loads a DLL, the thread created by the Service Control Manager (SCM) to execute ServiceMain() can be terminated while it is still executing DLL_THREAD_DETACH code in the library's DllMain() function. This premature termination can cause unpredictable results if the thread was supposed to perform critical cleanup code in DllMain().


CAUSE

When the primary thread of a service calls StartServiceCtrlDispatcher(), the SCM creates a thread to do the actual work in ServiceMain(). Therefore, all services have at least two threads: a primary thread and a ServiceMain() thread.

When all of the services in a process have stopped; that is, each one has set its status to SERVICE_STOPPED, the primary thread returns from StartServiceCtrlDispatcher(). If this thread exits immediately, the ServiceMain() thread may still be performing cleanup work in the DLL_THREAD_DETACH code of an implicitly loaded DLL. If the primary thread calls ExitProcess() on its way out (which is the behavior of applications using the Microsoft C-Runtime Library), the ServiceMain() thread will be terminated without warning.


RESOLUTION

To work around this problem, the primary thread can wait on the ServiceMain() thread to complete its cleanup work by waiting on a handle to the ServiceMain() thread after returning from StartServiceCtrlDispatcher(). This approach is demonstrated in the code sample at the end of this article.


STATUS

This behavior is by design.


MORE INFORMATION

By default, the primary thread does not have a handle to the ServiceMain() thread, since the ServiceMain() thread is created indirectly by the SCM. To make this handle available to the primary thread, the ServiceMain() thread duplicates its own handle when it begins executing. The duplicate handle is stored in a global variable. After the primary thread returns from StartServiceCtrlDispatcher(), it waits on the global thread handle using WaitForSingleObject(). This allows the ServiceMain() thread to complete any cleanup work it needs to perform before the primary thread exits. The primary thread then closes the duplicated ServiceMain() thread handle and continues executing its natural conclusion.

The following bare-bones service demonstrates this approach:


   //**********************************************************************
   // 
   //  Bare Bones Service
   // 
   //  This program is a mere skeleton for a Windows NT/Windows 2000
   //  service. In its current state, it provides virtually no useful
   //  functionality whatsoever. Do with it as you will, but keep in
   //  mind the following...
   // 
   //  THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
   //  ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
   //  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
   //  PARTICULAR PURPOSE.
   // 
   //  Copyright (C) 1999 Microsoft Corporation. All rights reserved.
   //  Author: Jonathan Russ (jruss)
   // 
   //********************************************************************** 

   #define _WIN32_WINNT    0x0400
   #define SERVICE_NAME    TEXT("BareBonesService")

   #include <windows.h>

   // declare global handle for the ServiceMain() thread
   HANDLE g_hServiceMainThread = NULL;

   // declare global handle for the completion port that
   // will be used to pass service control requests to the 
   // ServiceMain() thread
   HANDLE g_hCompPort = NULL;


   void WINAPI ServiceMain( DWORD dwArgc, LPTSTR *lpszArgv );
   void WINAPI ServiceHandler( DWORD dwControlCode );


   void main( void ) {
   
      SERVICE_TABLE_ENTRY ste[] = {
         { SERVICE_NAME, ServiceMain },
         { NULL,     NULL }
      };

      StartServiceCtrlDispatcher( ste );

      // wait for the ServiceMain() thread to exit
      if ( g_hServiceMainThread )
         WaitForSingleObject( g_hServiceMainThread, INFINITE );

      // release global thread handle
      CloseHandle( g_hServiceMainThread );
      return;
   }


   void WINAPI ServiceMain( DWORD dwArgc, LPTSTR *lpszArgv ) {

      SERVICE_STATUS          ss;
      SERVICE_STATUS_HANDLE   hService;
      DWORD                   dwBytesTransferred;
      DWORD                   dwControlCode;
      OVERLAPPED *            po;
 
      // set static members of service status structure
      ss.dwServiceType = SERVICE_WIN32_OWN_PROCESS; // one service
      ss.dwControlsAccepted = SERVICE_ACCEPT_PAUSE_CONTINUE 
            | SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;

      // set error members of service status structure
      ss.dwWin32ExitCode = NO_ERROR;
      ss.dwServiceSpecificExitCode = NO_ERROR;
   
      // create a duplicate of this thread's handle so the
      // primary thread can wait on it before exiting
      if ( !DuplicateHandle( GetCurrentProcess(), GetCurrentThread(),
            GetCurrentProcess(), &g_hServiceMainThread, 0, 0, 
            DUPLICATE_SAME_ACCESS ) ) {

         g_hServiceMainThread = NULL;
         goto cleanup;
      }

      // create the completion port
      g_hCompPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE,
            NULL, 0, 0 );

      if ( !g_hCompPort )
         goto cleanup;

      // register the ServiceHandler() function to receive control requests
      hService = RegisterServiceCtrlHandler( SERVICE_NAME,
            ServiceHandler );

      // set service status to start pending
      ss.dwCurrentState = SERVICE_START_PENDING;
      ss.dwCheckPoint = 1; 
      ss.dwWaitHint = 500;
      SetServiceStatus( hService, &ss );

      // TODO: add additional initialization code here...

      // set service status to running
      ss.dwCurrentState = SERVICE_RUNNING;
      ss.dwCheckPoint = ss.dwWaitHint = 0;   
      SetServiceStatus( hService, &ss );

      do {

         GetQueuedCompletionStatus( g_hCompPort, &dwBytesTransferred, 
               &dwControlCode, &po, INFINITE );

         switch (dwControlCode) {

         case SERVICE_CONTROL_PAUSE:
            if ( ss.dwCurrentState = SERVICE_RUNNING ) {

               // set service status to pause pending
               ss.dwCurrentState = SERVICE_PAUSE_PENDING;
               ss.dwCheckPoint = 1; 
               ss.dwWaitHint = 500;
               SetServiceStatus( hService, &ss );

               // TODO: pause all execution here...

               // set service status to paused
               ss.dwCurrentState = SERVICE_PAUSED;
               ss.dwCheckPoint = ss.dwWaitHint = 0;   
               SetServiceStatus( hService, &ss );
            }
            break;

         case SERVICE_CONTROL_CONTINUE:
            if ( ss.dwCurrentState = SERVICE_PAUSED ) {
               // set service status to continue pending
               ss.dwCurrentState = SERVICE_CONTINUE_PENDING;
               ss.dwCheckPoint = 1; 
               ss.dwWaitHint = 500;
               SetServiceStatus( hService, &ss );

               // TODO: continue execution here...

               // set service status to running
               ss.dwCurrentState = SERVICE_RUNNING;
               ss.dwCheckPoint = ss.dwWaitHint = 0;   
               SetServiceStatus( hService, &ss );
            }
            break;

         case SERVICE_CONTROL_STOP:
         case SERVICE_CONTROL_SHUTDOWN:
            if ( ss.dwCurrentState = SERVICE_RUNNING ) {

               // set service status to stop pending
               ss.dwCurrentState = SERVICE_STOP_PENDING;
               ss.dwCheckPoint = 1; 
               ss.dwWaitHint = 500;
               SetServiceStatus( hService, &ss );
            }
            break;

         case SERVICE_CONTROL_INTERROGATE:
            ss.dwCheckPoint = ss.dwWaitHint = 0;   
            SetServiceStatus( hService, &ss );
            break;
         }

      } while ( ss.dwCurrentState != SERVICE_STOP_PENDING );

   cleanup:
      // TODO: perform cleanup work here...

      // set service status to stopped
      ss.dwCurrentState = SERVICE_STOPPED;
      ss.dwCheckPoint = ss.dwWaitHint = 0;   
      SetServiceStatus( hService, &ss );

      // safely perform final cleanup work while confidently
      // knowing the primary thread will not terminate this thread...
      CloseHandle( g_hCompPort );

   }


   void WINAPI ServiceHandler( DWORD dwControlCode ) {

      // post status to a completion port to avoid tying up
      // the primary thread
      PostQueuedCompletionStatus( g_hCompPort, 0, dwControlCode, NULL );

   } 

Additional query words: kbDSupport


Keywords          : kbAPI kbDLL kbKernBase kbSCM kbService 
Version           : winnt:4.0
Platform          : winnt 
Issue type        : kbprb 

Last Reviewed: June 24, 1999