"Handle is invalid" error when opening SqlConnection

After searching an answer for a while with no success the only thing that finally fixed the error was iisreset.


As it turns out, we tracked the error down to deserializing a CancellationToken with Json.Net.

The underlying problem occurs when code is still trying to use an OS handle which has been freed. Of course, this can happen when your code works with handles directly. Our code does not do this, but it turns out that this can happen with Json.Net. Here's how:

We had a class as follows:

public class MyClass
{
   ...
}

// in one part of the code, this class was serialized & deserialized using Json.Net:
JsonConvert.SerializeObject(...);
JsonConvert.DeserializeObject<MyClass>(...);

The problem occurred when someone added a property to MyClass of type CancellationToken:

public class MyClass
{
    ...
    public CancellationToken Token { get; set; }
}

Here's the issue. When serialized, a CancellationToken looks like this:

{"IsCancellationRequested":false,"CanBeCanceled":true,"WaitHandle":{"Handle":{"value":1508},"SafeWaitHandle":{"IsInvalid":false,"IsClosed":false}}}

Note that doing so lazy-creates the token's WaitHandle property, and serializes the value of it underlying OS handle (1508).

When we deserialize the token, Json.Net will start with new CancellationToken() (equivalent to CancellationToken.None). It then will proceed to populate the Handle property of that token's WaitHandle using the saved IntPtr value. One obvious way in which this makes things go wrong is that the default CancellationToken's WaitHandle now points to a likely invalid handle. However, the bigger issue is that updating the handle dereferences the WaitHandle's original SafeHandle, thus allowing the garbage collector to run its finalizer and clean it up. You can then fall victim to the following set of events:

  1. Handle 123 is allocated to a pooled database connection
  2. A deserialization assigns handle 123 to the default cancellation token's WaitHandle
  3. A second deserialization assigns a new handle value to the default cancellation token's WaitHandle
  4. The garbage collector runs and finalizes the released 123 safe handle value
  5. The database connection now points to an invalid handle

Here's some code which deliberately replicates the issue using a FileStream:

// serialize 2 tokens
var source = new CancellationTokenSource();
var serialized = JsonConvert.SerializeObject(source.Token);
var serialized2 = JsonConvert.SerializeObject(new CancellationTokenSource().Token);
var handle = source.Token.WaitHandle.Handle;
source.Dispose(); // releases source's handle

// spin until the OS gives us back that same handle as
// a file handle
FileStream fileStream;
while (true)
{
    fileStream = new FileStream(Path.GetTempFileName(), FileMode.OpenOrCreate);
    if (fileStream.Handle == handle) { break; }
}

// deserialize both tokens, thus releasing the conflicting handle
var deserialized = JsonConvert.DeserializeObject<CancellationToken>(serialized);
var deserialized2 = JsonConvert.DeserializeObject<CancellationToken>(serialized2);

GC.Collect();
GC.WaitForPendingFinalizers();

fileStream.WriteByte(1);
fileStream.Flush(); // fails with IOException "The handle is invalid"