|
|
/*++
Copyright (c) 2001-2001 Microsoft Corporation
Module Name:
rpc.c
Abstract:
DNS Resolver Service
RPC intialization, shutdown and utility routines.
Author:
Jim Gilroy (jamesg) April 19, 2001
Revision History:
--*/
#include "local.h"
#include <rpc.h>
#include "rpcdce.h"
#include "secobj.h"
#undef UNICODE
//
// RPC globals
//
BOOL g_fRpcInitialized = FALSE;
DWORD g_RpcProtocol = RESOLVER_RPC_USE_LPC;
PSECURITY_DESCRIPTOR g_pRpcSecurityDescriptor;
#define AUTO_BIND
//
// Resolver access control
//
PSECURITY_DESCRIPTOR g_pAccessSecurityDescriptor = NULL; PACL g_pAccessAcl = NULL; PSID g_pAccessOwnerSid = NULL;
GENERIC_MAPPING g_AccessGenericMapping = { RESOLVER_GENERIC_READ, RESOLVER_GENERIC_WRITE, RESOLVER_GENERIC_EXECUTE, RESOLVER_GENERIC_ALL };
#define SECURITY_LOCK() EnterCriticalSection( &CacheCS )
#define SECURITY_UNLOCK() LeaveCriticalSection( &CacheCS )
//
// Privileges
//
#if 0
#define RESOLVER_PRIV_READ 1
#define RESOLVER_PRIV_ENUM 2
#define RESOLVER_PRIV_FLUSH 3
#define RESOLVER_PRIV_REGISTER 4
#endif
DNS_STATUS Rpc_InitAccessChecking( VOID );
VOID Rpc_CleanupAccessChecking( VOID );
BOOL Rpc_IsProtoLpcA( IN PVOID pContext ) /*++
Routine Description:
Check if access if over LPC
Arguments:
pContext -- client RPC context
Return Value:
TRUE if protocol is LPC FALSE otherwise
--*/ { DNS_STATUS status; BOOL fisLpc = FALSE; PSTR pbinding = NULL; PSTR pprotoString = NULL;
DNSDBG( RPC, ( "Rpc_IsAccessOverLpc( context=%p )\n", pContext ));
//
// get string binding
//
status = RpcBindingToStringBindingA( pContext, & pbinding );
if ( status != NO_ERROR ) { goto Cleanup; }
//
// get protocol as string
//
status = RpcStringBindingParseA( pbinding, NULL, & pprotoString, NULL, NULL, NULL );
if ( status != NO_ERROR ) { goto Cleanup; }
//
// check for LPC
//
fisLpc = ( _stricmp( pprotoString, "ncalrpc" ) == 0 );
RpcStringFreeA( &pprotoString );
Cleanup:
if ( pbinding ) { RpcStringFreeA( &pbinding ); }
return( fisLpc ); }
BOOL Rpc_IsProtoLpc( IN PVOID pContext ) /*++
Routine Description:
Check if access if over LPC
Arguments:
pContext -- client RPC context
Return Value:
TRUE if protocol is LPC FALSE otherwise
--*/ { DNS_STATUS status; BOOL fisLpc = FALSE; PWSTR pbinding = NULL; PWSTR pprotoString = NULL;
DNSDBG( RPC, ( "Rpc_IsAccessOverLpc( context=%p )\n", pContext ));
//
// get string binding
//
status = RpcBindingToStringBinding( pContext, & pbinding );
if ( status != NO_ERROR ) { goto Cleanup; }
//
// get protocol as string
//
status = RpcStringBindingParse( pbinding, NULL, & pprotoString, NULL, NULL, NULL );
if ( status != NO_ERROR ) { goto Cleanup; }
//
// check for LPC
//
fisLpc = ( _wcsicmp( pprotoString, L"ncalrpc" ) == 0 );
RpcStringFree( &pprotoString );
Cleanup:
if ( pbinding ) { RpcStringFree( &pbinding ); }
return( fisLpc ); }
RPC_STATUS RPC_ENTRY Rpc_SecurityCallback( IN RPC_IF_HANDLE IfHandle, IN PVOID pContext ) /*++
Routine Description:
RPC callback security check.
Arguments:
IfHandle -- interface handle
pContext -- client RPC context
Return Value:
NO_ERROR if security check allows access. ErrorCode on security failure.
--*/ { DNSDBG( RPC, ( "Rpc_SecurityCallback( context=%p )\n", pContext ));
//
// check if connection over LPC
//
if ( !Rpc_IsProtoLpc(pContext) ) { return ERROR_ACCESS_DENIED; }
return NO_ERROR; }
DNS_STATUS Rpc_Initialize( VOID ) /*++
Routine Description:
Initialize server side RPC.
Arguments:
None.
Return Value:
ERROR_SUCCESS if successful. Error status on failure.
--*/ { RPC_STATUS status; BOOL fusingTcpip = FALSE;
DNSDBG( RPC, ( "Rpc_Initialize()\n" "\tIF handle = %p\n" "\tprotocol = %d\n", DnsResolver_ServerIfHandle, g_RpcProtocol ));
//
// RPC disabled?
//
if ( ! g_RpcProtocol ) { g_RpcProtocol = RESOLVER_RPC_USE_LPC; }
#if 0
//
// Create security for RPC API
//
status = NetpCreateWellKnownSids( NULL ); if ( status != ERROR_SUCCESS ) { DNS_PRINT(( "ERROR: Creating well known SIDs.\n" )); return( status ); }
status = RpcUtil_CreateSecurityObjects(); if ( status != ERROR_SUCCESS ) { DNS_PRINT(( "ERROR: Creating DNS security object.\n" )); return( status ); } #endif
//
// build security descriptor
//
// NULL security descriptor gives some sort of default security
// on the interface
// - owner is this service (currently "Network Service")
// - read access for everyone
//
// note: if roll your own, remember to avoid NULL DACL, this
// puts NO security on interface including the right to
// change security, so any app can hijack the ACL and
// deny access to folks; the default SD==NULL security
// doesn't give everyone WRITE_DACL
//
g_pRpcSecurityDescriptor = NULL;
//
// RPC over LPC
//
if( g_RpcProtocol & RESOLVER_RPC_USE_LPC ) { status = RpcServerUseProtseqEpW( L"ncalrpc", // protocol string.
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // maximum concurrent calls
RESOLVER_RPC_LPC_ENDPOINT_W, // endpoint
g_pRpcSecurityDescriptor // security
);
// duplicate endpoint is ok
if ( status == RPC_S_DUPLICATE_ENDPOINT ) { status = RPC_S_OK; } if ( status != RPC_S_OK ) { DNSDBG( INIT, ( "ERROR: RpcServerUseProtseqEp() for LPC failed.]n" "\tstatus = %d 0x%08lx.\n", status, status )); return( status ); } }
#if 0 // use use LPC interface
//
// RCP over TCP/IP
//
if( g_RpcProtocol & RESOLVER_RPC_USE_TCPIP ) { #ifdef AUTO_BIND
RPC_BINDING_VECTOR * bindingVector;
status = RpcServerUseProtseqW( L"ncacn_ip_tcp", // protocol string.
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // max concurrent calls
g_pRpcSecurityDescriptor );
if ( status != RPC_S_OK ) { DNSDBG( INIT, ( "ERROR: RpcServerUseProtseq() for TCP/IP failed.]n" "\tstatus = %d 0x%08lx.\n", status, status )); return( status ); }
status = RpcServerInqBindings( &bindingVector );
if ( status != RPC_S_OK ) { DNSDBG( INIT, ( "ERROR: RpcServerInqBindings failed.\n" "\tstatus = %d 0x%08lx.\n", status, status )); return( status ); }
//
// register interface(s)
// since only one DNS server on a host can use
// RpcEpRegister() rather than RpcEpRegisterNoReplace()
//
status = RpcEpRegisterW( DnsResolver_ServerIfHandle, bindingVector, NULL, L"" ); if ( status != RPC_S_OK ) { DNSDBG( ANY, ( "ERROR: RpcEpRegisterNoReplace() failed.\n" "\tstatus = %d %p.\n", status, status )); return( status ); }
//
// free binding vector
//
status = RpcBindingVectorFree( &bindingVector ); ASSERT( status == RPC_S_OK ); status = RPC_S_OK;
#else // not AUTO_BIND
status = RpcServerUseProtseqEpW( L"ncacn_ip_tcp", // protocol string.
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // maximum concurrent calls
RESOLVER_RPC_SERVER_PORT_W, // endpoint
g_pRpcSecurityDescriptor // security
);
if ( status != RPC_S_OK ) { DNSDBG( ANY, ( "ERROR: RpcServerUseProtseqEp() for TCP/IP failed.]n" "\tstatus = %d 0x%08lx.\n", status, status )); return( status ); }
#endif // AUTO_BIND
fusingTcpip = TRUE; }
//
// RPC over named pipes
//
if ( g_RpcProtocol & RESOLVER_RPC_USE_NAMED_PIPE ) { status = RpcServerUseProtseqEpW( L"ncacn_np", // protocol string.
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // maximum concurrent calls
RESOLVER_RPC_PIPE_NAME_W, // endpoint
g_pRpcSecurityDescriptor );
// duplicate endpoint is ok
if ( status == RPC_S_DUPLICATE_ENDPOINT ) { status = RPC_S_OK; } if ( status != RPC_S_OK ) { DNSDBG( INIT, ( "ERROR: RpcServerUseProtseqEp() for named pipe failed.]n" "\tstatus = %d 0x%08lx.\n", status, status )); return( status ); } } #endif // only LPC interface
//
// register DNS RPC interface(s)
//
status = RpcServerRegisterIfEx( DnsResolver_ServerIfHandle, NULL, NULL, 0, RPC_C_LISTEN_MAX_CALLS_DEFAULT, Rpc_SecurityCallback ); if ( status != RPC_S_OK ) { DNSDBG( INIT, ( "ERROR: RpcServerRegisterIfEx() failed.]n" "\tstatus = %d 0x%08lx.\n", status, status )); return(status); }
#if 0
//
// for TCP/IP setup authentication
//
if ( fuseTcpip ) { status = RpcServerRegisterAuthInfoW( RESOLVER_RPC_SECURITY_W, // app name to security provider.
RESOLVER_RPC_SECURITY_AUTH_ID, // Auth package ID.
NULL, // Encryption function handle.
NULL ); // argment pointer to Encrypt function.
if ( status != RPC_S_OK ) { DNSDBG( INIT, ( "ERROR: RpcServerRegisterAuthInfo() failed.]n" "\tstatus = %d 0x%08lx.\n", status, status )); return( status ); } } #endif
//
// Listen on RPC
//
status = RpcServerListen( 1, // min threads
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // max concurrent calls
TRUE ); // return on completion
if ( status != RPC_S_OK ) { if ( status != RPC_S_ALREADY_LISTENING ) { DNS_PRINT(( "ERROR: RpcServerListen() failed\n" "\tstatus = %d 0x%p\n", status, status )); return( status ); } status = NO_ERROR; }
g_fRpcInitialized = TRUE; return( status );
} // Rpc_Initialize
VOID Rpc_Shutdown( VOID ) /*++
Routine Description:
Shutdown RPC on the server.
Arguments:
None.
Return Value:
None.
--*/ { DWORD status; RPC_BINDING_VECTOR * bindingVector = NULL;
DNSDBG( RPC, ( "Rpc_Shutdown().\n" ));
if( ! g_fRpcInitialized ) { DNSDBG( RPC, ( "RPC not active, no shutdown necessary.\n" )); return; }
#if 0
// can not stop server as another service may share this process
//
// stop server listen
// then wait for all RPC threads to go away
//
status = RpcMgmtStopServerListening( NULL // this app
); if ( status == RPC_S_OK ) { status = RpcMgmtWaitServerListen(); } #endif
//
// unbind / unregister endpoints
//
status = RpcServerInqBindings( &bindingVector ); ASSERT( status == RPC_S_OK );
if ( status == RPC_S_OK ) { status = RpcEpUnregister( DnsResolver_ServerIfHandle, bindingVector, NULL ); // Uuid vector.
if ( status != RPC_S_OK ) { DNSDBG( ANY, ( "ERROR: RpcEpUnregister, status = %d.\n", status )); } }
//
// free binding vector
//
if ( bindingVector ) { status = RpcBindingVectorFree( &bindingVector ); ASSERT( status == RPC_S_OK ); }
//
// wait for all calls to complete
//
status = RpcServerUnregisterIf( DnsResolver_ServerIfHandle, 0, TRUE ); ASSERT( status == ERROR_SUCCESS );
g_fRpcInitialized = FALSE;
//
// dump resolver access checking security structs
//
Rpc_CleanupAccessChecking();
DNSDBG( RPC, ( "RPC shutdown completed.\n" )); }
//
// RPC access control
//
// The RPC interface, by design, must be open to basically every
// process for query. So the RPC interface itself uses just
// default security (above).
//
// To get more detailed call-by-call access checking for special
// operations -- enum, flush, cluster registrations -- we need
// separate access checking.
//
DNS_STATUS Rpc_InitAccessChecking( VOID ) /*++
Routine Description:
Initialize resolver security.
Arguments:
None
Return Value:
NO_ERROR if successful. ErrorCode on failure.
--*/ { PSECURITY_DESCRIPTOR psd = NULL; SID_IDENTIFIER_AUTHORITY authority = SECURITY_NT_AUTHORITY; PACL pacl = NULL; PSID psidAdmin = NULL; PSID psidPowerUser = NULL; PSID psidLocalService = NULL; PSID psidNetworkService = NULL; PSID psidNetworkConfigOps = NULL; DWORD lengthAcl; DNS_STATUS status = NO_ERROR; BOOL bresult;
DNSDBG( INIT, ( "Rpc_InitAccessChecking()\n" ));
//
// check if already have SD
//
// explicitly "create once" semantics; once
// created SD is read-only and not destroyed until
// shutdown
//
if ( g_pAccessSecurityDescriptor ) { return NO_ERROR; }
// lock and retest
SECURITY_LOCK(); if ( g_pAccessSecurityDescriptor ) { status = NO_ERROR; goto Unlock; }
//
// build SIDs that will be allowed access
//
bresult = AllocateAndInitializeSid( &authority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, & psidAdmin );
bresult = bresult && AllocateAndInitializeSid( &authority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_POWER_USERS, 0, 0, 0, 0, 0, 0, & psidPowerUser ); bresult = bresult && AllocateAndInitializeSid( &authority, 1, SECURITY_LOCAL_SERVICE_RID, 0, 0, 0, 0, 0, 0, 0, &psidLocalService ); bresult = bresult && AllocateAndInitializeSid( &authority, 1, SECURITY_NETWORK_SERVICE_RID, 0, 0, 0, 0, 0, 0, 0, &psidNetworkService ); bresult = bresult && AllocateAndInitializeSid( &authority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_NETWORK_CONFIGURATION_OPS, 0, 0, 0, 0, 0, 0, &psidNetworkConfigOps ); if ( !bresult ) { status = GetLastError(); if ( status == NO_ERROR ) { status = DNS_ERROR_NO_MEMORY; } DNSDBG( ANY, ( "Failed building resolver ACEs!\n" )); goto Cleanup; }
//
// allocate ACL
//
lengthAcl = ( (ULONG)sizeof(ACL) + 5*((ULONG)sizeof(ACCESS_ALLOWED_ACE) - (ULONG)sizeof(ULONG)) + + GetLengthSid( psidAdmin ) + GetLengthSid( psidPowerUser ) + GetLengthSid( psidLocalService ) + GetLengthSid( psidNetworkService ) + GetLengthSid( psidNetworkConfigOps ) ); pacl = GENERAL_HEAP_ALLOC( lengthAcl ); if ( !pacl ) { status = GetLastError(); goto Cleanup; } bresult = InitializeAcl( pacl, lengthAcl, ACL_REVISION2 ); //
// init ACLs
//
// - default to generic READ\WRITE
// - local service GENERIC_ALL to include registration
// - admin gets GENERIC_ALL on debug to test registration
//
// DCR: GENERIC_ALL on admin subject to test flag
//
// note: the masks for the individual SIDs need not be
// GENERIC bits, they can be made completely with
// with individual bits or mixed\matched with GENERIC
// bits as desired
//
bresult = bresult && AddAccessAllowedAce( pacl, ACL_REVISION2, GENERIC_ALL, psidLocalService );
bresult = bresult && AddAccessAllowedAce( pacl, ACL_REVISION2, #ifdef DBG
GENERIC_ALL, #else
GENERIC_READ | GENERIC_WRITE, #endif
psidAdmin ); bresult = bresult && AddAccessAllowedAce( pacl, ACL_REVISION2, GENERIC_READ | GENERIC_WRITE, psidPowerUser );
bresult = bresult && AddAccessAllowedAce( pacl, ACL_REVISION2, GENERIC_READ | GENERIC_WRITE, psidNetworkService );
bresult = bresult && AddAccessAllowedAce( pacl, ACL_REVISION2, GENERIC_READ | GENERIC_WRITE, psidNetworkConfigOps );
if ( !bresult ) { status = GetLastError(); DNSDBG( ANY, ( "Failed building resolver ACEs!\n" )); goto Cleanup; }
//
// allocate security descriptor
// then init with ACL
//
psd = GENERAL_HEAP_ALLOC( SECURITY_DESCRIPTOR_MIN_LENGTH ); if ( !psd ) { status = DNS_ERROR_NO_MEMORY; goto Cleanup; } bresult = InitializeSecurityDescriptor( psd, SECURITY_DESCRIPTOR_REVISION ); bresult = bresult && SetSecurityDescriptorDacl( psd, TRUE, // have DACL
pacl, // DACL
FALSE // DACL not defaulted, explicit
);
// security folks indicate need owner to do AccessCheck
bresult = bresult && SetSecurityDescriptorOwner( psd, psidNetworkService, FALSE // owner not defaulted, explicit
);
#if 0
// adding group seemed to make it worse
bresult = bresult && SetSecurityDescriptorGroup( psd, psidNetworkService, FALSE // group not defaulted, explicit
); #endif
if ( !bresult ) { status = GetLastError(); DNSDBG( ANY, ( "Failed setting security descriptor!\n" )); goto Cleanup; }
Cleanup:
if ( psidAdmin ) { FreeSid( psidAdmin ); } if ( psidPowerUser ) { FreeSid( psidPowerUser ); } if ( psidLocalService ) { FreeSid( psidLocalService ); } if ( psidNetworkConfigOps ) { FreeSid( psidNetworkConfigOps ); }
// validate SD
if ( status == NO_ERROR ) { if ( psd && IsValidSecurityDescriptor(psd) ) { g_pAccessSecurityDescriptor = psd; g_pAccessAcl = pacl; g_pAccessOwnerSid = psidNetworkService; goto Unlock; }
status = GetLastError(); DNSDBG( RPC, ( "Invalid security descriptor\n", status )); ASSERT( FALSE ); }
// failed
if ( status == NO_ERROR ) { status = DNS_ERROR_NO_MEMORY; } GENERAL_HEAP_FREE( psd ); GENERAL_HEAP_FREE( pacl ); if ( psidNetworkService ) { FreeSid( psidNetworkService ); }
Unlock:
SECURITY_UNLOCK();
DNSDBG( INIT, ( "Leave Rpc_InitAccessChecking() = %d\n", status ));
return( status ); }
VOID Rpc_CleanupAccessChecking( VOID ) /*++
Routine Description:
Cleanup resolver security allocations for shutdown.
Arguments:
None
Return Value:
None
--*/ { GENERAL_HEAP_FREE( g_pAccessSecurityDescriptor ); GENERAL_HEAP_FREE( g_pAccessAcl );
if ( g_pAccessOwnerSid ) { FreeSid( g_pAccessOwnerSid ); g_pAccessOwnerSid = NULL; }
g_pAccessSecurityDescriptor = NULL; g_pAccessAcl = NULL; }
BOOL Rpc_AccessCheck( IN DWORD DesiredAccess ) /*++
Routine Description:
Access check on resolver operations.
Note, we do NOT do this on common operations -- query. This is pointless and way too expensive. We only protect
Arguments:
DesiredAccess -- access desired Return Value:
None
--*/ { DNS_STATUS status; BOOL bstatus; HANDLE hthread = NULL; HANDLE htoken = NULL; BOOL fimpersonating = FALSE; DWORD desiredAccess = DesiredAccess;
PRIVILEGE_SET privilegeSet; DWORD grantedAccess; DWORD privilegeSetLength;
DNSDBG( RPC, ( "Rpc_AccessCheck( priv=%08x )\n", DesiredAccess ));
//
// create security descriptor if not created yet
//
if ( !g_pAccessSecurityDescriptor ) { status = Rpc_InitAccessChecking(); if ( status != NO_ERROR ) { goto Failed; } DNS_ASSERT( g_pAccessSecurityDescriptor ); }
if ( !IsValidSecurityDescriptor( g_pAccessSecurityDescriptor ) ) { status = GetLastError(); DNSDBG( RPC, ( "ERROR Invalid access check SD %p => %u\n", g_pAccessSecurityDescriptor, status )); goto Failed; }
//
// impersonate and test access against mapping
//
status = RpcImpersonateClient( 0 ); if ( status != NO_ERROR ) { DNSDBG( RPC, ( "ERROR <%u>: failed RpcImpersonateClient()\n", status )); DNS_ASSERT( FALSE ); goto Failed; } fimpersonating = TRUE;
//
// get thread token
//
hthread = GetCurrentThread(); if ( !hthread ) { goto Failed; }
bstatus = OpenThreadToken( hthread, TOKEN_QUERY, TRUE, &htoken ); if ( !bstatus ) { status = GetLastError(); DNSDBG( RPC, ( "\nERROR <%lu>: failed to open thread token!\n", status )); ASSERT( FALSE ); goto Failed; }
//
// map generic bits
// - we should NOT be called with generic bits
// only bits for specific resolver operations
//
if ( (desiredAccess & SPECIFIC_RIGHTS_ALL) != desiredAccess ) { DNS_ASSERT( FALSE );
DNSDBG( RPC, ( "desiredAccess before MapGenericMask() = %p\n", desiredAccess )); MapGenericMask( & desiredAccess, & g_AccessGenericMapping ); DNSDBG( RPC, ( "desiredAccess after MapGenericMask() = %p\n", desiredAccess )); }
//
// do access check
//
privilegeSetLength = sizeof(privilegeSet);
if ( ! AccessCheck( g_pAccessSecurityDescriptor, htoken, desiredAccess, & g_AccessGenericMapping, & privilegeSet, & privilegeSetLength, & grantedAccess, & bstatus ) ) { status = GetLastError(); DNSDBG( RPC, ( "AccessCheck() Failed => %u\n" "\tsec descp = %p\n" "\thtoken = %p\n" "\tdesired access = %08x\n" "\tgeneric mapping = %p\n" "\tpriv set ptr = %p\n" "\tpriv set length = %p\n" "\tgranted ptr = %p\n" "\tbstatus ptr = %p\n", status, g_pAccessSecurityDescriptor, htoken, desiredAccess, & g_AccessGenericMapping, & privilegeSet, & privilegeSetLength, & grantedAccess, & bstatus ));
goto Failed; }
//
// access check successful
// - access is either granted or denied
if ( bstatus ) { DNSDBG( RPC, ( "RPC Client GRANTED access (%08x) by AccessCheck\n", DesiredAccess )); goto Cleanup; } else { DNSDBG( RPC, ( "Warning: Client DENIED by AccessCheck\n" "\trequested access = %08x\n", desiredAccess )); goto Cleanup; }
Failed:
//
// failure to do access check
//
// note again: NOT ACCESS_DENIED, but failure to be able to complete
// the test
//
// note: since we aren't in general protecting anything interesting,
// the we grant most access on failure --as admins may want the info
// for diagnostic purposes, the current lone exception is cluster
// registrations
//
// privilege:
// - enum => allow access
// - flush => allow access
// - cluster registration => deny access
//
DNSDBG( ANY, ( "ERROR: Failed to execute RPC access check status = %d\n", status )); #if 0
// DCR: FIX: can't screen REGISTER until AccessCheck() works
bstatus = !( desiredAccess & RESOLVER_ACCESS_REGISTER ); #else
bstatus = TRUE; #endif
Cleanup:
if ( htoken ) { CloseHandle( htoken ); } if ( hthread ) { CloseHandle( hthread ); } if ( fimpersonating ) { RpcRevertToSelf(); }
return( bstatus ); }
//
// End rpc.c
//
|