Skip to content

Commit 16139a4

Browse files
Fix: use rb_protect around rb_funcall so cleanup runs on exception paths
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 92bbc3b commit 16139a4

1 file changed

Lines changed: 23 additions & 4 deletions

File tree

scheduler.c

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,15 @@ rb_fiber_scheduler_address_resolve(VALUE scheduler, VALUE hostname)
10881088
* Thread.new { blocking_operation.call }.join
10891089
* end
10901090
*/
1091+
// Helper for rb_protect: calls scheduler.blocking_operation_wait(blocking_operation).
1092+
// args[0] = scheduler VALUE, args[1] = blocking_operation VALUE.
1093+
static VALUE
1094+
scheduler_blocking_operation_wait_call(VALUE _args)
1095+
{
1096+
VALUE *args = (VALUE *)_args;
1097+
return rb_funcall(args[0], id_blocking_operation_wait, 1, args[1]);
1098+
}
1099+
10911100
VALUE rb_fiber_scheduler_blocking_operation_wait(VALUE scheduler, void* (*function)(void *), void *data, rb_unblock_function_t *unblock_function, void *data2, int flags, struct rb_fiber_scheduler_blocking_operation_state *state)
10921101
{
10931102
// Check if scheduler supports blocking_operation_wait before creating the object
@@ -1098,25 +1107,35 @@ VALUE rb_fiber_scheduler_blocking_operation_wait(VALUE scheduler, void* (*functi
10981107
// Create a new BlockingOperation with the blocking operation
10991108
VALUE blocking_operation = rb_fiber_scheduler_blocking_operation_new(function, data, unblock_function, data2, flags, state);
11001109

1101-
rb_fiber_scheduler_blocking_operation_t *operation = NULL; // get_blocking_operation(blocking_operation);
1110+
rb_fiber_scheduler_blocking_operation_t *operation = get_blocking_operation(blocking_operation);
11021111

1103-
VALUE result = rb_funcall(scheduler, id_blocking_operation_wait, 1, blocking_operation);
1112+
// Use rb_protect so that cleanup runs even when the scheduler raises an exception
1113+
// (e.g. via rb_jump_tag from worker_pool_call). Without this, a longjmp from
1114+
// inside rb_funcall bypasses the cleanup below and RB_GC_GUARD, leaving
1115+
// operation with stale pointers and blocking_operation without a live GC root.
1116+
VALUE call_args[2] = {scheduler, blocking_operation};
1117+
int tag = 0;
1118+
VALUE result = rb_protect(scheduler_blocking_operation_wait_call, (VALUE)call_args, &tag);
11041119

11051120
operation = get_blocking_operation(blocking_operation);
11061121

11071122
// Get the operation data to check if it was executed
11081123
rb_atomic_t current_status = RUBY_ATOMIC_LOAD(operation->status);
11091124

1110-
// Invalidate the operation now that we're done with it
1125+
// Invalidate the operation now that we're done with it — must happen even on
1126+
// exception paths, since operation->state may point to a caller's stack frame.
11111127
operation->function = NULL;
11121128
operation->state = NULL;
11131129
operation->data = NULL;
11141130
operation->data2 = NULL;
11151131
operation->unblock_function = NULL;
11161132

1117-
// Ensure that the blocking operation remains visible until this point:
1133+
// Ensure that blocking_operation remains a live GC root through the cleanup above.
11181134
RB_GC_GUARD(blocking_operation);
11191135

1136+
// Re-raise any exception from the scheduler after cleanup.
1137+
if (tag) rb_jump_tag(tag);
1138+
11201139
// If the blocking operation was never executed, return Qundef to signal the caller to use rb_nogvl instead
11211140
if (current_status == RB_FIBER_SCHEDULER_BLOCKING_OPERATION_STATUS_QUEUED) {
11221141
return Qundef;

0 commit comments

Comments
 (0)