Windows with C++

Windows Services Enhancements

Kenny Kerr

Contents

Delaying Auto-Start Services
Improving Shutdown Predictability
Failure Actions and Controlled Stop
Reducing Privileges
Protecting Service Data
Protecting Others with Restricted Tokens
Receiving Service Notifications
What's Next

The state of Windows® service development was largely unchanged since the dawn of services in Windows NT®, but Windows Vista® and Windows Server® 2008 bring some significant changes to services. Many of these features are focused on making it simpler to produce services that are more secure, but among the non-security-related service features are a few aimed at improving the overall responsiveness and reliability of Windows.

Delaying Auto-Start Services

Services can be configured to start only when they are specifically requested to do so via the StartService function. Such a service is called a demand-start service. Alternatively, they can be configured to start automatically during the operating system's startup process. These are known as auto-start services, and this mode makes sense for a lot of services whose functionality should be available at all times or that may be required by other services. Some auto-start services, however, don't absolutely need to be running at the time the user logs on to the computer because they may not be used immediately.

Windows Vista takes another step toward reducing the Windows startup time by providing a facility for auto-start services to be started only after Windows has finished its own startup process. Specifically, a delayed auto-start service is activated roughly two minutes after regular auto-start services. The Service Control Manager (SCM) also creates the main service thread with a lower priority, ensuring that any logged on user isn't notably impacted by delayed auto-start services. The priority is set back to normal after the service updates its state indicating that it is running. You can force a delayed auto-start service to start sooner by using the StartService function if it turns out that it hasn't started by the time your application needs to make use of it.

The following function shows how you can control this option (it should be called by your service's setup program):

bool ChangeDelayedAutoStart(
    SC_HANDLE service, bool delayed)
{
    ASSERT(0 != service);

    SERVICE_DELAYED_AUTO_START_INFO info = { delayed };

    return 0 != ::ChangeServiceConfig2(
        service, 
        SERVICE_CONFIG_DELAYED_AUTO_START_INFO, 
        &info);
}

The service handle identifies the service to configure. This handle can be obtained by calling the OpenService or CreateService functions. The ChangeServiceConfig2 function lets you change a number of more advanced service configuration options. In this case, I'm using the SERVICE_CONFIG_DELAYED_AUTO_START_INFO flag to tell the function to expect a SERVICE_DELAYED_AUTO_START_INFO pointer as its third parameter. ChangeServiceConfig2 returns zero if it fails, and you can get extended error information by calling the GetLastError function.

Improving Shutdown Predictability

Prior to Windows Vista, it wasn't always possible to ensure that services were stopped gracefully when the computer was shutting down. When the system notified the SCM that it was shutting down, it had only about 20 seconds to instruct all the running services to stop. If it took too long, the system would eventually simply terminate the SCM process. This could cause a number of problems including, ironically, increasing startup time since services that didn't shut down gracefully would often need to deal with inconsistent state when they were started up once again.

Windows Vista introduced a new pre-shutdown notification that services can ask to receive. If you are developing a service that must shutdown gracefully and cannot necessarily do so quickly, then you can ask the SCM for a pre-shutdown notification. The SCM will wait (potentially indefinitely) for all of the pre-shutdown services to stop before proceeding with its traditional services shutdown process. Although this is good for services, it's not necessarily that great for users who may want their computers to shut down promptly. For this reason, you should limit the use of this feature to very critical services and, preferably, only in server scenarios.

A service indicates its desire to receive a pre-shutdown notification by including the SERVICE_ACCEPT_PRESHUTDOWN flag in its service status. The service's handler function will then be notified via the SERVICE_CONTROL_PRESHUTDOWN control code. Although Windows is willing to wait indefinitely for a pre-shutdown service to stop, the service is required to remain responsive to queries from the SCM and update the service's state by incrementing checkpoint values. If the SCM decides that your service is unresponsive, it will eventually give up and the system will continue to shut down. Keep in mind that if your service handles the SERVICE_CONTROL_PRESHUTDOWN control code, it will not receive the traditional SERVICE_CONTROL_SHUTDOWN control code. In most cases, it is best to use a single shutdown procedure for all stop-related control codes.

A significant behavioral characteristic of the SCM is that it only ever communicates with one service at a time. This has certain implications that will be obvious in a moment if they aren't already: if two applications (or threads) call service control functions, like StartService or ControlService, then the SCM will queue them and service the requests one at a time. Another more problematic scenario is when Window is shutting down. This communication is performed via a named pipe established between the SCM and the service when the SCM calls the StartServiceCtrlDispatcher function. When the SCM sends a service a control code, it communicates over the named pipe to the StartServiceCtrlDispatcher function within the service. It forwards the request to the handler for the particular service (for shared process services), and the handler is given the opportunity to process the request before it returns. The problem is that there is nothing to prevent a handler function from blocking for a long time. The request sent by the SCM will eventually time out if the service does not respond within 30 seconds or so, but in the meantime no other services can be controlled.

As a service developer, you need to keep this in mind and never block your service's handler function. A well-behaving service should acknowledge the request, set a flag or kick off some worker thread to handle the request, and return immediately. This allows the SCM to continue communicating with other services while your service has time to handle the request. Figure 1 shows how you might implement the stop and shutdown control codes.

Figure 1 Handling Stop and Shutdown

DWORD WINAPI StopThread(PVOID)
{
  // perform any length shutdown operation 

  m_status.dwCurrentState = SERVICE_STOPPED;

  VERIFY(::SetServiceStatus(m_handle,
                            &m_status));
  return 0;
}

...

switch (control)
{
  case SERVICE_CONTROL_STOP:
  case SERVICE_CONTROL_SHUTDOWN:
  case SERVICE_CONTROL_PRESHUTDOWN:
  {
    m_status.dwCurrentState = SERVICE_STOP_PENDING;

    VERIFY(::SetServiceStatus(m_handle,
                              &m_status));

    CHandle thread(::CreateThread(0, // default security
                                  0, // default stack size
                                  StopThread,
                                  this, // context
                                  0, // no flags
                                  0)); // ignore thread id

    break;
  }

  ...
}

The control handler, which handles the SERVICE_CONTROL_STOP, SERVICE_CONTROL_SHUTDOWN, and SERVICE_CONTROL_PRESHUTDOWN control codes, sets the service state to SERVICE_STOP_PENDING, creates a worker thread to do the work, and returns without further ado. The StopThread function can block as long as necessary and then set the service's state to SERVICE_STOPPED, informing the SCM, which can subsequently let the StartServiceCtrlDispatcher function return and terminate the process (provided it was the last service for shared process services). Just remember that if your service is in a pending state, it needs to regularly update the service's state with an incrementing checkpoint value to assure the SCM that it is not hanging.

Failure Actions and Controlled Stop

Failure actions have long been available as a means for services that terminate abruptly to recover in some way. Prior to Windows Vista, failure actions were only executed if the service process terminated without first setting the service's state to SERVICE_STOPPED. Windows Vista allows the service to request that its failure actions should also be executed if the service's state is set to SERVICE_STOPPED, but the service's exit code, provided by the SERVICE_STATUS::dwWin32ExitCode member, is set to something other than ERROR_SUCCESS. This allows you to employ failure actions when your service fails while still letting you stop your service gracefully.

The following function shows how you can control this option to stop a service (and again, it should be called by your service's setup program):

bool ChangeFailureActionsOnNonCrashFailures(
  SC_HANDLE service,
  bool failureActionsOnNonCrashFailures)
{
  ASSERT(0 != service);

  SERVICE_FAILURE_ACTIONS_FLAG flag = { 
   failureActionsOnNonCrashFailures };

  return 0 != ::ChangeServiceConfig2(service,
    SERVICE_CONFIG_FAILURE_ACTIONS_FLAG,
    &flag);
}

For backward compatibility, the default for this option is false, thus preserving the behavior of older versions of Windows.

Reducing Privileges

Most administrators prefer services that run under built-in Windows service accounts. This avoids the headaches inherent in password management for service accounts. Of course, there are only so many built-in service accounts, and the privileges and permissions they afford might be more than your service needs. Although Windows Vista has not added any more service accounts—that would simply not be practical—it has introduced some powerful new configuration options that allow you to precisely lock down a service using restricted tokens.

The first such configuration option allows you to specify exactly which privileges your service requires. The SCM will ensure that the service's process token includes only the privileges you need. Figure 2 shows how you can control this option. It should be called by your service's setup program.

Figure 2 Setting Service Privileges

bool ChangeRequiredPrivileges(
    SC_HANDLE service,
    const CAtlArray<PCWSTR>& requiredPrivileges)
{
  ASSERT(0 != service);

  size_t bufferSize = 1;

  for (size_t index = 0; index < requiredPrivileges.GetCount(); ++index)
  {
    bufferSize += wcslen(requiredPrivileges[index]) + 1;
  }

  CAtlArray<WCHAR> buffer;

  if (!buffer.SetCount(bufferSize))
  {
    ::SetLastError(ERROR_OUTOFMEMORY);
    return false;
  }

  PWSTR position = &buffer[0];

  for (size_t index = 0; index < requiredPrivileges.GetCount(); ++index)
  {
    PCWSTR name = requiredPrivileges[index];
    const size_t nameSize = wcslen(name);

    wcscpy_s(position,
             nameSize + 1,
             name);

    position += nameSize + 1;
  }

  SERVICE_REQUIRED_PRIVILEGES_INFO info = { &buffer[0] };

  return 0 != ::ChangeServiceConfig2(
    service,
    SERVICE_CONFIG_REQUIRED_PRIVILEGES_INFO,
    &info);
}

This function may look a bit complicated, but that's only because the ChangeServiceConfig2 function requires that the list of privilege names be provided as a sequence of null-terminated strings, terminated by an empty string. Using the ChangeRequiredPrivileges function is painless, however:

CAtlArray<PCWSTR> requiredPrivileges;
requiredPrivileges.Add(SE_CHANGE_NOTIFY_NAME);
requiredPrivileges.Add(SE_IMPERSONATE_NAME);

if (!ChangeRequiredPrivileges(service, requiredPrivileges))
{
    // call GetLastError for more info
}

It's important to keep in mind that the privileges you specify actually need to be granted to the account your service is configured to use. If, for example, you configure your service to require the SE_TCB_NAME privilege, but your service is configured to run under the LocalService account, which does not have this privilege, the SCM will fail to start the service because it assumes that your service will not function correctly without the required privilege.

The other thing to keep in mind is related to shared-process services. Since all services that share a process also share the same security context, the SCM will include the union of the privileges required by all the services that can potentially share the process. This may impact which services you decide are candidates for sharing an address space and a security context.

Protecting Service Data

Although reducing the available privileges is useful in limiting what an exploited service can do (to a degree), it does not limit what data the service can access. For all their administrative benefits, the built-in service accounts have never been ideal since all services that use the same service account gain access to the same secured resources. Prior to Windows Vista, there was no way to say that Service A running as LocalService should have certain permissions on some resource while Service B should not. This made it harder to secure service-specific data from attack by other services.

Windows Vista fixes this by introducing service security identifiers (SIDs) that can be used in access control lists (ACLs) to set the permissions not only for the particular service account but also for the particular service. Every service on Windows Vista and Windows Server 2008 has a SID that identifies it, and that SID can be used in access control editors and indeed almost anywhere a security descriptor is specified. Figure 3 provides an example of a folder with an access control entry specifically granting service permissions to the folder.

Figure 3 Permissions Granted to Service A

Figure 3** Permissions Granted to Service A **

You can specify a service name in access control editors using the NT Service domain. In the example shown in Figure 3, the account name is NT Service\ServiceA. You can also use the LookupAccountName function if you need to use the service SID programmatically.

By default, the process token created for a service does not include the service SID. Changing this default option is a job for the ChangeServiceConfig2 function. The following function shows how you can control this option (yes, it should be called by your service's setup program):

bool ChangeSidType(SC_HANDLE service,
  DWORD sidType)
{
  ASSERT(0 != service);

  SERVICE_SID_INFO info = { sidType };

  return 0 != ::ChangeServiceConfig2(service,
    SERVICE_CONFIG_SERVICE_SID_INFO,
    &info);
}

The default value for the SID type is SERVICE_SID_TYPE_NONE. This is for application compatibility and produces a similar security context as you might get for a service on an older version of Windows. Specifying SERVICE_SID_TYPE_UNRESTRICTED as the SID type instructs the SCM to add the service SID to the service's process token, thus allowing the service to gain access to resources that you may have configured to allow access only to your specific service.

Protecting Others with Restricted Tokens

While protecting a service's resources with a service SID is good for keeping other users and services from accessing or at least modifying its data or settings, it does nothing to prevent the service from accessing other resources that the service does not need access to. Why does this matter? Well, if your service is compromised, it could be used as an attack vector.

Windows provides a third option for the SERVICE_CONFIG_SERVICE_SID_INFO flag using the SID type SERVICE_SID_TYPE_RESTRICTED. This SID type results in the service SID being included in the service's process token, but it also instructs the SCM to add a list of restricting SIDs to the token to further constrain the resources to which the service has access.

Restricting SIDs have traditionally been hard to apply generally as it can be very hard to come up with a constrained list of SIDs that will effectively reduce the reach of a token without also constraining it so much that it becomes useless. To solve this problem, Windows Vista introduces a new type of token known as a write-restricted token. It is basically a relaxation of the rules governing restricting SIDs. Instead of being used to simply constrain access beyond that which is allowed by the discretionary access control list (DACL), the write-restricted token is used only when evaluating write-access checks. This increases the applicability of restricting SIDs considerably.

With the SID type of SERVICE_SID_TYPE_RESTRICTED, the SCM will create a restricted token for the service process with the following restricting SIDs: the Everyone SID, the Logon SID, the service SID, and the write-restricted SID. This allows you sufficient freedom to grant write access to resources based on a specific service SID or any write-restricted token. More importantly, it severely constrains where the service can write to, thereby limiting the impact it might have in the event that it is compromised.

Receiving Service Notifications

Controlling and querying service status has traditionally been a challenging task. You can, for example, call the StartService function to request that a particular service is started. You are, however, not immediately notified that the service has started successfully since it may take some time to complete its startup process. Instead, you're left to wait for the service to start and can call the QueryServiceStatus function periodically to check on its progress. Needless to say, this is not an ideal solution. Among other reasons, polling leads to poor performance, as the calling thread is needlessly scheduled in order to check the status of the service. Fortunately, Windows Vista finally addresses this problem with the introduction of service status notifications.

The new NotifyServiceStatusChange function enables callers to receive notifications in the form of asynchronous procedure calls (APCs). Each call to NotifyServiceStatusChange will result in, at most, one APC being queued, so if you need to be informed of multiple changes, you will need to call NotifyServiceStatusChange again. If you are no longer interested in receiving a notification, all you have to do is simply close the service handle, and the system will dequeue any pending APC.

NotifyServiceStatusChange has the ability to provide notifications for either a particular service or for the SCM itself. Figure 4 shows how you can receive a notification when the Windows Search service has started.

Figure 4 Receiving APC Notifications

void CALLBACK ServiceNotifyCallback(void* param)
{
  SERVICE_NOTIFY* serviceNotify = static_cast<SERVICE_NOTIFY*>(param);

  // Check the notification results
}

int main()
{
  SC_HANDLE scm = ::OpenSCManager(
    0,                              // local SCM
    SERVICES_ACTIVE_DATABASE,
    SC_MANAGER_ENUMERATE_SERVICE);

  ASSERT(0 != scm); // Call GetLastError for more info.

  SC_HANDLE service = ::OpenService(
    scm,
    L"wsearch",
    SERVICE_QUERY_STATUS);

  ASSERT(0 != service); // Call GetLastError for more info.

  SERVICE_NOTIFY serviceNotify = 
    { SERVICE_NOTIFY_STATUS_CHANGE, 
      ServiceNotifyCallback };

  const DWORD result = ::NotifyServiceStatusChange(
    service,
    SERVICE_NOTIFY_RUNNING,
    &serviceNotify);

  ASSERT(ERROR_SUCCESS == result);

  ::SleepEx(INFINITE, TRUE); // Wait for the notification

  ::CloseServiceHandle(service);
  ::CloseServiceHandle(scm);
}

If you would like to receive SCM notifications, simply use the handle for the SCM instead of the service handle. Note that different notification flags are used. Only the SERVICE_NOTIFY_CREATED and SERVICE_NOTIFY_DELETED flags apply to the SCM. The rest, including SERVICE_NOTIFY_RUNNING, SERVICE_NOTIFY_STOPPED, and others all require a service handle. For a complete list of flags, take a look at the NotifyServiceStatusChange documentation.

The address of the same SERVICE_NOTIFY structure is passed to the notification callback function. You can then determine the specifics of the notification.

Finally, the use of APCs for notifications might be a bit intimidating if you're not familiar with the process, but it's fairly straightforward once you get used to it. For a detailed look at APCs, be sure to check out my Parallel Programming with C++ series of articles at weblogs.asp.net/kennykerr.

What's Next

That about covers the Windows services enhancements introduced with Windows Vista. I should mention that the new Windows Firewall API has also been enhanced with support for restricting a service's access to the network. The Windows Firewall API is, however, beyond what I can cover in this issue, but I hope to dedicate some space in a future column to it.

Send your questions and comments for Kenny to mmwincpp@microsoft.com.

Kenny Kerr is a software craftsman specializing in software development for Windows. He has a passion for writing and teaching developers about programming and software design. Reach Kenny at weblogs.asp.net/kennykerr.