Skip to content

Segfaults, races, and ImageRef lifetimes under concurrency

Copy page

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.

  • 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.

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.

  • 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.

libvips’ internal worker threads and binding-object concurrency are different layers. Do not apply this page’s ImageRef rules unchanged to another binding.