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
add hooks to debug OpenSSL memory #101626
base: main
Are you sure you want to change the base?
Conversation
It looks like the build is failing because we are trying to build agains OpenSSL 1.0 that is EOS since 2019.
I was tempted to simply disable the debug feature for that version as the very old OpenSSL has different prototype. Any thoughts @bartonjs on moving the build to at least 1.1.1 that is EOS only since last year and some distributions we support are still using it? There are probably different ways how to solve the build problems but I feel it is perhaps finally time to ditch 1.0. |
We build against Ubuntu 16.04 headers and libs currently #83428 . It is likely going to stay that way for .NET 9. |
What is reason for it @jkotas? It seems like even 8.0 does not support 16.04: https://github.com/dotnet/core/blob/main/release-notes/8.0/supported-os.md |
It is same deal as Windows 7. It is not supported, but we avoid intentionally breaking it to help some important customers. |
...raries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Initialization.cs
Outdated
Show resolved
Hide resolved
...raries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Initialization.cs
Outdated
Show resolved
Hide resolved
...raries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Initialization.cs
Outdated
Show resolved
Hide resolved
...raries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Initialization.cs
Outdated
Show resolved
Hide resolved
OpenSSL version support is not as simple as the OpenSSL support policy. Distros will continue to use EOL versions of OpenSSL but backport fixes under their own LTS support policy. I don't think .NET actually "officially" supports any Linux distros with 1.0.2 anymore. However 1.0.2k/g has played an important role far past its 2019 EOL and there are a number of Linux distros that still support it today. |
That said, since this is a diagnostic feature, I don't know that it makes sense to go through any particular lengths to get it working with 1.0. |
...on/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.Initialization.cs
Outdated
Show resolved
Hide resolved
malloc/free can be used in more places than it is ok to run managed code. For example, you can use malloc/free in thread detach callback, but running managed code in thread detach callback is not safe/reliable. What's our confidence level that OpenSSL only ever calls malloc/free in places where it is safe to run managed code? At minimum, this should get a full CI run with this instrumentation unconditionally enabled to see whether it is going to hit any crashes. |
That is new in 3.0x and related to providers ... like FIPS. I can explore more. Is it not safe to use just the allocation functions or any managed code @jkotas? And I used generic tools before. They are difficult to use as it is difficult to focus only one particular part ... like SSL ... and tracking all runtime allocations is expensive. And my intention goes beyond just leaks. We have tight integration with OpenSSL and it would be good to havr more insight to what is happening inside. And the available hooks provide additional and useful information. I was originally thinking about make it available only in debug builds. That would certainly limit the scope of troubles. |
Any managed code. |
I think the logging is simple enough to be written in C and placed into the openssl PAL. That should make it easy to get rid of the concerns raised here. We can possibly make it even simpler, just writing the log entries into a log file and creating a simple tool to analyze it instead of processing the information in the runtime. |
...raries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Initialization.cs
Outdated
Show resolved
Hide resolved
Agreed. That said, I have a distilled version of the same concept (https://gist.github.com/filipnavara/7bf3791fb795266fe46f4383a075423c) deployed on a .NET 8 app (doesn't care about OpenSSL versioning since it's a one trick pony for diagnosing specific issue in a specific environment). It's likely possible to have that in separate library but it would come with additional complexities if the allocation callbacks needed to be written in C and compiled for multiple platforms. Having the C code in runtime takes care of this tricky part.
I'm not sure that would cut it. The app where we would use this diagnostic is operating on a scale where the allocations are multiple gigabytes at any given time and the allocation/free events are frequent, so summary information is what we need. Logging the calls [even in a compressed formats] would produce way too much information. |
Thank you for sharing this example. I like the flexibility of including it as source in the app. For example, you can monitor the total crypto memory consumption via performance counter using your existing monitoring solution as this example shows; you can add logging of stacktraces and only for certain allocation if you need that to diagnose them problem; etc. |
Since it is managed code the callbacks will have same safety problem, right @jkotas? I was thinking about similar solution @janvorli suggested - get the basic counters and hooks in c to make them safe. And find better way how to expose the details - either as event source, plain file writes as @janvorli suggested or whatever we agree to. |
one more note that the hooks are sensitive to the fact that it needs to be done before any allocations. That is not problem for the simple console app but it may be difficult for Kestrel where many things may happen IMHO before user code runs. |
Yes - if the app usage pattern results into OpenSSL calling malloc/free in places where it is not safe to run managed code.
It can be problem even for a console app - if some code in the app (e.g. 3rd party library) ends initializing OpenSSL before we get a chance to initialize it. |
Correct. We didn't encounter any issue in production with the managed callbacks. That said, we definitely use only subset of the OpenSSL functionality (a couple of crypto ciphers, Kestrel, SslStream, HttpClient).
We didn't have any problem injecting it early enough in the startup in the app that uses Kestrel.
Initial results from our experiment show that we don't see any OpenSSL related leaks. We still observe working set growing over time even if it's unaccounted for in the GC heap or other .NET heaps (eg. compiled code). Notably these graphs don't make the original issue entirely obvious. They just show that OpenSSL memory seems to stay at stable levels. Each instance has a HTTP endpoint for registration and then runs a large number of pollers that check for new email messages over variety of protocols (IMAP, Exchange Web Services, etc.) on numerous servers. We have a mechanism that rebalances all the polling registrations to a different instance so we should end up basically in the same "empty" state as on startup. When we do this and force a GC we see the managed heap going down but the working set never returns to the initial levels and it exceeds it by gigabyte(s). |
Sorry for off-topic
@filipnavara recently I have seen high memory usage due to lots of memory buffers being cached by malloc internally, see #101552 |
Debug.Assert(size == entry[0].Size); | ||
lock (_allocations!) | ||
{ | ||
_allocations!.Add(ptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understood the original comment from @jkotas, then this is still a potential problem
Or does that hold only for the malloc/free calls and not GC-allocated memory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not safe to use any managed code if this can be called from places like thread destructor. #101626 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right. I split the change now into two parts. The basic counters are implemented in native as @janvorli suggested. That also eliminates need to fiddle with the crypto initialization.
Now for the managed part. I made this whole section #if DEBUG
for now to limit the exposure. While this limits use in production it would allow us to experiment more and perhaps hook it to test runs. I'm yet to see case where it actually fails. Since this can be set during run for some particular operation(s) it may avoid the cases we are concern about e.g. threads operations. AFAIK there is API to get loaded providers so we may for example check FIPS or 3rd party modules.
src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs
Outdated
Show resolved
Hide resolved
src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs
Outdated
Show resolved
Hide resolved
OT: Turns out this was immensely useful hint. We added tracking of the |
Free = 3, | ||
} | ||
|
||
private static readonly unsafe nuint Offset = (nuint)sizeof(MemoryEntry); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private static readonly unsafe nuint Offset = (nuint)sizeof(MemoryEntry); | |
private static unsafe nuint Offset => (nuint)sizeof(MemoryEntry); |
This does not need to be cached in a static
src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs
Outdated
Show resolved
Hide resolved
@@ -29,6 +29,7 @@ static OpenSsl() | |||
|
|||
internal static partial class CryptoInitializer | |||
{ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -41,6 +42,7 @@ static CryptoInitializer() | |||
// these libraries will be unable to operate correctly. | |||
throw new InvalidOperationException(); | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#pragma warning disable CA1823 | ||
private static readonly bool MemoryDebug = GetMemoryDebug(); | ||
#pragma warning restore CA1823 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the field is not used anywhere, can we move the call to GetMemoryDebug
to cctor?
struct memoryEntry | ||
{ | ||
int size; | ||
int line; | ||
const char* file; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did we consider the alignment of this on 32-bit platforms? As jkotas said in one of the comments, this has 12B on 32 bit platforms, so after adjusting the returned ptr will not be aligned on 8B boundary.
According to the docs, OpenSSL has OPENSSL_aligned_malloc, which it probably uses internally when it matters, but we should still verify that this does not cause any problems.
@@ -173,7 +171,9 @@ static void DetectCiphersuiteConfiguration(void) | |||
#endif | |||
} | |||
|
|||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
void CryptoNative_EnsureLibSslInitialized(void) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -130,6 +130,7 @@ typedef void (*SslCtxSetKeylogCallback)(const SSL* ssl, const char *line); | |||
/* | |||
Ensures that libssl is correctly initialized and ready to use. | |||
*/ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
private static readonly unsafe nuint Offset = (nuint)sizeof(MemoryEntry); | ||
private static HashSet<UIntPtr>? _allocations; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using ConcurrentDictionary<UIntPtr, something> would allow us to remove some of the locks and reduce contention.
We had several cases when users complained about large memory use. For than native it is quite difficult to figure out where the memory goes. This PR aims to make that somewhat easier.
OpenSSL provides hooks for memory function so this PR adds switch to optimally hook into that.
The only one caveat that the
CRYPTO_set_mem_functions
works only if called before any allocations e.g. it needs to be done very early in the process. So I end up putting into initialization process .... even if I originally envisioned it somewhere else.The simple use pattern is something like
That provides insight how much memory is actually used by OpenSSL.
It allocates little bit more memory to store extra info but it should be reasonably cheap.
If somebody cares about more details they can do something like
this would provide something like
dumping large allocation data set is slow and expensive. It is done under local so it blocks all other OpenSSL allocations. I feel this is ok for now but it should be used with caution. I also feel that access through Reflection is OK since this is only last resort debug hook e.g. it does not need stable API and convenient access.