Digest allocates an EVP_MD_CTX in every constructor and frees it in exactly one place: inside final(). There is no _final anywhere in ssl/crypto, and no dispose. _ctx is a let field, so it cannot be nulled. A Digest that is created and dropped without calling final() leaks its OpenSSL context.
This is not an error path. It is what happens when a caller builds a digest, appends to it, and then abandons it — because the request was cancelled, because an error was raised, or because the code simply stopped early.
Measurement
Dropping Digest objects without calling final(), with the work chunked across behaviors so Pony collects the Pony objects each chunk. Peak RSS:
|
dropped digests |
peak RSS |
calls final() |
2,000,000 |
7,424 KB |
drops without final() |
500,000 |
108,416 KB |
drops without final() |
2,000,000 |
413,056 KB |
The control stays flat. The leak grows linearly, about 207 bytes per dropped digest, which is EVP_MD_CTX plus the SHA-256 state it points at.
Scope
Digest alone. It is the only type in ssl/crypto that holds a C resource across calls. HmacSha256, Pbkdf2Sha256, RandBytes and the HashFn primitives each make a single one-shot call that allocates nothing lasting. SSL and SSLContext both have finalizers.
Fix direction
A _final cannot simply free _ctx, because final() already freed it on the normal path and _ctx is let and cannot be nulled. The finalizer has to know whether final() ran. Freeing in _final only when the digest has not been finalized would do it, which means _ctx becomes var and gets nulled by final(), the same shape SSL.dispose uses.
Digestallocates anEVP_MD_CTXin every constructor and frees it in exactly one place: insidefinal(). There is no_finalanywhere inssl/crypto, and nodispose._ctxis aletfield, so it cannot be nulled. ADigestthat is created and dropped without callingfinal()leaks its OpenSSL context.This is not an error path. It is what happens when a caller builds a digest, appends to it, and then abandons it — because the request was cancelled, because an error was raised, or because the code simply stopped early.
Measurement
Dropping
Digestobjects without callingfinal(), with the work chunked across behaviors so Pony collects the Pony objects each chunk. Peak RSS:final()final()final()The control stays flat. The leak grows linearly, about 207 bytes per dropped digest, which is
EVP_MD_CTXplus the SHA-256 state it points at.Scope
Digestalone. It is the only type inssl/cryptothat holds a C resource across calls.HmacSha256,Pbkdf2Sha256,RandBytesand theHashFnprimitives each make a single one-shot call that allocates nothing lasting.SSLandSSLContextboth have finalizers.Fix direction
A
_finalcannot simply free_ctx, becausefinal()already freed it on the normal path and_ctxisletand cannot be nulled. The finalizer has to know whetherfinal()ran. Freeing in_finalonly when the digest has not been finalized would do it, which means_ctxbecomesvarand gets nulled byfinal(), the same shapeSSL.disposeuses.