Sophie

Sophie

distrib > Mageia > 9 > armv7hl > media > core-release-src > by-pkgid > 25ee958fc175c156d3730753632a77bf > files > 1

golang-github-datadog-zstd-1.4.5.1-4.mga9.src.rpm

From 36b22b9a268bcfda30a96349d61c5a150b5e0809 Mon Sep 17 00:00:00 2001
From: Evan Jones <evan.jones@datadoghq.com>
Date: Fri, 5 Feb 2021 09:17:03 -0500
Subject: [PATCH] Pass pointers to C as unsafe.Pointer, not uintptr (memory
 corruption)

The unsafe package says: "Conversion of a uintptr back to Pointer is
not valid in general." This code was violating this rule, by passing
uintptr value to C, which would then interpret them as pointers. This
causes memory corruption: https://github.com/DataDog/zstd/issues/90

This change replaces all uses of uintptr with unsafe.Pointer to avoid
this memory corruption. This has the disadvantage of marking every
argument as escaping to heap. This means the buffers used to call the
zstd functions must be allocated on the heap. I suspect this should
not be a huge problem, since I would expect that high performance
code should already be managing its zstd buffers carefully.

The bug is as follows:

* In zstd_stream_test.go: payload := []byte("Hello World!") is
  marked as "does not escape to heap" (from go test -gcflags -m).
  Therefore, it is allocated on the stack.
* The test calls Writer.Write, which converts the argument to uintptr:
  srcPtr = uintptr(unsafe.Pointer(&srcData[0]))
* Writer.Write then calls Cgo: C.ZSTD_compressStream2_wrapper(...)
* The Go runtime decides the stack needs to be larger, so it copies
  it to a new location.
* (Another thread): The Go runtime decides to reuse the old stack
  location, so it replaces the "Hello World!" bytes with new data.
* (Original thread): Calls zstd, which reads the wrong bytes.

This change adds a test which nearly always crashes for me. While
investigating the other uses of uintptr, I also was able to trigger
a similar crash when calling CompressLevel, but only with:
    GODEBUG=efence=1 go test .

I also added tests for `Ctx.CompressLevel` because it was not
obviously being tested. I did not reproduce this problem with that
function, but I suspect the same bug exists, since it uses the same
pattern.

For a minimal reproduction of this bug, see:
https://github.com/evanj/cgouintptrbug
---
 zstd.go             | 36 +++++++------------------
 zstd_ctx.go         | 35 +++++++-----------------
 zstd_ctx_test.go    | 26 ++++++++++++++++++
 zstd_stream.go      | 62 +++++++++++++++++++++---------------------
 zstd_stream_test.go | 62 +++++++++++++++++++++++++++++-------------
 zstd_test.go        | 65 +++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 185 insertions(+), 101 deletions(-)

diff --git a/zstd.go b/zstd.go
index b6af4eb..164a923 100644
--- a/zstd.go
+++ b/zstd.go
@@ -3,27 +3,12 @@ package zstd
 /*
 #define ZSTD_STATIC_LINKING_ONLY
 #include "zstd.h"
-#include "stdint.h"  // for uintptr_t
-
-// The following *_wrapper function are used for removing superflouos
-// memory allocations when calling the wrapped functions from Go code.
-// See https://github.com/golang/go/issues/24450 for details.
-
-static size_t ZSTD_compress_wrapper(uintptr_t dst, size_t maxDstSize, const uintptr_t src, size_t srcSize, int compressionLevel) {
-	return ZSTD_compress((void*)dst, maxDstSize, (const void*)src, srcSize, compressionLevel);
-}
-
-static size_t ZSTD_decompress_wrapper(uintptr_t dst, size_t maxDstSize, uintptr_t src, size_t srcSize) {
-	return ZSTD_decompress((void*)dst, maxDstSize, (const void *)src, srcSize);
-}
-
 */
 import "C"
 import (
 	"bytes"
 	"errors"
 	"io/ioutil"
-	"runtime"
 	"unsafe"
 )
 
@@ -73,19 +58,18 @@ func CompressLevel(dst, src []byte, level int) ([]byte, error) {
 		dst = make([]byte, bound)
 	}
 
-	srcPtr := C.uintptr_t(uintptr(0)) // Do not point anywhere, if src is empty
+	var srcPtr *byte // Do not point anywhere, if src is empty
 	if len(src) > 0 {
-		srcPtr = C.uintptr_t(uintptr(unsafe.Pointer(&src[0])))
+		srcPtr = &src[0]
 	}
 
-	cWritten := C.ZSTD_compress_wrapper(
-		C.uintptr_t(uintptr(unsafe.Pointer(&dst[0]))),
+	cWritten := C.ZSTD_compress(
+		unsafe.Pointer(&dst[0]),
 		C.size_t(len(dst)),
-		srcPtr,
+		unsafe.Pointer(srcPtr),
 		C.size_t(len(src)),
 		C.int(level))
 
-	runtime.KeepAlive(src)
 	written := int(cWritten)
 	// Check if the return is an Error code
 	if err := getError(written); err != nil {
@@ -103,13 +87,12 @@ func Decompress(dst, src []byte) ([]byte, error) {
 	}
 	decompress := func(dst, src []byte) ([]byte, error) {
 
-		cWritten := C.ZSTD_decompress_wrapper(
-			C.uintptr_t(uintptr(unsafe.Pointer(&dst[0]))),
+		cWritten := C.ZSTD_decompress(
+			unsafe.Pointer(&dst[0]),
 			C.size_t(len(dst)),
-			C.uintptr_t(uintptr(unsafe.Pointer(&src[0]))),
+			unsafe.Pointer(&src[0]),
 			C.size_t(len(src)))
 
-		runtime.KeepAlive(src)
 		written := int(cWritten)
 		// Check error
 		if err := getError(written); err != nil {
@@ -120,8 +103,7 @@ func Decompress(dst, src []byte) ([]byte, error) {
 
 	if len(dst) == 0 {
 		// Attempt to use zStd to determine decompressed size (may result in error or 0)
-		size := int(C.size_t(C.ZSTD_getDecompressedSize(unsafe.Pointer(&src[0]), C.size_t(len(src)))))
-
+		size := int(C.ZSTD_getDecompressedSize(unsafe.Pointer(&src[0]), C.size_t(len(src))))
 		if err := getError(size); err != nil {
 			return nil, err
 		}
diff --git a/zstd_ctx.go b/zstd_ctx.go
index 4eef913..e3f5a3b 100644
--- a/zstd_ctx.go
+++ b/zstd_ctx.go
@@ -3,20 +3,6 @@ package zstd
 /*
 #define ZSTD_STATIC_LINKING_ONLY
 #include "zstd.h"
-#include "stdint.h"  // for uintptr_t
-
-// The following *_wrapper function are used for removing superfluous
-// memory allocations when calling the wrapped functions from Go code.
-// See https://github.com/golang/go/issues/24450 for details.
-
-static size_t ZSTD_compressCCtx_wrapper(ZSTD_CCtx* cctx, uintptr_t dst, size_t maxDstSize, const uintptr_t src, size_t srcSize, int compressionLevel) {
-	return ZSTD_compressCCtx(cctx, (void*)dst, maxDstSize, (const void*)src, srcSize, compressionLevel);
-}
-
-static size_t ZSTD_decompressDCtx_wrapper(ZSTD_DCtx* dctx, uintptr_t dst, size_t maxDstSize, uintptr_t src, size_t srcSize) {
-	return ZSTD_decompressDCtx(dctx, (void*)dst, maxDstSize, (const void *)src, srcSize);
-}
-
 */
 import "C"
 import (
@@ -77,20 +63,21 @@ func (c *ctx) CompressLevel(dst, src []byte, level int) ([]byte, error) {
 		dst = make([]byte, bound)
 	}
 
-	srcPtr := C.uintptr_t(uintptr(0)) // Do not point anywhere, if src is empty
+	var srcPtr *byte // Do not point anywhere, if src is empty
 	if len(src) > 0 {
-		srcPtr = C.uintptr_t(uintptr(unsafe.Pointer(&src[0])))
+		srcPtr = &src[0]
 	}
 
-	cWritten := C.ZSTD_compressCCtx_wrapper(
+	// This does not use the dangerous uintptr trick because NewCtx returns a Ctx interface,
+	// which means that Go must assume all arguments escape to the heap.
+	cWritten := C.ZSTD_compressCCtx(
 		c.cctx,
-		C.uintptr_t(uintptr(unsafe.Pointer(&dst[0]))),
+		unsafe.Pointer(&dst[0]),
 		C.size_t(len(dst)),
-		srcPtr,
+		unsafe.Pointer(srcPtr),
 		C.size_t(len(src)),
 		C.int(level))
 
-	runtime.KeepAlive(src)
 	written := int(cWritten)
 	// Check if the return is an Error code
 	if err := getError(written); err != nil {
@@ -99,21 +86,19 @@ func (c *ctx) CompressLevel(dst, src []byte, level int) ([]byte, error) {
 	return dst[:written], nil
 }
 
-
 func (c *ctx) Decompress(dst, src []byte) ([]byte, error) {
 	if len(src) == 0 {
 		return []byte{}, ErrEmptySlice
 	}
 	decompress := func(dst, src []byte) ([]byte, error) {
 
-		cWritten := C.ZSTD_decompressDCtx_wrapper(
+		cWritten := C.ZSTD_decompressDCtx(
 			c.dctx,
-			C.uintptr_t(uintptr(unsafe.Pointer(&dst[0]))),
+			unsafe.Pointer(&dst[0]),
 			C.size_t(len(dst)),
-			C.uintptr_t(uintptr(unsafe.Pointer(&src[0]))),
+			unsafe.Pointer(&src[0]),
 			C.size_t(len(src)))
 
-		runtime.KeepAlive(src)
 		written := int(cWritten)
 		// Check error
 		if err := getError(written); err != nil {
diff --git a/zstd_ctx_test.go b/zstd_ctx_test.go
index cba72f7..831a21f 100644
--- a/zstd_ctx_test.go
+++ b/zstd_ctx_test.go
@@ -39,6 +39,32 @@ func TestCtxCompressDecompress(t *testing.T) {
 	}
 }
 
+func TestCtxCompressLevel(t *testing.T) {
+	inputs := [][]byte{
+		nil, {}, {0}, []byte("Hello World!"),
+	}
+
+	cctx := NewCtx()
+	for _, input := range inputs {
+		for level := BestSpeed; level <= BestCompression; level++ {
+			out, err := cctx.CompressLevel(nil, input, level)
+			if err != nil {
+				t.Errorf("input=%#v level=%d CompressLevel failed err=%s", string(input), level, err.Error())
+				continue
+			}
+
+			orig, err := Decompress(nil, out)
+			if err != nil {
+				t.Errorf("input=%#v level=%d Decompress failed err=%s", string(input), level, err.Error())
+				continue
+			}
+			if !bytes.Equal(orig, input) {
+				t.Errorf("input=%#v level=%d orig does not match: %#v", string(input), level, string(orig))
+			}
+		}
+	}
+}
+
 func TestCtxEmptySliceCompress(t *testing.T) {
 	ctx := NewCtx()
 
diff --git a/zstd_stream.go b/zstd_stream.go
index fe2397b..1ed0e98 100644
--- a/zstd_stream.go
+++ b/zstd_stream.go
@@ -2,7 +2,6 @@ package zstd
 
 /*
 #define ZSTD_STATIC_LINKING_ONLY
-#include "stdint.h"  // for uintptr_t
 #include "zstd.h"
 
 typedef struct compressStream2_result_s {
@@ -11,9 +10,10 @@ typedef struct compressStream2_result_s {
 	size_t bytes_written;
 } compressStream2_result;
 
-static void ZSTD_compressStream2_wrapper(compressStream2_result* result, ZSTD_CCtx* ctx, uintptr_t dst, size_t maxDstSize, const uintptr_t src, size_t srcSize) {
-	ZSTD_outBuffer outBuffer = { (void*)dst, maxDstSize, 0 };
-	ZSTD_inBuffer inBuffer = { (void*)src, srcSize, 0 };
+static void ZSTD_compressStream2_wrapper(compressStream2_result* result, ZSTD_CCtx* ctx,
+		void* dst, size_t maxDstSize, const void* src, size_t srcSize) {
+	ZSTD_outBuffer outBuffer = { dst, maxDstSize, 0 };
+	ZSTD_inBuffer inBuffer = { src, srcSize, 0 };
 	size_t retCode = ZSTD_compressStream2(ctx, &outBuffer, &inBuffer, ZSTD_e_continue);
 
 	result->return_code = retCode;
@@ -21,9 +21,10 @@ static void ZSTD_compressStream2_wrapper(compressStream2_result* result, ZSTD_CC
 	result->bytes_written = outBuffer.pos;
 }
 
-static void ZSTD_compressStream2_flush(compressStream2_result* result, ZSTD_CCtx* ctx, uintptr_t dst, size_t maxDstSize, const uintptr_t src, size_t srcSize) {
-	ZSTD_outBuffer outBuffer = { (void*)dst, maxDstSize, 0 };
-	ZSTD_inBuffer inBuffer = { (void*)src, srcSize, 0 };
+static void ZSTD_compressStream2_flush(compressStream2_result* result, ZSTD_CCtx* ctx,
+		void* dst, size_t maxDstSize, const void* src, size_t srcSize) {
+	ZSTD_outBuffer outBuffer = { dst, maxDstSize, 0 };
+	ZSTD_inBuffer inBuffer = { src, srcSize, 0 };
 	size_t retCode = ZSTD_compressStream2(ctx, &outBuffer, &inBuffer, ZSTD_e_flush);
 
 	result->return_code = retCode;
@@ -31,9 +32,10 @@ static void ZSTD_compressStream2_flush(compressStream2_result* result, ZSTD_CCtx
 	result->bytes_written = outBuffer.pos;
 }
 
-static void ZSTD_compressStream2_finish(compressStream2_result* result, ZSTD_CCtx* ctx, uintptr_t dst, size_t maxDstSize, const uintptr_t src, size_t srcSize) {
-	ZSTD_outBuffer outBuffer = { (void*)dst, maxDstSize, 0 };
-	ZSTD_inBuffer inBuffer = { (void*)src, srcSize, 0 };
+static void ZSTD_compressStream2_finish(compressStream2_result* result, ZSTD_CCtx* ctx,
+		void* dst, size_t maxDstSize, const void* src, size_t srcSize) {
+	ZSTD_outBuffer outBuffer = { dst, maxDstSize, 0 };
+	ZSTD_inBuffer inBuffer = { src, srcSize, 0 };
 	size_t retCode = ZSTD_compressStream2(ctx, &outBuffer, &inBuffer, ZSTD_e_end);
 
 	result->return_code = retCode;
@@ -48,9 +50,10 @@ typedef struct decompressStream2_result_s {
 	size_t bytes_written;
 } decompressStream2_result;
 
-static void ZSTD_decompressStream_wrapper(decompressStream2_result* result, ZSTD_DCtx* ctx, uintptr_t dst, size_t maxDstSize, const uintptr_t src, size_t srcSize) {
-	ZSTD_outBuffer outBuffer = { (void*)dst, maxDstSize, 0 };
-	ZSTD_inBuffer inBuffer = { (void*)src, srcSize, 0 };
+static void ZSTD_decompressStream_wrapper(decompressStream2_result* result, ZSTD_DCtx* ctx,
+		void* dst, size_t maxDstSize, const void* src, size_t srcSize) {
+	ZSTD_outBuffer outBuffer = { dst, maxDstSize, 0 };
+	ZSTD_inBuffer inBuffer = { src, srcSize, 0 };
 	size_t retCode = ZSTD_decompressStream(ctx, &outBuffer, &inBuffer);
 
 	result->return_code = retCode;
@@ -165,20 +168,19 @@ func (w *Writer) Write(p []byte) (int, error) {
 		srcData = w.srcBuffer
 	}
 
-	srcPtr := C.uintptr_t(uintptr(0)) // Do not point anywhere, if src is empty
+	var srcPtr *byte // Do not point anywhere, if src is empty
 	if len(srcData) > 0 {
-		srcPtr = C.uintptr_t(uintptr(unsafe.Pointer(&srcData[0])))
+		srcPtr = &srcData[0]
 	}
 
 	C.ZSTD_compressStream2_wrapper(
 		w.resultBuffer,
 		w.ctx,
-		C.uintptr_t(uintptr(unsafe.Pointer(&w.dstBuffer[0]))),
+		unsafe.Pointer(&w.dstBuffer[0]),
 		C.size_t(len(w.dstBuffer)),
-		srcPtr,
+		unsafe.Pointer(srcPtr),
 		C.size_t(len(srcData)),
 	)
-	runtime.KeepAlive(p) // Ensure p is kept until here so pointer doesn't disappear during C call
 	ret := int(w.resultBuffer.return_code)
 	if err := getError(ret); err != nil {
 		return 0, err
@@ -221,17 +223,17 @@ func (w *Writer) Flush() error {
 
 	ret := 1 // So we loop at least once
 	for ret > 0 {
-		srcPtr := C.uintptr_t(uintptr(0)) // Do not point anywhere, if src is empty
+		var srcPtr *byte // Do not point anywhere, if src is empty
 		if len(w.srcBuffer) > 0 {
-			srcPtr = C.uintptr_t(uintptr(unsafe.Pointer(&w.srcBuffer[0])))
+			srcPtr = &w.srcBuffer[0]
 		}
 
 		C.ZSTD_compressStream2_flush(
 			w.resultBuffer,
 			w.ctx,
-			C.uintptr_t(uintptr(unsafe.Pointer(&w.dstBuffer[0]))),
+			unsafe.Pointer(&w.dstBuffer[0]),
 			C.size_t(len(w.dstBuffer)),
-			srcPtr,
+			unsafe.Pointer(srcPtr),
 			C.size_t(len(w.srcBuffer)),
 		)
 		ret = int(w.resultBuffer.return_code)
@@ -265,17 +267,17 @@ func (w *Writer) Close() error {
 
 	ret := 1 // So we loop at least once
 	for ret > 0 {
-		srcPtr := C.uintptr_t(uintptr(0)) // Do not point anywhere, if src is empty
+		var srcPtr *byte // Do not point anywhere, if src is empty
 		if len(w.srcBuffer) > 0 {
-			srcPtr = C.uintptr_t(uintptr(unsafe.Pointer(&w.srcBuffer[0])))
+			srcPtr = &w.srcBuffer[0]
 		}
 
 		C.ZSTD_compressStream2_finish(
 			w.resultBuffer,
 			w.ctx,
-			C.uintptr_t(uintptr(unsafe.Pointer(&w.dstBuffer[0]))),
+			unsafe.Pointer(&w.dstBuffer[0]),
 			C.size_t(len(w.dstBuffer)),
-			srcPtr,
+			unsafe.Pointer(srcPtr),
 			C.size_t(len(w.srcBuffer)),
 		)
 		ret = int(w.resultBuffer.return_code)
@@ -445,17 +447,17 @@ func (r *reader) Read(p []byte) (int, error) {
 		src = src[:r.compressionLeft+n]
 
 		// C code
-		srcPtr := C.uintptr_t(uintptr(0)) // Do not point anywhere, if src is empty
+		var srcPtr *byte // Do not point anywhere, if src is empty
 		if len(src) > 0 {
-			srcPtr = C.uintptr_t(uintptr(unsafe.Pointer(&src[0])))
+			srcPtr = &src[0]
 		}
 
 		C.ZSTD_decompressStream_wrapper(
 			r.resultBuffer,
 			r.ctx,
-			C.uintptr_t(uintptr(unsafe.Pointer(&r.decompressionBuffer[0]))),
+			unsafe.Pointer(&r.decompressionBuffer[0]),
 			C.size_t(len(r.decompressionBuffer)),
-			srcPtr,
+			unsafe.Pointer(srcPtr),
 			C.size_t(len(src)),
 		)
 		retCode := int(r.resultBuffer.return_code)
diff --git a/zstd_stream_test.go b/zstd_stream_test.go
index 3bb3d9d..949bc88 100644
--- a/zstd_stream_test.go
+++ b/zstd_stream_test.go
@@ -3,6 +3,7 @@ package zstd
 import (
 	"bytes"
 	"errors"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"log"
@@ -92,35 +93,67 @@ func TestZstdReaderLong(t *testing.T) {
 	testCompressionDecompression(t, nil, long.Bytes())
 }
 
-func TestStreamCompressionDecompression(t *testing.T) {
+func doStreamCompressionDecompression() error {
 	payload := []byte("Hello World!")
 	repeat := 10000
 	var intermediate bytes.Buffer
 	w := NewWriterLevel(&intermediate, 4)
 	for i := 0; i < repeat; i++ {
 		_, err := w.Write(payload)
-		failOnError(t, "Failed writing to compress object", err)
+		if err != nil {
+			return fmt.Errorf("failed writing to compress object: %w", err)
+		}
 	}
-	w.Close()
+	err := w.Close()
+	if err != nil {
+		return fmt.Errorf("failed to close compressor: %w", err)
+	}
+
 	// Decompress
 	r := NewReader(&intermediate)
 	dst := make([]byte, len(payload))
 	for i := 0; i < repeat; i++ {
 		n, err := r.Read(dst)
-		failOnError(t, "Failed to decompress", err)
+		if err != nil {
+			return fmt.Errorf("failed to decompress: %w", err)
+		}
 		if n != len(payload) {
-			t.Fatalf("Did not read enough bytes: %v != %v", n, len(payload))
+			return fmt.Errorf("did not read enough bytes: %d != %d", n, len(payload))
 		}
 		if string(dst) != string(payload) {
-			t.Fatalf("Did not read the same %s != %s", string(dst), string(payload))
+			return fmt.Errorf("Did not read the same %s != %s", string(dst), string(payload))
 		}
 	}
 	// Check EOF
 	n, err := r.Read(dst)
 	if err != io.EOF {
-		t.Fatalf("Error should have been EOF, was %s instead: (%v bytes read: %s)", err, n, dst[:n])
+		return fmt.Errorf("Error should have been EOF (%v bytes read: %s): %w",
+			n, string(dst[:n]), err)
+	}
+	err = r.Close()
+	if err != nil {
+		return fmt.Errorf("failed to close decompress object: %w", err)
+	}
+	return nil
+}
+
+func TestStreamCompressionDecompressionParallel(t *testing.T) {
+	// start 100 goroutines: triggered Cgo stack growth related bugs
+	const threads = 100
+	errChan := make(chan error)
+
+	for i := 0; i < threads; i++ {
+		go func() {
+			errChan <- doStreamCompressionDecompression()
+		}()
+	}
+
+	for i := 0; i < threads; i++ {
+		err := <-errChan
+		if err != nil {
+			t.Error("task failed:", err)
+		}
 	}
-	failOnError(t, "Failed to close decompress object", r.Close())
 }
 
 func TestStreamRealPayload(t *testing.T) {
@@ -179,8 +212,8 @@ func TestStreamFlush(t *testing.T) {
 	failOnError(t, "Failed to close uncompress object", reader.Close())
 }
 
-type closeableWriter struct{
-	w io.Writer
+type closeableWriter struct {
+	w      io.Writer
 	closed bool
 }
 
@@ -240,15 +273,6 @@ func TestStreamDecompressionUnexpectedEOFHandling(t *testing.T) {
 	}
 }
 
-func TestStreamCompressionDecompressionParallel(t *testing.T) {
-	for i := 0; i < 200; i++ {
-		t.Run("", func(t2 *testing.T) {
-			t2.Parallel()
-			TestStreamCompressionDecompression(t2)
-		})
-	}
-}
-
 func TestStreamCompressionChunks(t *testing.T) {
 	MB := 1024 * 1024
 	totalSize := 100 * MB
diff --git a/zstd_test.go b/zstd_test.go
index 44c1af5..fda1973 100644
--- a/zstd_test.go
+++ b/zstd_test.go
@@ -84,6 +84,71 @@ func TestCompressDecompress(t *testing.T) {
 	}
 }
 
+func TestCompressLevel(t *testing.T) {
+	inputs := [][]byte{
+		nil, {}, {0}, []byte("Hello World!"),
+	}
+
+	for _, input := range inputs {
+		for level := BestSpeed; level <= BestCompression; level++ {
+			out, err := CompressLevel(nil, input, level)
+			if err != nil {
+				t.Errorf("input=%#v level=%d CompressLevel failed err=%s", string(input), level, err.Error())
+				continue
+			}
+
+			orig, err := Decompress(nil, out)
+			if err != nil {
+				t.Errorf("input=%#v level=%d Decompress failed err=%s", string(input), level, err.Error())
+				continue
+			}
+			if !bytes.Equal(orig, input) {
+				t.Errorf("input=%#v level=%d orig does not match: %#v", string(input), level, string(orig))
+			}
+		}
+	}
+}
+
+func doCompressLevel(payload []byte, out []byte) error {
+	out, err := CompressLevel(out, payload, DefaultCompression)
+	if err != nil {
+		return fmt.Errorf("failed calling CompressLevel: %w", err)
+	}
+
+	orig, err := Decompress(nil, out)
+	if err != nil {
+		return fmt.Errorf("failed calling Decompress: %w", err)
+	}
+	if !bytes.Equal(orig, payload) {
+		return fmt.Errorf("orig=%#v should match payload=%#v", string(orig), string(payload))
+	}
+	return nil
+}
+
+func useStackSpaceCompressLevel(payload []byte, out []byte, level int) error {
+	if level == 0 {
+		return doCompressLevel(payload, out)
+	}
+	return useStackSpaceCompressLevel(payload, out, level-1)
+}
+
+func TestCompressLevelStackCgoBug(t *testing.T) {
+	// CompressLevel previously had a bug where it would access the wrong pointer
+	// This test would crash when run with CGODEBUG=efence=1 go test .
+	const maxStackLevels = 100
+
+	payload := []byte("Hello World!")
+	// allocate the output buffer so CompressLevel does not allocate it
+	out := make([]byte, CompressBound(len(payload)))
+
+	for level := 0; level < maxStackLevels; level++ {
+		err := useStackSpaceCompressLevel(payload, out, level)
+		if err != nil {
+			t.Fatal("CompressLevel failed:", err)
+		}
+	}
+}
+
 func TestEmptySliceCompress(t *testing.T) {
 	compressed, err := Compress(nil, []byte{})
 	if err != nil {