#1378·gocv

Possible out-of-bounds read in `gocv.GetLastExceptionMessage` via a non-NUL-terminated 1024-byte global

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

Possible out-of-bounds read in gocv.GetLastExceptionMessage via a non-NUL-terminated 1024-byte global

I found a possible out-of-bounds read in GetLastExceptionMessage. setExceptionInfo copies the OpenCV exception text into a fixed 1024-byte global using strncpy(dst, src, 1024) — a size equal to the full buffer, which per the C standard leaves the destination unterminated whenever the source is 1024 bytes or longer. The Go side then reads that global with C.GoString, which scans for a NUL byte with no length bound. If an exception message reaches 1024 bytes, the scan runs past the end of the global and the adjacent data-segment bytes are copied into the returned Go string.

File: core.go (Go side), core.cpp (C++ side)

Function: gocv.io/x/gocv.GetLastExceptionMessage (core.go:22-24)

go
// GetLastExceptionMessage returns the last exception message from the OpenCV library.
func GetLastExceptionMessage() string {
	return C.GoString(C.GetOpenCVExceptionMessage())
}

core.cpp:4-23 — the buffer, the writer and the accessor:

c
int lastException = 0;
char lastExceptionMessage[1024];

int GetOpenCVException() {
    return lastException;
}

const char* GetOpenCVExceptionMessage() {
    return lastExceptionMessage;
}

void ClearOpenCVException() {
    lastException = 0;
    strncpy(lastExceptionMessage, "", 1024);
}

void setExceptionInfo(int code, const char* message) {
    lastException = code;
    strncpy(lastExceptionMessage, message, 1024);
}

A representative producer, imgcodecs.cpp:8-11 (there are ~250 such sites across the C++ layer):

c
    } catch(const cv::Exception& e){
        setExceptionInfo(e.code, e.what());
  1. Go calls a wrapped OpenCV function whose failure message embeds caller-controlled text — most directly a long filesystem path (gocv.IMRead, ReadNet with a model path, etc.).
  2. The C++ wrapper catches cv::Exception and calls setExceptionInfo(e.code, e.what()).
  3. strncpy(lastExceptionMessage, message, 1024) writes at most 1024 bytes and appends a terminator only if strlen(message) < 1024. With n equal to the full buffer size, a message of 1024 bytes or more fills all 1024 bytes and leaves no NUL anywhere in the buffer. (strncpy stays in bounds here — the write itself is fine; only the termination is missing.)
  4. GetOpenCVExceptionMessage() returns the bare char * to Go with no length. OpenCVResult carries a Length field (core.h:271) but it is not used on this path, and the Go side applies C.GoString, whose contract is "read until the first NUL" — no bound, no C.GoStringN.
  5. C.GoString therefore reads past the end of the 1024-byte global into whatever follows it in the data/BSS segment until it happens upon a zero byte, and materialises those bytes into the Go string that the application logs or returns — an out-of-bounds read plus disclosure of adjacent process memory.

Note that ClearOpenCVException does zero-fill the buffer (strncpy(dst, "", 1024) pads the whole 1024 bytes with NULs), but it must be called, and setExceptionInfo subsequently overwrites all 1024 bytes with no terminator. Its only caller is LastExceptionError (core.go:32-39), which defers ClearLastException() and so runs it after GetLastExceptionMessage() at line 38 — that is, after the over-read has already happened.

Go trigger (if applicable):

go
// A path long enough that OpenCV's formatted message exceeds 1024 bytes.
longPath := "/tmp/" + strings.Repeat("a", 1200) + ".png"

img := gocv.IMRead(longPath, gocv.IMReadColor) // raises cv::Exception in the C++ layer
defer img.Close()

fmt.Printf("%q\n", gocv.GetLastExceptionMessage())
// The returned string is longer than 1024 bytes and its tail is whatever
// data-segment bytes follow lastExceptionMessage.

A long path is the simplest lever; a long DNN layer/blob name or a long assertion expression also works. The read is fully synchronous — setExceptionInfo and GetLastExceptionMessage run on the same call, so no interleaving is needed, and no GC interaction is involved. It is not platform-specific: strncpy's no-terminator-on-truncation behaviour is standard C (C17 §7.24.2.4).

Separately, lastExceptionMessage is a process-global with no locking, so concurrent Go calls into OpenCV race on it; that is a distinct issue from the over-read reported here.

Suggested fix: guarantee termination on the C side, e.g.

c
void setExceptionInfo(int code, const char* message) {
    lastException = code;
    strncpy(lastExceptionMessage, message, sizeof(lastExceptionMessage) - 1);
    lastExceptionMessage[sizeof(lastExceptionMessage) - 1] = '\0';
}

(or use snprintf). Bounding the read on the Go side as well — C.GoStringN(C.GetOpenCVExceptionMessage(), 1024) with trailing-NUL trimming — would make the Go wrapper safe independently of the C fix.