Skip to content

PrincipalContext.Dispose() does not dispose LdapConnection(s) used by PrincipalContext.ValidateCredentials() #62035

@juliushardt

Description

@juliushardt

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

  1. Set up an Active Directory Domain Controller in the local network. In the following, we assume that the DC is available under dc01.corp.
  2. 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.");
  1. Add a breakpoint to the call of principalContext.Dispose().
  2. Build and debug the application. Step over the call of Dispose().
  3. 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 type LdapConnection. In the debugger, you can see that its member _disposed is 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions