Skip to content

Remove unneccesary allocations for hash structs and hmac #273

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

Merged
merged 14 commits into from
Apr 24, 2025
Merged
8 changes: 5 additions & 3 deletions hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ type evpHash struct {
// the state of ctx. Having it here allows reusing the
// same allocated object multiple times.
ctx2 ossl.EVP_MD_CTX_PTR
out [maxHashSize]byte
}

func newEvpHash(ch crypto.Hash) *evpHash {
Expand Down Expand Up @@ -351,12 +352,13 @@ func (h *evpHash) BlockSize() int {

func (h *evpHash) Sum(in []byte) []byte {
h.init()
out := make([]byte, h.Size(), maxHashSize) // explicit cap to allow stack allocation
if err := ossl.HashSum(h.ctx, h.ctx2, out); err != nil {
tmp := h.out[:h.Size()] // Create slice view
clear(tmp)
if err := ossl.HashSum(h.ctx, h.ctx2, tmp); err != nil {
panic(err)
}
runtime.KeepAlive(h)
return append(in, out...)
return append(in, tmp...)
}

// Clone returns a new evpHash object that is a deep clone of itself.
Expand Down
61 changes: 50 additions & 11 deletions hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,29 @@ func TestCgo(t *testing.T) {
openssl.SHA256(d.Data[:])
}

func verifySHA256(token, salt string) [32]byte {
return openssl.SHA256([]byte(token + salt))
}

func TestIssue71943(t *testing.T) {
// https://github.com/golang/go/issues/71943
if Asan() {
t.Skip("skipping allocations test with sanitizers")
}
n := int(testing.AllocsPerRun(10, func() {
runtime.KeepAlive(verifySHA256("teststring", "test"))
}))
want := 2
if compareCurrentVersion("go1.25") >= 0 {
want = 0
} else if compareCurrentVersion("go1.24") >= 0 {
want = 1
}
if n > want {
t.Errorf("allocs = %d, want %d", n, want)
}
}

func TestHashAllocations(t *testing.T) {
if Asan() {
t.Skip("skipping allocations test with sanitizers")
Expand All @@ -385,23 +408,39 @@ func TestHashAllocations(t *testing.T) {
}
}

func verifySHA256(token, salt string) [32]byte {
return openssl.SHA256([]byte(token + salt))
}

func TestIssue71943(t *testing.T) {
// https://github.com/golang/go/issues/71943
func TestHashStructAllocations(t *testing.T) {
if Asan() {
t.Skip("skipping allocations test with sanitizers")
}
msg := []byte("testing")

sha1Hash := openssl.NewSHA1()
sha224Hash := openssl.NewSHA1()
sha256Hash := openssl.NewSHA256()
sha512Hash := openssl.NewSHA512()

sum := make([]byte, sha512Hash.Size())
n := int(testing.AllocsPerRun(10, func() {
runtime.KeepAlive(verifySHA256("teststring", "test"))
sha1Hash.Write(msg)
sha224Hash.Write(msg)
sha256Hash.Write(msg)
sha512Hash.Write(msg)

sha1Hash.Sum(sum[:0])
sha224Hash.Sum(sum[:0])
sha256Hash.Sum(sum[:0])
sha512Hash.Sum(sum[:0])

sha1Hash.Reset()
sha224Hash.Reset()
sha256Hash.Reset()
sha512Hash.Reset()
}))
want := 2
if compareCurrentVersion("go1.25") >= 0 {
want := 4
if compareCurrentVersion("go1.24") >= 0 {
// The go1.24 compiler is able to optimize the allocation away.
// See cgo_go124.go for more information.
want = 0
} else if compareCurrentVersion("go1.24") >= 0 {
want = 1
}
if n > want {
t.Errorf("allocs = %d, want %d", n, want)
Expand Down
13 changes: 4 additions & 9 deletions hmac.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ type opensslHMAC struct {
ctx3 hmacCtx3
size int
blockSize int
sum []byte
sum [maxHashSize]byte
}

func newHMAC1(key []byte, md ossl.EVP_MD_PTR) hmacCtx1 {
Expand Down Expand Up @@ -180,7 +180,6 @@ func (h *opensslHMAC) Reset() {
}

runtime.KeepAlive(h) // Next line will keep h alive too; just making doubly sure.
h.sum = nil
}

func (h *opensslHMAC) finalize() {
Expand Down Expand Up @@ -216,10 +215,6 @@ func (h *opensslHMAC) BlockSize() int {
}

func (h *opensslHMAC) Sum(in []byte) []byte {
if h.sum == nil {
size := h.Size()
h.sum = make([]byte, size)
}
// Make copy of context because Go hash.Hash mandates
// that Sum has no effect on the underlying stream.
// In particular it is OK to Sum, then Write more, then Sum again,
Expand All @@ -234,16 +229,16 @@ func (h *opensslHMAC) Sum(in []byte) []byte {
if _, err := ossl.HMAC_CTX_copy(ctx2, h.ctx1.ctx); err != nil {
panic(err)
}
ossl.HMAC_Final(ctx2, base(h.sum), nil)
ossl.HMAC_Final(ctx2, base(h.sum[:h.size]), nil)
case 3:
ctx2, err := ossl.EVP_MAC_CTX_dup(h.ctx3.ctx)
if err != nil {
panic(err)
}
defer ossl.EVP_MAC_CTX_free(ctx2)
ossl.EVP_MAC_final(ctx2, base(h.sum), nil, len(h.sum))
ossl.EVP_MAC_final(ctx2, base(h.sum[:h.size]), nil, len(h.sum))
default:
panic(errUnsupportedVersion())
}
return append(in, h.sum...)
return append(in, h.sum[:h.size]...)
}
21 changes: 21 additions & 0 deletions hmac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@ func TestHMACUnsupportedHash(t *testing.T) {
}
}

func TestHMACAllocations(t *testing.T) {
h := openssl.NewHMAC(openssl.NewSHA256, nil)
msg := []byte("hello world")
sum := make([]byte, openssl.NewSHA256().Size())
n := int(testing.AllocsPerRun(10, func() {
h.Write(msg)
h.Sum(sum[:0])
h.Reset()
}))

want := 2
if compareCurrentVersion("go1.24") >= 0 {
// The go1.24 compiler is able to optimize the allocation away.
// See cgo_go124.go for more information.
want = 0
}
if n > want {
t.Errorf("allocs = %d, want %d", n, want)
}
}

func BenchmarkHMACSHA256_32(b *testing.B) {
b.StopTimer()
key := make([]byte, 32)
Expand Down
4 changes: 2 additions & 2 deletions internal/ossl/shims.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,13 @@ const _EVP_MD_PTR EVP_sha3_512(void) __attribute__((tag("111"),noerror));

_EVP_MD_CTX_PTR EVP_MD_CTX_new(void);
void EVP_MD_CTX_free(_EVP_MD_CTX_PTR ctx);
int EVP_MD_CTX_copy(_EVP_MD_CTX_PTR out, const _EVP_MD_CTX_PTR in);
int EVP_MD_CTX_copy(_EVP_MD_CTX_PTR out, const _EVP_MD_CTX_PTR in) __attribute__((noescape,nocallback));
int EVP_MD_CTX_copy_ex(_EVP_MD_CTX_PTR out, const _EVP_MD_CTX_PTR in);
int EVP_Digest(const void *data, size_t count, unsigned char *md, unsigned int *size, const _EVP_MD_PTR type, _ENGINE_PTR impl) __attribute__((noescape,nocallback,nocheckptr("data")));
int EVP_DigestInit_ex(_EVP_MD_CTX_PTR ctx, const _EVP_MD_PTR type, _ENGINE_PTR impl);
int EVP_DigestInit(_EVP_MD_CTX_PTR ctx, const _EVP_MD_PTR type);
int EVP_DigestUpdate(_EVP_MD_CTX_PTR ctx, const void *d, size_t cnt) __attribute__((noescape,nocallback,nocheckptr("d")));
int EVP_DigestFinal_ex(_EVP_MD_CTX_PTR ctx, unsigned char *md, unsigned int *s);
int EVP_DigestFinal_ex(_EVP_MD_CTX_PTR ctx, unsigned char *md, unsigned int *s) __attribute__((noescape,nocallback));
int EVP_DigestSign(_EVP_MD_CTX_PTR ctx, unsigned char *sigret, size_t *siglen, const unsigned char *tbs, size_t tbslen) __attribute__((tag("111"),noescape,nocallback));
int EVP_DigestSignInit(_EVP_MD_CTX_PTR ctx, _EVP_PKEY_CTX_PTR *pctx, const _EVP_MD_PTR type, _ENGINE_PTR e, _EVP_PKEY_PTR pkey);
int EVP_DigestSignFinal(_EVP_MD_CTX_PTR ctx, unsigned char *sig, size_t *siglen);
Expand Down
4 changes: 4 additions & 0 deletions internal/ossl/zossl_go124.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading