-
Notifications
You must be signed in to change notification settings - Fork 485
Description
@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:
Core/src/Castle.Core/DynamicProxy/Generators/BaseProxyGenerator.cs
Lines 398 to 423 in 6959806
| // 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:
Core/src/Castle.Core/DynamicProxy/Generators/NamingScope.cs
Lines 39 to 54 in 6959806
| 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