Home All Groups Group Topic Archive Search About

D=E9j=E0_vu_bug:_The_Matrix_uses_the_.NET_Framework!

Author
2 Oct 2005 4:57 AM
laurentfromparis
You remember the Matrix "Déjà vu" bug where the same sequence is
executed twice. I may have I found the root cause of this. The Matrix
encounters a .NET Framework bug.

To make a long debugging story short I have been chasing an
intermittent lock leak that was not the kind you find in text book. One
of my threads is acquiring a lock but is not releasing it (so far
nothing exiting). The thread in question runs the following code:

lock (this)
{
    // Do A

    Monitor.Exit(this);
    try
    {
        // Do B
    }
    finally
    {
        Monitor.Enter(this);
    }

    // Do C
}

In theory, this code is safe and the lock on this should be released no
matter if the normal execution flow A, B, and C is performed or if an
exception is thrown in either A, B or C.
In practice, the lock is not released when my thread live this code.

Root Cause


I initially thought the lock leak was the result of mixing the C# lock
statement with Monitor.Exit/Enter. I quickly ruled this out after I
tested that the same leak was happening no matter if I used the lock
statement, the calls to Monitor.Enter/Exit or the .NET attribute
[MethodImpl(MethodImplOptions.Synchronized)].

Then I put together a simple C# console application to repro and study
the behavior of try-finally and was very surprised by what I discover.
I hit the Déjà vu bug: the finally clause is some times executed
twice!!!

Hard to believe, here is the code that proves me right:

using System;

delegate void Handler(int i);

class DejaVu
{
    static void Main(string[] args)
    {
        int recurse = 2;

        try
        {
            Handler h = new Handler(Throw);

            Console.WriteLine("BeginInvoke");
            IAsyncResult ar = h.BeginInvoke(recurse, null, null);

            Console.WriteLine("WaitOne");
            ar.AsyncWaitHandle.WaitOne();

            Console.WriteLine("EndInvoke");
            h.EndInvoke(ar);
        }
        catch
        {
            Console.WriteLine("Caught exception");
        }
    }

    private static void Throw(int i)
    {
        try
        {
            Console.WriteLine("Try({0})", i);

            if (i == 0)
                m_object.ToString(); // cause access violation
            else
                Throw(i-1);
        }
        catch
        {
            Console.WriteLine("Catch({0})", i);
            throw;
        }
        finally
        {
            Console.WriteLine("Finally({0})", i);
        }
    }

    private static object m_object = null;
}

The main thread of my application executes the method Throw
asynchronously using the .NET Framework asynchronous delegates
capability. The method Throw calls itself recursively until the i
parameter reaches 0. When 0, the method causes an access violation
exception.

In theory, this code should produce the following output.

BeginInvoke
WaitOne
Try(2)
Try(1)
Try(0)
Catch(0)
Finally(0)
Catch(1)
Finally(1)
Catch(2)
Finally(2)
EndInvoke
Caught exception

In practice, you get this one:

BeginInvoke
WaitOne
Try(2)
Try(1)
Try(0)
Catch(0)
Finally(0)
Catch(1)
Finally(1)
Catch(2)
Finally(0)
Finally(1)
Finally(2)
EndInvoke
Caught exception

The first two finally clauses Finally(0) and Finally(1) are executed
twice! The first when the exception is re-thrown from Catch(0) and
Catch(1) handlers respectively (this is normal). The second time when
the exception is re-thrown from the Catch(2) handler (this is
abnormal).

This bug only occurs if the method Throw gets called via an
asynchronous delegate. When the Throw method is called directly

    Throw(recurse);

Or via asynchronous delegate

    Handler h = new Handler(Throw);
    h(recurse);

The behavior and output is correct. The behavior is also correct if you
replace the access violation exception caused by m_object.ToString()
with an explicit exception like

    throw new ArgumentException();

More testing showed me that the bug occurs only in the following
situation:

1.    The method is called via asynchronous delegate,
2.    The method itself or any of sub methods throws an Win32 exception (I
have only tested access violation and divide by zero but I bet the
other would cause the same issue),
3.    The exception is caught and re-thrown.

You are going to tell me that this is a rare situation and I would
agree but I hit this anyway. Murphy law!
Workaround

The workaround I am using is simple: I always make sure that no
exceptions ever leave a method invoked via asynchronous delegates. To
this end I am using a mediator method that catches all exceptions and
return them to the caller in a custom fashion. In the snippet below the
mediator just writes the exception to the console.

    delegate void MediatorHandler(ThrowHandler h, int i);

    static void Main(string[] args)
    {
        int recurse = 2;

        try
        {
            Handler h = new Handler(Throw);
            MediatorHandler mh = new MediatorHandler(Mediator);

            Console.WriteLine("BeginInvoke");
            IAsyncResult ar = mh.BeginInvoke(h, max, null, null);

            Console.WriteLine("WaitOne");
            ar.AsyncWaitHandle.WaitOne();

            Console.WriteLine("EndInvoke");
            mh.EndInvoke(ar);
        }
        catch
        {
            Console.WriteLine("Caught exception");
        }
    }

    private static void Mediator(Handler h, int i)
    {
        try
        {
            h(i);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Mediator caught exception {0}", ex);
        }
    }

Since the exception never leave the mediator, the finally clauses are
called once and only once.

Finally :-)

I have been looking on the Web and Newsgroups for references to this
issue but haven't found any. Even though this should be a rare case,
I doubt I am the only one to have encountered this problem.

I have tested this against the .NET Framework 1.1 SP1. I plan to test
it against .NET Framework 2 beta 2 ASAP.

Author
2 Oct 2005 6:33 PM
Jon Skeet [C# MVP]
<laurentfrompa***@gmail.com> wrote:

<snip>

> I have been looking on the Web and Newsgroups for references to this
> issue but haven't found any. Even though this should be a rare case,
> I doubt I am the only one to have encountered this problem.

It does seem very odd. I've seen something similar on the compact
framework (not quite the same thing, but similarly wacky exception
situation) but not on the main framework.

> I have tested this against the .NET Framework 1.1 SP1. I plan to test
> it against .NET Framework 2 beta 2 ASAP.

I've just done so, and your test code does the expected things.

--
Jon Skeet - <sk***@pobox.com>
http://www.pobox.com/~skeet   Blog: http://www.msmvps.com/jon.skeet
If replying to the group, please do not mail me too

AddThis Social Bookmark Button