Skip to content

SafeEvpPKeyHandle.DuplicateHandle is not thread safe #124484

@vcsjones

Description

@vcsjones

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.
at Program.

$(String[] args) in /home/vcsjones/Projects/scratch-cs/Program.cs:line 39

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

_key = new Lazy<SafeEvpPKeyHandle>(pkeyHandle.DuplicateHandle());
KeySizeValue = Interop.Crypto.EvpPKeyBits(_key.Value);

The race is likely here:

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:

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.

Metadata

Metadata

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions