Segfaults, races, and ImageRef lifetimes under concurrency
If SIGSEGV, bogus dimensions, or invalid band counts appear only under concurrent load, first check whether an ImageRef is shared across goroutines or an input image is closed before an operation has finished with it. A single-request test rarely exercises either lifetime error.
Symptoms
Section titled “Symptoms”- Intermittent segmentation faults (SIGSEGV) under concurrency, stable when not concurrent.
- Bogus return values when an ImageRef is shared across goroutines (a known case returned
Bands()= 5376256 when the real channel count was small). - Single-connection load tests pass; concurrency starts crashing.
The key distinction: libvips and govips thread models differ
Section titled “The key distinction: libvips and govips thread models differ”libvips itself is thread-safe within a single call — each operation starts an internal thread pool, and the shared vips_concurrency setting controls thread count. But govips ImageRef is not thread-safe: it is a cgo handle owning a VipsImage*, and operating on one ImageRef from multiple goroutines concurrently produces data races, dangling pointers, and segfaults.
Do not mistake libvips’ per-call concurrency for permission to share an ImageRef.
Correct approach
Section titled “Correct approach”1. Create an ImageRef per goroutine
Section titled “1. Create an ImageRef per goroutine”Do not reuse a single ImageRef across requests. Each request loads its own image via LoadImageFromFile / LoadImageFromBuffer / LoadThumbnailFromFile and calls Close() itself.
func dimensions(buf []byte) (int, int, error) { importParams := vips.NewImportParams() image, err := vips.LoadImageFromBuffer(buf, importParams) if err != nil { return 0, 0, err } defer image.Close() return image.Width(), image.Height(), nil}Leave concurrency to Go’s goroutine scheduling; do not cache and share that ImageRef outside the function.
2. Do not close a source ImageRef early once passed into another operation
Section titled “2. Do not close a source ImageRef early once passed into another operation”When an operation takes another image as an input (for example, compositing or overlay), the source ImageRef’s C memory is referenced by that operation. Closing it early risks a dangling-pointer access during a later GC phase. Keep the source alive for the whole operation call, using runtime.KeepAlive when needed.
3. Validate with concurrent load tests, not single requests
Section titled “3. Validate with concurrent load tests, not single requests”Exercise the edge cases of the pipeline: shared slices, storing an ImageRef in a global cache, or passing a slice header directly as the backing array to C. Such problems surface only under concurrency.
Affected versions and scope
Section titled “Affected versions and scope”- Different goroutines each loading their own image is the recommended pattern, not a race.
- The crash comes from a version defect: govips merged memory/race fixes in older releases (#493) and is refactoring its locking model (#524/#525). Upgrade to a version containing the fixes before debugging your own code.
- This page targets govips v2.19.0. Concurrency guarantees and object-ownership rules must be checked separately for other bindings.
Sources
Section titled “Sources”- govips #291 — segfault from sharing an ImageRef
- govips #493 — official memory-leak/race fix
- govips #547 / #543 — GC and cgo lifecycle, KeepAlive
- govips #545 — vipsImageSetBlob crash
- govips #154 — bogus Bands() return value
- govips #524 / #525 — composable pipeline locking refactor
libvips’ internal worker threads and binding-object concurrency are different layers. Do not apply this page’s ImageRef rules unchanged to another binding.