Skip to content

Commit

Permalink
[3.12] gh-99108: Release the GIL around hashlib built-in computation (G…
Browse files Browse the repository at this point in the history
…H-104675) (#104776)

gh-99108: Release the GIL around hashlib built-in computation (GH-104675)

This matches the GIL releasing behavior of our existing `_hashopenssl`
module, extending it to the HACL* built-ins.

Includes adding comments to better describe the ENTER/LEAVE macros
purpose and explain the lock strategy in both existing and new code.
(cherry picked from commit 2e5d8a9)

Co-authored-by: Gregory P. Smith [Google] <greg@krypto.org>
  • Loading branch information
miss-islington and gpshead committed May 23, 2023
1 parent e43fbbd commit 9aea1f2
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 15 deletions.
@@ -0,0 +1,3 @@
We now release the GIL around built-in :mod:`hashlib` computations of
reasonable size for the SHA families and MD5 hash functions, matching
what our OpenSSL backed hash computations already does.
6 changes: 6 additions & 0 deletions Modules/_hashopenssl.c
Expand Up @@ -227,12 +227,16 @@ get_hashlib_state(PyObject *module)
typedef struct {
PyObject_HEAD
EVP_MD_CTX *ctx; /* OpenSSL message digest context */
// Prevents undefined behavior via multiple threads entering the C API.
// The lock will be NULL before threaded access has been enabled.
PyThread_type_lock lock; /* OpenSSL context lock */
} EVPobject;

typedef struct {
PyObject_HEAD
HMAC_CTX *ctx; /* OpenSSL hmac context */
// Prevents undefined behavior via multiple threads entering the C API.
// The lock will be NULL before threaded access has been enabled.
PyThread_type_lock lock; /* HMAC context lock */
} HMACobject;

Expand Down Expand Up @@ -896,6 +900,8 @@ py_evp_fromname(PyObject *module, const char *digestname, PyObject *data_obj,

if (view.buf && view.len) {
if (view.len >= HASHLIB_GIL_MINSIZE) {
/* We do not initialize self->lock here as this is the constructor
* where it is not yet possible to have concurrent access. */
Py_BEGIN_ALLOW_THREADS
result = EVP_hash(self, view.buf, view.len);
Py_END_ALLOW_THREADS
Expand Down
9 changes: 8 additions & 1 deletion Modules/hashlib.h
Expand Up @@ -37,6 +37,13 @@
* LEAVE_HASHLIB block or explicitly acquire and release the lock inside
* a PY_BEGIN / END_ALLOW_THREADS block if they wish to release the GIL for
* an operation.
*
* These only drop the GIL if the lock acquisition itself is likely to
* block. Thus the non-blocking acquire gating the GIL release for a
* blocking lock acquisition. The intent of these macros is to surround
* the assumed always "fast" operations that you aren't releasing the
* GIL around. Otherwise use code similar to what you see in hash
* function update() methods.
*/

#include "pythread.h"
Expand All @@ -53,7 +60,7 @@
PyThread_release_lock((obj)->lock); \
}

/* TODO(gps): We should probably make this a module or EVPobject attribute
/* TODO(gpshead): We should make this a module or class attribute
* to allow the user to optimize based on the platform they're using. */
#define HASHLIB_GIL_MINSIZE 2048

37 changes: 34 additions & 3 deletions Modules/md5module.c
Expand Up @@ -49,7 +49,9 @@ typedef long long MD5_INT64; /* 64-bit integer */

typedef struct {
PyObject_HEAD

// Prevents undefined behavior via multiple threads entering the C API.
// The lock will be NULL before threaded access has been enabled.
PyThread_type_lock lock;
Hacl_Streaming_MD5_state *hash_state;
} MD5object;

Expand All @@ -72,6 +74,7 @@ static MD5object *
newMD5object(MD5State * st)
{
MD5object *md5 = (MD5object *)PyObject_GC_New(MD5object, st->md5_type);
md5->lock = NULL;
PyObject_GC_Track(md5);
return md5;
}
Expand All @@ -88,6 +91,9 @@ static void
MD5_dealloc(MD5object *ptr)
{
Hacl_Streaming_MD5_legacy_free(ptr->hash_state);
if (ptr->lock != NULL) {
PyThread_free_lock(ptr->lock);
}
PyTypeObject *tp = Py_TYPE(ptr);
PyObject_GC_UnTrack(ptr);
PyObject_GC_Del(ptr);
Expand Down Expand Up @@ -115,7 +121,9 @@ MD5Type_copy_impl(MD5object *self, PyTypeObject *cls)
if ((newobj = newMD5object(st))==NULL)
return NULL;

ENTER_HASHLIB(self);
newobj->hash_state = Hacl_Streaming_MD5_legacy_copy(self->hash_state);
LEAVE_HASHLIB(self);
return (PyObject *)newobj;
}

Expand All @@ -130,7 +138,9 @@ MD5Type_digest_impl(MD5object *self)
/*[clinic end generated code: output=eb691dc4190a07ec input=bc0c4397c2994be6]*/
{
unsigned char digest[MD5_DIGESTSIZE];
ENTER_HASHLIB(self);
Hacl_Streaming_MD5_legacy_finish(self->hash_state, digest);
LEAVE_HASHLIB(self);
return PyBytes_FromStringAndSize((const char *)digest, MD5_DIGESTSIZE);
}

Expand All @@ -145,7 +155,9 @@ MD5Type_hexdigest_impl(MD5object *self)
/*[clinic end generated code: output=17badced1f3ac932 input=b60b19de644798dd]*/
{
unsigned char digest[MD5_DIGESTSIZE];
ENTER_HASHLIB(self);
Hacl_Streaming_MD5_legacy_finish(self->hash_state, digest);
LEAVE_HASHLIB(self);
return _Py_strhex((const char*)digest, MD5_DIGESTSIZE);
}

Expand Down Expand Up @@ -177,7 +189,18 @@ MD5Type_update(MD5object *self, PyObject *obj)

GET_BUFFER_VIEW_OR_ERROUT(obj, &buf);

update(self->hash_state, buf.buf, buf.len);
if (self->lock == NULL && buf.len >= HASHLIB_GIL_MINSIZE) {
self->lock = PyThread_allocate_lock();
}
if (self->lock != NULL) {
Py_BEGIN_ALLOW_THREADS
PyThread_acquire_lock(self->lock, 1);
update(self->hash_state, buf.buf, buf.len);
PyThread_release_lock(self->lock);
Py_END_ALLOW_THREADS
} else {
update(self->hash_state, buf.buf, buf.len);
}

PyBuffer_Release(&buf);
Py_RETURN_NONE;
Expand Down Expand Up @@ -279,7 +302,15 @@ _md5_md5_impl(PyObject *module, PyObject *string, int usedforsecurity)
return NULL;
}
if (string) {
update(new->hash_state, buf.buf, buf.len);
if (buf.len >= HASHLIB_GIL_MINSIZE) {
/* We do not initialize self->lock here as this is the constructor
* where it is not yet possible to have concurrent access. */
Py_BEGIN_ALLOW_THREADS
update(new->hash_state, buf.buf, buf.len);
Py_END_ALLOW_THREADS
} else {
update(new->hash_state, buf.buf, buf.len);
}
PyBuffer_Release(&buf);
}

Expand Down
37 changes: 34 additions & 3 deletions Modules/sha1module.c
Expand Up @@ -48,7 +48,9 @@ typedef long long SHA1_INT64; /* 64-bit integer */

typedef struct {
PyObject_HEAD

// Prevents undefined behavior via multiple threads entering the C API.
// The lock will be NULL before threaded access has been enabled.
PyThread_type_lock lock;
Hacl_Streaming_SHA1_state *hash_state;
} SHA1object;

Expand All @@ -71,6 +73,7 @@ static SHA1object *
newSHA1object(SHA1State *st)
{
SHA1object *sha = (SHA1object *)PyObject_GC_New(SHA1object, st->sha1_type);
sha->lock = NULL;
PyObject_GC_Track(sha);
return sha;
}
Expand All @@ -88,6 +91,9 @@ static void
SHA1_dealloc(SHA1object *ptr)
{
Hacl_Streaming_SHA1_legacy_free(ptr->hash_state);
if (ptr->lock != NULL) {
PyThread_free_lock(ptr->lock);
}
PyTypeObject *tp = Py_TYPE(ptr);
PyObject_GC_UnTrack(ptr);
PyObject_GC_Del(ptr);
Expand Down Expand Up @@ -115,7 +121,9 @@ SHA1Type_copy_impl(SHA1object *self, PyTypeObject *cls)
if ((newobj = newSHA1object(st)) == NULL)
return NULL;

ENTER_HASHLIB(self);
newobj->hash_state = Hacl_Streaming_SHA1_legacy_copy(self->hash_state);
LEAVE_HASHLIB(self);
return (PyObject *)newobj;
}

Expand All @@ -130,7 +138,9 @@ SHA1Type_digest_impl(SHA1object *self)
/*[clinic end generated code: output=2f05302a7aa2b5cb input=13824b35407444bd]*/
{
unsigned char digest[SHA1_DIGESTSIZE];
ENTER_HASHLIB(self);
Hacl_Streaming_SHA1_legacy_finish(self->hash_state, digest);
LEAVE_HASHLIB(self);
return PyBytes_FromStringAndSize((const char *)digest, SHA1_DIGESTSIZE);
}

Expand All @@ -145,7 +155,9 @@ SHA1Type_hexdigest_impl(SHA1object *self)
/*[clinic end generated code: output=4161fd71e68c6659 input=97691055c0c74ab0]*/
{
unsigned char digest[SHA1_DIGESTSIZE];
ENTER_HASHLIB(self);
Hacl_Streaming_SHA1_legacy_finish(self->hash_state, digest);
LEAVE_HASHLIB(self);
return _Py_strhex((const char *)digest, SHA1_DIGESTSIZE);
}

Expand Down Expand Up @@ -177,7 +189,18 @@ SHA1Type_update(SHA1object *self, PyObject *obj)

GET_BUFFER_VIEW_OR_ERROUT(obj, &buf);

update(self->hash_state, buf.buf, buf.len);
if (self->lock == NULL && buf.len >= HASHLIB_GIL_MINSIZE) {
self->lock = PyThread_allocate_lock();
}
if (self->lock != NULL) {
Py_BEGIN_ALLOW_THREADS
PyThread_acquire_lock(self->lock, 1);
update(self->hash_state, buf.buf, buf.len);
PyThread_release_lock(self->lock);
Py_END_ALLOW_THREADS
} else {
update(self->hash_state, buf.buf, buf.len);
}

PyBuffer_Release(&buf);
Py_RETURN_NONE;
Expand Down Expand Up @@ -279,7 +302,15 @@ _sha1_sha1_impl(PyObject *module, PyObject *string, int usedforsecurity)
return NULL;
}
if (string) {
update(new->hash_state, buf.buf, buf.len);
if (buf.len >= HASHLIB_GIL_MINSIZE) {
/* We do not initialize self->lock here as this is the constructor
* where it is not yet possible to have concurrent access. */
Py_BEGIN_ALLOW_THREADS
update(new->hash_state, buf.buf, buf.len);
Py_END_ALLOW_THREADS
} else {
update(new->hash_state, buf.buf, buf.len);
}
PyBuffer_Release(&buf);
}

Expand Down

0 comments on commit 9aea1f2

Please sign in to comment.