-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Excessive memory usage while working with X509Certificate (Linux, NET 5 and NET Core 3.1). #55672
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq, @GrabYourPitchforks Issue DetailsDescriptionWe found an excessive memory usage in Linux containers. Every time we make a call which goes all way down to OpenSSL process working set grows up. The chart below shows working set and managed heap size for the test application. As you can see working set grows up to 1.8 GB and then stabilizes at this point. At the same time the managed heap size remains very small. Test application is available here: ConfigurationLinux container based on OR Linux container based on Host: any compatible. Regression?The problem doesn't exist on Windows.
|
Since this is reported against 3.1 and 5, and 6 is already locking down, I've assigned it to 7. Since it goes stable I don't think there's a leak, per se, but we might be throwing things off to the finalizer queue and it is deciding to not run since there's still more memory available. Or it's something about OpenSSL's memory management. |
Would it be possible for you to try with |
Tested it with NET 6, the behavior is exactly the same. |
Thanks for checking and letting us know. |
Using this loop using (X509Certificate2 cert = new X509Certificate2(path))
{
for (int iter = 0; iter < Iterations; iter++)
{
if ((iter & mask) == 0 || iter == 1)
{
Console.Write($"At iteration {iter}: ");
ShowMem(ref lastMem);
}
X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
bool result = chain.Build(cert);
chain.Dispose();
if (!result)
{
throw new InvalidOperationException();
}
}
} tracking WorkingSet64 shows
Indicating that things eventually stabilize (matching the chart above, but at much lower numbers). We get a big number at 0 because that woke up almost all of the .NET crypto stack, including waking up OpenSSL. We get another big number at 1 because the first chain build woke up the X509Chain system and our caches for building chains. Since we're effectively done growing by 4096, let's zoom in.
So there's definitely some finalization going on, which seems to happen in fits and spurts. While some could be caused by general background operations, it's probably something we're doing. Looking at our loop again, we call chain.Build, and then move on. It turns out that chain.Build populates the chain.ChainElements collection, and when it does so it creates new certificate objects for each element. Disposing the chain is goodness, but it doesn't free those elements, since you could have already saved a reference to any of them. So let's change our loop. using (X509Certificate2 cert = new X509Certificate2(path))
{
for (int iter = 0; iter < Iterations; iter++)
{
if ((iter & mask) == 0 || iter == 1)
{
Console.Write($"At iteration {iter}: ");
ShowMem(ref lastMem);
}
X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
bool result = chain.Build(cert);
if (dispose)
{
foreach (X509ChainElement element in chain.ChainElements)
{
element.Certificate.Dispose();
}
}
if (!result)
{
throw new InvalidOperationException();
}
chain.Dispose();
}
} And run the test again with dispose=true
That didn't significantly change our total memory profile, but does change the number of objects that were hanging around. So, what does explain your graph, then? Well, let's run the app with the environment variable
and with disposing
Wow, unbounded growth over that range! So... let's change our loop. using (X509Certificate2 cert = new X509Certificate2(path))
{
for (int iter = 0; iter < Iterations; iter++)
{
if ((iter & mask) == 0 || iter == 1)
{
Console.Write($"At iteration {iter}: ");
ShowMem(ref lastMem);
}
byte[] buf = new byte[1024];
if (buf[0] != 0)
{
throw new InvalidOperationException();
}
}
} Turns out, that's "unbounded", too:
Server GC is pretty lazy. As long as it can find more memory it doesn't like to clean things up. The problem as described can be attributed mostly to running with server GC. The application disposing the certificates that come back from the chain build will help make the native portion of memory more predictable (and reduce finalization stalls). There's probably room for improvement in the X509 stack to reduce the use of temporary buffers; but it's not something that we'd keep a major tracking issue on. |
Description
We found an excessive memory usage in Linux containers. Every time we make a call which goes all way down to OpenSSL process working set grows up. The chart below shows working set and managed heap size for the test application. As you can see working set grows up to 1.8 GB and then stabilizes at this point. At the same time the managed heap size remains very small.
Test application is available here:
https://github.com/serpop/CertMemLeak
Configuration
Linux container based on
mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim
image (NET 5, x64).OR
Linux container based on
mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim
image (NET Core 3.1, x64)Host: any compatible.
Regression?
The problem doesn't exist on Windows.
The text was updated successfully, but these errors were encountered: