// Copyright (c) 1996-1999 Microsoft Corporation //+------------------------------------------------------------------------- // // Microsoft Windows // // File: port.cxx // // Contents: Code that receives notifications of moves from // kernel. // // Classes: // // Functions: // // // // History: // // Notes: // // Codework: Security on semaphore and port objects // _hDllReference when put in services.exe // InitializeObjectAttributes( &oa, &name, 0, NULL, NULL /* &sd */ ); // //-------------------------------------------------------------------------- #include "pch.cxx" #pragma hdrstop #include "trkwks.hxx" #define THIS_FILE_NUMBER PORT_CXX_FILE_NO DWORD WINAPI PortThreadStartRoutine( LPVOID lpThreadParameter ); //+---------------------------------------------------------------------------- // // CSystemSD::Initialize // CSystemSD::UnInitialize // // Init and uninit the security descriptor that gives access only // to System, or to System and Administrators. // //+---------------------------------------------------------------------------- void CSystemSD::Initialize( ESystemSD eSystemSD ) { // Add ACEs to the DACL in a Security Descriptor which give the // System and Administrators full access. _csd.Initialize(); if( SYSTEM_AND_ADMINISTRATOR == eSystemSD ) { _csidAdministrators.Initialize( CSID::CSID_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS ); _csd.AddAce( CSecDescriptor::ACL_IS_DACL, CSecDescriptor::AT_ACCESS_ALLOWED, FILE_ALL_ACCESS, _csidAdministrators ); } else TrkAssert( SYSTEM_ONLY == eSystemSD ); _csidSystem.Initialize( CSID::CSID_NT_AUTHORITY, SECURITY_LOCAL_SYSTEM_RID ); _csd.AddAce( CSecDescriptor::ACL_IS_DACL, CSecDescriptor::AT_ACCESS_ALLOWED, FILE_ALL_ACCESS, _csidSystem ); } void CSystemSD::UnInitialize() { _csidAdministrators.UnInitialize(); _csidSystem.UnInitialize(); _csd.UnInitialize(); } //+---------------------------------------------------------------------------- // // CPort::Initialize // // Create an LPC port to which the kernel will send move notifications, // and open an event created by the kernel with which we'll signal // our readiness to receive requests. // //+---------------------------------------------------------------------------- void CPort::Initialize( CTrkWksSvc *pTrkWks, DWORD dwThreadKeepAliveTime ) { NTSTATUS Status; OBJECT_ATTRIBUTES oa; UNICODE_STRING name; CSystemSD ssd; DWORD dwThreadId; __try { _hListenPort = NULL; _hEvent = NULL; _pTrkWks = pTrkWks; _hLpcPort = NULL; _hRegisterWaitForSingleObjectEx = NULL; _fTerminating = FALSE; // Create an LPC port to which the kernel will send move-notification requests RtlInitUnicodeString( &name, TRKWKS_PORT_NAME ); ssd.Initialize(); InitializeObjectAttributes( &oa, &name, 0, NULL, ssd.operator const PSECURITY_DESCRIPTOR() ); Status = NtCreateWaitablePort(&_hListenPort, &oa, sizeof(ULONG), // IN ULONG MaxConnectionInfoLength sizeof(TRKWKS_PORT_REQUEST), // IN ULONG MaxMessageLength 0); // not used : IN ULONG MaxPoolUsage if (!NT_SUCCESS(Status)) { TrkLog(( TRKDBG_ERROR, TEXT("Couldn't create LPC connect port") )); TrkReportInternalError( THIS_FILE_NUMBER, __LINE__, Status, TRKWKS_PORT_NAME ); TrkRaiseException(Status); } // Show that we need to do work in the UnInitialize method. _fInitializeCalled = TRUE; // Open the event which is created by the kernel for synchronization. // We tell the kernel that we're available for move-notification requests // by setting this event. RtlInitUnicodeString( &name, TRKWKS_PORT_EVENT_NAME ); Status = NtOpenEvent( &_hEvent, EVENT_ALL_ACCESS, &oa ); if (!NT_SUCCESS(Status)) { TrkReportInternalError( THIS_FILE_NUMBER, __LINE__, Status, TRKWKS_PORT_EVENT_NAME ); TrkLog(( TRKDBG_ERROR, TEXT("Couldn't open %s event"), TRKWKS_PORT_EVENT_NAME )); TrkRaiseException(Status); } // Register our LPC connect port with the thread pool. When that handle signals, // we'll run CPort::DoWork (it signals when we get any message, including // LPC_CONNECT_REQUEST). if( !RegisterWorkItemWithThreadPool() ) { TrkLog(( TRKDBG_ERROR, TEXT("Failed RegisterWaitForSingleObjectEx in CPort::Initialize (%lu)"), GetLastError() )); TrkReportInternalError( THIS_FILE_NUMBER, __LINE__, Status, TRKREPORT_LAST_PARAM ); TrkRaiseLastError(); } // When we receive a move notification and we get a thread from the pool, // we'll keep that thread until we've gone idle for this amount of time. // So if we receive several requests in a short period of time, we won't // have to get a thread out of the pool for each. _ThreadKeepAliveTime.QuadPart = -static_cast(dwThreadKeepAliveTime) * 10000000; } __finally { ssd.UnInitialize(); } } //+---------------------------------------------------------------------------- // // CPort::RegisterWorkItemWithThreadPool // // Register the LPC connect port (_hListenPort) with the thread pool. // //+---------------------------------------------------------------------------- BOOL CPort::RegisterWorkItemWithThreadPool() { // This is an execute-only-once work item, so it's inactive now. // Delete it, specifying that there should be no completion event // (if a completion event were used, this call would hang forever). if( NULL != _hRegisterWaitForSingleObjectEx ) TrkUnregisterWait( _hRegisterWaitForSingleObjectEx, NULL ); // Now register it again. _hRegisterWaitForSingleObjectEx = TrkRegisterWaitForSingleObjectEx( _hListenPort, ThreadPoolCallbackFunction, static_cast(this), INFINITE, WT_EXECUTEONLYONCE ); if( NULL == _hRegisterWaitForSingleObjectEx ) { TrkLog(( TRKDBG_ERROR, TEXT("Failed RegisterWaitForSingleObjectEx in CPort::DoWork (%lu)"), GetLastError() )); return( FALSE ); } else TrkLog(( TRKDBG_PORT, TEXT("Registered LPC port work item") )); return( TRUE ); } //+---------------------------------------------------------------------------- // // CPort::OnConnectionRequest // // Called when a connection request has been received. It is either // accepted or rejected, depending on the request and the current state // of the service. // // When the service is shutting down, CPort::UnInitialize posts a connection // request with some connection information. When that connection request // is received, it is rejected, and the pfStopPortThread is set True. // //+---------------------------------------------------------------------------- NTSTATUS CPort::OnConnectionRequest( TRKWKS_PORT_CONNECT_REQUEST *pPortConnectRequest, BOOL *pfStopPortThread ) { HANDLE hLpcPortT = NULL; HANDLE *phLpcPort = NULL; NTSTATUS Status = STATUS_SUCCESS; BOOL fAccept = TRUE; *pfStopPortThread = FALSE; // Determine if we should accept or reject this connection request. if( pPortConnectRequest->PortMessage.u1.s1.DataLength >= sizeof(pPortConnectRequest->Info) ) { // There's extra connection info in this request. See if it's a request // code that indicates that we should close down the port. if( TRKWKS_RQ_EXIT_PORT_THREAD == pPortConnectRequest->Info.dwRequest ) { fAccept = FALSE; *pfStopPortThread = TRUE; TrkLog(( TRKDBG_PORT, TEXT("Received port shutdown connection request") )); } else { fAccept = FALSE; TrkLog(( TRKDBG_ERROR, TEXT("CPort: unknown Info.dwRequest (%d)"), pPortConnectRequest->Info.dwRequest )); } } else if( _fTerminating ) { // We're shutting down, reject the request fAccept = FALSE; TrkLog(( TRKDBG_PORT, TEXT("Received connect request during service shutdown") )); } // Point phLpcPort to the real communications handle, or the dummy one used // for rejections. if( fAccept ) { phLpcPort = &_hLpcPort; // Close out any existing communication port if( NULL != _hLpcPort ) { NtClose( _hLpcPort ); _hLpcPort = NULL; } } else { phLpcPort = &hLpcPortT; } // Accept or reject the new connection. // In the reject case, this could create a race condition. After we make the // NtAcceptConnectPort call, the CPort::UnInitialize thread might wake up and // delete the CPort before this thread runs again. So, after making this // call, we cannot touch anything in 'this'. TrkLog(( TRKDBG_PORT, TEXT("%s connect request"), fAccept ? TEXT("Accepting") : TEXT("Rejecting") )); TRKWKS_PORT_REQUEST *pPortRequest = (TRKWKS_PORT_REQUEST*) pPortConnectRequest; pPortRequest->PortMessage.u1.s1.TotalLength = sizeof(*pPortRequest); pPortRequest->PortMessage.u1.s1.DataLength = sizeof(pPortRequest->Request); // MaxMessageLength Status = NtAcceptConnectPort( phLpcPort, // PortHandle, NULL, // PortContext OPTIONAL, &pPortRequest->PortMessage, (BOOLEAN)fAccept, // AcceptConnection, NULL, // ServerView OPTIONAL, NULL); // ClientView OPTIONAL if( !NT_SUCCESS(Status) ) { TrkLog(( TRKDBG_ERROR, TEXT("Failed NtAcceptConnectPort(%s) %08x"), fAccept ? TEXT("accept"):TEXT("reject"), Status )); goto Exit; } // If we rejected it, then phLpcPort was hLpcPortT, and it's just // a dummy argument which must be present but isn't set by NtAcceptConnectPort. TrkAssert( NULL != *phLpcPort || !fAccept ); // Wake up the client thread (unblock its call to NtConnectPort) if( fAccept ) { Status = NtCompleteConnectPort( _hLpcPort ); if( !NT_SUCCESS(Status) ) { TrkLog(( TRKDBG_ERROR, TEXT("Failed NtCompleteConnectPort %08x"), Status )); goto Exit; } } Exit: return( Status ); } //+---------------------------------------------------------------------------- // // CPort::DoWork // // This method is called by the thread pool when our LPC connect port // (_hLpcListenPort) is signaled to indicate that a request is available. // If the request is a connection request, we accept or reject it and continue. // If the request is a move notification request, we send it to // CTrkWksSvc for processing. // // This work item is registerd with the thread pool with the // WT_EXECUTEONLYONCE flag, since the connection port isn't // auto-reset. So after processing, we must // re-register. Before doing so, or if re-registration fails, // we keep the thread in a NtReplyWaitReceiveEx call for several // (configurable) seconds. This way, if several requests arrive // in a short amount of time, we don't have to thrash the thread pool. // // During service termination, _fTerminate is set, and a connection // request is made by CPort::UnInitialize. This request is rejected, // and in that case we don't re-register the connect port with the thread // pool. // //+---------------------------------------------------------------------------- void CPort::DoWork() { NTSTATUS Status = STATUS_SUCCESS; TRKWKS_PORT_REQUEST PortRequest; TRKWKS_PORT_REPLY PortReply; PortRequest.PortMessage.u1.s1.TotalLength = sizeof(PortRequest.PortMessage); PortRequest.PortMessage.u1.s1.DataLength = (CSHORT)0; BOOL fReuseThread = FALSE; // The fact that we're running indicates that there's a request // waiting for us, and the first NtReplyWaitReceivePortEx below will // immediately return. We loop until nothing is received for 30 // seconds. while( TRUE ) { BOOL fTerminating = FALSE; // TRUE => service is shutting down BOOL fStopPortThread = FALSE; // TRUE => we should shut down this port Status = NtReplyWaitReceivePortEx( _hListenPort, //_hLpcPort, NULL, NULL, &PortRequest.PortMessage, &_ThreadKeepAliveTime ); // Cache a local copy of _fTerminating. In the shutdown case, there's // a race condition where the CPort object gets deleted before this routine // can finish. By caching this flag, we don't have to touch this 'this' // pointer in that case, and therefore avoid the problem. fTerminating = _fTerminating; // If we timeed out, then let the thread return to the thread pool. if( STATUS_TIMEOUT != Status ) { // We didn't time out. #if DBG if( fReuseThread ) TrkLog(( TRKDBG_PORT, TEXT("CPort re-using thread") )); #endif fReuseThread = TRUE; // Is this a request for a new connection? if( NT_SUCCESS(Status) && LPC_CONNECTION_REQUEST == PortRequest.PortMessage.u2.s2.Type ) { TrkLog(( TRKDBG_PORT, TEXT("Received LPC connect request") )); Status = OnConnectionRequest( (TRKWKS_PORT_CONNECT_REQUEST*) &PortRequest, &fStopPortThread ); #if DBG if( !NT_SUCCESS(Status) ) TrkLog(( TRKDBG_ERROR, TEXT("CPort::DoWork couldn't handle connection request %08x"), Status )); #endif } // if( ... LPC_CONNECTION_REQUEST == PortRequest.PortMessage.u2.s2.Type ) // Or, is this a good move notification? else if( NT_SUCCESS(Status) && NULL != _hLpcPort ) { // Process the move notification in CTrkWksSvc. If we're in the proces, // though, of shutting the service down, then return the same error that // the kernel would see if DisableKernelNotifications had been called // in time. if( _fTerminating ) PortReply.Reply.Status = STATUS_OBJECT_NAME_NOT_FOUND; else // The following doesn't raise. PortReply.Reply.Status = _pTrkWks->OnPortNotification( &PortRequest.Request ); // Send the resulting Status back to the kernel. PortReply.PortMessage = PortRequest.PortMessage; PortReply.PortMessage.u1.s1.TotalLength = sizeof(PortReply); PortReply.PortMessage.u1.s1.DataLength = sizeof(PortReply.Reply); Status = NtReplyPort( _hLpcPort, &PortReply.PortMessage ); #if DBG if( !NT_SUCCESS(Status) ) TrkLog(( TRKDBG_ERROR, TEXT("Failed NtReplyPort (%08x)"), Status )); #endif } // Otherwise, we either got an error, or a non-connect message on an // unconnected port. else { if( NT_SUCCESS(Status) ) Status = STATUS_CONNECTION_INVALID; TrkLog(( TRKDBG_ERROR, TEXT("CPort::PortThread - NtReplyWaitReceivePortEx failed %0X/%p"), Status, _hLpcPort )); } // To be robust against some unknown bug causing thrashing, sleep // if there was an error. if( !NT_SUCCESS(Status) && !fTerminating && !fStopPortThread ) Sleep( 1000 ); // Unless the service is shutting down, we don't want to fall // through and re-register yet. We should go back to the // NtReplyWaitReceivePortEx to see if there are more requests // or will be soon. if( !fStopPortThread ) continue; } // if( STATUS_TIMEOUT != Status ) // Re-register the connect port with the thread pool, unless we're supposed // to stop the port thread. if( fStopPortThread ) { TrkLog(( TRKDBG_PORT, TEXT("Stopping port work item") )); } else { // If we can't re-register for some reason, just continue back to the top // and sit in the NtReplyWaitReceiveEx for a while. if( !RegisterWorkItemWithThreadPool() ) { TrkLog(( TRKDBG_PORT, TEXT("Re-using port thread due to registration error (%lu)"), GetLastError() )); continue; } else TrkLog(( TRKDBG_PORT, TEXT("Returning port thread to pool") )); } // We're either terminating or we've successfully re-registered. In either case, we can let // the thread go back to the pool. break; } // while( TRUE ) } //+---------------------------------------------------------------------------- // // CPort::UnInitialize // // Remove the LPC connect port work item from the thread pool, and // clean everything up. // // To remove the work item, we can't safely call UnregisterWait, because // we register with WT_EXECUTEONLYONCE. Thus when we call UnregisterWait, // the wait may have already been deleted. So, instead, we attempt a connection // to the LPC connect port, after first setting _fTerminating. This will be // picked up on a thread pool thread in DoWork, the connection will be // rejected, and the work item will not be re-registered. // //+---------------------------------------------------------------------------- void CPort::UnInitialize() { if (_fInitializeCalled) { NTSTATUS status = STATUS_SUCCESS; UNICODE_STRING usPortName; OBJECT_ATTRIBUTES oa; HANDLE hPort = NULL; ULONG cbMaxMessage = 0; TRKWKS_CONNECTION_INFO ConnectionInformation = { TRKWKS_RQ_EXIT_PORT_THREAD }; ULONG cbConnectionInformation = sizeof(ConnectionInformation); _fTerminating = TRUE; // Attempt to connect to _hLpcListenPort RtlInitUnicodeString( &usPortName, TRKWKS_PORT_NAME ); SECURITY_QUALITY_OF_SERVICE dynamicQos; dynamicQos.ImpersonationLevel = SecurityImpersonation; dynamicQos.ContextTrackingMode = SECURITY_DYNAMIC_TRACKING; dynamicQos.EffectiveOnly = TRUE; TrkLog(( TRKDBG_PORT, TEXT("CPort::UnInitialize doing an NtConnectPort to %s"), TRKWKS_PORT_NAME )); status = NtConnectPort( &hPort, &usPortName, &dynamicQos, NULL, NULL, &cbMaxMessage, &ConnectionInformation, &cbConnectionInformation ); TrkLog(( TRKDBG_PORT, TEXT("CPort::UnInitialize, NtConnectPort completed (0x%08x)"), status )); if( NT_SUCCESS(status) ) { TrkLog(( TRKDBG_PORT, TEXT("CPort::UnInitialize NtConnectPort unexpectedly succeeded"), status )); if( NULL != hPort ) NtClose( hPort ); } #if DBG else { TrkAssert( NULL == hPort ); if( STATUS_PORT_CONNECTION_REFUSED != status ) TrkLog(( TRKDBG_ERROR, TEXT("CPort::UnInitialize NtConnectPort failed (%08x)"), status )); } #endif // DoWork has been called and is done. Unregister the work item, waiting for the thread // to complete. if( NULL != _hRegisterWaitForSingleObjectEx ) TrkUnregisterWait( _hRegisterWaitForSingleObjectEx ); _hRegisterWaitForSingleObjectEx = NULL; // Clean up the port. if (_hLpcPort != NULL) TrkVerify( NT_SUCCESS( NtClose(_hLpcPort) ) ); _hLpcPort = NULL; if (_hListenPort != NULL) TrkVerify( NT_SUCCESS( NtClose(_hListenPort) ) ); _hListenPort = NULL; if( NULL != _hEvent ) NtClose( _hEvent ); _hEvent = NULL; _fInitializeCalled = FALSE; } } //+---------------------------------------------------------------------------- // // CPort::EnableKernelNotifications // CPort::DisableKernelNotifications // // Set/clear the event which tells nt!IopConnectLinkTrackingPort that we're // up and ready to receive a connection. // //+---------------------------------------------------------------------------- void CPort::EnableKernelNotifications() { NTSTATUS Status; Status = NtSetEvent( _hEvent, NULL ); TrkVerify( NT_SUCCESS( Status ) ); } void CPort::DisableKernelNotifications() { if (_fInitializeCalled) { NTSTATUS Status; Status = NtClearEvent( _hEvent ); TrkVerify( NT_SUCCESS( Status ) ); } }