curb 1.3.5 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/ext/curb_easy.h CHANGED
@@ -11,6 +11,18 @@
11
11
 
12
12
  #include <curl/easy.h>
13
13
 
14
+ #define CURB_NETWORK_POLICY_NONE 0
15
+ #define CURB_NETWORK_POLICY_PUBLIC 1
16
+
17
+ #define CURB_CIDR_FAMILY_IPV4 4
18
+ #define CURB_CIDR_FAMILY_IPV6 6
19
+
20
+ typedef struct {
21
+ unsigned char family;
22
+ unsigned char prefix_bits;
23
+ unsigned char address[16];
24
+ } curb_cidr_rule;
25
+
14
26
  #ifdef CURL_VERSION_SSL
15
27
  #if LIBCURL_VERSION_NUM >= 0x070b00
16
28
  # if LIBCURL_VERSION_NUM <= 0x071004
@@ -38,6 +50,7 @@ typedef struct {
38
50
 
39
51
  /* Buffer for error details from CURLOPT_ERRORBUFFER */
40
52
  char err_buf[CURL_ERROR_SIZE];
53
+ char unsafe_destination_error[CURL_ERROR_SIZE];
41
54
 
42
55
  VALUE self; /* owning Ruby object */
43
56
  VALUE opts; /* rather then allocate everything we might need to store, allocate a Hash and only store objects we actually use... */
@@ -67,6 +80,7 @@ typedef struct {
67
80
  long ftp_filemethod;
68
81
  long http_version;
69
82
  unsigned short resolve_mode;
83
+ unsigned short network_policy;
70
84
 
71
85
  /* bool flags */
72
86
  char proxy_tunnel;
@@ -83,13 +97,25 @@ typedef struct {
83
97
  char cookielist_engine_enabled; /* track if CURLOPT_COOKIELIST was used with a non-command to enable engine */
84
98
  char ignore_content_length;
85
99
  char callback_active;
100
+ char unsafe_destination_blocked;
101
+ char allow_proxy;
102
+ char allow_unix_socket;
103
+ char forbid_reuse_set;
104
+ unsigned int native_active;
105
+ long forbid_reuse;
86
106
 
87
107
  struct curl_slist *curl_headers;
88
108
  struct curl_slist *curl_proxy_headers;
89
109
  struct curl_slist *curl_ftp_commands;
90
110
  struct curl_slist *curl_resolve;
111
+ struct curl_slist *curl_connect_to;
112
+ curb_cidr_rule *network_allowed_cidr_rules;
113
+ char **network_allowed_hosts;
91
114
 
92
115
  unsigned long multi_attachment_generation;
116
+ curl_off_t downloaded_body_bytes;
117
+ size_t network_allowed_cidr_rule_count;
118
+ size_t network_allowed_host_count;
93
119
  int last_result; /* last result code from multi loop */
94
120
 
95
121
  } ruby_curl_easy;
data/ext/curb_errors.c CHANGED
@@ -110,6 +110,7 @@ VALUE eCurlErrSSLShutdownFailed;
110
110
  VALUE eCurlErrAgain;
111
111
  VALUE eCurlErrSSLCRLBadfile;
112
112
  VALUE eCurlErrSSLIssuerError;
113
+ VALUE eCurlErrUnsafeDestination;
113
114
 
114
115
  /* multi errors */
115
116
  VALUE mCurlErrFailedInit;
@@ -699,6 +700,7 @@ void init_curb_errors() {
699
700
  eCurlErrSSLCacertBadfile = rb_define_class_under(mCurlErr, "SSLCaertBadFile", eCurlErrError);
700
701
  eCurlErrSSLCRLBadfile = rb_define_class_under(mCurlErr, "SSLCRLBadfile", eCurlErrError);
701
702
  eCurlErrSSLIssuerError = rb_define_class_under(mCurlErr, "SSLIssuerError", eCurlErrError);
703
+ eCurlErrUnsafeDestination = rb_define_class_under(mCurlErr, "UnsafeDestinationError", eCurlErrError);
702
704
  eCurlErrSSLShutdownFailed = rb_define_class_under(mCurlErr, "SSLShutdownFailed", eCurlErrError);
703
705
  eCurlErrSSH = rb_define_class_under(mCurlErr, "SSH", eCurlErrError);
704
706
 
data/ext/curb_errors.h CHANGED
@@ -106,6 +106,7 @@ extern VALUE eCurlErrSSLShutdownFailed;
106
106
  extern VALUE eCurlErrAgain;
107
107
  extern VALUE eCurlErrSSLCRLBadfile;
108
108
  extern VALUE eCurlErrSSLIssuerError;
109
+ extern VALUE eCurlErrUnsafeDestination;
109
110
 
110
111
  /* multi errors */
111
112
  extern VALUE mCurlErrFailedInit;
data/ext/curb_multi.c CHANGED
@@ -52,6 +52,11 @@
52
52
  #define CURB_MAYBE_UNUSED_DECL
53
53
  #endif
54
54
 
55
+ /* Older rubies do not define UNDEF_P. */
56
+ #ifndef UNDEF_P
57
+ #define UNDEF_P(obj) ((VALUE)(obj) == Qundef)
58
+ #endif
59
+
55
60
  #ifdef _WIN32
56
61
  // for O_RDWR and O_BINARY
57
62
  #include <fcntl.h>
@@ -70,10 +75,93 @@ static void *curl_multi_wait_wrapper(void *p) {
70
75
  }
71
76
  #endif
72
77
 
78
+ static VALUE cRubyFiber;
79
+ static ID id_fiber_blocking_p;
80
+ static ID id_fiber_scheduler;
81
+ static ID id_kernel_sleep;
82
+ static ID id_io_wait;
83
+
84
+ /*
85
+ * Ruby 3.0 introduced the Fiber scheduler at the Ruby level, but did not ship
86
+ * the public ruby/fiber/scheduler.h C API. Use the public C entry points when
87
+ * available and otherwise dispatch through Fiber and the scheduler object.
88
+ */
89
+ static VALUE curb_fiber_scheduler_current(void) {
90
+ #if defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
91
+ return rb_fiber_scheduler_current();
92
+ #else
93
+ VALUE scheduler;
94
+
95
+ if (!rb_respond_to(cRubyFiber, id_fiber_scheduler)) return Qnil;
96
+
97
+ scheduler = rb_funcall(cRubyFiber, id_fiber_scheduler, 0);
98
+ if (NIL_P(scheduler)) return Qnil;
99
+
100
+ /* Fiber.scheduler returns the thread scheduler even from a blocking fiber,
101
+ * while rb_fiber_scheduler_current returns nil there. Match the latter so
102
+ * ordinary blocking fibers keep using the legacy multi loop. */
103
+ if (rb_respond_to(cRubyFiber, id_fiber_blocking_p) &&
104
+ RTEST(rb_funcall(cRubyFiber, id_fiber_blocking_p, 0))) {
105
+ return Qnil;
106
+ }
107
+
108
+ return scheduler;
109
+ #endif
110
+ }
111
+
112
+ static VALUE curb_fiber_scheduler_kernel_sleep(VALUE scheduler, VALUE timeout) {
113
+ #if defined(HAVE_RB_FIBER_SCHEDULER_KERNEL_SLEEP)
114
+ return rb_fiber_scheduler_kernel_sleep(scheduler, timeout);
115
+ #else
116
+ return rb_funcall(scheduler, id_kernel_sleep, 1, timeout);
117
+ #endif
118
+ }
119
+
120
+ static VALUE curb_fiber_scheduler_io_wait(VALUE scheduler, VALUE io, VALUE events, VALUE timeout) {
121
+ #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT)
122
+ return rb_fiber_scheduler_io_wait(scheduler, io, events, timeout);
123
+ #else
124
+ return rb_funcall(scheduler, id_io_wait, 3, io, events, timeout);
125
+ #endif
126
+ }
127
+
128
+ /*
129
+ * Sleep for the given interval without stalling sibling fibers: when the
130
+ * current fiber runs under a fiber scheduler, wait via the scheduler's
131
+ * kernel_sleep hook. rb_thread_fd_select(0, ...) and rb_thread_wait_for are
132
+ * thread-level waits that never consult the scheduler, so they are only used
133
+ * when no scheduler is active.
134
+ */
135
+ static void curb_multi_scheduler_sleep(struct timeval *tv) {
136
+ VALUE scheduler = curb_fiber_scheduler_current();
137
+ if (scheduler != Qnil) {
138
+ curb_fiber_scheduler_kernel_sleep(scheduler, rb_float_new((double)tv->tv_sec + ((double)tv->tv_usec / 1e6)));
139
+ return;
140
+ }
141
+ #ifdef HAVE_RB_THREAD_FD_SELECT
142
+ {
143
+ struct timeval tv_sleep = *tv;
144
+ rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
145
+ }
146
+ #else
147
+ rb_thread_wait_for(*tv);
148
+ #endif
149
+ }
150
+
151
+ /* Yield a nonblocking fiber without changing legacy behavior when no
152
+ * scheduler is active. */
153
+ static void curb_multi_scheduler_yield(void) {
154
+ VALUE scheduler = curb_fiber_scheduler_current();
155
+ if (scheduler != Qnil) {
156
+ curb_fiber_scheduler_kernel_sleep(scheduler, INT2FIX(0));
157
+ }
158
+ }
159
+
73
160
  extern VALUE mCurl;
74
161
  static VALUE idCall;
75
162
  static ID id_deferred_exception_ivar;
76
163
  static ID id_deferred_exception_source_id_ivar;
164
+ static ID id_native_safety_signatures_ivar;
77
165
  static ID id_socket_io_cache_ivar;
78
166
 
79
167
  #ifdef RDOC_NEVER_DEFINED
@@ -286,6 +374,33 @@ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
286
374
  st_delete(rbcm->attached, &key, NULL);
287
375
  }
288
376
 
377
+ CURLMcode rb_curl_multi_detach_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
378
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
379
+ st_data_t key;
380
+
381
+ if (!rbcm || !rbce || !rbcm->attached) {
382
+ return CURLM_OK;
383
+ }
384
+
385
+ key = (st_data_t)rbce;
386
+ if (!st_delete(rbcm->attached, &key, NULL)) {
387
+ return CURLM_OK;
388
+ }
389
+
390
+ if (rbcm->handle && rbce->curl) {
391
+ CURLMcode result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
392
+ if (result != CURLM_OK) {
393
+ return result;
394
+ }
395
+ }
396
+
397
+ if (rbcm->active > 0) {
398
+ rbcm->active--;
399
+ }
400
+
401
+ return CURLM_OK;
402
+ }
403
+
289
404
  static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
290
405
  if (!rbcm || !rbcm->attached) {
291
406
  return;
@@ -314,6 +429,8 @@ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce) {
314
429
 
315
430
  static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
316
431
  VALUE requests;
432
+ VALUE object_id;
433
+ VALUE safety_signatures;
317
434
 
318
435
  if (NIL_P(self) || NIL_P(easy)) {
319
436
  return;
@@ -324,7 +441,17 @@ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
324
441
  return;
325
442
  }
326
443
 
327
- rb_hash_delete(requests, rb_obj_id(easy));
444
+ object_id = rb_obj_id(easy);
445
+ rb_hash_delete(requests, object_id);
446
+
447
+ if (!rb_ivar_defined(self, id_native_safety_signatures_ivar)) {
448
+ return;
449
+ }
450
+
451
+ safety_signatures = rb_ivar_get(self, id_native_safety_signatures_ivar);
452
+ if (RB_TYPE_P(safety_signatures, T_HASH)) {
453
+ rb_hash_delete(safety_signatures, object_id);
454
+ }
328
455
  }
329
456
 
330
457
  /* TypedData-compatible free function */
@@ -1132,30 +1259,6 @@ static const char *cselect_flags_str(int flags, char *buf, size_t n) {
1132
1259
  #define cselect_flags_str(...) ""
1133
1260
  #endif
1134
1261
 
1135
- #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1136
- /* Protected call to rb_fiber_scheduler_io_wait to avoid unwinding into C on TypeError. */
1137
- struct fiber_io_wait_args { VALUE scheduler; VALUE io; VALUE events; VALUE timeout; };
1138
- static VALUE fiber_io_wait_protected(VALUE argp) {
1139
- struct fiber_io_wait_args *a = (struct fiber_io_wait_args *)argp;
1140
- return rb_fiber_scheduler_io_wait(a->scheduler, a->io, a->events, a->timeout);
1141
- }
1142
- #endif
1143
-
1144
- #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1145
- struct fiber_io_select_args {
1146
- VALUE scheduler;
1147
- VALUE readables;
1148
- VALUE writables;
1149
- VALUE exceptables;
1150
- VALUE timeout;
1151
- };
1152
-
1153
- static VALUE fiber_io_select_protected(VALUE argp) {
1154
- struct fiber_io_select_args *a = (struct fiber_io_select_args *)argp;
1155
- return rb_fiber_scheduler_io_select(a->scheduler, a->readables, a->writables, a->exceptables, a->timeout);
1156
- }
1157
- #endif
1158
-
1159
1262
  #if defined(RB_INTEGER_TYPE_P)
1160
1263
  #define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
1161
1264
  #else
@@ -1347,6 +1450,7 @@ struct build_io_select_arrays_args {
1347
1450
  VALUE writables;
1348
1451
  VALUE exceptables;
1349
1452
  int failed;
1453
+ int state;
1350
1454
  };
1351
1455
 
1352
1456
  static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp) {
@@ -1365,12 +1469,7 @@ static int build_io_select_arrays_i(st_data_t key, st_data_t val, st_data_t argp
1365
1469
  io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1366
1470
  if (io_state || NIL_P(io)) {
1367
1471
  if (io_state) {
1368
- #if CURB_SOCKET_DEBUG
1369
- VALUE err = rb_errinfo();
1370
- VALUE msg = rb_obj_as_string(err);
1371
- curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1372
- #endif
1373
- rb_set_errinfo(Qnil);
1472
+ a->state = io_state;
1374
1473
  }
1375
1474
  a->failed = 1;
1376
1475
  return ST_STOP;
@@ -1415,23 +1514,31 @@ static int collect_io_select_ready_i(st_data_t key, st_data_t val, st_data_t arg
1415
1514
  #endif
1416
1515
 
1417
1516
  static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
1418
- /* prime the state: let libcurl act on timeouts to setup sockets */
1419
- CURLMcode mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1420
- if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1421
- curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1422
- rb_curl_multi_read_info(self, rbcm->handle);
1423
- rb_curl_multi_yield_if_given(self, block);
1517
+ CURLMcode mrc;
1424
1518
 
1425
- while (rbcm->running) {
1519
+ do {
1520
+ /* Prime the state: let libcurl act on timeouts to set up sockets. */
1521
+ mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1522
+ if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1523
+ curb_debugf("[curb.socket] drive: initial socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1524
+ rb_curl_multi_read_info(self, rbcm->handle);
1525
+ rb_curl_multi_yield_if_given(self, block);
1526
+
1527
+ while (rbcm->running) {
1426
1528
  struct timeval tv = {0, 0};
1427
1529
  long wait_ms = cCurlMutiDefaulttimeout;
1428
1530
 
1429
1531
  if (multi_socket_timer_due(ctx)) {
1532
+ ctx->timeout_deadline_ms = -1;
1430
1533
  mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1431
1534
  curb_debugf("[curb.socket] socket_action timeout(due) -> mrc=%d running=%d", mrc, rbcm->running);
1432
1535
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1433
1536
  rb_curl_multi_read_info(self, rbcm->handle);
1434
1537
  rb_curl_multi_yield_if_given(self, block);
1538
+ if (rbcm->running && multi_socket_timer_due(ctx)) {
1539
+ /* A repeatedly rearmed 0ms timer must still let sibling fibers run. */
1540
+ curb_multi_scheduler_yield();
1541
+ }
1435
1542
  continue;
1436
1543
  }
1437
1544
 
@@ -1469,26 +1576,24 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1469
1576
  if (count_tracked > 1) {
1470
1577
  #if defined(HAVE_RB_FIBER_SCHEDULER_IO_SELECT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1471
1578
  {
1472
- VALUE scheduler = rb_fiber_scheduler_current();
1579
+ VALUE scheduler = curb_fiber_scheduler_current();
1473
1580
  if (scheduler != Qnil) {
1474
1581
  VALUE readables = rb_ary_new();
1475
1582
  VALUE writables = rb_ary_new();
1476
1583
  VALUE exceptables = rb_ary_new();
1477
- struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0 };
1584
+ struct build_io_select_arrays_args build_args = { ctx, readables, writables, exceptables, 0, 0 };
1478
1585
  st_foreach(ctx->sock_map, build_io_select_arrays_i, (st_data_t)&build_args);
1586
+ if (build_args.state) rb_jump_tag(build_args.state);
1479
1587
  if (!build_args.failed) {
1480
1588
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1481
1589
  VALUE timeout = rb_float_new(timeout_s);
1482
- struct fiber_io_select_args select_args = { scheduler, readables, writables, exceptables, timeout };
1483
- int state = 0;
1484
- VALUE ready = rb_protect(fiber_io_select_protected, (VALUE)&select_args, &state);
1485
- if (state) {
1486
- #if CURB_SOCKET_DEBUG
1487
- VALUE err = rb_errinfo();
1488
- VALUE msg = rb_obj_as_string(err);
1489
- curb_debugf("[curb.socket] scheduler io_select failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1490
- #endif
1491
- rb_set_errinfo(Qnil);
1590
+ VALUE ready = rb_fiber_scheduler_io_select(scheduler, readables, writables, exceptables, timeout);
1591
+ if (UNDEF_P(ready)) {
1592
+ /* The scheduler does not implement the optional io_select hook:
1593
+ * rb_fiber_scheduler_io_select returns Qundef immediately.
1594
+ * Leave handled_wait unset so the io_wait fallback below can
1595
+ * perform a cooperative wait. */
1596
+ curb_debugf("[curb.socket] scheduler io_select not implemented; falling back to io_wait");
1492
1597
  } else {
1493
1598
  handled_wait = 1;
1494
1599
  any_ready = RB_TYPE_P(ready, T_ARRAY);
@@ -1527,8 +1632,36 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1527
1632
  }
1528
1633
  }
1529
1634
  #endif
1635
+ /* Ruby versions without the public io_select C API, and newer
1636
+ * schedulers that omit the optional hook, wait cooperatively on one
1637
+ * representative descriptor, then use a zero-time select below to
1638
+ * collect readiness across the whole set. */
1639
+ if (!handled_wait && wait_fd >= 0) {
1640
+ VALUE scheduler = curb_fiber_scheduler_current();
1641
+ if (scheduler != Qnil) {
1642
+ if (!multi_socket_fd_valid_p(wait_fd)) {
1643
+ multi_socket_forget_fd(ctx, wait_fd);
1644
+ did_timeout = 1;
1645
+ handled_wait = 1;
1646
+ } else {
1647
+ struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1648
+ int io_state = 0;
1649
+ VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1650
+ if (io_state) rb_jump_tag(io_state);
1651
+ if (!NIL_P(io)) {
1652
+ int events = multi_socket_wait_events_for_curl_poll(wait_what);
1653
+ double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1654
+ curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events), rb_float_new(timeout_s));
1655
+ tv.tv_sec = 0;
1656
+ tv.tv_usec = 0;
1657
+ }
1658
+ }
1659
+ }
1660
+ }
1530
1661
  if (!handled_wait) {
1531
- /* Multi-fd wait using scheduler-aware rb_thread_fd_select. */
1662
+ /* Multi-fd select. After the scheduler io_wait fallback above, tv is
1663
+ * zero and this only polls the complete descriptor set. Without a
1664
+ * scheduler it performs the normal GVL-releasing wait. */
1532
1665
  rb_fdset_t rfds, wfds, efds;
1533
1666
  rb_fd_init(&rfds); rb_fd_init(&wfds); rb_fd_init(&efds);
1534
1667
  int maxfd = -1;
@@ -1567,9 +1700,8 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1567
1700
  handled_wait = 1;
1568
1701
  }
1569
1702
  } else if (count_tracked == 1) {
1570
- #if defined(HAVE_RB_FIBER_SCHEDULER_IO_WAIT) && defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1571
1703
  {
1572
- VALUE scheduler = rb_fiber_scheduler_current();
1704
+ VALUE scheduler = curb_fiber_scheduler_current();
1573
1705
  if (scheduler != Qnil) {
1574
1706
  int scheduler_wait_handled = 0;
1575
1707
  int events = 0;
@@ -1579,11 +1711,9 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1579
1711
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1580
1712
  VALUE timeout = rb_float_new(timeout_s);
1581
1713
  if (wait_fd < 0) {
1582
- #ifdef HAVE_RB_THREAD_FD_SELECT
1583
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1584
- #else
1585
- rb_thread_wait_for(tv);
1586
- #endif
1714
+ /* No pollable fd yet: sleep via the scheduler's kernel_sleep so
1715
+ * sibling fibers keep running while libcurl gets ready. */
1716
+ curb_multi_scheduler_sleep(&tv);
1587
1717
  did_timeout = multi_socket_timer_due(ctx);
1588
1718
  scheduler_wait_handled = 1;
1589
1719
  } else if (!multi_socket_fd_valid_p(wait_fd)) {
@@ -1594,44 +1724,25 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1594
1724
  struct io_for_fd_args io_args = { ctx, wait_fd, wait_what };
1595
1725
  int io_state = 0;
1596
1726
  VALUE io = rb_protect(multi_socket_io_for_fd_protected, (VALUE)&io_args, &io_state);
1597
- if (io_state || NIL_P(io)) {
1598
- if (io_state) {
1599
- #if CURB_SOCKET_DEBUG
1600
- VALUE err = rb_errinfo();
1601
- VALUE msg = rb_obj_as_string(err);
1602
- curb_debugf("[curb.socket] IO.for_fd failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1603
- #endif
1604
- rb_set_errinfo(Qnil);
1605
- }
1727
+ if (io_state) rb_jump_tag(io_state);
1728
+ if (NIL_P(io)) {
1606
1729
  any_ready = 0;
1607
1730
  } else {
1608
- struct fiber_io_wait_args args = { scheduler, io, INT2NUM(events), timeout };
1609
- int state = 0;
1610
- VALUE ready = rb_protect(fiber_io_wait_protected, (VALUE)&args, &state);
1611
- if (state) {
1612
- #if CURB_SOCKET_DEBUG
1613
- VALUE err = rb_errinfo();
1614
- VALUE msg = rb_obj_as_string(err);
1615
- curb_debugf("[curb.socket] scheduler io_wait failed: %s: %s", rb_obj_classname(err), StringValueCStr(msg));
1616
- #endif
1617
- rb_set_errinfo(Qnil);
1618
- any_ready = 0;
1619
- } else {
1620
- scheduler_wait_handled = 1;
1621
- any_ready = (ready != Qfalse && !NIL_P(ready));
1622
- did_timeout = !any_ready && multi_socket_timer_due(ctx);
1623
- if (any_ready) {
1624
- if (ready == Qtrue) {
1625
- ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1626
- } else if (CURB_INTEGER_P(ready)) {
1627
- ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1628
- if (ready_flags == 0) {
1629
- any_ready = 0;
1630
- did_timeout = multi_socket_timer_due(ctx);
1631
- }
1632
- } else {
1633
- ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1731
+ VALUE ready = curb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events), timeout);
1732
+ scheduler_wait_handled = 1;
1733
+ any_ready = (ready != Qfalse && !NIL_P(ready));
1734
+ did_timeout = !any_ready && multi_socket_timer_due(ctx);
1735
+ if (any_ready) {
1736
+ if (ready == Qtrue) {
1737
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1738
+ } else if (CURB_INTEGER_P(ready)) {
1739
+ ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1740
+ if (ready_flags == 0) {
1741
+ any_ready = 0;
1742
+ did_timeout = multi_socket_timer_due(ctx);
1634
1743
  }
1744
+ } else {
1745
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1635
1746
  }
1636
1747
  }
1637
1748
  }
@@ -1639,7 +1750,6 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1639
1750
  if (scheduler_wait_handled) handled_wait = 1;
1640
1751
  }
1641
1752
  }
1642
- #endif
1643
1753
  #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
1644
1754
  if (!handled_wait && wait_fd >= 0) {
1645
1755
  int ev = multi_socket_wait_events_for_curl_poll(wait_what);
@@ -1683,15 +1793,18 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1683
1793
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1684
1794
  }
1685
1795
  } else { /* count_tracked == 0 */
1686
- #ifdef HAVE_RB_THREAD_FD_SELECT
1687
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv);
1688
- #else
1689
- rb_thread_wait_for(tv);
1690
- #endif
1691
- did_timeout = multi_socket_timer_due(ctx);
1796
+ /* No sockets exposed yet (e.g. libcurl's threaded resolver doing DNS):
1797
+ * sleep via the scheduler's kernel_sleep when one is active so sibling
1798
+ * fibers keep running, otherwise a plain thread wait. */
1799
+ curb_multi_scheduler_sleep(&tv);
1800
+ /* libcurl can report active work without a socket callback or deadline;
1801
+ * drive the timeout socket after the sleep so the state machine does
1802
+ * not stall indefinitely. */
1803
+ did_timeout = 1;
1692
1804
  }
1693
1805
 
1694
1806
  if (did_timeout) {
1807
+ ctx->timeout_deadline_ms = -1;
1695
1808
  mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1696
1809
  curb_debugf("[curb.socket] socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1697
1810
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
@@ -1714,7 +1827,13 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1714
1827
  rb_curl_multi_read_info(self, rbcm->handle);
1715
1828
  curb_debugf("[curb.socket] processed completions; running=%d", rbcm->running);
1716
1829
  rb_curl_multi_yield_if_given(self, block);
1717
- }
1830
+ }
1831
+
1832
+ /* Match the legacy perform contract: the block gets a final idle yield,
1833
+ * and work queued from that yield is driven before perform returns. */
1834
+ rb_curl_multi_read_info(self, rbcm->handle);
1835
+ rb_curl_multi_yield_if_given(self, block);
1836
+ } while (rbcm->running);
1718
1837
  }
1719
1838
 
1720
1839
  struct socket_drive_args { VALUE self; ruby_curl_multi *rbcm; multi_socket_ctx *ctx; VALUE block; };
@@ -1777,8 +1896,6 @@ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE se
1777
1896
  rb_ensure(ruby_curl_multi_socket_drive_body, (VALUE)&body_args, ruby_curl_multi_socket_drive_ensure, (VALUE)&ensure_args);
1778
1897
 
1779
1898
  /* finalize */
1780
- rb_curl_multi_read_info(self, rbcm->handle);
1781
- rb_curl_multi_yield_if_given(self, block);
1782
1899
  if (cCurlMutiAutoClose == 1) {
1783
1900
  rbcm->allow_close_during_perform = 1;
1784
1901
  rb_funcall(self, rb_intern("_autoclose"), 0);
@@ -1914,11 +2031,9 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1914
2031
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1915
2032
  rb_curl_multi_read_info( self, rbcm->handle );
1916
2033
  rb_curl_multi_yield_if_given(self, block);
1917
- #if defined(HAVE_RB_FIBER_SCHEDULER_CURRENT)
1918
- if (rb_fiber_scheduler_current() != Qnil) {
1919
- rb_thread_schedule();
1920
- }
1921
- #endif
2034
+ /* Yield the fiber so repeated immediate curl timeouts cannot starve
2035
+ * sibling work on this thread. */
2036
+ curb_multi_scheduler_yield();
1922
2037
  continue;
1923
2038
  }
1924
2039
 
@@ -1934,11 +2049,11 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1934
2049
  wait_args.timeout_ms = timeout_milliseconds;
1935
2050
  wait_args.numfds = 0;
1936
2051
  /*
1937
- * When a Fiber scheduler is available (Ruby >= 3.x), rb_thread_fd_select
1938
- * integrates with it. If we have rb_thread_fd_select available at build
1939
- * time, we avoid curl_multi_wait entirely (see preprocessor guard above)
1940
- * and use the fdset branch below. Otherwise, we use curl_multi_wait and
1941
- * release the GVL so Ruby threads can continue to run.
2052
+ * Wait via curl_multi_wait with the GVL released so other Ruby
2053
+ * threads can continue to run. Like rb_thread_fd_select, this wait
2054
+ * does not consult the fiber scheduler; scheduler-aware waiting is
2055
+ * provided by the socket-action loop, which perform routes to when a
2056
+ * scheduler is active (see ruby_curl_multi_perform).
1942
2057
  */
1943
2058
  CURLMcode wait_rc;
1944
2059
  #if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
@@ -1952,13 +2067,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1952
2067
  raise_curl_multi_error_exception(wait_rc);
1953
2068
  }
1954
2069
  if (wait_args.numfds == 0) {
1955
- #ifdef HAVE_RB_THREAD_FD_SELECT
1956
- struct timeval tv_sleep = tv_100ms;
1957
- /* Sleep in a scheduler-aware way. */
1958
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1959
- #else
1960
- rb_thread_wait_for(tv_100ms);
1961
- #endif
2070
+ curb_multi_scheduler_sleep(&tv_100ms);
1962
2071
  }
1963
2072
  /* Process pending transfers after waiting */
1964
2073
  rb_curl_multi_run(self, rbcm->handle, &(rbcm->running));
@@ -1982,12 +2091,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1982
2091
 
1983
2092
  if (maxfd == -1) {
1984
2093
  /* libcurl recommends sleeping for 100ms */
1985
- #ifdef HAVE_RB_THREAD_FD_SELECT
1986
- struct timeval tv_sleep = tv_100ms;
1987
- rb_thread_fd_select(0, NULL, NULL, NULL, &tv_sleep);
1988
- #else
1989
- rb_thread_wait_for(tv_100ms);
1990
- #endif
2094
+ curb_multi_scheduler_sleep(&tv_100ms);
1991
2095
  rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
1992
2096
  rb_curl_multi_read_info( self, rbcm->handle );
1993
2097
  rb_curl_multi_yield_if_given(self, block);
@@ -2010,7 +2114,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
2010
2114
  #endif
2011
2115
 
2012
2116
  #ifdef HAVE_RB_THREAD_FD_SELECT
2013
- /* Prefer scheduler-aware waiting when available. Build rb_fdset_t sets. */
2117
+ /* Wait with the GVL released. Note rb_thread_fd_select does not consult
2118
+ * the fiber scheduler — under a scheduler, perform routes to the
2119
+ * socket-action loop instead (see ruby_curl_multi_perform). Build
2120
+ * rb_fdset_t sets. */
2014
2121
  {
2015
2122
  rb_fdset_t rfds, wfds, efds;
2016
2123
  rb_fd_init(&rfds);
@@ -2122,6 +2229,18 @@ static VALUE ruby_curl_multi_with_perform_guard(int argc, VALUE *argv, VALUE sel
2122
2229
  }
2123
2230
 
2124
2231
  VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
2232
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
2233
+ /*
2234
+ * The legacy fdset loop waits with rb_thread_fd_select, which releases the
2235
+ * GVL but never consults the fiber scheduler, stalling every other fiber on
2236
+ * this thread for the duration of the transfers. When the calling fiber
2237
+ * runs under a fiber scheduler, use the socket-action drive loop, which
2238
+ * waits through the scheduler's io_wait/io_select/kernel_sleep hooks.
2239
+ */
2240
+ if (curb_fiber_scheduler_current() != Qnil) {
2241
+ return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
2242
+ }
2243
+ #endif
2125
2244
  return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
2126
2245
  }
2127
2246
 
@@ -2188,8 +2307,15 @@ static void curl_multi_mark(void *ptr) {
2188
2307
  /* =================== INIT LIB =====================*/
2189
2308
  void init_curb_multi() {
2190
2309
  idCall = rb_intern("call");
2310
+ cRubyFiber = rb_const_get(rb_cObject, rb_intern("Fiber"));
2311
+ rb_global_variable(&cRubyFiber);
2312
+ id_fiber_blocking_p = rb_intern("blocking?");
2313
+ id_fiber_scheduler = rb_intern("scheduler");
2314
+ id_kernel_sleep = rb_intern("kernel_sleep");
2315
+ id_io_wait = rb_intern("io_wait");
2191
2316
  id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
2192
2317
  id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2318
+ id_native_safety_signatures_ivar = rb_intern("@__curb_native_safety_signatures");
2193
2319
  id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
2194
2320
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
2195
2321
 
@@ -2208,9 +2334,10 @@ void init_curb_multi() {
2208
2334
  rb_define_method(cCurlMulti, "_add", ruby_curl_multi_add, 1);
2209
2335
  rb_define_method(cCurlMulti, "_remove", ruby_curl_multi_remove, 1);
2210
2336
  /*
2211
- * The legacy fdset loop is the stable default. The newer socket-action path
2212
- * is kept in-tree, but it has shown scheduler regressions for one-handle
2213
- * multi usage (for example Curl::Easy#perform under Async).
2337
+ * perform uses the legacy fdset loop by default, but routes to the
2338
+ * socket-action drive loop when the calling fiber runs under a fiber
2339
+ * scheduler so sibling fibers keep running during transfers. The
2340
+ * socket-action path is also exposed directly as _socket_perform.
2214
2341
  */
2215
2342
  rb_define_method(cCurlMulti, "perform", ruby_curl_multi_perform, -1);
2216
2343
  #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)