Skip to content

Commit a980176

Browse files
committed
libclamav: cache EVP_MD handles to fix per-hash provider-fetch churn and a FIPS memory leak
On OpenSSL 3, the hashing helpers (cl_hash_init, cl_hash_data, cl_hash_file_fd and their _ex variants) created a fresh OSSL_LIB_CTX and called EVP_MD_fetch() on every hash, then tore both down. A freshly-allocated library context has no method-store cache, so each fetch pays the full provider-bootstrap + method-construction cost (on the order of hundreds of microseconds), and this runs for md5/sha1/sha2-256 on essentially every scanned object (whole-file and section hashes plus the SHA2-256 clean-cache lookup in fmap_get_hash). The per-object hashing plumbing ends up costing far more than the hashing itself, whether or not the host is in FIPS mode. Fetch each FIPS-bypass digest once into a single shared, long-lived non-FIPS OSSL_LIB_CTX and reuse it (a fetched EVP_MD is reference counted and safe to share across threads; the cache is guarded by a mutex). The FIPS-bypass semantics are unchanged -- the shared context is still created with OSSL_LIB_CTX_new() and digests are still fetched with the "-fips" property, so MD5/SHA1 signature hashing still works on FIPS hosts where it can. The FIPS-compliant (non-bypass) path is intentionally not cached: it is still fetched from the default library context on every call so it keeps honoring the current default property query. This also removes the per-hash OSSL_LIB_CTX_new(), which fixes a memory leak. The previous code allocated the context before the fetch and did not free it on the "md == NULL" return path, so on a host whose FIPS policy makes an algorithm unavailable (e.g. MD5), every hashed object leaked an OSSL_LIB_CTX. Because libclamav hashes MD5 for nearly every scanned file, a full scan grew without bound until it was OOM-killed. With a single shared context there is nothing to leak per hash; an unavailable algorithm now degrades to a clean, bounded failure instead of exhausting memory.
1 parent a937323 commit a980176

1 file changed

Lines changed: 99 additions & 66 deletions

File tree

libclamav/crypto.c

Lines changed: 99 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@
6868
#include "str.h"
6969
#include "iowrap.h"
7070

71+
#ifdef CL_THREAD_SAFE
72+
#include <pthread.h>
73+
#endif
74+
7175
#if defined(_WIN32)
7276
char *strptime(const char *buf, const char *fmt, struct tm *tm);
7377
#endif
@@ -162,6 +166,95 @@ void cl_cleanup_crypto(void)
162166
return;
163167
}
164168

169+
#if OPENSSL_VERSION_MAJOR >= 3
170+
/*
171+
* Digest handle cache.
172+
*
173+
* On OpenSSL 3, EVP_MD_fetch() into a freshly-allocated OSSL_LIB_CTX is
174+
* expensive: the empty context must bootstrap a provider and construct the
175+
* method from scratch (on the order of hundreds of microseconds). ClamAV
176+
* hashes md5/sha1/sha2-256 for essentially every scanned object, so fetching
177+
* per hash would dominate scan time while doing no actual hashing.
178+
*
179+
* Fetch each FIPS-bypass digest once into a single shared non-FIPS library
180+
* context and reuse it (a fetched EVP_MD is reference counted and safe to share
181+
* across threads). cli_get_md() returns a reference the caller owns and must
182+
* release with EVP_MD_free().
183+
*
184+
* Only the bypass digests are cached. The FIPS-compliant path is fetched from
185+
* the default library context on every call, so it keeps honoring the current
186+
* default property query -- e.g. if the embedding process enables FIPS at
187+
* runtime, a non-bypass MD5/SHA1 request must start failing rather than return
188+
* a stale non-FIPS handle. That default-context fetch is already inexpensive
189+
* because OpenSSL caches provider methods per library context.
190+
*/
191+
#ifdef CL_THREAD_SAFE
192+
static pthread_mutex_t md_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
193+
#endif
194+
195+
/* Single shared non-FIPS library context backing all "-fips" (bypass) fetches. */
196+
static OSSL_LIB_CTX *nonfips_libctx = NULL;
197+
198+
static struct {
199+
const char *alg; /* string literal returned by to_openssl_alg(); stable */
200+
EVP_MD *md;
201+
} md_cache[8];
202+
static size_t md_cache_len = 0;
203+
204+
static EVP_MD *cli_get_md(const char *alg, int fips_bypass)
205+
{
206+
const char *ossl_alg = to_openssl_alg(alg);
207+
EVP_MD *result = NULL;
208+
size_t i;
209+
210+
if (NULL == ossl_alg) {
211+
return NULL;
212+
}
213+
214+
if (!fips_bypass) {
215+
/* FIPS-compliant path: fetch from the default library context every
216+
* time so the current default property query -- including any runtime
217+
* FIPS change -- is honored. */
218+
return EVP_MD_fetch(NULL, ossl_alg, NULL);
219+
}
220+
221+
#ifdef CL_THREAD_SAFE
222+
pthread_mutex_lock(&md_cache_mutex);
223+
#endif
224+
225+
for (i = 0; i < md_cache_len; i++) {
226+
if (0 == strcmp(md_cache[i].alg, ossl_alg)) {
227+
result = md_cache[i].md;
228+
goto done;
229+
}
230+
}
231+
232+
/* Miss: create the shared non-FIPS context on first use and fetch once. */
233+
if (NULL == nonfips_libctx) {
234+
nonfips_libctx = OSSL_LIB_CTX_new();
235+
if (NULL == nonfips_libctx) {
236+
cli_errmsg("cli_get_md: Failed to create OpenSSL library context\n");
237+
goto done;
238+
}
239+
}
240+
result = EVP_MD_fetch(nonfips_libctx, ossl_alg, "-fips");
241+
if (NULL != result && md_cache_len < sizeof(md_cache) / sizeof(md_cache[0])) {
242+
md_cache[md_cache_len].alg = ossl_alg;
243+
md_cache[md_cache_len].md = result; /* cache holds this reference */
244+
md_cache_len++;
245+
}
246+
247+
done:
248+
if (NULL != result) {
249+
EVP_MD_up_ref(result); /* give the caller its own reference to free */
250+
}
251+
#ifdef CL_THREAD_SAFE
252+
pthread_mutex_unlock(&md_cache_mutex);
253+
#endif
254+
return result;
255+
}
256+
#endif /* OPENSSL_VERSION_MAJOR >= 3 */
257+
165258
/**
166259
* @brief Generate a hash of data.
167260
*
@@ -218,20 +311,7 @@ extern cl_error_t cl_hash_data_ex(
218311
}
219312

220313
#if OPENSSL_VERSION_MAJOR >= 3
221-
if (flags & CL_HASH_FLAG_FIPS_BYPASS) {
222-
/* Bypass FIPS restrictions the OpenSSL 3.0 way */
223-
ossl_ctx = OSSL_LIB_CTX_new();
224-
if (NULL == ossl_ctx) {
225-
cli_errmsg("cl_hash_data_ex: Failed to create new OpenSSL library context\n");
226-
status = CL_EMEM;
227-
goto done;
228-
}
229-
230-
md = EVP_MD_fetch(ossl_ctx, to_openssl_alg(alg), "-fips");
231-
} else {
232-
/* Use FIPS compliant algorithms */
233-
md = EVP_MD_fetch(NULL, to_openssl_alg(alg), NULL);
234-
}
314+
md = cli_get_md(alg, (flags & CL_HASH_FLAG_FIPS_BYPASS) ? 1 : 0);
235315
#else
236316
md = EVP_get_digestbyname(to_openssl_alg(alg));
237317
#endif
@@ -369,20 +449,7 @@ extern cl_error_t cl_hash_init_ex(
369449
}
370450

371451
#if OPENSSL_VERSION_MAJOR >= 3
372-
if (flags & CL_HASH_FLAG_FIPS_BYPASS) {
373-
/* Bypass FIPS restrictions the OpenSSL 3.0 way */
374-
ossl_ctx = OSSL_LIB_CTX_new();
375-
if (NULL == ossl_ctx) {
376-
cli_errmsg("cl_hash_data_ex: Failed to create new OpenSSL library context\n");
377-
status = CL_EMEM;
378-
goto done;
379-
}
380-
381-
md = EVP_MD_fetch(ossl_ctx, to_openssl_alg(alg), "-fips");
382-
} else {
383-
/* Use FIPS compliant algorithms */
384-
md = EVP_MD_fetch(NULL, to_openssl_alg(alg), NULL);
385-
}
452+
md = cli_get_md(alg, (flags & CL_HASH_FLAG_FIPS_BYPASS) ? 1 : 0);
386453
#else
387454
md = EVP_get_digestbyname(to_openssl_alg(alg));
388455
#endif
@@ -641,20 +708,7 @@ extern cl_error_t cl_hash_file_fd_ex(
641708
}
642709

643710
#if OPENSSL_VERSION_MAJOR >= 3
644-
if (flags & CL_HASH_FLAG_FIPS_BYPASS) {
645-
/* Bypass FIPS restrictions the OpenSSL 3.0 way */
646-
ossl_ctx = OSSL_LIB_CTX_new();
647-
if (NULL == ossl_ctx) {
648-
cli_errmsg("cl_hash_data_ex: Failed to create new OpenSSL library context\n");
649-
status = CL_EMEM;
650-
goto done;
651-
}
652-
653-
md = EVP_MD_fetch(ossl_ctx, to_openssl_alg(alg), "-fips");
654-
} else {
655-
/* Use FIPS compliant algorithms */
656-
md = EVP_MD_fetch(NULL, to_openssl_alg(alg), NULL);
657-
}
711+
md = cli_get_md(alg, (flags & CL_HASH_FLAG_FIPS_BYPASS) ? 1 : 0);
658712
#else
659713
md = EVP_get_digestbyname(to_openssl_alg(alg));
660714
#endif
@@ -807,14 +861,7 @@ unsigned char *cl_hash_data(const char *alg, const void *buf, size_t len, unsign
807861
#endif
808862

809863
#if OPENSSL_VERSION_MAJOR >= 3
810-
/* Bypass FIPS restrictions the OpenSSL 3.0 way */
811-
ossl_ctx = OSSL_LIB_CTX_new();
812-
if (NULL == ossl_ctx) {
813-
cli_errmsg("cl_hash_data_ex: Failed to create new OpenSSL library context\n");
814-
return NULL;
815-
}
816-
817-
md = EVP_MD_fetch(ossl_ctx, to_openssl_alg(alg), "-fips");
864+
md = cli_get_md(alg, 1);
818865
#else
819866
md = EVP_get_digestbyname(to_openssl_alg(alg));
820867
#endif
@@ -948,14 +995,7 @@ unsigned char *cl_hash_file_fd(int fd, const char *alg, unsigned int *olen)
948995
unsigned char *res;
949996

950997
#if OPENSSL_VERSION_MAJOR >= 3
951-
/* Bypass FIPS restrictions the OpenSSL 3.0 way */
952-
ossl_ctx = OSSL_LIB_CTX_new();
953-
if (NULL == ossl_ctx) {
954-
cli_errmsg("cl_hash_data_ex: Failed to create new OpenSSL library context\n");
955-
return NULL;
956-
}
957-
958-
md = EVP_MD_fetch(ossl_ctx, to_openssl_alg(alg), "-fips");
998+
md = cli_get_md(alg, 1);
959999
#else
9601000
md = EVP_get_digestbyname(to_openssl_alg(alg));
9611001
#endif
@@ -1852,14 +1892,7 @@ void *cl_hash_init(const char *alg)
18521892
#endif
18531893

18541894
#if OPENSSL_VERSION_MAJOR >= 3
1855-
/* Bypass FIPS restrictions the OpenSSL 3.0 way */
1856-
ossl_ctx = OSSL_LIB_CTX_new();
1857-
if (NULL == ossl_ctx) {
1858-
cli_errmsg("cl_hash_data_ex: Failed to create new OpenSSL library context\n");
1859-
return NULL;
1860-
}
1861-
1862-
md = EVP_MD_fetch(ossl_ctx, to_openssl_alg(alg), "-fips");
1895+
md = cli_get_md(alg, 1);
18631896
#else
18641897
md = EVP_get_digestbyname(to_openssl_alg(alg));
18651898
#endif

0 commit comments

Comments
 (0)