Skip to content

Test that a coerced TYPE_CONST_STRING argument stays in place - #213

Open
jeremy wants to merge 1 commit into
ruby:masterfrom
jeremy:const-string-compaction-test
Open

Test that a coerced TYPE_CONST_STRING argument stays in place#213
jeremy wants to merge 1 commit into
ruby:masterfrom
jeremy:const-string-compaction-test

Conversation

@jeremy

@jeremy jeremy commented Aug 8, 2026

Copy link
Copy Markdown

Problem

TYPE_CONST_STRING stores a pointer into a Ruby String:

case TYPE_CONST_STRING:
    ...
    else { dst->pointer = rb_string_value_cstr(src); }

A String argument is safe. argv holds it and keeps it in place.

A to_str object is not. rb_string_value_cstr writes the coerced String back into src. argv still holds the original object, not the String. The next loop turn overwrites src. After that, only converted_args holds the String.

The pointer must stay valid until ffi_call returns. ffi_call runs under rb_thread_call_without_gvl, so another thread can compact the heap during the call. converted_args must keep the String in place, not only alive.

Master does this. converted_args sits in the ALLOCV buffer. The GC scans that buffer conservatively, and a conservative scan pins the object.

Gap

No test covers this.

Before #205, converted_args was a Ruby Array. The GC may move an Array's elements, so the String moved while the pointer stayed, and the C function read the vacated slot. #205 replaced the Array with the buffer and fixed the fault as a side effect. Nothing records that the buffer is required.

Change

  1. test_call_const_string_from_to_str_after_compaction in test/fiddle/test_function.rb.
  2. A comment at converted_args in ext/fiddle/function.c stating the requirement.

The test calls strcspn as [TYPE_CONST_STRING, TYPE_VOIDP] -> TYPE_SIZE_T. Arg 0 is a to_str object returning a fresh 100-byte String. Arg 1 is a to_ptr object, which makes Fiddle call Pointer.[]; that allocation lets the GC run after the pointer is stored.

The subject holds no "Z"; the reject set is "Z". A correct call returns 100. A stale pointer returns 0, because a vacated slot holds zero bytes.

Three details are load-bearing. Remove any one and the test passes on a faulty build; each has a comment:

  • Churn is the subject's byte size, half of it dropped. The compactor only evacuates non-full pages.
  • to_ptr returns a new Pointer each time. An existing Pointer gives Fiddle nothing to allocate.
  • The intact-case call runs after the loop. A call before it prepares the CIF, and that preparation is part of what opens the window.

Result

tree arm64-darwin, ruby 3.4.10 aarch64-linux, ruby 3.4.7
d389bbf (before #205) fail fail
master pass pass

Three runs per tree on darwin, same result each time. Full suite on master with this change: 242 tests, 668 assertions, 0 failures, 1 error, 3 omissions. The error is test_nogvl_poll needing envutil; it fails the same way without this change.

Environment: ruby 3.4.10 [arm64-darwin23]; ruby 3.4.7 [aarch64-linux], glibc 2.36, libffi 3.4.4.

Note

Released versions still have the fault. I measured 1.1.6 and 1.1.8 on both platforms: a two-argument call of this shape returns the wrong result on every iteration under GC.stress with GC.auto_compact. Master is correct. You may want a release.

I report this as latent. The trigger is the declared argument type plus a to_str object in place of a String. In one application that loads fiddle at boot, all 227 bundled gems put every Fiddle::Function.new behind a Windows-only branch.

A TYPE_CONST_STRING argument that comes from a to_str object is coerced
inside Fiddle, so the caller's argv holds that object and not the String
that the char * points into. Only converted_args holds the coerced
String, and the conservative scan of the ALLOCV buffer is what keeps it
in place while ffi_call runs without the GVL.

Nothing tested that. Before ruby#205, converted_args was a Ruby Array, whose
elements the GC may move, and the C function read the slot the String
left. ruby#205 replaced the Array with this buffer and corrected the fault as
a side effect. A later change could move these values back to storage the
GC may relocate, and no test would notice.

Add the missing test, and record the requirement next to the buffer.
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.

1 participant