-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Description
Assume we have an instance pc of System.DirectoryServices.AccountManagement.PrincipalContext initialised with ContextType.Domain. The first time ValidateCredentials(string userName, string password) or ValidateCredentials(string userName, string password, ContextOptions options) is called on pc, a new LdapConnection is created and cached. Depending on the actual value of the parameter options of further invocations of ValidateCredentials(), additional instances of LdapConnection may be added to the cache. The cache is implemented as a hashtable _connCache inside of the internal class CredentialValidator.
All LDAP connections added to this cache are private to the respective instance of PrincipalContext and not shared among different instances of PrincipalContext or CredentialValidator. Moreover, it is impossible to obtain a reference to a cached LDAP connection using public APIs (without reflection).
When an instance of PrincipalContext is disposed, its cached LdapConnections are not used anymore. Consequently, they should be disposed in order to free resources and close the underlying LDAP connections. Unfortunately, the current implementation of PrincipalContext.Dispose() does not dispose them, so the LdapConnections remain open until their finalizers run. This may lead to resource exhaustion in scenarios where an LDAP server has a maximum number of simultaneous LDAP connections.
Reproduction Steps
- Set up an Active Directory Domain Controller in the local network. In the following, we assume that the DC is available under
dc01.corp. - Create a new C# Console Application with the following code:
PrincipalContext principalContext = new PrincipalContext(ContextType.Domain, "dc01.corp");
bool result = principalContext.ValidateCredentials("test", "test");
Console.WriteLine(result);
principalContext.Dispose();
Console.WriteLine("Finished.");- Add a breakpoint to the call of
principalContext.Dispose(). - Build and debug the application. Step over the call of
Dispose(). - Verify that the cached
LdapConnections are not disposed: In the debugger, open Locals >principalContext> Non-Public members >_credValidate> Non-Public members >_connCache> Raw View > Values > Results View >[0]. This instance is of typeLdapConnection. In the debugger, you can see that its member_disposedis set to false, i.e. it has not been disposed.
Expected behavior
The implementation of PrincipalContext.Dispose() should call the Dispose() method on all of the LdapConnections it caches.
Actual behavior
PrincipalContext.Dispose() does not dispose the LdapConnections the CredentialValidator caches.
Regression?
This does not appear to be a regression, because the same behaviour can be observed in all public versions of PrincipalContext in .NET/.NET Core. See for example this version from 2016. Unfortunately, I wasn't able to find PrincipalContext in the reference source of .NET Framework, so I don't know whether the .NET Framework implementation is affected.
Known Workarounds
It is possible to use reflection to close the LdapConnections manually:
// The following code is added immediately before or after the call to principalContext.Dispose(),
// e.g. at the end of the using block of a PrincipalContext instance.
if (Environment.Version >= new Version(3, 0) && Environment.Version <= new Version(6, 0))
{
FieldInfo? fieldCredValidate = typeof(PrincipalContext).GetField("_credValidate", BindingFlags.NonPublic | BindingFlags.Instance);
object? credValidate = fieldCredValidate?.GetValue(principalContext);
FieldInfo? fieldConnCache = credValidate?.GetType().GetField("_connCache", BindingFlags.NonPublic | BindingFlags.Instance);
Hashtable? connCache = fieldConnCache?.GetValue(credValidate) as Hashtable;
if (connCache is not null)
{
foreach (LdapConnection connection in connCache.Values)
{
connection.Dispose();
}
}
}While this works, this workaround should be used with caution and only temporarily because it accesses implementation details of the framework that are not intended to be accessed outside of the implementation of PrincipalContext.
Configuration
- Versions: I reproduced this issue on .NET 6.0 and nightly builds of .NET 7.0. However, I assume that all versions of .NET Core are affected.
- OS: Windows 11 Pro Insider Preview Version 10.0.22504.1010
- Architecture: x64
- This issue is not specific to this configuration.
Other information
I wrote a fix for this and will send a PR in the next few minutes.