-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
If a SafeEvpPKeyHandle is disposed by one thread while another thread is calling DuplicateHandle, the DuplicateHandle call can succeed, but the resulting SafeEvpPKeyHandle will not be closed and the handle value will be zero.
To demonstrate:
using System.Security.Cryptography;
using ECDsaOpenSsl ecdsa = new ECDsaOpenSsl();
for (int i = 0; i < int.MaxValue - 1; i++)
{
SafeEvpPKeyHandle handle = ecdsa.DuplicateKeyHandle();
SafeEvpPKeyHandle? duplicate = null;
Thread t1 = new Thread(() => {
Thread.Sleep(Random.Shared.Next(0, 10));
try { duplicate = handle.DuplicateHandle(); } catch { }
});
Thread t2 = new Thread(() => {
Thread.Sleep(Random.Shared.Next(0, 10));
handle.Dispose();
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
if (duplicate is null)
{
continue;
}
bool addedRef = false;
try
{
duplicate.DangerousAddRef(ref addedRef);
IntPtr value = duplicate.DangerousGetHandle();
if (value == IntPtr.Zero)
{
throw new Exception($"We somehow got a handle that was not closed but invalid on iteration {i}.");
}
}
finally
{
if (addedRef)
{
duplicate.DangerousRelease();
}
}
duplicate.Dispose();
}
Console.WriteLine("Didn't crash?");After a few thousand runs, this will throw an exception:
Unhandled exception. System.Exception: We somehow got a handle that was not closed but invalid on iteration 1887.
$(String[] args) in /home/vcsjones/Projects/scratch-cs/Program.cs:line 39
at Program.
This should not happen. This means DuplicateHandle returned a SafeEvpPKeyHandle that is not closed, but is invalid.
This is likely the underlying issue that is causing #116307.
In that case, we are calling DuplicateHandle on the key supplied, then passing the safe handle to EvpPKeyBits which gets marshaled as NULL
Lines 39 to 40 in f92f9de
| _key = new Lazy<SafeEvpPKeyHandle>(pkeyHandle.DuplicateHandle()); | |
| KeySizeValue = Interop.Crypto.EvpPKeyBits(_key.Value); |
The race is likely here:
Lines 74 to 86 in f92f9de
| int success = Interop.Crypto.UpRefEvpPkey(this); | |
| if (success != 1) | |
| { | |
| Debug.Fail("Called UpRefEvpPkey on a key which was already marked for destruction"); | |
| Exception e = Interop.Crypto.CreateOpenSslCryptographicException(); | |
| safeHandle.Dispose(); | |
| throw e; | |
| } | |
| // Since we didn't actually create a new handle, copy the handle | |
| // to the new SafeHandle. | |
| safeHandle.SetHandle(handle); |
If the current instance is disposed right before line 86, then handle will be IntPtr.Zero. That is because ReleaseHandle explicitly zeros the handle:
Line 51 in f92f9de
| SetHandle(IntPtr.Zero); |
The fix is probably that DuplicateHandle should do a DangerousAddRef and DangerousRelease around itself so the handle cannot be destroyed while it is duplicating. This is currently only being done implicitly on line 74 when up-ref-ing the key handle, but if its disposed after the up-ref but before the handle field is read, the resulting duplicated handle will be NULL.