Skip to content

Request for review: Possible threading issue / synchronization bug in DynamicProxy #382

@stakx

Description

@stakx

@BrunoJuchli and I have once again been working on #193. In #193 (comment) he observed a NullReferenceException being thrown from some Dictionary<,> used by DynamicProxy, which got me curious.

Perhaps I'm a little sleepy and not paying full attention, but I think I have found a major threading issue in DynamicProxy. Could someone please validate the following observation:

I'm looking at the method BaseProxyGenerator.ObtainProxyType. This is the method in DynamicProxy where a proxy type is either fetched from a cache, or where its generation is triggered if it isn't in the cache yet:

protected Type ObtainProxyType(CacheKey cacheKey, Func<string, INamingScope, Type> factory)

The following section is interesting. Take note of what happens after we get hold of an upgradeable read lock, but before we upgrade it to a write lock:

// This is to avoid generating duplicate types under heavy multithreaded load.
using (var locker = Scope.Lock.ForReadingUpgradeable())
{
// Only one thread at a time may enter an upgradable read lock.
// See if an earlier lock holder populated the cache.
cacheType = GetFromCache(cacheKey);
if (cacheType != null)
{
Logger.DebugFormat("Found cached proxy type {0} for target type {1}.", cacheType.FullName, targetType.FullName);
return cacheType;
}
// Log details about the cache miss
Logger.DebugFormat("No cached proxy type was found for target type {0}.", targetType.FullName);
EnsureOptionsOverrideEqualsAndGetHashCode(ProxyGenerationOptions);
var name = Scope.NamingScope.GetUniqueName("Castle.Proxies." + targetType.Name + "Proxy");
var proxyType = factory.Invoke(name, Scope.NamingScope.SafeSubScope());
// Upgrade the lock to a write lock.
using (locker.Upgrade())
{
AddToCache(cacheKey, proxyType);
}
return proxyType;
}

In line 414, we are calling into Scope.NamingScope.GetUniqueName while we still don't have a write lock. However, that method writes to a private dictionary:

public string GetUniqueName(string suggestedName)
{
Debug.Assert(string.IsNullOrEmpty(suggestedName) == false,
"string.IsNullOrEmpty(suggestedName) == false");
int counter;
if (!names.TryGetValue(suggestedName, out counter))
{
names.Add(suggestedName, 0);
return suggestedName;
}
counter++;
names[suggestedName] = counter;
return suggestedName + "_" + counter.ToString();
}

If my understanding of the reader-writer locks is correct, there's the possibility of any number of observers reading from that dictionary concurrently (because we're not in a write lock yet).

But what's even worse, this is (line 415) where System.Reflection.Emit happens!

I see nothing that prevents System.Reflection.Emit generation of two proxy types at the same time... and although System.Reflection.Emit does apparently some locking of its own, it doesn't actually give any official thread-safety guarantees. (Update: The upgradeable read lock should ensure that only one thread can ever trigger System.Reflection.Emit at a time. But the problematic access to NamingScope likely remains.)

Finally, we're reaching the section where we're getting hold of a write lock. This is what will block multiple readers. But the only thing it protects against multiple readers is the cache update, not the dynamic type generation itself.

This is what I believe should be happening:

-	var name = Scope.NamingScope.GetUniqueName("Castle.Proxies." + targetType.Name + "Proxy"); 
-	var proxyType = factory.Invoke(name, Scope.NamingScope.SafeSubScope()); 
  
 	// Upgrade the lock to a write lock.  
 	using (locker.Upgrade()) 
 	{ 
+		var name = Scope.NamingScope.GetUniqueName("Castle.Proxies." + targetType.Name + "Proxy"); 
+		var proxyType = factory.Invoke(name, Scope.NamingScope.SafeSubScope()); 
 		AddToCache(cacheKey, proxyType); 
 	}

Am I misunderstanding something here, or do we really have concurrent System.Reflection.Emit-ting into the same ModuleBuilder going on here?

/cc @TimLovellSmith

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions