#1376·gocv

Possible memory leak of the C-allocated `OpenCVResult.Message` in `cuda.OpenCVResult`

Author: OvOhaoCreated Aug 28, 2026Updated Aug 28, 2026

Possible memory leak of the C-allocated OpenCVResult.Message in cuda.OpenCVResult

I found a possible memory leak in cuda.OpenCVResult. The C++ layer reports errors by malloc-ing a copy of cv::Exception::what() and returning it inside an OpenCVResult struct. The converter in the root package frees that pointer, but the identical converter in the cuda package copies it with C.GoString and returns without calling C.free, so every failed cuda operation leaks one heap allocation. This affects the ordinary error path of all 144 cuda call sites, so a long-running service that encounters recoverable OpenCV errors (bad input frames, size or depth mismatches) accumulates leaked message buffers indefinitely.

File: cuda/errors.go

Function: gocv.io/x/gocv/cuda.OpenCVResult

go
package cuda

/*
#include "../core.h"
*/
import "C"
import "errors"

// Converts a OpenCVResult struct to an error.
func OpenCVResult(result C.OpenCVResult) error {
	if result.Code == 0 {
		return nil
	}
	return errors.New(C.GoString(result.Message))
}

The message is allocated in core.cpp:30-40:

c
OpenCVResult errorResult(int code, const char* message) {
    OpenCVResult ri;
    ri.Code = code;

    auto res = (char*)malloc(strlen(message)+1);
    memset(res, 0, strlen(message)+1);
    memcpy(res, message, strlen(message));
    ri.Message = res;
    ri.Length = strlen(message);
    return ri;
}

and the same function in the root package (core.go:42-52) does release it:

go
func OpenCVResult(result C.OpenCVResult) error {
	if result.Code == 0 {
		return nil
	}
	if result.Message == nil {
		return errors.New("unknown openCV error")
	}
	defer C.free(unsafe.Pointer(result.Message))
	return errors.New(C.GoString(result.Message))
}
  1. Go calls any wrapped cuda operation — e.g. cuda/arithm.go:25 (return OpenCVResult(C.GpuAbs(src.p, dst.p, nil))).
  2. The C++ wrapper runs the OpenCV call inside try/catch; on cv::Exception it returns errorResult(e.code, e.what()).
  3. errorResult mallocs strlen(message)+1 bytes, copies the message in, and stores the pointer in the returned-by-value struct field ri.Message. cgo copies the struct into the Go frame, so result.Message becomes the only reference to that heap block in the program.
  4. cuda/errors.go:14 calls C.GoString(result.Message), which copies the bytes into a new Go string, then returns. There is no defer, no unsafe import, and no C.free anywhere in the file; result is a by-value parameter, so after the function returns no Go code holds the pointer and no other function is able to free it.
  5. One malloc block of strlen(e.what())+1 bytes is leaked per failed operation. OpenCV exception strings have the form OpenCV(<ver>) <abs-path>:<line>: error: (<code>:<name>) <detail> in function '<fn>', typically 120–400 bytes and growing with the source path length.

Go trigger (if applicable):

go
for i := 0; i < 1_000_000; i++ {
	// Any op that raises cv::Exception takes the errorResult path.
	_ = cuda.Abs(src, &dst)
}

Any input that makes an OpenCV call throw works (empty matrix, mismatched sizes, unsupported depth); no special privileges are needed. The leak is purely synchronous — no goroutine, callback or GC step is involved, and Go's collector never reclaims malloc memory. It requires a CUDA-enabled build.

Suggested fix: mirror the root package's converter exactly — nil-check the message and release it with defer:

go
import "unsafe"

func OpenCVResult(result C.OpenCVResult) error {
	if result.Code == 0 {
		return nil
	}
	if result.Message == nil {
		return errors.New("unknown openCV error")
	}
	defer C.free(unsafe.Pointer(result.Message))
	return errors.New(C.GoString(result.Message))
}

(The same omission exists in the sibling package's errors.go, reported separately.)