|
|
/*++
Copyright (c) 1992 Microsoft Corporation
Module Name:
crash.cxx
Abstract:
Contains code concerned with recovery actions that are taken when a service crashes. This file contains the following functions: ScQueueRecoveryAction CCrashRecord::IncrementCount CRestartContext::Perform CRebootMessageContext::Perform CRebootContext::Perform CRunCommandContext::Perform
Author:
Anirudh Sahni (anirudhs) 02-Dec-1996
Environment:
User Mode -Win32
Revision History:
22-Oct-1998 jschwart Convert SCM to use NT thread pool APIs
02-Dec-1996 AnirudhS Created
--*/
//
// INCLUDES
//
#include "precomp.hxx"
#include <lmcons.h> // needed for other lm headers
#include <lmerr.h> // NERR_Success
#include <lmshare.h> // NetSessionEnum
#include <lmmsg.h> // NetMessageBufferSend
#include <lmapibuf.h> // NetApiBufferFree
#include <valid.h> // ACTION_TYPE_INVALID
#include <svcslib.h> // CWorkItemContext
#include "smartp.h" // CHeapPtr
#include "scconfig.h" // ScReadFailureActions, etc.
#include "depend.h" // ScStartServiceAndDependencies
#include "account.h" // ScLogonService
#include "scseclib.h" // ScCreateAndSetSD
#include "start.h" // ScAllowInteractiveServices, ScInitStartupInfo
#include "resource.h" // IDS_SC_ACTION_BASE
//
// Defines and Typedefs
//
#define FILETIMES_PER_SEC ((__int64) 10000000) // (1 second)/(100 ns)
#define LENGTH(array) (sizeof(array)/sizeof((array)[0]))
//
// Globals
//
//
// Local Function Prototypes
//
VOID ScLogRecoveryFailure( IN SC_ACTION_TYPE ActionType, IN LPCWSTR ServiceDisplayName, IN DWORD Error );
inline LPWSTR LocalDup( LPCWSTR String ) { LPWSTR Dup = (LPWSTR) LocalAlloc(0, WCSSIZE(String)); if (Dup != NULL) { wcscpy(Dup, String); } return Dup; }
//
// Callback context for restarting a service
//
class CRestartContext : public CWorkItemContext { DECLARE_CWorkItemContext public: CRestartContext( IN LPSERVICE_RECORD ServiceRecord ) : _ServiceRecord(ServiceRecord) { ServiceRecord->UseCount++; SC_LOG2(USECOUNT, "CRestartContext: %ws increment " "USECOUNT=%lu\n", ServiceRecord->ServiceName, ServiceRecord->UseCount); }
~CRestartContext() { CServiceRecordExclusiveLock RLock; ScDecrementUseCountAndDelete(_ServiceRecord); }
private: LPSERVICE_RECORD _ServiceRecord; };
//
// Callback context for broadcasting a reboot message
//
class CRebootMessageContext : public CWorkItemContext { DECLARE_CWorkItemContext public: CRebootMessageContext( IN LPWSTR RebootMessage, IN DWORD Delay, IN LPWSTR DisplayName ) : _RebootMessage(RebootMessage), _Delay(Delay), _DisplayName(LocalDup(DisplayName)) { }
~CRebootMessageContext() { LocalFree(_RebootMessage); }
private: LPWSTR _RebootMessage; DWORD _Delay; LPWSTR _DisplayName; };
//
// Callback context for a reboot
// (The service name is used only for logging)
//
class CRebootContext : public CWorkItemContext { DECLARE_CWorkItemContext public: CRebootContext( IN DWORD ActionDelay, IN LPWSTR DisplayName ) : _Delay(ActionDelay), _DisplayName(DisplayName) { }
~CRebootContext() { LocalFree(_DisplayName); }
private: DWORD _Delay; LPWSTR _DisplayName; };
//
// Callback context for running a recovery command
//
class CRunCommandContext : public CWorkItemContext { DECLARE_CWorkItemContext public: CRunCommandContext( IN LPSERVICE_RECORD ServiceRecord, IN LPWSTR FailureCommand ) : _ServiceRecord(ServiceRecord), _FailureCommand(FailureCommand) { //
// The service record is used to get the
// account name to run the command in.
//
ServiceRecord->UseCount++; SC_LOG2(USECOUNT, "CRunCommandContext: %ws increment " "USECOUNT=%lu\n", ServiceRecord->ServiceName, ServiceRecord->UseCount); }
~CRunCommandContext() { LocalFree(_FailureCommand); CServiceRecordExclusiveLock RLock; ScDecrementUseCountAndDelete(_ServiceRecord); }
private: LPSERVICE_RECORD _ServiceRecord; LPWSTR _FailureCommand; };
/****************************************************************************/
VOID ScQueueRecoveryAction( IN LPSERVICE_RECORD ServiceRecord )
/*++
Routine Description:
Arguments:
Return Value:
none.
--*/ { SC_ACTION_TYPE ActionType = SC_ACTION_NONE; DWORD ActionDelay = 0; DWORD FailNum = 1; NTSTATUS ntStatus;
//
// See if there is any recovery action configured for this service.
//
HKEY Key = NULL; { DWORD ResetPeriod = INFINITE; LPSERVICE_FAILURE_ACTIONS_WOW64 psfa = NULL;
DWORD Error = ScOpenServiceConfigKey( ServiceRecord->ServiceName, KEY_READ, FALSE, // don't create if missing
&Key );
if (Error == ERROR_SUCCESS) { Error = ScReadFailureActions(Key, &psfa); }
if (Error != ERROR_SUCCESS) { SC_LOG(ERROR, "Couldn't read service's failure actions, %lu\n", Error); } else if (psfa != NULL && psfa->cActions > 0) { ResetPeriod = psfa->dwResetPeriod; }
//
// Allocate a crash record for the service.
// Increment the service's crash count, subject to the reset period
// we just read from the registry (INFINITE if we read none).
//
if (ServiceRecord->CrashRecord == NULL) { ServiceRecord->CrashRecord = new CCrashRecord; }
if (ServiceRecord->CrashRecord == NULL) { SC_LOG0(ERROR, "Couldn't allocate service's crash record\n"); //
// NOTE: We still continue, taking the failure count to be 1.
// (The crash record is used only in the "else" clause.)
//
} else { FailNum = ServiceRecord->CrashRecord->IncrementCount(ResetPeriod); }
//
// Figure out which recovery action we're going to take.
//
if (psfa != NULL && psfa->cActions > 0) { SC_ACTION * lpsaActions = (SC_ACTION *) ((LPBYTE) psfa + psfa->dwsaActionsOffset); DWORD i = min(FailNum, psfa->cActions);
ActionType = lpsaActions[i - 1].Type; ActionDelay = lpsaActions[i - 1].Delay;
if (ACTION_TYPE_INVALID(ActionType)) { SC_LOG(ERROR, "Service has invalid action type %lu\n", ActionType); ActionType = SC_ACTION_NONE; } }
LocalFree(psfa); }
//
// Log an event about this service failing, and about the proposed
// recovery action.
//
if (ActionType != SC_ACTION_NONE) { WCHAR wszActionString[50]; if (!LoadString(GetModuleHandle(NULL), IDS_SC_ACTION_BASE + ActionType, wszActionString, LENGTH(wszActionString))) { SC_LOG(ERROR, "LoadString failed %lu\n", GetLastError()); wszActionString[0] = L'\0'; }
SC_LOG2(ERROR, "The following recovery action will be taken in %d ms: %ws.\n", ActionDelay, wszActionString);
ScLogEvent(NEVENT_SERVICE_CRASH, ServiceRecord->DisplayName, FailNum, ActionDelay, ActionType, wszActionString); } else { ScLogEvent(NEVENT_SERVICE_CRASH_NO_ACTION, ServiceRecord->DisplayName, FailNum); }
//
// Queue a work item that will actually carry out the action after the
// delay has elapsed.
//
switch (ActionType) { case SC_ACTION_NONE: break;
case SC_ACTION_RESTART: { CRestartContext * pCtx = new CRestartContext(ServiceRecord); if (pCtx == NULL) { SC_LOG0(ERROR, "Couldn't allocate restart context\n"); break; }
ntStatus = pCtx->AddDelayedWorkItem(ActionDelay, WT_EXECUTEONLYONCE);
if (!NT_SUCCESS(ntStatus)) { SC_LOG(ERROR, "Couldn't add restart work item 0x%x\n", ntStatus); delete pCtx; }
break; }
case SC_ACTION_REBOOT: { //
// Get the reboot message for the service, if any
//
LPWSTR RebootMessage = NULL; ScReadRebootMessage(Key, &RebootMessage); if (RebootMessage != NULL) { //
// Broadcast the message to all users. Do this in a separate
// thread so that we can release our exclusive lock on the
// service database quickly.
//
CRebootMessageContext * pCtx = new CRebootMessageContext( RebootMessage, ActionDelay, ServiceRecord->DisplayName ); if (pCtx == NULL) { SC_LOG0(ERROR, "Couldn't allocate restart context\n"); LocalFree(RebootMessage); break; }
ntStatus = pCtx->AddWorkItem(WT_EXECUTEONLYONCE);
if (!NT_SUCCESS(ntStatus)) { SC_LOG(ERROR, "Couldn't add restart work item 0x%x\n", ntStatus); delete pCtx; } } else { //
// Queue a work item to perform the reboot after the delay has
// elapsed.
// (CODEWORK Share this code with CRebootMessageContext::Perform)
//
LPWSTR DisplayNameCopy = LocalDup(ServiceRecord->DisplayName); CRebootContext * pCtx = new CRebootContext( ActionDelay, DisplayNameCopy ); if (pCtx == NULL) { SC_LOG0(ERROR, "Couldn't allocate reboot context\n"); LocalFree(DisplayNameCopy); } else { ntStatus = pCtx->AddWorkItem(WT_EXECUTEONLYONCE);
if (!NT_SUCCESS(ntStatus)) { SC_LOG(ERROR, "Couldn't add reboot work item 0x%x\n", ntStatus); delete pCtx; } } } }
break;
case SC_ACTION_RUN_COMMAND: { //
// Get the failure command for the service, if any
//
CHeapPtr<LPWSTR> FailureCommand; ScReadFailureCommand(Key, &FailureCommand); if (FailureCommand == NULL) { SC_LOG0(ERROR, "Asked to run a failure command, but found " "none for this service\n"); ScLogRecoveryFailure( SC_ACTION_RUN_COMMAND, ServiceRecord->DisplayName, ERROR_NO_RECOVERY_PROGRAM ); break; }
//
// Replace %1% in the failure command with the failure count.
// (FormatMessage is *useless* for this purpose because it AV's
// if the failure command contains a %2, %3 etc.!)
//
UNICODE_STRING Formatted; { UNICODE_STRING Unformatted; RtlInitUnicodeString(&Unformatted, FailureCommand);
Formatted.Length = 0; Formatted.MaximumLength = Unformatted.MaximumLength + 200; Formatted.Buffer = (LPWSTR) LocalAlloc(0, Formatted.MaximumLength); if (Formatted.Buffer == NULL) { SC_LOG(ERROR, "Couldn't allocate formatted string, %lu\n", GetLastError()); break; }
WCHAR Environment[30]; wsprintf(Environment, L"1=%lu%c", FailNum, L'\0');
NTSTATUS ntstatus = RtlExpandEnvironmentStrings_U( Environment, &Unformatted, &Formatted, NULL);
if (!NT_SUCCESS(ntstatus)) { SC_LOG(ERROR, "RtlExpandEnvironmentStrings_U failed %#lx\n", ntstatus); wcscpy(Formatted.Buffer, FailureCommand); } }
CRunCommandContext * pCtx = new CRunCommandContext(ServiceRecord, Formatted.Buffer); if (pCtx == NULL) { SC_LOG0(ERROR, "Couldn't allocate RunCommand context\n"); LocalFree(Formatted.Buffer); break; }
ntStatus = pCtx->AddDelayedWorkItem(ActionDelay, WT_EXECUTEONLYONCE);
if (!NT_SUCCESS(ntStatus)) { SC_LOG(ERROR, "Couldn't add RunCommand work item 0x%x\n", ntStatus); delete pCtx; } } break;
default: SC_ASSERT(0); }
if (Key != NULL) { ScRegCloseKey(Key); } }
DWORD CCrashRecord::IncrementCount( DWORD ResetSeconds ) /*++
Routine Description:
Increments a service's crash count.
Arguments:
ResetSeconds - Length, in seconds, of a period of no crashes after which the crash count should be reset to zero.
Return Value:
The service's new crash count.
--*/ { __int64 SecondLastCrashTime = _LastCrashTime; GetSystemTimeAsFileTime((FILETIME *) &_LastCrashTime);
if (ResetSeconds == INFINITE || SecondLastCrashTime + ResetSeconds * FILETIMES_PER_SEC > _LastCrashTime) { _Count++; } else { SC_LOG(CONFIG_API, "More than %lu seconds have elapsed since last " "crash, resetting crash count.\n", ResetSeconds); _Count = 1; }
SC_LOG(CONFIG_API, "Service's crash count is now %lu\n", _Count); return _Count; }
VOID CRestartContext::Perform( IN BOOLEAN fWaitStatus )
/*++
Routine Description:
--*/ { //
// Make sure we were called because of a timeout
//
SC_ASSERT(fWaitStatus == TRUE);
SC_LOG(CONFIG_API, "Restarting %ws service...\n", _ServiceRecord->ServiceName);
RemoveDelayedWorkItem();
//
// CODEWORK Allow arguments to the service.
//
DWORD status = ScStartServiceAndDependencies(_ServiceRecord, 0, NULL, FALSE);
if (status == NO_ERROR) { status = _ServiceRecord->StartError; SC_LOG(CONFIG_API, "ScStartServiceAndDependencies succeeded, StartError = %lu\n", status); } else { SC_LOG(CONFIG_API, "ScStartServiceAndDependencies failed, %lu\n", status); //
// Should we treat ERROR_SERVICE_ALREADY_RUNNING as a success?
// No, because it could alert the administrator to a less-than-
// optimal system configuration wherein something else is
// restarting the service.
//
ScLogRecoveryFailure( SC_ACTION_RESTART, _ServiceRecord->DisplayName, status ); }
delete this; }
VOID CRebootMessageContext::Perform( IN BOOLEAN fWaitStatus )
/*++
Routine Description:
--*/ { //
// Broadcast the reboot message to all users
//
SESSION_INFO_0 * Buffer = NULL; DWORD EntriesRead = 0, TotalEntries = 0; NTSTATUS ntStatus;
NET_API_STATUS Status = NetSessionEnum( NULL, // servername
NULL, // UncClientName
NULL, // username
0, // level
(LPBYTE *) &Buffer, 0xFFFFFFFF, // prefmaxlen
&EntriesRead, &TotalEntries, NULL // resume_handle
);
if (EntriesRead > 0) { SC_ASSERT(EntriesRead == TotalEntries); SC_ASSERT(Status == NERR_Success); WCHAR ComputerName[MAX_COMPUTERNAME_LENGTH + 1]; DWORD nSize = LENGTH(ComputerName); if (!GetComputerName(ComputerName, &nSize)) { SC_LOG(ERROR, "GetComputerName failed! %lu\n", GetLastError()); } else { DWORD MsgLen = (DWORD) WCSSIZE(_RebootMessage); for (DWORD i = 0; i < EntriesRead; i++) { Status = NetMessageBufferSend( NULL, // servername
Buffer[i].sesi0_cname, // msgname
ComputerName, // fromname
(LPBYTE) _RebootMessage,// buf
MsgLen // buflen
); if (Status != NERR_Success) { SC_LOG2(ERROR, "NetMessageBufferSend to %ws failed %lu\n", Buffer[i].sesi0_cname, Status); } } } } else if (Status != NERR_Success) { SC_LOG(ERROR, "NetSessionEnum failed %lu\n", Status); }
if (Buffer != NULL) { NetApiBufferFree(Buffer); }
//
// Queue a work item to perform the reboot after the delay has elapsed.
// Note: We're counting the delay from the time that the broadcast finished.
//
CRebootContext * pCtx = new CRebootContext(_Delay, _DisplayName); if (pCtx == NULL) { SC_LOG0(ERROR, "Couldn't allocate reboot context\n"); } else { _DisplayName = NULL; // pCtx will free it
ntStatus = pCtx->AddWorkItem(WT_EXECUTEONLYONCE);
if (!NT_SUCCESS(ntStatus)) { SC_LOG(ERROR, "Couldn't add reboot work item 0x%x\n", ntStatus); delete pCtx; } }
delete this; }
VOID CRebootContext::Perform( IN BOOLEAN fWaitStatus )
/*++
Routine Description:
--*/ { SC_LOG0(CONFIG_API, "Rebooting machine...\n"); // Write an event log entry?
//
// Enable our shutdown privilege. Since we are shutting down, don't
// bother doing it for only the current thread and don't bother
// disabling it afterwards.
//
BOOLEAN WasEnabled; NTSTATUS Status = RtlAdjustPrivilege( SE_SHUTDOWN_PRIVILEGE, TRUE, // enable
FALSE, // this thread only? - No
&WasEnabled);
if (!NT_SUCCESS(Status)) { SC_LOG(ERROR, "RtlAdjustPrivilege failed! %#lx\n", Status); SC_ASSERT(0); } else { WCHAR wszShutdownText[128]; WCHAR wszPrintableText[128 + MAX_SERVICE_NAME_LENGTH];
if (LoadString(GetModuleHandle(NULL), IDS_SC_REBOOT_MESSAGE, wszShutdownText, LENGTH(wszShutdownText))) { wsprintf(wszPrintableText, wszShutdownText, _DisplayName); } else { //
// If LoadString failed, it probably means the buffer
// is too small to hold the localized string
//
SC_LOG(ERROR, "LoadString failed! %lu\n", GetLastError()); SC_ASSERT(FALSE);
wszShutdownText[0] = L'\0'; }
if (!InitiateSystemShutdown(NULL, // machine name
wszPrintableText, // reboot message
_Delay / 1000, // timeout in seconds
TRUE, // force apps closed
TRUE)) // reboot
{ DWORD dwError = GetLastError();
//
// If two services fail simultaneously and both are configured
// to reboot the machine, InitiateSystemShutdown will fail all
// calls past the first with ERROR_SHUTDOWN_IN_PROGRESS. We
// don't want to log an event in this case.
//
if (dwError != ERROR_SHUTDOWN_IN_PROGRESS) {
SC_LOG(ERROR, "InitiateSystemShutdown failed! %lu\n", dwError); ScLogRecoveryFailure( SC_ACTION_REBOOT, _DisplayName, dwError ); } } }
delete this; }
VOID CRunCommandContext::Perform( IN BOOLEAN fWaitStatus )
/*++
Routine Description:
CODEWORK Share this code with ScLogonAndStartImage
--*/ { //
// Make sure we were called because of a timeout
//
SC_ASSERT(fWaitStatus == TRUE);
DWORD status = NO_ERROR;
HANDLE Token = NULL; PSID ServiceSid = NULL; // SID is returned only if not LocalSystem
LPWSTR AccountName = NULL; SECURITY_ATTRIBUTES SaProcess; // Process security info (used only if not LocalSystem)
STARTUPINFOW StartupInfo; PROCESS_INFORMATION ProcessInfo;
RemoveDelayedWorkItem();
//
// Get the Account Name for the service. A NULL Account Name means the
// service is configured to run in the LocalSystem account.
//
status = ScLookupServiceAccount( _ServiceRecord->ServiceName, &AccountName );
// We only need to log on if it's not the LocalSystem account
if (AccountName != NULL) { //
// CODEWORK: Keep track of recovery EXEs spawned so we can
// load/unload the user profile for the process.
//
status = ScLogonService( _ServiceRecord->ServiceName, AccountName, &Token, NULL, &ServiceSid );
if (status != NO_ERROR) { SC_LOG(ERROR, "CRunCommandContext: ScLogonService failed, %lu\n", status); goto Clean0; }
SaProcess.nLength = sizeof(SECURITY_ATTRIBUTES); SaProcess.bInheritHandle = FALSE;
SC_ACE_DATA AceData[] = { {ACCESS_ALLOWED_ACE_TYPE, 0, 0, PROCESS_ALL_ACCESS, &ServiceSid},
{ACCESS_ALLOWED_ACE_TYPE, 0, 0, PROCESS_SET_INFORMATION | PROCESS_TERMINATE | SYNCHRONIZE, &LocalSystemSid} };
NTSTATUS ntstatus = ScCreateAndSetSD( AceData, // AceData
LENGTH(AceData), // AceCount
NULL, // OwnerSid (optional)
NULL, // GroupSid (optional)
&SaProcess.lpSecurityDescriptor // pNewDescriptor
);
LocalFree(ServiceSid);
if (! NT_SUCCESS(ntstatus)) { SC_LOG(ERROR, "CRunCommandContext: ScCreateAndSetSD failed %#lx\n", ntstatus); status = RtlNtStatusToDosError(ntstatus); goto Clean1; }
SC_LOG2(CONFIG_API,"CRunCommandContext: about to spawn recovery program in account %ws: %ws\n", AccountName, _FailureCommand);
//
// Impersonate the user so we don't give access to
// EXEs that have been locked down for the account.
//
if (!ImpersonateLoggedOnUser(Token)) { status = GetLastError();
SC_LOG1(ERROR, "ScLogonAndStartImage: ImpersonateLoggedOnUser failed %d\n", status);
goto Clean2; }
//
// Spawn the Image Process
//
ScInitStartupInfo(&StartupInfo, FALSE);
if (!CreateProcessAsUserW( Token, // logon token
NULL, // lpApplicationName
_FailureCommand, // lpCommandLine
&SaProcess, // process' security attributes
NULL, // first thread's security attributes
FALSE, // whether new process inherits handles
CREATE_NEW_CONSOLE, // creation flags
NULL, // environment block
NULL, // current directory
&StartupInfo, // startup info
&ProcessInfo // process info
)) { status = GetLastError(); SC_LOG(ERROR, "CRunCommandContext: CreateProcessAsUser failed %lu\n", status); RevertToSelf(); goto Clean2; }
RevertToSelf(); } else { //
// It's the LocalSystem account
//
//
// If the process is to be interactive, set the appropriate flags.
//
BOOL bInteractive = FALSE;
if (AccountName == NULL && _ServiceRecord->ServiceStatus.dwServiceType & SERVICE_INTERACTIVE_PROCESS) { bInteractive = ScAllowInteractiveServices();
if (!bInteractive) { //
// Write an event to indicate that an interactive service
// was started, but the system is configured to not allow
// services to be interactive.
//
ScLogEvent(NEVENT_SERVICE_NOT_INTERACTIVE, _ServiceRecord->DisplayName); } }
ScInitStartupInfo(&StartupInfo, bInteractive);
SC_LOG1(CONFIG_API,"CRunCommandContext: about to spawn recovery program in " "the LocalSystem account: %ws\n", _FailureCommand);
//
// Spawn the Image Process
//
if (!CreateProcessW( NULL, // lpApplicationName
_FailureCommand, // lpCommandLine
NULL, // process' security attributes
NULL, // first thread's security attributes
FALSE, // whether new process inherits handles
CREATE_NEW_CONSOLE, // creation flags
NULL, // environment block
NULL, // current directory
&StartupInfo, // startup info
&ProcessInfo // process info
)) { status = GetLastError(); SC_LOG(ERROR, "CRunCommandContext: CreateProcess failed %lu\n", status); goto Clean2; } }
SC_LOG0(CONFIG_API, "Recovery program spawned successfully.\n");
CloseHandle(ProcessInfo.hThread); CloseHandle(ProcessInfo.hProcess);
Clean2: if (AccountName != NULL) { RtlDeleteSecurityObject(&SaProcess.lpSecurityDescriptor); }
Clean1: if (AccountName != NULL) { CloseHandle(Token); }
Clean0: if (status != NO_ERROR) { ScLogRecoveryFailure( SC_ACTION_RUN_COMMAND, _ServiceRecord->DisplayName, status ); }
delete this; }
VOID ScLogRecoveryFailure( IN SC_ACTION_TYPE ActionType, IN LPCWSTR ServiceDisplayName, IN DWORD Error )
/*++
Routine Description:
--*/ { WCHAR wszActionString[50]; if (!LoadString(GetModuleHandle(NULL), IDS_SC_ACTION_BASE + ActionType, wszActionString, LENGTH(wszActionString))) { SC_LOG(ERROR, "LoadString failed %lu\n", GetLastError()); wszActionString[0] = L'\0'; }
ScLogEvent( NEVENT_SERVICE_RECOVERY_FAILED, ActionType, wszActionString, (LPWSTR) ServiceDisplayName, Error ); }
|