// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.ConstrainedExecution; using System.Threading; namespace System.Runtime.InteropServices { // This implementation does not employ critical execution regions and thus cannot // reliably guarantee handle release in the face of thread aborts. /// <summary>Represents a wrapper class for operating system handles.</summary> public abstract partial class SafeHandle : CriticalFinalizerObject, IDisposable { #if DEBUG && CORECLR /// <summary>Indicates whether debug tracking and logging of SafeHandle finalization is enabled.</summary> private static readonly bool s_logFinalization = Environment.GetEnvironmentVariable("DEBUG_SAFEHANDLE_FINALIZATION") == "1"; /// <summary>Debug counter for the number of SafeHandles that have been finalized.</summary> private static long s_safeHandlesFinalized; #endif #if DEBUG && CORECLR private readonly string? _ctorStackTrace; #endif /// <summary>Specifies the handle to be wrapped.</summary> protected IntPtr handle; /// <summary>Combined ref count and closed/disposed flags (so we can atomically modify them).</summary> private volatile int _state; /// <summary>Whether we can release this handle.</summary> private readonly bool _ownsHandle; /// <summary>Whether constructor completed.</summary> private readonly bool _fullyInitialized; /// <summary>Bitmasks for the <see cref="_state"/> field.</summary> /// <remarks> /// 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 been (or will be shortly) released. /// </remarks> private static class StateBits { public const int Closed = 0b01; public const int Disposed = 0b10; public const int RefCount = unchecked(~0b11); // 2 bits reserved for closed/disposed; ref count gets 30 bits public const int RefCountOne = 1 << 2; } /// <summary>Creates a SafeHandle class.</summary> protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle) { handle = invalidHandleValue; _state = StateBits.RefCountOne; // Ref count 1 and not closed or disposed. _ownsHandle = ownsHandle; if (!ownsHandle) { GC.SuppressFinalize(this); } #if DEBUG && CORECLR else if (s_logFinalization) { int lastError = Marshal.GetLastPInvokeError(); _ctorStackTrace = Environment.StackTrace; Marshal.SetLastPInvokeError(lastError); } #endif Volatile.Write(ref _fullyInitialized, true); } ~SafeHandle() { if (_fullyInitialized) { Dispose(disposing: false); } } internal bool OwnsHandle => _ownsHandle; protected internal void SetHandle(IntPtr handle) => this.handle = handle; public IntPtr DangerousGetHandle() => handle; public bool IsClosed => (_state & StateBits.Closed) == StateBits.Closed; public abstract bool IsInvalid { get; } public void Close() => Dispose(); public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { #if DEBUG && CORECLR if (!disposing && _ctorStackTrace is not null) { long count = Interlocked.Increment(ref s_safeHandlesFinalized); Internal.Console.WriteLine($"{Environment.NewLine}*** #{count} {GetType()} (0x{handle.ToInt64():x}) finalized! Ctor stack:{Environment.NewLine}{_ctorStackTrace}{Environment.NewLine}"); } #endif Debug.Assert(_fullyInitialized); InternalRelease(disposeOrFinalizeOperation: true); } public void SetHandleAsInvalid() { Debug.Assert(_fullyInitialized); // Set closed state (low order bit of the _state field). Interlocked.Or(ref _state, StateBits.Closed); GC.SuppressFinalize(this); } protected abstract bool ReleaseHandle(); public void DangerousAddRef(ref bool success) { // 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 int 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. Debug.Assert(_fullyInitialized); // Might have to perform the following steps multiple times due to // interference from other AddRef's and Release's. int 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). // Check for closed state. oldState = _state; ObjectDisposedException.ThrowIf((oldState & StateBits.Closed) != 0, this); // Not closed, let's propose an update (to the ref count, just add // StateBits.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 + StateBits.RefCountOne; } while (Interlocked.CompareExchange(ref _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. success = true; } // Used by internal callers to avoid declaring a bool to pass by ref internal void DangerousAddRef() { bool success = false; DangerousAddRef(ref success); } public void DangerousRelease() => InternalRelease(disposeOrFinalizeOperation: false); private void InternalRelease(bool disposeOrFinalizeOperation) { Debug.Assert(_fullyInitialized || disposeOrFinalizeOperation); // 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 performRelease; // Might have to perform the following steps multiple times due to // interference from other AddRef's and Release's. int 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 = _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. if (disposeOrFinalizeOperation && ((oldState & StateBits.Disposed) != 0)) { 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). ObjectDisposedException.ThrowIf((oldState & StateBits.RefCount) == 0, this); // 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. 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. performRelease = ((oldState & (StateBits.RefCount | StateBits.Closed)) == StateBits.RefCountOne) && _ownsHandle && !IsInvalid; // 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 // subtracting StateBits.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 - StateBits.RefCountOne; if ((oldState & StateBits.RefCount) == StateBits.RefCountOne) { newState |= StateBits.Closed; } if (disposeOrFinalizeOperation) { newState |= StateBits.Disposed; } } while (Interlocked.CompareExchange(ref _state, newState, oldState) != oldState); // If we get here we successfully decremented the ref count. Additionally we // may have decremented it to zero and set the handle state as closed. In // this case (providing we own the handle) we will call the ReleaseHandle // method on the SafeHandle subclass. if (performRelease) { // 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). int lastError = Marshal.GetLastPInvokeError(); ReleaseHandle(); Marshal.SetLastPInvokeError(lastError); } } } }