Golang is known for its simplicity and reliability—but even solid code can have bugs. This year, a serious vulnerability (CVE-2024-1394) was found in Go’s RSA encryption/decryption code. If you use the github.com/golang-fips/openssl module for cryptography, read on: the bug can let attackers exhaust system resources via memory leaks by sending specially crafted inputs.
This long-read covers what went wrong, how the leak works, and practical steps you can take to avoid this pitfall.
CVE: CVE-2024-1394
- Module: github.com/golang-fips/openssl
- File/line: rsa.go#L113
- Issue: Memory leak in RSA encrypting/decrypting code
What’s the Bug?
The problem is in how Go code handles cleanup of native pointers (pkey and ctx) when errors occur during RSA setup.
Most functions in rsa.go use named return parameters and defer to free resources if anything fails ("error handling with deferred cleanup"). But this pattern is broken if the function returns before the resources get initialized! If an error occurs early, the deferred function tries to free nil pointers—forgetting to clean up actual objects, leaking memory.
Here’s a conceptual version of the troublesome code
func encryptRSA(input []byte) ([]byte, error) {
var pkey, ctx *C.SomeStruct
defer func() {
if pkey != nil {
C.FreePKey(pkey)
}
if ctx != nil {
C.CtxFree(ctx)
}
}()
pkey = C.NewPKey()
if pkey == nil {
return nil, fail("No key")
}
ctx = C.NewCtx()
if ctx == nil {
return nil, fail("No context")
}
// ... more code ...
return encryptedOutput, nil
}
If any return nil, nil, fail(...) happens before pkey or ctx is set, then the deferred cleanup does nothing, leaking memory allocated inside the C library.
Where Exactly Does It Happen?
You can see the exact code in the github.com/golang-fips/openssl/rsa.go#L113:
func XXX() (ret1 *big.Int, ret2 []byte, err error) {
var pkey *C.EVP_PKEY = nil
var ctx *C.EVP_PKEY_CTX = nil
defer func() {
if pkey != nil { C.EVP_PKEY_free(pkey) }
if ctx != nil { C.EVP_PKEY_CTX_free(ctx) }
}()
// code that initializes pkey, ctx ...
if failure {
return nil, nil, fail("Something failed")
// pkey and ctx are nil here, nothing is freed!
}
// more code ...
}
This is called by various RSA functions. When input is attacker-controlled, it may hit those early error returns many times, causing repeated leaks.
Each leaked context (C struct) isn’t garbage collected by Go.
- An attacker can repeatedly trigger the error condition, causing the server to keep allocating memory, eventually hitting a process OOM (Out Of Memory) error or crashing.
- If this code is used in a cryptographic API (e.g., a web service that decrypts client messages), attackers don’t need authentication—they can just keep sending malformed requests.
Suppose you run a Go web server that decrypts incoming payloads
http.HandleFunc("/api/decrypt", func(w http.ResponseWriter, r *http.Request) {
blob, _ := io.ReadAll(r.Body)
out, err := encryptRSA(blob) // vulnerable!
if err != nil {
w.WriteHeader(400)
w.Write([]byte(err.Error()))
return
}
w.Write(out)
})
An attacker could simply POST thousands of requests with corrupt blobs—each one leaks a small chunk of native memory. With enough requests, the server eventually slows or even crashes.
Black-Box Attack
You don’t need to see the code. Any public API wrapping these vulnerable functions can be DOS’d with enough malformed input.
What Is the Fix?
The core mistake is assuming named return parameters are always non-nil in a defer. Go sets named return params before the defer executes. But if you explicitly return return nil, nil, fail(...) without assigning the C pointers to the named variables, the defer sees nils and skips freeing.
The Corrected Pattern
Assign the C pointers directly to the named return vars, or (better) always clean up with explicit error handling before every return, like this:
func encryptRSA(input []byte) ([]byte, error) {
var pkey, ctx *C.SomeStruct
defer func() {
if pkey != nil { C.FreePKey(pkey) }
if ctx != nil { C.CtxFree(ctx) }
}()
pkey = C.NewPKey()
if pkey == nil {
goto cleanup
}
ctx = C.NewCtx()
if ctx == nil {
goto cleanup
}
// more, success code
return encrypted, nil
cleanup:
// manually free here if needed!
if pkey != nil { C.FreePKey(pkey) }
if ctx != nil { C.CtxFree(ctx) }
return nil, errors.New("encryption failed")
}
Or, avoid relying on named returns in combination with deferred cleanups when dealing with native resources.
Upstream Patch
Check the latest version—github.com/golang-fips/openssl should have a fix soon or already applied (see commit history).
Official References & Further Reading
- Upstream repo: github.com/golang-fips/openssl
- Original CVE Record: CVE-2024-1394 *(pending public update, as of writing)*
- Vulnerability Discussion, Go Forum
- Relevant Go Lang Issue *(search for "CVE-2024-1394" when available)*
Summary
The memory leak in Go’s FIPS OpenSSL wrapper is a subtle but powerful DOS vector if you use native crypto code handling attacker input. Fix your code, stay updated, and avoid mixing deferred cleanups with low-level resource allocation.
Timeline
Published on: 03/21/2024 13:00:08 UTC
Last modified on: 03/26/2024 00:15:08 UTC