Skip to content

Keep the Closure in place for libffi across GC compaction - #212

Open
jeremy wants to merge 2 commits into
ruby:masterfrom
jeremy:closure-compaction
Open

Keep the Closure in place for libffi across GC compaction#212
jeremy wants to merge 2 commits into
ruby:masterfrom
jeremy:closure-compaction

Conversation

@jeremy

@jeremy jeremy commented Aug 7, 2026

Copy link
Copy Markdown

Closes #211.

Problem

Fiddle::Closure gives libffi the VALUE of the Closure object as user data. The trampoline casts that value back on each call.

closure_data_type sets .dmark = 0 and declares no .dcompact. Nothing keeps the object in place. GC compaction can move the Closure. After a move, libffi gives back a dead address, and the process stops with SIGSEGV.

Fiddle::Closure::BlockCaller and Fiddle::Importer#bind fail in the same way.

Change

libffi now receives the fiddle_closure struct. That memory comes from xmalloc and does not move.

The struct holds the Closure VALUE in a new self member:

  • closure_mark marks the member with rb_gc_mark_movable.
  • closure_compact updates the member with rb_gc_location.
  • The trampoline reads ((fiddle_closure *)x->ctx)->self.

The Closure stays movable. The ffi gem uses the same method.

Test

The new test is test_call_after_compaction in test/fiddle/test_closure.rb.

The test keeps the Closure in an array. A local variable is pinned by the conservative machine-stack scan. A pinned Closure does not move, and then the test cannot fail. A comment in the test records this reason.

build result
before this change SIGSEGV at 0x4
after this change pass

The full suite after this change: 242 tests, 670 assertions, 0 failures, 0 errors, 3 omissions.

Environment: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [arm64-darwin23].

Alternative

rb_gc_mark also corrects the fault, and the diff is smaller. But it pins one object for each live Closure. This pull request uses rb_gc_mark_movable to prevent the pin. Please tell me if you prefer the smaller diff.

Fiddle::Closure gave libffi the VALUE of the Closure object as user data.
The trampoline cast that value back on each call. closure_data_type set
.dmark = 0 and declared no .dcompact, so nothing kept the object in place.
When GC compaction moved the Closure, libffi returned a dead address and
the process stopped with SIGSEGV. Closure::BlockCaller and Importer#bind
failed in the same way.

libffi now receives the fiddle_closure struct. That memory comes from
xmalloc and does not move. The struct holds the Closure VALUE in a new
self member. closure_mark marks the member with rb_gc_mark_movable and
closure_compact updates it with rb_gc_location, so the Closure stays
movable. The ffi gem uses the same method.

The new test keeps the Closure in an array. A local variable is pinned by
the conservative machine-stack scan, and a pinned Closure does not move.
The test then cannot fail, so the comment records the reason.

Before this change the test stops with SIGSEGV at 0x4. After it, the test
passes and the full suite reports 242 tests, 670 assertions, 0 failures,
0 errors and 3 omissions on ruby 4.0.6 (arm64-darwin23).

Fixes ruby#211

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.

@kou
kou requested a lite review from Copilot August 7, 2026 01:23

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

ext/fiddle/closure.c:70

  • Like .dmark, the new .dcompact can be invoked with ptr == NULL after closure_free/initialize_rescue set RTYPEDDATA_DATA(self) = NULL. Guard against NULL to avoid dereferencing it during compaction.
closure_compact(void *ptr)
{
    fiddle_closure *closure = ptr;
    closure->self = rb_gc_location(closure->self);
}

Comment thread ext/fiddle/closure.c
Comment thread test/fiddle/test_closure.rb Outdated
Fiddle::Closure#free and initialize_rescue set the data pointer to NULL.
The garbage collector does not call a mark function with a NULL data
pointer, so this is not a defect that a test can show. But closure.c is
the only file in the extension that clears the pointer, so both new
functions now check it.

Also change the test to a block form of unless, as the review asks.
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.

Fiddle::Closure segfaults after GC compaction: raw VALUE handed to libffi as closure user-data

3 participants