cool.io 1.9.4 → 1.9.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f15a89de41069e6ccb37eb28befd01d601851ae1bb9ee2a5081e1c26e0bcc8da
4
- data.tar.gz: de69fe197c029bbb4438e2e1c751580712ecc1f1b8326803d2089b738d3f74cc
3
+ metadata.gz: 3da0843b8a62fd01aab297ce31f885a5856b4326bef2fb3c90d363f9576fe9a3
4
+ data.tar.gz: d073dd104500ccc2c1b62411c8f02c083df004353f9d2798ccf8ae41166effbd
5
5
  SHA512:
6
- metadata.gz: c88454501a2b042515b0571a729bac9a829ed7af72be377caf5ab6db08f90bb48adb1e19a69dee1b38eb1c77df9c084c88faa19d0077503c85a965e6c0c556ac
7
- data.tar.gz: 8fd2af4f085bbb253df79cb8c9c685df97c5dc233f4be4891cef796120e9b265676e6fa4fe7c39b0b3b77447c000e41cbb1bc66b8939869e8747ac270619c436
6
+ metadata.gz: b3f2fab2151abdbd3e0762aa04675cab8b4d196990227bf5d068f12360a62c87e669480f50e70b47b289a9c0f29deeddb28836590422faafe5d0a5ecaf6173fb
7
+ data.tar.gz: 233ea14784f4390b6b1f1b8fb131d5b1d20d14d0b78f9f1fd3b39f89bd25d34be505267d23d1b5647be2febcd5216e33a459c0e135ee49737334a06a0626bb47
@@ -0,0 +1,44 @@
1
+ name: Memcheck
2
+
3
+ on: [push, pull_request]
4
+
5
+ permissions:
6
+ contents: read
7
+
8
+ jobs:
9
+ test:
10
+ name: ${{matrix.ruby}} on ${{matrix.os}}
11
+ runs-on: ${{matrix.os}}-latest
12
+ continue-on-error: ${{matrix.experimental}}
13
+
14
+ strategy:
15
+ matrix:
16
+ os:
17
+ - ubuntu
18
+
19
+ ruby:
20
+ - "3.2"
21
+ - "3.3"
22
+ - "3.4"
23
+ - "4.0"
24
+
25
+ experimental: [false]
26
+
27
+ include:
28
+ - os: ubuntu
29
+ ruby: head
30
+ experimental: true
31
+
32
+ steps:
33
+ - uses: actions/checkout@v6
34
+ - uses: ruby/setup-ruby@v1
35
+ with:
36
+ ruby-version: ${{matrix.ruby}}
37
+ bundler-cache: true
38
+ - name: Install Valgrind
39
+ run: |
40
+ sudo apt-get update
41
+ sudo apt-get install -y valgrind
42
+ - name: Run tests
43
+ timeout-minutes: 10
44
+ run: bundle exec rake spec:valgrind
data/Rakefile CHANGED
@@ -22,7 +22,7 @@ end
22
22
 
23
23
  require 'rake/extensiontask'
24
24
 
25
- spec = eval(File.read("cool.io.gemspec"))
25
+ spec = Gem::Specification.load("cool.io.gemspec")
26
26
 
27
27
  def configure_cross_compilation(ext)
28
28
  unless RUBY_PLATFORM =~ /mswin|mingw/
@@ -48,24 +48,11 @@ namespace :build do
48
48
  end
49
49
  end
50
50
 
51
- # adapted from http://flavoriffic.blogspot.com/2009/06/easily-valgrind-gdb-your-ruby-c.html
52
- def specs_command
53
- require "find"
54
- files = []
55
- Find.find("spec") do |f|
56
- files << f if File.basename(f) =~ /.*spec.*\.rb$/
57
- end
58
- cmdline = "#{RUBY} -I.:lib:ext:spec \
59
- -e '%w[#{files.join(' ')}].each { |f| require f }'"
60
- end
61
-
62
- namespace :spec do
63
- desc "run specs with valgrind"
64
- task :valgrind => :compile do
65
- system "valgrind --num-callers=15 \
66
- --partial-loads-ok=yes --undef-value-errors=no \
67
- --tool=memcheck --leak-check=yes --track-fds=yes \
68
- --show-reachable=yes #{specs_command}"
51
+ if RUBY_PLATFORM.include?('linux')
52
+ require 'ruby_memcheck'
53
+ require 'ruby_memcheck/rspec/rake_task'
54
+ namespace :spec do
55
+ RubyMemcheck::RSpec::RakeTask.new(valgrind: :compile)
69
56
  end
70
57
  end
71
58
 
data/cool.io.gemspec CHANGED
@@ -1,6 +1,4 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "cool.io/version"
1
+ require_relative "lib/cool.io/version"
4
2
 
5
3
  Gem::Specification.new do |s|
6
4
  s.name = "cool.io"
data/ext/cool.io/buffer.c CHANGED
@@ -28,17 +28,22 @@
28
28
 
29
29
  /* Default number of bytes in each node's buffer. Should be >= MTU */
30
30
  #define DEFAULT_NODE_SIZE 16384
31
- static unsigned default_node_size = DEFAULT_NODE_SIZE;
31
+ static size_t default_node_size = DEFAULT_NODE_SIZE;
32
32
 
33
+ /*
34
+ * Byte counts are kept in size_t so the accounting cannot wrap while the
35
+ * buffer itself still holds the data. node_size is capped at MAX_BUFFER_SIZE,
36
+ * but size grows with whatever the buffer is asked to hold.
37
+ */
33
38
  struct buffer {
34
- unsigned size, node_size;
39
+ size_t size, node_size;
35
40
  struct buffer_node *head, *tail;
36
41
  struct buffer_node *pool_head, *pool_tail;
37
42
 
38
43
  };
39
44
 
40
45
  struct buffer_node {
41
- unsigned start, end;
46
+ size_t start, end;
42
47
  struct buffer_node *next;
43
48
  unsigned char data[0];
44
49
  };
@@ -68,13 +73,13 @@ static struct buffer *buffer_init(struct buffer *);
68
73
  static void buffer_clear(struct buffer * buf);
69
74
  static void buffer_free(struct buffer * buf);
70
75
  static void buffer_free_pool(struct buffer * buf);
71
- static void buffer_prepend(struct buffer * buf, char *str, unsigned len);
72
- static void buffer_append(struct buffer * buf, char *str, unsigned len);
73
- static void buffer_read(struct buffer * buf, char *str, unsigned len);
76
+ static void buffer_prepend(struct buffer * buf, char *str, size_t len);
77
+ static void buffer_append(struct buffer * buf, char *str, size_t len);
78
+ static void buffer_read(struct buffer * buf, char *str, size_t len);
74
79
  static int buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark);
75
- static void buffer_copy(struct buffer * buf, char *str, unsigned len);
76
- static int buffer_read_from(struct buffer * buf, int fd);
77
- static int buffer_write_to(struct buffer * buf, int fd);
80
+ static void buffer_copy(struct buffer * buf, char *str, size_t len);
81
+ static ssize_t buffer_read_from(struct buffer * buf, int fd);
82
+ static ssize_t buffer_write_to(struct buffer * buf, int fd);
78
83
 
79
84
  /*
80
85
  * High-performance I/O buffer intended for use in non-blocking programs
@@ -145,21 +150,21 @@ Coolio_Buffer_free(void * buf)
145
150
 
146
151
  /**
147
152
  * call-seq:
148
- * Coolio::Buffer.default_node_size -> 4096
153
+ * Coolio::Buffer.default_node_size -> 16384
149
154
  *
150
155
  * Retrieves the current value of the default node size.
151
156
  */
152
157
  static VALUE
153
158
  Coolio_Buffer_default_node_size(VALUE klass)
154
159
  {
155
- return UINT2NUM(default_node_size);
160
+ return SIZET2NUM(default_node_size);
156
161
  }
157
162
 
158
163
  /*
159
164
  * safely converts node sizes from Ruby numerics to C and raising
160
165
  * ArgumentError or RangeError on invalid sizes
161
166
  */
162
- static unsigned
167
+ static size_t
163
168
  convert_node_size(VALUE size)
164
169
  {
165
170
  if (
@@ -168,7 +173,7 @@ convert_node_size(VALUE size)
168
173
  )
169
174
  rb_raise(rb_eArgError, "invalid buffer size");
170
175
 
171
- return (unsigned) NUM2INT(size);
176
+ return NUM2SIZET(size);
172
177
  }
173
178
 
174
179
  /**
@@ -241,7 +246,7 @@ Coolio_Buffer_size(VALUE self)
241
246
  struct buffer *buf;
242
247
  TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
243
248
 
244
- return INT2NUM(buf->size);
249
+ return SIZET2NUM(buf->size);
245
250
  }
246
251
 
247
252
  /**
@@ -311,15 +316,18 @@ static VALUE
311
316
  Coolio_Buffer_read(int argc, VALUE * argv, VALUE self)
312
317
  {
313
318
  VALUE length_obj, str;
314
- int length;
319
+ size_t length;
315
320
  struct buffer *buf;
316
321
 
317
322
  TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
318
323
 
319
324
  if (rb_scan_args(argc, argv, "01", &length_obj) == 1) {
320
- length = NUM2INT(length_obj);
321
- if(length < 1)
325
+ /* Read signed so a negative argument still raises ArgumentError here
326
+ * rather than RangeError out of the conversion */
327
+ long requested = NUM2LONG(length_obj);
328
+ if(requested < 1)
322
329
  rb_raise(rb_eArgError, "length must be greater than zero");
330
+ length = (size_t) requested;
323
331
  if(length > buf->size)
324
332
  length = buf->size;
325
333
  } else
@@ -328,7 +336,7 @@ Coolio_Buffer_read(int argc, VALUE * argv, VALUE self)
328
336
  if(buf->size == 0)
329
337
  return rb_str_new2("");
330
338
 
331
- str = rb_str_new(0, length);
339
+ str = rb_str_new(0, (long) length);
332
340
  buffer_read(buf, RSTRING_PTR(str), length);
333
341
 
334
342
  return str;
@@ -352,6 +360,9 @@ Coolio_Buffer_read_frame(VALUE self, VALUE data, VALUE mark)
352
360
 
353
361
  TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
354
362
 
363
+ StringValue(data);
364
+ rb_str_modify(data);
365
+
355
366
  if (buffer_read_frame(buf, data, mark_c)) {
356
367
  return Qtrue;
357
368
  } else {
@@ -373,7 +384,7 @@ Coolio_Buffer_to_str(VALUE self)
373
384
 
374
385
  TypedData_Get_Struct(self, struct buffer, &Coolio_Buffer_type, buf);
375
386
 
376
- str = rb_str_new(0, buf->size);
387
+ str = rb_str_new(0, (long) buf->size);
377
388
  buffer_copy(buf, RSTRING_PTR(str), buf->size);
378
389
 
379
390
  return str;
@@ -391,7 +402,7 @@ static VALUE
391
402
  Coolio_Buffer_read_from(VALUE self, VALUE io)
392
403
  {
393
404
  struct buffer *buf;
394
- int ret;
405
+ ssize_t ret;
395
406
  #if defined(HAVE_RB_IO_T) || defined(HAVE_RB_IO_DESCRIPTOR)
396
407
  rb_io_t *fptr;
397
408
  #else
@@ -408,7 +419,7 @@ Coolio_Buffer_read_from(VALUE self, VALUE io)
408
419
  #else
409
420
  ret = buffer_read_from(buf, FPTR_TO_FD(fptr));
410
421
  #endif
411
- return ret == -1 ? Qnil : INT2NUM(ret);
422
+ return ret == -1 ? Qnil : SSIZET2NUM(ret);
412
423
  }
413
424
 
414
425
  /**
@@ -435,9 +446,9 @@ Coolio_Buffer_write_to(VALUE self, VALUE io)
435
446
  rb_io_set_nonblock(fptr);
436
447
 
437
448
  #ifdef HAVE_RB_IO_DESCRIPTOR
438
- return INT2NUM(buffer_write_to(buf, rb_io_descriptor(io)));
449
+ return SSIZET2NUM(buffer_write_to(buf, rb_io_descriptor(io)));
439
450
  #else
440
- return INT2NUM(buffer_write_to(buf, FPTR_TO_FD(fptr)));
451
+ return SSIZET2NUM(buffer_write_to(buf, FPTR_TO_FD(fptr)));
441
452
  #endif
442
453
  }
443
454
 
@@ -535,7 +546,7 @@ buffer_node_free(struct buffer * buf, struct buffer_node * node)
535
546
 
536
547
  /* Prepend data to the front of the buffer */
537
548
  static void
538
- buffer_prepend(struct buffer * buf, char *str, unsigned len)
549
+ buffer_prepend(struct buffer * buf, char *str, size_t len)
539
550
  {
540
551
  struct buffer_node *node, *tmp;
541
552
  buf->size += len;
@@ -576,9 +587,9 @@ buffer_prepend(struct buffer * buf, char *str, unsigned len)
576
587
 
577
588
  /* Append data to the front of the buffer */
578
589
  static void
579
- buffer_append(struct buffer * buf, char *str, unsigned len)
590
+ buffer_append(struct buffer * buf, char *str, size_t len)
580
591
  {
581
- unsigned nbytes;
592
+ size_t nbytes;
582
593
  buf->size += len;
583
594
 
584
595
  /* If it fits in the remaining space in the tail */
@@ -613,9 +624,9 @@ buffer_append(struct buffer * buf, char *str, unsigned len)
613
624
 
614
625
  /* Read data from the buffer (and clear what we've read) */
615
626
  static void
616
- buffer_read(struct buffer * buf, char *str, unsigned len)
627
+ buffer_read(struct buffer * buf, char *str, size_t len)
617
628
  {
618
- unsigned nbytes;
629
+ size_t nbytes;
619
630
  struct buffer_node *tmp;
620
631
 
621
632
  while (buf->size > 0 && len > 0) {
@@ -649,7 +660,7 @@ buffer_read(struct buffer * buf, char *str, unsigned len)
649
660
  static int
650
661
  buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark)
651
662
  {
652
- unsigned nbytes = 0;
663
+ size_t nbytes = 0;
653
664
  struct buffer_node *tmp;
654
665
 
655
666
  while (buf->size > 0) {
@@ -664,7 +675,7 @@ buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark)
664
675
  }
665
676
 
666
677
  /* Copy less than everything if we found a frame byte */
667
- rb_str_cat(str, s, nbytes);
678
+ rb_str_cat(str, s, (long) nbytes);
668
679
 
669
680
  /* Fixup the buffer pointers to indicate the bytes were consumed */
670
681
  head->start += nbytes;
@@ -688,9 +699,9 @@ buffer_read_frame(struct buffer * buf, VALUE str, char frame_mark)
688
699
 
689
700
  /* Copy data from the buffer without clearing it */
690
701
  static void
691
- buffer_copy(struct buffer * buf, char *str, unsigned len)
702
+ buffer_copy(struct buffer * buf, char *str, size_t len)
692
703
  {
693
- unsigned nbytes;
704
+ size_t nbytes;
694
705
  struct buffer_node *node;
695
706
 
696
707
  node = buf->head;
@@ -709,10 +720,10 @@ buffer_copy(struct buffer * buf, char *str, unsigned len)
709
720
  }
710
721
 
711
722
  /* Write data from the buffer to a file descriptor */
712
- static int
723
+ static ssize_t
713
724
  buffer_write_to(struct buffer * buf, int fd)
714
725
  {
715
- int bytes_written, total_bytes_written = 0;
726
+ ssize_t bytes_written, total_bytes_written = 0;
716
727
  struct buffer_node *tmp;
717
728
 
718
729
  while (buf->head) {
@@ -730,7 +741,7 @@ buffer_write_to(struct buffer * buf, int fd)
730
741
  buf->size -= bytes_written;
731
742
 
732
743
  /* If the write blocked... */
733
- if (bytes_written < buf->head->end - buf->head->start) {
744
+ if ((size_t) bytes_written < buf->head->end - buf->head->start) {
734
745
  buf->head->start += bytes_written;
735
746
  return total_bytes_written;
736
747
  }
@@ -748,11 +759,11 @@ buffer_write_to(struct buffer * buf, int fd)
748
759
 
749
760
  /* Read data from a file descriptor to a buffer */
750
761
  /* Append data to the front of the buffer */
751
- static int
762
+ static ssize_t
752
763
  buffer_read_from(struct buffer * buf, int fd)
753
764
  {
754
- int bytes_read, total_bytes_read = 0;
755
- unsigned nbytes;
765
+ ssize_t bytes_read, total_bytes_read = 0;
766
+ size_t nbytes;
756
767
 
757
768
  /* Empty list needs initialized */
758
769
  if (!buf->head) {
@@ -782,7 +793,7 @@ buffer_read_from(struct buffer * buf, int fd)
782
793
  buf->tail->next = buffer_node_new(buf);
783
794
  buf->tail = buf->tail->next;
784
795
  }
785
- } while (bytes_read == nbytes);
796
+ } while ((size_t) bytes_read == nbytes);
786
797
 
787
798
  return total_bytes_read;
788
799
  }
@@ -55,6 +55,12 @@ struct Coolio_Watcher
55
55
  int enabled;
56
56
  VALUE loop;
57
57
 
58
+ /* Stable copy of the watched path for ev_stat watchers. libev retains the
59
+ * path pointer passed to ev_stat_init() for the lifetime of the watcher, so
60
+ * it must not point into a Ruby String whose buffer GC.compact may relocate.
61
+ * NULL for watcher types that don't use it. */
62
+ char *stat_path;
63
+
58
64
  void (*dispatch_callback)(VALUE self, int revents);
59
65
  };
60
66
 
@@ -11,7 +11,6 @@ have_func('rb_thread_alone')
11
11
  have_func('rb_str_set_len')
12
12
  have_library('rt', 'clock_gettime')
13
13
 
14
- have_func("rb_io_descriptor")
15
14
  have_library("c", "main")
16
15
  if have_macro("HAVE_RB_IO_T", "ruby/io.h")
17
16
  have_struct_member("rb_io_t", "fd", "ruby/io.h")
data/ext/cool.io/loop.c CHANGED
@@ -110,8 +110,7 @@ void Coolio_Loop_process_event(VALUE watcher, int revents)
110
110
  struct Coolio_Loop *loop_data;
111
111
  struct Coolio_Watcher *watcher_data;
112
112
 
113
- /* The Global VM lock isn't held right now, but hopefully
114
- * we can still do this safely */
113
+ /* The Global VM Lock is held here, see the explanation below */
115
114
  watcher_data = Coolio_Watcher_ptr(watcher);
116
115
 
117
116
  if (watcher_data->enabled == 0) {
@@ -126,13 +125,14 @@ void Coolio_Loop_process_event(VALUE watcher, int revents)
126
125
  *
127
126
  * Our call path up to here looks a little something like:
128
127
  *
129
- * -> release GVL -> event syscall -> libev callback
130
- * (GVL = Global VM Lock) ^^^ You are here
128
+ * -> release GVL -> event syscall -> reacquire GVL -> libev callback
129
+ * (GVL = Global VM Lock) ^^^ You are here
131
130
  *
132
- * We released the GVL in the Coolio_Loop_run_once() function
133
- * so other Ruby threads can run while we make a blocking
134
- * system call (one of epoll, kqueue, port, poll, or select,
135
- * depending on the platform).
131
+ * libev is patched (see ev.c) to release the GVL around the blocking
132
+ * system call (one of epoll, kqueue, port, poll, or select, depending
133
+ * on the platform) so other Ruby threads can run while we wait there.
134
+ * Only that call runs without the GVL: libev invokes the events it
135
+ * collected after the call has returned, so we hold the GVL here.
136
136
  *
137
137
  * More specifically, this is a libev callback abstraction
138
138
  * called from a real libev callback in every watcher,
@@ -150,19 +150,19 @@ void Coolio_Loop_process_event(VALUE watcher, int revents)
150
150
  * event fired, why the hell is it telling the loop? Why
151
151
  * doesn't it just rb_funcall() the appropriate callback?
152
152
  *
153
- * Well, the problem is the Global VM Lock isn't held right
154
- * now, so we can't rb_funcall() anything. In order to get
155
- * it back we have to:
153
+ * Because Ruby code doesn't run from inside libev's own event
154
+ * invocation. Instead:
156
155
  *
157
- * stash event and return -> acquire GVL -> dispatch to Ruby
156
+ * stash event and return -> ev_loop() returns -> dispatch to Ruby
158
157
  *
159
- * Which is kinda ugly and confusing, but still gives us
158
+ * Which is kinda ugly and confusing, but still gives us
160
159
  * an O(1) event loop whose heart is in the kernel itself. w00t!
161
160
  *
162
161
  * So, stash the event in the loop's data struct. When we return
163
162
  * the ev_loop() call being made in the Coolio_Loop_run_once_blocking()
164
- * function below will also return, at which point the GVL is
165
- * reacquired and we can call out to Ruby */
163
+ * function below will also return, and Coolio_Loop_dispatch_events()
164
+ * walks what we stashed and calls out to Ruby. A watcher detached
165
+ * along the way nils its own stashed entries, which that walk skips */
166
166
 
167
167
  /* Grow the event buffer if it's too small */
168
168
  if(loop_data->events_received >= loop_data->eventbuf_size) {
@@ -79,7 +79,9 @@ void Init_coolio_stat_watcher()
79
79
  */
80
80
  static VALUE Coolio_StatWatcher_initialize(int argc, VALUE *argv, VALUE self)
81
81
  {
82
- VALUE path, interval;
82
+ VALUE path, interval;
83
+ const char *path_str;
84
+ long path_len;
83
85
  struct Coolio_Watcher *watcher_data;
84
86
 
85
87
  rb_scan_args(argc, argv, "11", &path, &interval);
@@ -87,15 +89,26 @@ static VALUE Coolio_StatWatcher_initialize(int argc, VALUE *argv, VALUE self)
87
89
  interval = rb_convert_type(interval, T_FLOAT, "Float", "to_f");
88
90
 
89
91
  path = rb_String(path);
92
+ path_str = StringValueCStr(path);
90
93
  rb_iv_set(self, "@path", path);
91
94
 
92
95
  watcher_data = Coolio_Watcher_ptr(self);
93
96
 
97
+ /* libev keeps the path pointer passed to ev_stat_init() for the lifetime of
98
+ * the watcher. RSTRING_PTR(path) points into a Ruby String whose buffer
99
+ * GC.compact may relocate, leaving libev with a dangling pointer. Keep an
100
+ * owned copy instead; it is released in Coolio_Watcher_free(). */
101
+ path_len = strlen(path_str);
102
+ if(watcher_data->stat_path)
103
+ xfree(watcher_data->stat_path);
104
+ watcher_data->stat_path = xmalloc(path_len + 1);
105
+ memcpy(watcher_data->stat_path, path_str, path_len + 1);
106
+
94
107
  watcher_data->dispatch_callback = Coolio_StatWatcher_dispatch_callback;
95
108
  ev_stat_init(
96
109
  &watcher_data->event_types.ev_stat,
97
110
  Coolio_StatWatcher_libev_callback,
98
- RSTRING_PTR(path),
111
+ watcher_data->stat_path,
99
112
  interval == Qnil ? 0 : NUM2DBL(interval)
100
113
  );
101
114
  watcher_data->event_types.ev_stat.data = (void *)self;
@@ -14,6 +14,7 @@ static VALUE cCoolio_Watcher = Qnil;
14
14
 
15
15
  static VALUE Coolio_Watcher_allocate(VALUE klass);
16
16
  static void Coolio_Watcher_mark(void *data);
17
+ static void Coolio_Watcher_free(void *data);
17
18
 
18
19
  static VALUE Coolio_Watcher_initialize(VALUE self);
19
20
  static VALUE Coolio_Watcher_attach(VALUE self, VALUE loop);
@@ -56,7 +57,7 @@ static const rb_data_type_t Coolio_Watcher_type = {
56
57
  "Coolio::Watcher",
57
58
  {
58
59
  Coolio_Watcher_mark,
59
- RUBY_DEFAULT_FREE,
60
+ Coolio_Watcher_free,
60
61
  },
61
62
  };
62
63
 
@@ -75,6 +76,7 @@ static VALUE Coolio_Watcher_allocate(VALUE klass)
75
76
 
76
77
  watcher_data->loop = Qnil;
77
78
  watcher_data->enabled = 0;
79
+ watcher_data->stat_path = NULL;
78
80
 
79
81
  return watcher;
80
82
  }
@@ -87,6 +89,16 @@ static void Coolio_Watcher_mark(void *data)
87
89
  rb_gc_mark(watcher_data->loop);
88
90
  }
89
91
 
92
+ static void Coolio_Watcher_free(void *data)
93
+ {
94
+ struct Coolio_Watcher *watcher_data = data;
95
+
96
+ if(watcher_data->stat_path)
97
+ xfree(watcher_data->stat_path);
98
+
99
+ xfree(watcher_data);
100
+ }
101
+
90
102
  static VALUE Coolio_Watcher_initialize(VALUE self)
91
103
  {
92
104
  rb_raise(rb_eRuntimeError, "watcher base class should not be initialized directly");
data/ext/libev/ev.c CHANGED
@@ -210,7 +210,13 @@
210
210
  #else
211
211
  # include <io.h>
212
212
  # define WIN32_LEAN_AND_MEAN
213
- # define FD_SETSIZE 1024
213
+ /* ruby.h above already pulled in winsock2.h, so fd_set may be dimensioned
214
+ * already. Defining FD_SETSIZE unconditionally here would only move the bound
215
+ * used by EV_WIN_FD_SET, not the array it indexes. Take whatever is in effect
216
+ * and only supply a default when nothing has been decided yet. */
217
+ # ifndef FD_SETSIZE
218
+ # define FD_SETSIZE 1024
219
+ # endif
214
220
  # include <winsock2.h>
215
221
  # include <windows.h>
216
222
  # ifndef EV_SELECT_IS_WINSOCKET
@@ -107,6 +107,16 @@ if (__i == ((fd_set *)(set))->fd_count) {\
107
107
  #define EV_WIN_FD_ZERO(set) (((fd_set *)(set))->fd_count=0)
108
108
  #define EV_WIN_FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set *)(set))
109
109
  #define EV_WIN_FD_COUNT(set) (((fd_set *)(set))->fd_count)
110
+
111
+ /*
112
+ fd_set is dimensioned by whatever FD_SETSIZE was in effect when winsock2.h was
113
+ first pulled in, but EV_WIN_FD_SET and select_modify bound-check against the
114
+ FD_SETSIZE visible here. The two are decided by separate paths, so if the bound
115
+ ever exceeds the declared array we would write past the end of the allocation in
116
+ select_init. Catch that at build time instead.
117
+ */
118
+ typedef char coolio_fd_setsize_matches_fd_set[
119
+ (sizeof (((fd_set *)0)->fd_array) / sizeof (SOCKET) >= (size_t)FD_SETSIZE) ? 1 : -1];
110
120
  /* ######################################## */
111
121
  #else
112
122
  #define EV_WIN_FD_CLR FD_CLR
data/gems.rb CHANGED
@@ -7,3 +7,7 @@ group :maintenance, optional: true do
7
7
  gem "bake-gem"
8
8
  gem "bake-modernize"
9
9
  end
10
+
11
+ group :development, :test do
12
+ gem 'ruby_memcheck', '~> 3.0' if RUBY_PLATFORM.include?('linux')
13
+ end
@@ -18,6 +18,8 @@
18
18
  #++
19
19
 
20
20
  require 'resolv'
21
+ require 'securerandom'
22
+ require 'socket'
21
23
 
22
24
  module Coolio
23
25
  # A non-blocking DNS resolver. It provides interfaces for querying both
@@ -67,14 +69,22 @@ module Coolio
67
69
  # list of nameservers to query. By default the resolver will
68
70
  # use nameservers listed in /etc/resolv.conf
69
71
  def initialize(hostname, *nameservers)
72
+ nameservers = reject_ipv6_nameservers(nameservers)
70
73
  if nameservers.empty?
71
- nameservers = Resolv::DNS::Config.default_config_hash[:nameserver]
74
+ nameservers = reject_ipv6_nameservers(Resolv::DNS::Config.default_config_hash[:nameserver])
72
75
  raise RuntimeError, "no nameservers found" if nameservers.empty? # TODO just call resolve_failed, not raise [also handle Errno::ENOENT)]
73
76
  end
74
77
 
75
78
  @nameservers = nameservers.dup
76
79
  @question = request_question hostname
77
80
 
81
+ # A guessable ID would let an off-path attacker forge a response
82
+ @request_id = SecureRandom.random_number(1 << 16)
83
+
84
+ # Numeric addresses this query was sent to, and the lookups behind them
85
+ @queried_addresses = []
86
+ @numeric_addresses = {}
87
+
78
88
  @socket = UDPSocket.new
79
89
  @timer = Timeout.new(self)
80
90
 
@@ -113,27 +123,44 @@ module Coolio
113
123
 
114
124
  # Send a request to the DNS server
115
125
  def send_request
116
- nameserver = @nameservers.shift
117
- @nameservers << nameserver # rotate them
126
+ @nameservers.rotate!
127
+
128
+ # Send to the numeric address, so we know where a response must come from
129
+ address = numeric_address(@nameservers.first)
130
+ @queried_addresses << address unless @queried_addresses.include?(address)
131
+
118
132
  begin
119
- @socket.send request_message, 0, @nameservers.first, DNS_PORT
133
+ @socket.send request_message, 0, address, DNS_PORT
120
134
  rescue Errno::EHOSTUNREACH # TODO figure out why it has to be wrapper here, when the other wrapper should be wrapping this one!
121
135
  end
122
136
  end
123
137
 
124
138
  # Called by the subclass when the DNS response is available
125
139
  def on_readable
126
- datagram = nil
140
+ datagram = sender = nil
127
141
  begin
128
- datagram = @socket.recvfrom_nonblock(DATAGRAM_SIZE).first
142
+ datagram, sender = @socket.recvfrom_nonblock(DATAGRAM_SIZE)
129
143
  rescue Errno::ECONNREFUSED
130
144
  end
131
145
 
146
+ # Ignore anything we didn't ask for, rather than resolving or failing on it.
147
+ # The query stays outstanding, so the retry timer still bounds us.
148
+ return if datagram and not solicited_response?(datagram, sender)
149
+
132
150
  address = response_address datagram rescue nil
133
151
  address ? on_success(address) : on_failure
134
152
  detach
135
153
  end
136
154
 
155
+ # Is this a reply to our query, from an address we sent it to?
156
+ # Retries rotate through @nameservers, so any address already queried counts.
157
+ def solicited_response?(datagram, sender)
158
+ return false unless datagram.size >= 12
159
+ return false unless sender and sender[1] == DNS_PORT and @queried_addresses.include?(sender[3])
160
+
161
+ datagram[0..1].unpack('n').first.to_i == @request_id
162
+ end
163
+
137
164
  def request_question(hostname)
138
165
  raise ArgumentError, "hostname cannot be nil" if hostname.nil?
139
166
 
@@ -151,7 +178,7 @@ module Coolio
151
178
 
152
179
  def request_message
153
180
  # Standard query header
154
- message = [2, 1, 0].pack('nCC')
181
+ message = [@request_id, 1, 0].pack('nCC')
155
182
 
156
183
  # One entry
157
184
  qdcount = 1
@@ -166,7 +193,7 @@ module Coolio
166
193
  def response_address(message)
167
194
  # Confirm the ID field
168
195
  id = message[0..1].unpack('n').first.to_i
169
- return unless id == 2
196
+ return unless id == @request_id
170
197
 
171
198
  # Check the QR value and confirm this message is a response
172
199
  qr = message[2..2].unpack('B1').first.to_i
@@ -204,6 +231,23 @@ module Coolio
204
231
  nil
205
232
  end
206
233
 
234
+ private
235
+
236
+ def reject_ipv6_nameservers(nameservers)
237
+ nameservers.reject { |ns| ns.include?(':') }
238
+ end
239
+
240
+ # The address of a nameserver, which may be given as a hostname.
241
+ # Only successful lookups are cached, so a transient failure is looked up again.
242
+ def numeric_address(nameserver)
243
+ @numeric_addresses[nameserver] ||= begin
244
+ addrinfo = Addrinfo.getaddrinfo(nameserver, nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM).first
245
+ raise SocketError, "getaddrinfo: no IPv4 address for #{nameserver}" if addrinfo.nil?
246
+
247
+ addrinfo.ip_address
248
+ end
249
+ end
250
+
207
251
  class Timeout < TimerWatcher
208
252
  def initialize(resolver)
209
253
  @resolver = resolver
@@ -1,6 +1,6 @@
1
1
  module Coolio
2
- VERSION = "1.9.4"
3
-
2
+ VERSION = "1.9.5"
3
+
4
4
  def self.version
5
5
  VERSION
6
6
  end
@@ -1,20 +1,26 @@
1
1
  diff --git a/ext/libev/ev.c b/ext/libev/ev.c
2
- index dae87f1..d15f6bd 100644
2
+ index a59efb2..5c18fd6 100644
3
3
  --- a/ext/libev/ev.c
4
4
  +++ b/ext/libev/ev.c
5
- @@ -207,6 +207,7 @@
5
+ @@ -210,6 +210,13 @@
6
6
  #else
7
7
  # include <io.h>
8
8
  # define WIN32_LEAN_AND_MEAN
9
- +# define FD_SETSIZE 1024
9
+ +/* ruby.h above already pulled in winsock2.h, so fd_set may be dimensioned
10
+ + * already. Defining FD_SETSIZE unconditionally here would only move the bound
11
+ + * used by EV_WIN_FD_SET, not the array it indexes. Take whatever is in effect
12
+ + * and only supply a default when nothing has been decided yet. */
13
+ +# ifndef FD_SETSIZE
14
+ +# define FD_SETSIZE 1024
15
+ +# endif
10
16
  # include <winsock2.h>
11
17
  # include <windows.h>
12
18
  # ifndef EV_SELECT_IS_WINSOCKET
13
19
  diff --git a/ext/libev/ev_select.c b/ext/libev/ev_select.c
14
- index f38d6ca..7050778 100644
20
+ index ed1fc7a..eccff2b 100644
15
21
  --- a/ext/libev/ev_select.c
16
22
  +++ b/ext/libev/ev_select.c
17
- @@ -67,6 +67,54 @@
23
+ @@ -67,6 +67,64 @@
18
24
 
19
25
  #include <string.h>
20
26
 
@@ -58,6 +64,16 @@ index f38d6ca..7050778 100644
58
64
  +#define EV_WIN_FD_ZERO(set) (((fd_set *)(set))->fd_count=0)
59
65
  +#define EV_WIN_FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set *)(set))
60
66
  +#define EV_WIN_FD_COUNT(set) (((fd_set *)(set))->fd_count)
67
+ +
68
+ +/*
69
+ +fd_set is dimensioned by whatever FD_SETSIZE was in effect when winsock2.h was
70
+ +first pulled in, but EV_WIN_FD_SET and select_modify bound-check against the
71
+ +FD_SETSIZE visible here. The two are decided by separate paths, so if the bound
72
+ +ever exceeds the declared array we would write past the end of the allocation in
73
+ +select_init. Catch that at build time instead.
74
+ +*/
75
+ +typedef char coolio_fd_setsize_matches_fd_set[
76
+ + (sizeof (((fd_set *)0)->fd_array) / sizeof (SOCKET) >= (size_t)FD_SETSIZE) ? 1 : -1];
61
77
  +/* ######################################## */
62
78
  +#else
63
79
  +#define EV_WIN_FD_CLR FD_CLR
@@ -69,7 +85,7 @@ index f38d6ca..7050778 100644
69
85
  static void
70
86
  select_modify (EV_P_ int fd, int oev, int nev)
71
87
  {
72
- @@ -91,17 +139,17 @@ select_modify (EV_P_ int fd, int oev, int nev)
88
+ @@ -91,17 +149,17 @@ select_modify (EV_P_ int fd, int oev, int nev)
73
89
  if ((oev ^ nev) & EV_READ)
74
90
  #endif
75
91
  if (nev & EV_READ)
@@ -91,7 +107,7 @@ index f38d6ca..7050778 100644
91
107
 
92
108
  #else
93
109
 
94
- @@ -197,8 +245,8 @@ select_poll (EV_P_ ev_tstamp timeout)
110
+ @@ -197,8 +255,8 @@ select_poll (EV_P_ ev_tstamp timeout)
95
111
  {
96
112
  if (timeout)
97
113
  {
@@ -102,7 +118,7 @@ index f38d6ca..7050778 100644
102
118
  }
103
119
 
104
120
  return;
105
- @@ -230,10 +278,10 @@ select_poll (EV_P_ ev_tstamp timeout)
121
+ @@ -230,10 +288,10 @@ select_poll (EV_P_ ev_tstamp timeout)
106
122
  int handle = fd;
107
123
  #endif
108
124
 
@@ -116,7 +132,7 @@ index f38d6ca..7050778 100644
116
132
  #endif
117
133
 
118
134
  if (expect_true (events))
119
- @@ -279,9 +327,9 @@ select_init (EV_P_ int flags)
135
+ @@ -280,9 +338,9 @@ select_init (EV_P_ int flags)
120
136
  backend_poll = select_poll;
121
137
 
122
138
  #if EV_SELECT_USE_FD_SET
@@ -30,7 +30,9 @@ describe Cool.io::AsyncWatcher, :env => :exclude_win do
30
30
  end
31
31
 
32
32
  # ensure children are ready
33
- nr_fork.times { expect(rd.sysread(1)).to eq('.') }
33
+ # rd may be O_NONBLOCK on macOS Ruby 3.2+ (IO.pipe sets O_NONBLOCK); use read
34
+ # instead of sysread so EAGAIN on an empty pipe is retried transparently.
35
+ nr_fork.times { expect(rd.read(1)).to eq('.') }
34
36
 
35
37
  # send our signals
36
38
  nr_signal.times { aw.signal }
@@ -1,10 +1,13 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Cool.io::Loop do
4
+ # An IOWatcher that drains its pipe and then runs a user-supplied block,
5
+ # receiving itself as the argument.
4
6
  class Victim < Cool.io::IOWatcher
5
- def initialize(io)
6
- super
7
+ def initialize(io, &on_readable)
8
+ super(io)
7
9
  @io = io
10
+ @on_readable = on_readable
8
11
  end
9
12
 
10
13
  def on_readable
@@ -12,38 +15,57 @@ describe Cool.io::Loop do
12
15
  @io.read_nonblock(1024)
13
16
  rescue IO::WaitReadable, EOFError
14
17
  end
18
+ @on_readable.call(self) if @on_readable
15
19
  end
16
20
  end
17
21
 
18
22
  # https://github.com/socketry/cool.io/issues/87
19
- it "does not raise TypeError when a watcher is detached while an event is pending" do
20
- loop = Cool.io::Loop.default
21
-
23
+ #
24
+ # Several watchers have an event pending in the same loop iteration. When the
25
+ # first one dispatched detaches the others, the loop must skip their now-stale
26
+ # pending events instead of dispatching them to a detached watcher. Before the
27
+ # fix that raised "TypeError: wrong argument type nil (expected Coolio::Loop)"
28
+ # and could crash the VM.
29
+ #
30
+ # This is exercised deterministically within a single thread (a preceding
31
+ # callback detaching another watcher in the same loop cycle). The original
32
+ # reproduction detached from a separate thread while the loop was polling,
33
+ # which is an unsupported concurrent mutation of libev (not thread-safe) and
34
+ # crashed intermittently on macOS.
35
+ it "does not raise when a watcher with a pending event is detached during dispatch" do
22
36
  iterations = 200
23
37
 
24
38
  expect {
25
39
  iterations.times do
26
- r_victim, w_victim = IO.pipe
27
- victim_watcher = Victim.new(r_victim)
28
- victim_watcher.attach(loop)
29
-
30
- t1 = Thread.new do
31
- sleep 0.01
32
- w_victim.write("dummy\n")
33
- end
34
-
35
- t2 = Thread.new do
36
- sleep 0.01
37
- victim_watcher.detach
40
+ coolio_loop = Cool.io::Loop.new
41
+ pipes = []
42
+ watchers = []
43
+
44
+ 5.times do
45
+ r, w = IO.pipe
46
+ pipes << [r, w]
47
+
48
+ watcher = Victim.new(r) do |fired|
49
+ # Detach every other watcher whose event is already queued for this
50
+ # same loop iteration.
51
+ watchers.each do |other|
52
+ other.detach if !other.equal?(fired) && other.attached?
53
+ end
54
+ end
55
+ watcher.attach(coolio_loop)
56
+ watchers << watcher
57
+
58
+ w.write("dummy\n") # make the read end readable so an event is pending
38
59
  end
39
60
 
40
- loop.run_once
61
+ coolio_loop.run_once
41
62
 
42
- t1.join
43
- t2.join
63
+ # Only the first dispatched watcher runs; it detaches the other four,
64
+ # whose pending events are then skipped.
65
+ expect(watchers.count(&:attached?)).to eq(1)
44
66
 
45
- r_victim.close
46
- w_victim.close
67
+ watchers.each { |watcher| watcher.detach if watcher.attached? }
68
+ pipes.each { |r, w| r.close; w.close }
47
69
  end
48
70
  }.not_to raise_error
49
71
  end
data/spec/dns_spec.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require File.expand_path('../spec_helper', __FILE__)
2
+ require 'tempfile'
2
3
 
3
4
  VALID_DOMAIN = "google.com"
4
5
  INVALID_DOMAIN = "gibidigibigididibitidibigitibidigitidididi.com"
@@ -55,4 +56,189 @@ describe "DNS" do
55
56
  expect( Coolio::DNSResolver.hosts("localhost", file.path)).to eq @preferred_localhost_address
56
57
  end
57
58
  end
59
+
60
+ describe "IPv6 nameserver filtering" do
61
+ it "ignores IPv6 nameservers provided in arguments" do
62
+ resolver = Coolio::DNSResolver.new("example.com", "8.8.8.8", "2001:4860:4860::8888", "1.1.1.1")
63
+
64
+ nameservers = resolver.instance_variable_get(:@nameservers)
65
+ expect(nameservers).to eq(["8.8.8.8", "1.1.1.1"])
66
+ end
67
+
68
+ it "falls back to default IPv4 config if only IPv6 addresses are provided" do
69
+ allow(Resolv::DNS::Config).to receive(:default_config_hash).and_return({
70
+ nameserver: ["8.8.4.4", "2001:4860:4860::8844"]
71
+ })
72
+
73
+ resolver = Coolio::DNSResolver.new("example.com", "2001:4860:4860::8888")
74
+
75
+ nameservers = resolver.instance_variable_get(:@nameservers)
76
+ expect(nameservers).to eq(["8.8.4.4"])
77
+ end
78
+ end
79
+
80
+ describe "nameserver normalization" do
81
+ let(:localhost_address) do
82
+ Addrinfo.getaddrinfo("localhost", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM).first.ip_address
83
+ end
84
+
85
+ it "keeps the nameserver list as given" do
86
+ resolver = Coolio::DNSResolver.new("example.com", "localhost")
87
+
88
+ expect(resolver.instance_variable_get(:@nameservers)).to eq(["localhost"])
89
+ end
90
+
91
+ it "queries the numeric address of a nameserver given as a hostname" do
92
+ resolver = Coolio::DNSResolver.new("example.com", "localhost")
93
+ resolver.__send__(:send_request)
94
+
95
+ expect(resolver.instance_variable_get(:@queried_addresses)).to eq([localhost_address])
96
+ end
97
+
98
+ it "accepts responses from a nameserver which was given as a hostname" do
99
+ resolver = Coolio::DNSResolver.new("example.com", "localhost")
100
+ resolver.__send__(:send_request)
101
+ response = dns_response_for(resolver)
102
+
103
+ expect(
104
+ resolver.__send__(:solicited_response?, response, ["AF_INET", 53, localhost_address, localhost_address])
105
+ ).to be true
106
+ end
107
+
108
+ it "looks a nameserver up once and reuses the result on retries" do
109
+ resolved = Addrinfo.getaddrinfo("127.0.0.1", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM)
110
+ resolver = Coolio::DNSResolver.new("example.com", "127.0.0.1")
111
+
112
+ expect(Addrinfo).to receive(:getaddrinfo).once.and_return(resolved)
113
+
114
+ 3.times { resolver.__send__(:send_request) }
115
+ end
116
+
117
+ it "does not reject an unresolvable nameserver at construction" do
118
+ allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError, "getaddrinfo: Name or service not known")
119
+
120
+ expect do
121
+ Coolio::DNSResolver.new("example.com", "no-such-nameserver.invalid")
122
+ end.to_not raise_error
123
+ end
124
+
125
+ it "surfaces an unresolvable nameserver as a SocketError from the request" do
126
+ allow(Addrinfo).to receive(:getaddrinfo).and_raise(SocketError, "getaddrinfo: Name or service not known")
127
+ resolver = Coolio::DNSResolver.new("example.com", "no-such-nameserver.invalid")
128
+
129
+ expect { resolver.attach(@loop) }.to raise_error(SocketError)
130
+ expect(@loop.watchers).to be_empty
131
+ end
132
+
133
+ it "recovers when a nameserver is only transiently unresolvable" do
134
+ resolved = Addrinfo.getaddrinfo("127.0.0.1", nil, ::Socket::AF_INET, ::Socket::SOCK_DGRAM)
135
+ resolver = Coolio::DNSResolver.new("example.com", "ns.example.test")
136
+
137
+ attempts = 0
138
+ allow(Addrinfo).to receive(:getaddrinfo) do
139
+ attempts += 1
140
+ raise SocketError, "getaddrinfo: Name or service not known" if attempts == 1
141
+
142
+ resolved
143
+ end
144
+
145
+ expect { resolver.__send__(:send_request) }.to raise_error(SocketError)
146
+ expect { resolver.__send__(:send_request) }.to_not raise_error
147
+ expect(resolver.instance_variable_get(:@queried_addresses)).to eq(["127.0.0.1"])
148
+ end
149
+ end
150
+
151
+ describe "response validation" do
152
+ let(:nameserver) { "127.0.0.1" }
153
+ let(:sender) { ["AF_INET", 53, nameserver, nameserver] }
154
+ let(:resolver) do
155
+ Coolio::DNSResolver.new("example.com", nameserver).tap { |r| r.__send__(:send_request) }
156
+ end
157
+
158
+ it "uses an unpredictable transaction ID for each query" do
159
+ ids = 10.times.map do
160
+ request_id_of(Coolio::DNSResolver.new("example.com", nameserver))
161
+ end
162
+
163
+ expect(ids.uniq.size).to be > 1
164
+ end
165
+
166
+ it "accepts a response carrying our transaction ID from the queried nameserver" do
167
+ expect(
168
+ resolver.__send__(:solicited_response?, dns_response_for(resolver), sender)
169
+ ).to be true
170
+ end
171
+
172
+ it "rejects a response carrying a different transaction ID" do
173
+ forged = dns_response_for(resolver, id: (request_id_of(resolver) + 1) % 65536)
174
+
175
+ expect(resolver.__send__(:solicited_response?, forged, sender)).to be false
176
+ end
177
+
178
+ it "rejects a response from a source address we did not query" do
179
+ response = dns_response_for(resolver)
180
+
181
+ expect(
182
+ resolver.__send__(:solicited_response?, response, ["AF_INET", 53, "10.11.12.13", "10.11.12.13"])
183
+ ).to be false
184
+ end
185
+
186
+ it "rejects a response arriving before the request was sent" do
187
+ unsent = Coolio::DNSResolver.new("example.com", nameserver)
188
+
189
+ expect(unsent.__send__(:solicited_response?, dns_response_for(unsent), sender)).to be false
190
+ end
191
+
192
+ it "rejects a response from a source port other than the DNS port" do
193
+ response = dns_response_for(resolver)
194
+
195
+ expect(
196
+ resolver.__send__(:solicited_response?, response, ["AF_INET", 4444, nameserver, nameserver])
197
+ ).to be false
198
+ end
199
+
200
+ it "rejects a truncated datagram" do
201
+ expect(resolver.__send__(:solicited_response?, "\0\0", sender)).to be false
202
+ end
203
+
204
+ it "resolves from a response sent by the queried nameserver" do
205
+ response = dns_response_for(resolver, address: "1.2.3.4")
206
+ allow(resolver.instance_variable_get(:@socket)).to receive(:recvfrom_nonblock).and_return([response, sender])
207
+
208
+ expect(resolver).to receive(:on_success).with("1.2.3.4")
209
+ expect(resolver).to receive(:detach)
210
+
211
+ resolver.__send__(:on_readable)
212
+ end
213
+
214
+ it "ignores a spoofed response instead of resolving or failing it" do
215
+ forged = dns_response_for(resolver, address: "6.6.6.6")
216
+ allow(resolver.instance_variable_get(:@socket)).to receive(:recvfrom_nonblock)
217
+ .and_return([forged, ["AF_INET", 53, "10.11.12.13", "10.11.12.13"]])
218
+
219
+ expect(resolver).to_not receive(:on_success)
220
+ expect(resolver).to_not receive(:on_failure)
221
+ expect(resolver).to_not receive(:detach)
222
+
223
+ resolver.__send__(:on_readable)
224
+ end
225
+ end
226
+
227
+ def request_id_of(resolver)
228
+ resolver.__send__(:request_message)[0..1].unpack('n').first
229
+ end
230
+
231
+ # A response to the resolver's own query: header plus the echoed question,
232
+ # and an A record when an address is given.
233
+ def dns_response_for(resolver, id: request_id_of(resolver), address: nil)
234
+ question = resolver.instance_variable_get(:@question)
235
+ answer = if address
236
+ # Compressed name pointer, type A, class IN, TTL, RDLENGTH, RDATA
237
+ [0xc00c, 1, 1, 60, 4].pack('nnnNn') + address.split('.').map(&:to_i).pack('CCCC')
238
+ else
239
+ ""
240
+ end
241
+
242
+ [id, 0x81, 0x80, 1, answer.empty? ? 0 : 1, 0, 0].pack('nCCnnnn') + question + answer
243
+ end
58
244
  end
@@ -31,6 +31,18 @@ describe Cool.io::Buffer do
31
31
  expect(buffer << "baz").to eq "baz"
32
32
  expect(buffer.read 3).to eq "arb"
33
33
  end
34
+
35
+ it "raises ArgumentError for a length below one" do
36
+ buffer << "foo"
37
+ expect { buffer.read 0 }.to raise_error ArgumentError
38
+ expect { buffer.read(-1) }.to raise_error ArgumentError
39
+ end
40
+
41
+ it "clamps a length which does not fit in a C int to the buffer size" do
42
+ buffer << "foobar"
43
+ expect(buffer.read 2**31).to eq "foobar"
44
+ expect(buffer.size).to eq 0
45
+ end
34
46
  end
35
47
 
36
48
  describe "provides methods for performing non-blocking I/O" do
@@ -142,6 +154,25 @@ describe Cool.io::Buffer do
142
154
  expect(data).to eq "foo\nbarbaz"
143
155
  expect(buffer.to_str).to eq ""
144
156
  end
157
+
158
+ it "raises TypeError instead of crashing when data is not a String" do
159
+ buffer << "hello world"
160
+ expect { buffer.read_frame 12345, " ".ord }.to raise_error(TypeError)
161
+ expect { buffer.read_frame nil, " ".ord }.to raise_error(TypeError)
162
+ expect { buffer.read_frame [], " ".ord }.to raise_error(TypeError)
163
+ end
164
+
165
+ it "raises FrozenError when data is a frozen String" do
166
+ buffer << "hello world"
167
+ expect { buffer.read_frame "frozen".freeze, " ".ord }.to raise_error(FrozenError)
168
+ end
169
+
170
+ it "coerces objects responding to #to_str" do
171
+ buffer << "foo\nbar"
172
+ convertible = Object.new
173
+ def convertible.to_str; +""; end
174
+ expect(buffer.read_frame convertible, "\n".ord).to eq true
175
+ end
145
176
  end
146
177
 
147
178
  end
@@ -18,12 +18,31 @@ class MyStatWatcher < Cool.io::StatWatcher
18
18
  end
19
19
  end
20
20
 
21
- def run_with_file_change(path)
21
+ def run_with_file_change(path, compact: false)
22
22
  reactor = Cool.io::Loop.new
23
23
 
24
24
  sw = MyStatWatcher.new(path)
25
25
  sw.attach(reactor)
26
26
 
27
+ # libev retains the path pointer passed to ev_stat_init() for the lifetime of
28
+ # the watcher. If the watcher held RSTRING_PTR(@path) directly, a compaction
29
+ # that relocates the @path String would leave libev dereferencing a stale
30
+ # address on every stat. Force a maximal relocation here so the watcher keeps
31
+ # operating against a path buffer it owns rather than Ruby-managed memory.
32
+ if compact
33
+ if GC.respond_to?(:verify_compaction_references)
34
+ # verify_compaction_references moves every movable object to a fresh slot.
35
+ # Its keyword arguments have varied across Ruby versions, so fall back.
36
+ begin
37
+ GC.verify_compaction_references(expand_heap: true, toplevel: true)
38
+ rescue ArgumentError
39
+ GC.verify_compaction_references(expand_heap: true)
40
+ end
41
+ elsif GC.respond_to?(:compact)
42
+ GC.compact
43
+ end
44
+ end
45
+
27
46
  tw = Cool.io::TimerWatcher.new(INTERVAL, true)
28
47
  tw.on_timer do
29
48
  reactor.stop if sw.accessed
@@ -69,6 +88,17 @@ describe Cool.io::StatWatcher do
69
88
  expect(watcher.previous.ino).to eq(watcher.current.ino)
70
89
  end
71
90
 
91
+ it "keeps firing on_change after GC compaction relocates objects" do
92
+ skip "GC compaction not available" unless GC.respond_to?(:compact)
93
+
94
+ watcher = run_with_file_change(TEMP_FILE_PATH, compact: true)
95
+ expect(watcher.accessed).to eq(true)
96
+ end
97
+
98
+ it "raises ArgumentError when the path contains a null byte" do
99
+ expect { MyStatWatcher.new("foo\0bar") }.to raise_error(ArgumentError)
100
+ end
101
+
72
102
  it "should raise when the handler does not take 2 parameters" do
73
103
  class MyStatWatcher < Cool.io::StatWatcher
74
104
  remove_method :on_change
@@ -1,4 +1,5 @@
1
1
  require File.expand_path('../spec_helper', __FILE__)
2
+ require 'timeout'
2
3
 
3
4
  TIMEOUT = 0.010
4
5
  HOST = '127.0.0.1'
@@ -39,13 +40,31 @@ def on_message(data)
39
40
  @data = data
40
41
  end
41
42
 
43
+ # The reactor can be an order of magnitude slower under valgrind, so sleeping a
44
+ # fixed interval and assuming the event was processed by then makes these specs
45
+ # fail intermittently. Wait for the result instead. The timeout is generous
46
+ # enough that a slow machine still passes, and a genuine hang raises
47
+ # Timeout::Error here instead of blocking the suite forever.
48
+ WAIT_TIMEOUT = 5.0
49
+
50
+ def wait_until(timeout = WAIT_TIMEOUT)
51
+ Timeout.timeout(timeout) do
52
+ sleep 0.001 until yield
53
+ end
54
+ end
55
+
42
56
  def test_run(data = nil)
57
+ @data = ""
43
58
  reactor = Coolio::Loop.new
44
59
  server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
45
60
  reactor.attach(server)
46
61
  thread = Thread.new { reactor.run }
47
- send_data(data) if data
48
- sleep TIMEOUT
62
+ if data
63
+ send_data(data)
64
+ wait_until { @data == data }
65
+ else
66
+ sleep TIMEOUT
67
+ end
49
68
  reactor.stop
50
69
  server.detach
51
70
  send_data('') # to leave from blocking loop
@@ -86,6 +105,7 @@ ensure
86
105
  end
87
106
 
88
107
  def test_run_timeout(data = nil, timeout = TIMEOUT)
108
+ @data = ""
89
109
  reactor = Coolio::Loop.new
90
110
  server = Cool.io::TCPServer.new(HOST, PORT, MyConnection, method(:on_message))
91
111
  reactor.attach(server)
@@ -95,8 +115,12 @@ def test_run_timeout(data = nil, timeout = TIMEOUT)
95
115
  reactor.run_once(timeout)
96
116
  end
97
117
  end
98
- send_data(data) if data
99
- sleep timeout
118
+ if data
119
+ send_data(data)
120
+ wait_until { @data == data }
121
+ else
122
+ sleep timeout
123
+ end
100
124
  server.detach
101
125
  running = false # another send is not required
102
126
  thread.join
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cool.io
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.4
4
+ version: 1.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
@@ -77,6 +77,7 @@ extensions:
77
77
  extra_rdoc_files: []
78
78
  files:
79
79
  - ".github/workflows/test.yaml"
80
+ - ".github/workflows/valgrind.yaml"
80
81
  - ".gitignore"
81
82
  - ".rspec"
82
83
  - CHANGES.md
@@ -169,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
170
  - !ruby/object:Gem::Version
170
171
  version: '0'
171
172
  requirements: []
172
- rubygems_version: 4.0.6
173
+ rubygems_version: 4.0.16
173
174
  specification_version: 4
174
175
  summary: A cool framework for doing high performance I/O in Ruby
175
176
  test_files: