Skip to content

Source.new_from_memory: keep a ref to the memory area - #438

Open
jeremy wants to merge 1 commit into
libvips:masterfrom
jeremy:source-new-from-memory-keeps-ref
Open

Source.new_from_memory: keep a ref to the memory area#438
jeremy wants to merge 1 commit into
libvips:masterfrom
jeremy:source-new-from-memory-keeps-ref

Conversation

@jeremy

@jeremy jeremy commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Vips::Source.new_from_memory hands data to libvips and keeps no Ruby reference to it:

def self.new_from_memory(data)
  ptr = Vips.vips_source_new_from_memory data, data.bytesize
  raise Vips::Error if ptr.null?

  # FIXME do we need to keep a ref to the underlying memory area? what
  # about Image.new_from_buffer? Does that need a secret ref too?

  Vips::Source.new ptr
end

libvips aliases that buffer rather than copying it — libvips/iofuncs/source.c:

 * You must not free @data while the source is active.
...
	/* We don't take a copy of the data or free it.
	 */
	blob = vips_blob_new(NULL, data, length);

NULL free_fn, so no ownership transfer: libvips reads the caller's bytes for as long as the source lives. Nothing on the Ruby side was keeping them alive, so the GC is free to collect the string first. Image.new_from_memory two files over already handles this — image.references << data. This does the same for Source.

Answering the FIXME

The FIXME asks two questions. This PR answers both and removes it.

Does the source need a ref? Yes — demonstrated, not theoretical. New spec, which drops the caller's reference and calls GC.start before loading (same shape as the existing Image.new_from_memory specs). On master, ruby 4.0.6 / arm64-darwin23 / libvips 8.18.4, it fails 14 times out of 20 runs, with the source's bytes visibly overwritten:

Vips::Error:
  VipsJpeg: Not a JPEG file: starts with 0xb7 0x80
  jpegload_source: load error
Vips::Error:
  VipsJpeg: Corrupt JPEG data: 38382 extraneous bytes before marker 0xda
  VipsJpeg: Inconsistent progression sequence for component 1 coefficient 35
expected 104.98296735180038 to be within 0.001 of 109.789

With the ref, 0 failures out of 20.

Does Image.new_from_buffer need one too? No. It goes through Operation.call, which sets the loader's buffer argument via GValue#set, and for Vips::BLOB_TYPE that allocates a fresh block and copies into it (lib/vips/gvalue.rb):

when Vips::BLOB_TYPE
  len = value.bytesize
  ptr = GLib.g_malloc len
  Vips.vips_value_set_blob self, GLib::G_FREE, ptr, len
  ptr.write_bytes value

The GLib::G_FREE free_fn hands the copy to libvips, so libvips owns it and the caller's string is irrelevant afterwards. This is worth stating explicitly because it's easy to conclude the opposite: Operation.call's deduped_references machinery only propagates references between Vips::Image inputs and outputs and does nothing for a plain String buffer, so reading only that code makes new_from_buffer look unprotected. The protection is one level down, in GValue#set.

Scope and severity

lib/vips/source.rb is byte-identical (md5 eb69926777374771d845a579151c4cb0) in 2.2.2, 2.2.5, 2.3.0 and current master, so this has been present unchanged across all of them.

There are no callers of Source.new_from_memory inside the gem itself, and none in our own applications — this is a latent defect being fixed preemptively, not a live incident.

One limitation worth naming: references << fixes liveness only, not movement. A Ruby String at or below the embedded boundary keeps its bytes inside the object slot and relocates under GC compaction, so a live ref wouldn't stop the pointer going stale. I measured that boundary at 616 bytes on ruby 3.4.7, 3.4.8, 3.4.10 and 4.0.6 (ObjectSpace.dump(s)["embedded"] flips at "a" * 616). It doesn't matter for real image buffers, which are far above it — but it would if a small-buffer path were ever added.

Also not addressed here, but adjacent: Image.new_from_memory has a JRuby branch that copies the string into an FFI::MemoryPointer first, because JRuby's FFI passes a temporary native buffer for a Ruby String rather than the string's own storage. Source.new_from_memory has no equivalent, and a Ruby-side ref doesn't help there. I left it alone rather than add a branch I can't exercise locally — happy to follow up if you'd like it.

Tests

Full suite on ruby 4.0.6 / arm64-darwin23 / libvips 8.18.4:

125 examples, 0 failures

standardrb clean on both changed files.

vips_source_new_from_memory() aliases the caller's buffer rather than
copying it -- vips_blob_new(NULL, data, length), NULL free_fn -- so
libvips reads it for as long as the source is alive. We kept no Ruby
reference to it, so nothing stopped the GC from freeing it first.

Mirrors Image.new_from_memory, which already does this.

Also answers and removes the FIXME: Image.new_from_buffer does not need
a ref. It goes through GValue#set for Vips::BLOB_TYPE, which g_mallocs
its own block, memcpys into it and hands libvips ownership via
vips_value_set_blob(..., GLib::G_FREE, ...).
Copilot AI lite review requested due to automatic review settings August 5, 2026 22:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants