// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. // /*============================================================ ** ** Class: SafeHandle ** ** ** Purpose: The unmanaged implementation of the SafeHandle ** class ** ===========================================================*/ #include "common.h" #include "vars.hpp" #include "object.h" #include "excep.h" #include "frames.h" #include "eecontract.h" #include "mdaassistants.h" #include "typestring.h" WORD SafeHandle::s_IsInvalidHandleMethodSlot = MethodTable::NO_SLOT; WORD SafeHandle::s_ReleaseHandleMethodSlot = MethodTable::NO_SLOT; void SafeHandle::Init() { CONTRACTL { THROWS; GC_TRIGGERS; MODE_ANY; } CONTRACTL_END; // For reliability purposes, we need to eliminate all possible failure // points before making a call to a CER method. IsInvalidHandle, and // ReleaseHandle methods are critical calls that are already prepared (code: // PrepareCriticalFinalizerObject). As a performance optimization, we are // calling these methods through a fast macro that assumes the method slot // has been already cached. Since figuring out the method slot for these 2 // methods involves calling .GetMethod which can fail, we are doing this // eagerly here, Otherwise we will have to do it at the time of the call, // and this could be at risk if .GetMethod failed. MethodDesc* pMD = MscorlibBinder::GetMethod(METHOD__SAFE_HANDLE__GET_IS_INVALID); s_IsInvalidHandleMethodSlot = pMD->GetSlot(); pMD = MscorlibBinder::GetMethod(METHOD__SAFE_HANDLE__RELEASE_HANDLE); s_ReleaseHandleMethodSlot = pMD->GetSlot(); } void SafeHandle::AddRef() { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; INSTANCE_CHECK; } CONTRACTL_END; // Cannot use "this" after Release, which toggles the GC mode. SAFEHANDLEREF sh(this); #ifdef _DEBUG VALIDATEOBJECTREF(sh->m_debugStackTrace); #endif _ASSERTE(sh->IsFullyInitialized()); // To prevent handle recycling security attacks we must enforce the // following invariant: we cannot successfully AddRef a handle on which // we've committed to the process of releasing. // We ensure this by never AddRef'ing a handle that is marked closed and // never marking a handle as closed while the ref count is non-zero. For // this to be thread safe we must perform inspection/updates of the two // values as a single atomic operation. We achieve this by storing them both // in a single aligned DWORD and modifying the entire state via interlocked // compare exchange operations. // Additionally we have to deal with the problem of the Dispose operation. // We must assume that this operation is directly exposed to untrusted // callers and that malicious callers will try and use what is basically a // Release call to decrement the ref count to zero and free the handle while // it's still in use (the other way a handle recycling attack can be // mounted). We combat this by allowing only one Dispose to operate against // a given safe handle (which balances the creation operation given that // Dispose suppresses finalization). We record the fact that a Dispose has // been requested in the same state field as the ref count and closed state. // So the state field ends up looking like this: // // 31 2 1 0 // +-----------------------------------------------------------+---+---+ // | Ref count | D | C | // +-----------------------------------------------------------+---+---+ // // Where D = 1 means a Dispose has been performed and C = 1 means the // underlying handle has (or will be shortly) released. // Might have to perform the following steps multiple times due to // interference from other AddRef's and Release's. INT32 oldState, newState; do { // First step is to read the current handle state. We use this as a // basis to decide whether an AddRef is legal and, if so, to propose an // update predicated on the initial state (a conditional write). oldState = sh->m_state; // Check for closed state. if (oldState & SH_State_Closed) COMPlusThrow(kObjectDisposedException, IDS_EE_SAFEHANDLECLOSED); // Not closed, let's propose an update (to the ref count, just add // SH_RefCountOne to the state to effectively add 1 to the ref count). // Continue doing this until the update succeeds (because nobody // modifies the state field between the read and write operations) or // the state moves to closed. newState = oldState + SH_RefCountOne; } while (InterlockedCompareExchange((LONG*)&sh->m_state, newState, oldState) != oldState); // If we got here we managed to update the ref count while the state // remained non closed. So we're done. } void SafeHandle::Release(bool fDispose) { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; INSTANCE_CHECK; } CONTRACTL_END; // Cannot use "this" after RunReleaseMethod, which toggles the GC mode. SAFEHANDLEREF sh(this); #ifdef _DEBUG VALIDATEOBJECTREF(sh->m_debugStackTrace); #endif _ASSERTE(sh->IsFullyInitialized()); // See AddRef above for the design of the synchronization here. Basically we // will try to decrement the current ref count and, if that would take us to // zero refs, set the closed state on the handle as well. bool fPerformRelease = false; // Might have to perform the following steps multiple times due to // interference from other AddRef's and Release's. INT32 oldState, newState; do { // First step is to read the current handle state. We use this cached // value to predicate any modification we might decide to make to the // state). oldState = sh->m_state; // If this is a Dispose operation we have additional requirements (to // ensure that Dispose happens at most once as the comments in AddRef // detail). We must check that the dispose bit is not set in the old // state and, in the case of successful state update, leave the disposed // bit set. Silently do nothing if Dispose has already been called // (because we advertise that as a semantic of Dispose). if (fDispose && (oldState & SH_State_Disposed)) return; // We should never see a ref count of zero (that would imply we have // unbalanced AddRef and Releases). (We might see a closed state before // hitting zero though -- that can happen if SetHandleAsInvalid is // used). if ((oldState & SH_State_RefCount) == 0) COMPlusThrow(kObjectDisposedException, IDS_EE_SAFEHANDLECLOSED); // If we're proposing a decrement to zero and the handle is not closed // and we own the handle then we need to release the handle upon a // successful state update. fPerformRelease = ((oldState & (SH_State_RefCount | SH_State_Closed)) == SH_RefCountOne) && m_ownsHandle; // If so we need to check whether the handle is currently invalid by // asking the SafeHandle subclass. We must do this *before* // transitioning the handle to closed, however, since setting the closed // state will cause IsInvalid to always return true. if (fPerformRelease) { GCPROTECT_BEGIN(sh); CLR_BOOL fIsInvalid = FALSE; DECLARE_ARGHOLDER_ARRAY(args, 1); args[ARGNUM_0] = OBJECTREF_TO_ARGHOLDER(sh); PREPARE_SIMPLE_VIRTUAL_CALLSITE_USING_SLOT(s_IsInvalidHandleMethodSlot, sh); CRITICAL_CALLSITE; CALL_MANAGED_METHOD(fIsInvalid, CLR_BOOL, args); if (fIsInvalid) { fPerformRelease = false; } GCPROTECT_END(); } // Attempt the update to the new state, fail and retry if the initial // state has been modified in the meantime. Decrement the ref count by // substracting SH_RefCountOne from the state then OR in the bits for // Dispose (if that's the reason for the Release) and closed (if the // initial ref count was 1). newState = (oldState - SH_RefCountOne) | ((oldState & SH_State_RefCount) == SH_RefCountOne ? SH_State_Closed : 0) | (fDispose ? SH_State_Disposed : 0); } while (InterlockedCompareExchange((LONG*)&sh->m_state, newState, oldState) != oldState); // If we get here we successfully decremented the ref count. Additonally we // may have decremented it to zero and set the handle state as closed. In // this case (providng we own the handle) we will call the ReleaseHandle // method on the SafeHandle subclass. if (fPerformRelease) RunReleaseMethod((SafeHandle*) OBJECTREFToObject(sh)); } void SafeHandle::Dispose() { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; INSTANCE_CHECK; } CONTRACTL_END; // You can't use the "this" pointer after the call to Release because // Release may trigger a GC. SAFEHANDLEREF sh(this); #ifdef _DEBUG VALIDATEOBJECTREF(sh->m_debugStackTrace); #endif _ASSERTE(sh->IsFullyInitialized()); GCPROTECT_BEGIN(sh); sh->Release(true); // Suppress finalization on this object (we may be racing here but the // operation below is idempotent and a dispose should never race a // finalization). GCHeapUtilities::GetGCHeap()->SetFinalizationRun(OBJECTREFToObject(sh)); GCPROTECT_END(); } void SafeHandle::SetHandle(LPVOID handle) { CONTRACTL { THROWS; MODE_COOPERATIVE; INSTANCE_CHECK; SO_TOLERANT; } CONTRACTL_END; _ASSERTE(IsFullyInitialized()); // The SafeHandle's handle field can only be set it if the SafeHandle isn't // closed or disposed and its ref count is 1. if (m_state != (LONG)SH_RefCountOne) COMPlusThrow(kObjectDisposedException, IDS_EE_SAFEHANDLECANNOTSETHANDLE); m_handle = handle; } void AcquireSafeHandle(SAFEHANDLEREF* s) { WRAPPER_NO_CONTRACT; GCX_COOP(); _ASSERTE(s != NULL && *s != NULL); (*s)->AddRef(); } void ReleaseSafeHandle(SAFEHANDLEREF* s) { WRAPPER_NO_CONTRACT; GCX_COOP(); _ASSERTE(s != NULL && *s != NULL); (*s)->Release(false); } // This could theoretically be an instance method, but we'd need to // somehow GC protect the this pointer or never dereference any // field within the object. It's a lot simpler if we simply make // this method static. void SafeHandle::RunReleaseMethod(SafeHandle* psh) { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; } CONTRACTL_END; SAFEHANDLEREF sh(psh); _ASSERTE(sh != NULL); _ASSERTE(sh->m_ownsHandle); _ASSERTE(sh->IsFullyInitialized()); GCPROTECT_BEGIN(sh); // Save last error from P/Invoke in case the implementation of ReleaseHandle // trashes it (important because this ReleaseHandle could occur implicitly // as part of unmarshaling another P/Invoke). Thread *pThread = GetThread(); DWORD dwSavedError = pThread->m_dwLastError; CLR_BOOL fReleaseHandle = FALSE; DECLARE_ARGHOLDER_ARRAY(args, 1); args[ARGNUM_0] = OBJECTREF_TO_ARGHOLDER(sh); PREPARE_SIMPLE_VIRTUAL_CALLSITE_USING_SLOT(s_ReleaseHandleMethodSlot, sh); CRITICAL_CALLSITE; CALL_MANAGED_METHOD(fReleaseHandle, CLR_BOOL, args); if (!fReleaseHandle) { #ifdef MDA_SUPPORTED MDA_TRIGGER_ASSISTANT(ReleaseHandleFailed, ReportViolation(sh->GetTypeHandle(), sh->m_handle)); #endif } pThread->m_dwLastError = dwSavedError; GCPROTECT_END(); } FCIMPL1(void, SafeHandle::DisposeNative, SafeHandle* refThisUNSAFE) { FCALL_CONTRACT; SAFEHANDLEREF sh(refThisUNSAFE); if (sh == NULL) FCThrowVoid(kNullReferenceException); HELPER_METHOD_FRAME_BEGIN_1(sh); _ASSERTE(sh->IsFullyInitialized()); sh->Dispose(); HELPER_METHOD_FRAME_END(); } FCIMPLEND FCIMPL1(void, SafeHandle::Finalize, SafeHandle* refThisUNSAFE) { FCALL_CONTRACT; SAFEHANDLEREF sh(refThisUNSAFE); _ASSERTE(sh != NULL); HELPER_METHOD_FRAME_BEGIN_1(sh); if (sh->IsFullyInitialized()) sh->Dispose(); // By the time we get here we better have gotten rid of any handle resources // we own (unless we were force finalized during shutdown). // It's possible to have a critical finalizer reference a // safehandle that ends up calling DangerousRelease *after* this finalizer // is run. In that case we assert since the state is not closed. // _ASSERTE(!sh->IsFullyInitialized() || (sh->m_state & SH_State_Closed) || g_fEEShutDown); HELPER_METHOD_FRAME_END(); } FCIMPLEND FCIMPL1(void, SafeHandle::SetHandleAsInvalid, SafeHandle* refThisUNSAFE) { FCALL_CONTRACT; SAFEHANDLEREF sh(refThisUNSAFE); _ASSERTE(sh != NULL); // Attempt to set closed state (low order bit of the m_state field). // Might have to attempt these repeatedly, if the operation suffers // interference from an AddRef or Release. INT32 oldState, newState; do { // First step is to read the current handle state so we can predicate a // state update on it. oldState = sh->m_state; // New state has the same ref count but is now closed. Attempt to write // this new state but fail if the state was updated in the meantime. newState = oldState | SH_State_Closed; } while (InterlockedCompareExchange((LONG*)&sh->m_state, newState, oldState) != oldState); GCHeapUtilities::GetGCHeap()->SetFinalizationRun(OBJECTREFToObject(sh)); } FCIMPLEND FCIMPL2(void, SafeHandle::DangerousAddRef, SafeHandle* refThisUNSAFE, CLR_BOOL *pfSuccess) { FCALL_CONTRACT; SAFEHANDLEREF sh(refThisUNSAFE); HELPER_METHOD_FRAME_BEGIN_1(sh); if (pfSuccess == NULL) COMPlusThrow(kNullReferenceException); sh->AddRef(); *pfSuccess = TRUE; HELPER_METHOD_FRAME_END(); } FCIMPLEND FCIMPL1(void, SafeHandle::DangerousRelease, SafeHandle* refThisUNSAFE) { FCALL_CONTRACT; SAFEHANDLEREF sh(refThisUNSAFE); HELPER_METHOD_FRAME_BEGIN_1(sh); sh->Release(FALSE); HELPER_METHOD_FRAME_END(); } FCIMPLEND FCIMPL1(void, CriticalHandle::FireCustomerDebugProbe, CriticalHandle* refThisUNSAFE) { FCALL_CONTRACT; CRITICALHANDLEREF ch(refThisUNSAFE); HELPER_METHOD_FRAME_BEGIN_1(ch); #ifdef MDA_SUPPORTED MDA_TRIGGER_ASSISTANT(ReleaseHandleFailed, ReportViolation(ch->GetTypeHandle(), ch->m_handle)); #else FCUnique(0x53); #endif HELPER_METHOD_FRAME_END(); } FCIMPLEND FCIMPL1(UINT, SafeBuffer::SizeOfType, ReflectClassBaseObject* typeUNSAFE) { FCALL_CONTRACT; REFLECTCLASSBASEREF type(typeUNSAFE); MethodTable* pMT = type->GetType().AsMethodTable(); if (!pMT->IsValueType() || pMT->ContainsPointers()) FCThrowArgument(W("type"), W("Argument_NeedStructWithNoRefs")); FC_GC_POLL_RET(); return pMT->GetNumInstanceFieldBytes(); } FCIMPLEND FCIMPL1(UINT, SafeBuffer::AlignedSizeOfType, ReflectClassBaseObject* typeUNSAFE) { FCALL_CONTRACT; REFLECTCLASSBASEREF type(typeUNSAFE); MethodTable* pMT = type->GetType().AsMethodTable(); if (!pMT->IsValueType() || pMT->ContainsPointers()) FCThrowArgument(W("type"), W("Argument_NeedStructWithNoRefs")); FC_GC_POLL_RET(); return pMT->GetAlignedNumInstanceFieldBytes(); } FCIMPLEND FCIMPL3_IVI(void, SafeBuffer::PtrToStructure, BYTE* ptr, FC_TypedByRef structure, UINT32 sizeofT) { FCALL_CONTRACT; LPVOID structData = structure.data; _ASSERTE(ptr != NULL && structData != NULL); memcpyNoGCRefs(structData, ptr, sizeofT); FC_GC_POLL(); } FCIMPLEND FCIMPL3_VII(void, SafeBuffer::StructureToPtr, FC_TypedByRef structure, BYTE* ptr, UINT32 sizeofT) { FCALL_CONTRACT; LPVOID structData = structure.data; _ASSERTE(ptr != NULL && structData != NULL); memcpyNoGCRefs(ptr, structData, sizeofT); FC_GC_POLL(); } FCIMPLEND