mini_racer 0.21.1 → 0.21.2

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: 6c838b1bc35ed1adb0156f0e01d699afc180c174718f197a6afc1356fe9e4815
4
- data.tar.gz: 7a4b00c4bcd4572bf7fdcce3df6455ded628706975fa8540946faee17e53617d
3
+ metadata.gz: 70ca9828687a7fcdb44048eb1a3e9ad2fe3f38082492504fc22563fe94fa524a
4
+ data.tar.gz: 61719146ed1430235269e80a21a60ef59b98f22f470cda13820074f9cecea818
5
5
  SHA512:
6
- metadata.gz: d826ca742dbe6580f92faebf11c827795536c55596c279fa070dd38836d03b7dcecc0b43fe254b6d95590bea1a24d8ec168f9d5008b668ba488a5aff7b6e3aea
7
- data.tar.gz: 985effeb709d8dfc1481c679dfdc86a3930a0d84de6d3b6d9d154d6801a6766af76ffecd468e22d8b20b68952e02b756ab0ec11b1bc3c93736b078d046719798
6
+ metadata.gz: 944e4a0e403b134e0206c6d6381efa3d9f120af9cfce18f61fc85c3eef5e6a077c64a457142d347041059a6b76f24990dbd15c17691bbcb3306ff1ea877983a4
7
+ data.tar.gz: f3003a61146634c76ad28ab99be4d6e96b34490522edb13607b03fcf9354fce135844947af3d23f9d37cc012a26ba4c280c8d4b19d410d47bcebc753cdc6c34f
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ - 0.21.2 - 11-06-2026
2
+ - Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks
3
+ - Fix native memory leaks in `Context#heap_snapshot`/`Context#write_heap_snapshot`; thanks to Pranjali Thakur from depthfirst.com
4
+ - Fix large integral JavaScript numbers wrapping to negative Ruby integers; thanks to Pranjali Thakur from depthfirst.com
5
+ - Fix Ruby callback exceptions with embedded NUL bytes permanently deadlocking a context; thanks to Pranjali Thakur from depthfirst.com
6
+ - Preserve embedded NUL bytes in JavaScript exception messages and attached function names, and reject unsafe V8 flags containing NUL bytes or overly long values
7
+
1
8
  - 0.21.1 - 25-05-2026
2
9
  - Run `:single_threaded` V8 dispatches on a reusable mini_racer-owned native thread so V8 does not execute on Ruby-owned threads
3
10
  - Stop and join the reusable `:single_threaded` runner when contexts are disposed
data/README.md CHANGED
@@ -348,6 +348,38 @@ Performance is slightly better than running `context.eval("hello('George')")` si
348
348
  * compilation of eval'd string is avoided
349
349
  * function arguments don't need to be converted to JSON
350
350
 
351
+ ### Microtask checkpoints
352
+
353
+ V8 drains its microtask queue (e.g. callbacks queued via `Promise.resolve().then(...)`) automatically when script execution returns to the embedder, so most code "just works":
354
+
355
+ ```ruby
356
+ context = MiniRacer::Context.new
357
+ context.eval(<<~JS)
358
+ let x = 0;
359
+ Promise.resolve().then(() => x = 99);
360
+ JS
361
+ context.eval("x")
362
+ # => 99
363
+ ```
364
+
365
+ When JavaScript invokes a Ruby callback synchronously and you need queued microtasks to drain mid-execution — e.g. for spec-compliant ordering across a chain of synchronous `dispatchEvent` listeners — call `context.perform_microtask_checkpoint` from the callback:
366
+
367
+ ```ruby
368
+ context = MiniRacer::Context.new
369
+ context.attach("drain", -> { context.perform_microtask_checkpoint })
370
+ context.eval(<<~JS)
371
+ globalThis.log = [];
372
+ Promise.resolve().then(() => log.push("microtask"));
373
+ log.push("before");
374
+ drain();
375
+ log.push("after");
376
+ JS
377
+ context.eval("log")
378
+ # => ["before", "microtask", "after"]
379
+ ```
380
+
381
+ Without `drain()` the order would be `["before", "after", "microtask"]` because the microtask only runs once the outermost script returns. `perform_microtask_checkpoint` is a thin wrapper over V8's `MicrotasksScope::PerformCheckpoint`.
382
+
351
383
  ## Performance
352
384
 
353
385
  The `bench` folder contains benchmark.
@@ -316,13 +316,19 @@ static void des_bool(void *arg, int v)
316
316
 
317
317
  static void des_int(void *arg, int64_t v)
318
318
  {
319
- put(arg, LONG2FIX(v));
319
+ put(arg, LL2NUM((LONG_LONG)v));
320
320
  }
321
321
 
322
322
  static void des_num(void *arg, double v)
323
323
  {
324
- if (isfinite(v) && v == trunc(v) && v >= INT64_MIN && v <= INT64_MAX) {
325
- put(arg, LONG2FIX(v));
324
+ if (isfinite(v) && v == trunc(v)) {
325
+ // INT64_MAX is not exactly representable as a double: it rounds up to
326
+ // 2^63, which would let 2^63 through and make the cast undefined.
327
+ if (v >= -0x1p63 && v < 0x1p63) {
328
+ put(arg, LL2NUM((LONG_LONG)v));
329
+ } else {
330
+ put(arg, rb_dbl2big(v));
331
+ }
326
332
  } else {
327
333
  put(arg, DBL2NUM(v));
328
334
  }
@@ -364,19 +370,8 @@ static void des_bigint(void *arg, const void *p, size_t n, int sign)
364
370
  if (t >> 63)
365
371
  *a++ = 0; // suppress sign extension
366
372
  v = rb_big_unpack(limbs, a-limbs);
367
- if (sign < 0) {
368
- // rb_big_unpack returns T_FIXNUM for smallish bignums
369
- switch (TYPE(v)) {
370
- case T_BIGNUM:
371
- v = rb_big_mul(v, LONG2FIX(-1));
372
- break;
373
- case T_FIXNUM:
374
- v = LONG2FIX(-1 * FIX2LONG(v));
375
- break;
376
- default:
377
- abort();
378
- }
379
- }
373
+ if (sign < 0)
374
+ v = rb_funcall(v, rb_intern("-@"), 0);
380
375
  put(c, v);
381
376
  }
382
377
 
@@ -810,6 +805,7 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
810
805
  case 'C': return v8_timedwait(c, p+1, n-1, v8_call);
811
806
  case 'E': return v8_timedwait(c, p+1, n-1, v8_eval);
812
807
  case 'H': return v8_heap_snapshot(c->pst);
808
+ case 'M': return v8_perform_microtask_checkpoint(c->pst);
813
809
  case 'P': return v8_pump_message_loop(c->pst);
814
810
  case 'S': return v8_heap_stats(c->pst);
815
811
  case 'T': return v8_snapshot(c->pst, p+1, n-1);
@@ -936,6 +932,7 @@ static VALUE rendezvous_callback_do(VALUE arg)
936
932
  Context *c;
937
933
  DesCtx d;
938
934
  Buf *b;
935
+ long id;
939
936
 
940
937
  a = (void *)arg;
941
938
  b = a->res;
@@ -945,7 +942,12 @@ static VALUE rendezvous_callback_do(VALUE arg)
945
942
  DesCtx_init(&d);
946
943
  args = deserialize1(&d, b->buf+1, b->len-1); // skip 'c' marker
947
944
  func = rb_ary_pop(args); // callback id
948
- func = rb_ary_entry(c->procs, FIX2LONG(func));
945
+ if (!RB_INTEGER_TYPE_P(func))
946
+ rb_raise(runtime_error, "bad callback id");
947
+ id = NUM2LONG(func);
948
+ if (id < 0 || id >= RARRAY_LEN(c->procs))
949
+ rb_raise(runtime_error, "bad callback id");
950
+ func = rb_ary_entry(c->procs, id);
949
951
  return rb_funcall2(func, rb_intern("call"), RARRAY_LENINT(args), RARRAY_PTR(args));
950
952
  }
951
953
 
@@ -981,9 +983,9 @@ fail:
981
983
  ser_init0(&s); // ruby exception pending
982
984
  w_byte(&s, 'e'); // send ruby error message to v8 thread
983
985
  r = rb_funcall(c->exception, rb_intern("to_s"), 0);
984
- err = StringValueCStr(r);
986
+ err = StringValuePtr(r);
985
987
  if (err)
986
- w(&s, err, strlen(err));
988
+ w(&s, err, RSTRING_LEN(r));
987
989
  goto out;
988
990
  }
989
991
 
@@ -1115,16 +1117,35 @@ static VALUE rendezvous(Context *c, Buf *req)
1115
1117
  return rendezvous1(c, req, &d);
1116
1118
  }
1117
1119
 
1120
+ static void raise_exception_with_message(VALUE klass, VALUE e)
1121
+ {
1122
+ long n;
1123
+ VALUE message;
1124
+
1125
+ if (NIL_P(e))
1126
+ return;
1127
+ e = StringValue(e);
1128
+ n = RSTRING_LEN(e);
1129
+ if (n == 0 || RSTRING_PTR(e)[0] == NO_ERROR)
1130
+ return;
1131
+ message = rb_str_subseq(e, 1, n - 1);
1132
+ rb_exc_raise(rb_exc_new_str(klass, message));
1133
+ }
1134
+
1118
1135
  static void handle_exception(VALUE e)
1119
1136
  {
1120
1137
  const char *s;
1121
1138
  VALUE klass;
1139
+ long n;
1122
1140
 
1123
1141
  if (NIL_P(e))
1124
1142
  return;
1125
1143
  e = StringValue(e);
1126
- s = StringValueCStr(e);
1127
- switch (*s) {
1144
+ n = RSTRING_LEN(e);
1145
+ if (n == 0)
1146
+ return;
1147
+ s = RSTRING_PTR(e);
1148
+ switch ((unsigned char)*s) {
1128
1149
  case NO_ERROR:
1129
1150
  return;
1130
1151
  case INTERNAL_ERROR:
@@ -1143,9 +1164,9 @@ static void handle_exception(VALUE e)
1143
1164
  klass = terminated_error;
1144
1165
  break;
1145
1166
  default:
1146
- rb_raise(internal_error, "bad error class %02x", *s);
1167
+ rb_raise(internal_error, "bad error class %02x", (unsigned char)*s);
1147
1168
  }
1148
- rb_enc_raise(rb_enc_get(e), klass, "%s", s+1);
1169
+ raise_exception_with_message(klass, e);
1149
1170
  }
1150
1171
 
1151
1172
  static VALUE context_alloc(VALUE klass)
@@ -1317,13 +1338,17 @@ static VALUE context_attach(VALUE self, VALUE name, VALUE proc)
1317
1338
  Context *c;
1318
1339
  VALUE e;
1319
1340
  Ser s;
1341
+ long id;
1320
1342
 
1321
1343
  TypedData_Get_Struct(self, Context, &context_type, c);
1344
+ id = RARRAY_LEN(c->procs);
1345
+ if (id > INT32_MAX)
1346
+ rb_raise(runtime_error, "too many callbacks");
1322
1347
  // request is (A)ttach, [name, id] array
1323
1348
  ser_init1(&s, 'A');
1324
1349
  ser_array_begin(&s, 2);
1325
1350
  add_string(&s, name);
1326
- ser_int(&s, RARRAY_LENINT(c->procs));
1351
+ ser_int(&s, id);
1327
1352
  ser_array_end(&s, 2);
1328
1353
  rb_ary_push(c->procs, proc);
1329
1354
  // response is an exception or undefined
@@ -1457,6 +1482,20 @@ static VALUE context_heap_stats(VALUE self)
1457
1482
  return h;
1458
1483
  }
1459
1484
 
1485
+ static VALUE buf_reset_ensure(VALUE arg)
1486
+ {
1487
+ buf_reset((Buf *)arg);
1488
+ return Qnil;
1489
+ }
1490
+
1491
+ static VALUE heap_snapshot_to_str(VALUE arg)
1492
+ {
1493
+ Buf *res;
1494
+
1495
+ res = (Buf *)arg;
1496
+ return rb_utf8_str_new((char *)res->buf, res->len);
1497
+ }
1498
+
1460
1499
  static VALUE context_heap_snapshot(VALUE self)
1461
1500
  {
1462
1501
  Buf req, res;
@@ -1466,7 +1505,19 @@ static VALUE context_heap_snapshot(VALUE self)
1466
1505
  buf_init(&req);
1467
1506
  buf_putc(&req, 'H'); // (H)eap snapshot, returns plain bytes
1468
1507
  rendezvous_no_des(c, &req, &res); // takes ownership of |req|
1469
- return rb_utf8_str_new((char *)res.buf, res.len);
1508
+ return rb_ensure(heap_snapshot_to_str, (VALUE)&res,
1509
+ buf_reset_ensure, (VALUE)&res);
1510
+ }
1511
+
1512
+ static VALUE context_perform_microtask_checkpoint(VALUE self)
1513
+ {
1514
+ Context *c;
1515
+ Buf b;
1516
+
1517
+ TypedData_Get_Struct(self, Context, &context_type, c);
1518
+ buf_init(&b);
1519
+ buf_putc(&b, 'M'); // (M)icrotask checkpoint, returns nil
1520
+ return rendezvous(c, &b); // takes ownership of |b|
1470
1521
  }
1471
1522
 
1472
1523
  static VALUE context_pump_message_loop(VALUE self)
@@ -1489,12 +1540,14 @@ static VALUE context_low_memory_notification(VALUE self)
1489
1540
  buf_init(&req);
1490
1541
  buf_putc(&req, 'L'); // (L)ow memory notification, returns nothing
1491
1542
  rendezvous_no_des(c, &req, &res); // takes ownership of |req|
1543
+ buf_reset(&res);
1492
1544
  return Qnil;
1493
1545
  }
1494
1546
 
1495
1547
  static int platform_set_flag1(VALUE k, VALUE v)
1496
1548
  {
1497
- char *p, *q, buf[256];
1549
+ char *p, *q, *r, buf[256];
1550
+ long pn, vn, len;
1498
1551
  int ok;
1499
1552
 
1500
1553
  k = rb_funcall(k, rb_intern("to_s"), 0);
@@ -1504,12 +1557,40 @@ static int platform_set_flag1(VALUE k, VALUE v)
1504
1557
  Check_Type(v, T_STRING);
1505
1558
  }
1506
1559
  p = RSTRING_PTR(k);
1507
- if (!strncmp(p, "--", 2))
1560
+ pn = RSTRING_LEN(k);
1561
+ if (memchr(p, '\0', pn))
1562
+ rb_raise(rb_eArgError, "flag contains NUL byte");
1563
+ if (pn >= 2 && p[0] == '-' && p[1] == '-') {
1508
1564
  p += 2;
1565
+ pn -= 2;
1566
+ }
1509
1567
  if (NIL_P(v)) {
1510
- snprintf(buf, sizeof(buf), "--%s", p);
1568
+ len = 2 + pn;
1569
+ if (len >= (long)sizeof(buf))
1570
+ rb_raise(rb_eArgError, "flag too long");
1571
+ q = buf;
1572
+ *q++ = '-';
1573
+ *q++ = '-';
1574
+ memcpy(q, p, pn);
1575
+ q += pn;
1576
+ *q = '\0';
1511
1577
  } else {
1512
- snprintf(buf, sizeof(buf), "--%s=%s", p, RSTRING_PTR(v));
1578
+ q = RSTRING_PTR(v);
1579
+ vn = RSTRING_LEN(v);
1580
+ if (memchr(q, '\0', vn))
1581
+ rb_raise(rb_eArgError, "flag contains NUL byte");
1582
+ len = 3 + pn + vn;
1583
+ if (len >= (long)sizeof(buf))
1584
+ rb_raise(rb_eArgError, "flag too long");
1585
+ r = buf;
1586
+ *r++ = '-';
1587
+ *r++ = '-';
1588
+ memcpy(r, p, pn);
1589
+ r += pn;
1590
+ *r++ = '=';
1591
+ memcpy(r, q, vn);
1592
+ r += vn;
1593
+ *r = '\0';
1513
1594
  }
1514
1595
  p = buf;
1515
1596
  pthread_mutex_lock(&flags_mtx);
@@ -1722,8 +1803,7 @@ static VALUE snapshot_initialize(int argc, VALUE *argv, VALUE self)
1722
1803
  a = rendezvous1(c, &s.b, &d);
1723
1804
  e = rb_ary_pop(a);
1724
1805
  context_dispose(cv);
1725
- if (*RSTRING_PTR(e))
1726
- rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1);
1806
+ raise_exception_with_message(snapshot_error, e);
1727
1807
  ss->blob = rb_ary_pop(a);
1728
1808
  return Qnil;
1729
1809
  }
@@ -1753,8 +1833,7 @@ static VALUE snapshot_warmup(VALUE self, VALUE arg)
1753
1833
  a = rendezvous1(c, &s.b, &d);
1754
1834
  e = rb_ary_pop(a);
1755
1835
  context_dispose(cv);
1756
- if (*RSTRING_PTR(e))
1757
- rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1);
1836
+ raise_exception_with_message(snapshot_error, e);
1758
1837
  ss->blob = rb_ary_pop(a);
1759
1838
  return self;
1760
1839
  }
@@ -1824,6 +1903,7 @@ void Init_mini_racer_extension(void)
1824
1903
  rb_define_method(c, "eval", context_eval, -1);
1825
1904
  rb_define_method(c, "heap_stats", context_heap_stats, 0);
1826
1905
  rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0);
1906
+ rb_define_method(c, "perform_microtask_checkpoint", context_perform_microtask_checkpoint, 0);
1827
1907
  rb_define_method(c, "pump_message_loop", context_pump_message_loop, 0);
1828
1908
  rb_define_method(c, "low_memory_notification", context_low_memory_notification, 0);
1829
1909
  rb_define_alloc_func(c, context_alloc);
@@ -5,11 +5,12 @@
5
5
  #include <memory>
6
6
  #include <vector>
7
7
  #include <cassert>
8
+ #include <cstdarg>
8
9
  #include <cstdio>
9
10
  #include <cstdint>
10
11
  #include <cstdlib>
11
12
  #include <cstring>
12
- #include <vector>
13
+ #include <limits>
13
14
 
14
15
  // note: the filter function gets called inside the safe context,
15
16
  // i.e., the context that has not been tampered with by user JS
@@ -123,6 +124,106 @@ struct Serialized
123
124
  }
124
125
  };
125
126
 
127
+ void append_bytes(std::vector<char>& out, const char *p, size_t n)
128
+ {
129
+ out.insert(out.end(), p, p + n);
130
+ }
131
+
132
+ void append_literal(std::vector<char>& out, const char *s)
133
+ {
134
+ append_bytes(out, s, strlen(s));
135
+ }
136
+
137
+ bool append_utf8(std::vector<char>& out, const v8::String::Utf8Value& s)
138
+ {
139
+ if (!*s) return false;
140
+ append_bytes(out, *s, s.length());
141
+ return true;
142
+ }
143
+
144
+ void append_format(std::vector<char>& out, const char *fmt, ...)
145
+ {
146
+ char buf[128];
147
+ va_list ap;
148
+ va_start(ap, fmt);
149
+ int n = vsnprintf(buf, sizeof(buf), fmt, ap);
150
+ va_end(ap);
151
+ if (n <= 0) return;
152
+ if (static_cast<size_t>(n) < sizeof(buf)) {
153
+ append_bytes(out, buf, n);
154
+ return;
155
+ }
156
+
157
+ std::vector<char> tmp(static_cast<size_t>(n) + 1);
158
+ va_start(ap, fmt);
159
+ vsnprintf(tmp.data(), tmp.size(), fmt, ap);
160
+ va_end(ap);
161
+ append_bytes(out, tmp.data(), n);
162
+ }
163
+
164
+ v8::Local<v8::String> string_from_bytes(v8::Isolate *isolate, const std::vector<char>& bytes)
165
+ {
166
+ v8::Local<v8::String> s;
167
+ auto type = v8::NewStringType::kNormal;
168
+ if (bytes.size() <= static_cast<size_t>(std::numeric_limits<int>::max()) &&
169
+ v8::String::NewFromUtf8(isolate, bytes.data(), type,
170
+ static_cast<int>(bytes.size())).ToLocal(&s)) {
171
+ return s;
172
+ }
173
+
174
+ char fallback[] = {
175
+ bytes.empty() ? static_cast<char>(INTERNAL_ERROR) : bytes[0],
176
+ 'u', 'n', 'e', 'x', 'p', 'e', 'c', 't', 'e', 'd', ' ', 'e', 'r', 'r', 'o', 'r'
177
+ };
178
+ if (v8::String::NewFromUtf8(isolate, fallback, type, static_cast<int>(sizeof(fallback))).ToLocal(&s)) return s;
179
+ return v8::String::Empty(isolate);
180
+ }
181
+
182
+ void set_error_message(std::vector<char>& out, int cause, const char *message)
183
+ {
184
+ out.clear();
185
+ out.push_back(static_cast<char>(cause));
186
+ append_literal(out, message);
187
+ }
188
+
189
+ bool set_error_message(std::vector<char>& out, int cause, const v8::String::Utf8Value& message)
190
+ {
191
+ out.clear();
192
+ out.push_back(static_cast<char>(cause));
193
+ return append_utf8(out, message);
194
+ }
195
+
196
+ void set_fallback_error(State& st, v8::TryCatch *try_catch, int cause,
197
+ std::vector<char>& out)
198
+ {
199
+ v8::String::Utf8Value s(st.isolate, try_catch->Exception());
200
+ const char *message = *s ? nullptr : "unexpected failure";
201
+ if (cause == MEMORY_ERROR) message = "out of memory";
202
+ if (cause == TERMINATED_ERROR) message = "terminated";
203
+ if (message) {
204
+ set_error_message(out, cause, message);
205
+ } else if (!set_error_message(out, cause, s)) {
206
+ set_error_message(out, cause, "unexpected failure");
207
+ }
208
+ }
209
+
210
+ bool read_path_key(State& st, const char *&p, const char *pe,
211
+ v8::Local<v8::String> *key, bool *last)
212
+ {
213
+ const char *dot;
214
+ size_t n;
215
+
216
+ if (p > pe) return false;
217
+ dot = static_cast<const char*>(memchr(p, '.', pe - p));
218
+ *last = (dot == nullptr);
219
+ n = *last ? static_cast<size_t>(pe - p) : static_cast<size_t>(dot - p);
220
+ if (n > static_cast<size_t>(std::numeric_limits<int>::max())) return false;
221
+ auto type = v8::NewStringType::kNormal;
222
+ if (!v8::String::NewFromUtf8(st.isolate, p, type, static_cast<int>(n)).ToLocal(key)) return false;
223
+ p = *last ? pe : dot + 1;
224
+ return true;
225
+ }
226
+
126
227
  bool bubble_up_ruby_exception(State& st, v8::TryCatch *try_catch)
127
228
  {
128
229
  auto exception = try_catch->Exception();
@@ -249,35 +350,40 @@ v8::Local<v8::Value> sanitize(State& st, v8::Local<v8::Value> v)
249
350
 
250
351
  v8::Local<v8::String> to_error(State& st, v8::TryCatch *try_catch, int cause)
251
352
  {
252
- v8::Local<v8::Value> t;
253
- char buf[1024];
353
+ std::vector<char> buf;
254
354
 
255
- *buf = '\0';
256
355
  if (cause == NO_ERROR) {
257
- // nothing to do
356
+ return v8::String::Empty(st.isolate);
258
357
  } else if (cause == PARSE_ERROR) {
259
358
  auto message = try_catch->Message();
260
359
  v8::String::Utf8Value s(st.isolate, message->Get());
261
360
  v8::String::Utf8Value name(st.isolate, message->GetScriptResourceName());
262
- if (!*s || !*name) goto fallback;
263
- auto line = message->GetLineNumber(st.context).FromMaybe(0);
264
- auto column = message->GetStartColumn(st.context).FromMaybe(0);
265
- snprintf(buf, sizeof(buf), "%c%s at %s:%d:%d", cause, *s, *name, line, column);
266
- } else if (try_catch->StackTrace(st.context).ToLocal(&t)) {
267
- v8::String::Utf8Value s(st.isolate, t);
268
- if (!*s) goto fallback;
269
- snprintf(buf, sizeof(buf), "%c%s", cause, *s);
361
+ if (*s && *name) {
362
+ buf.push_back(static_cast<char>(cause));
363
+ append_utf8(buf, s);
364
+ append_literal(buf, " at ");
365
+ append_utf8(buf, name);
366
+ auto line = message->GetLineNumber(st.context).FromMaybe(0);
367
+ auto column = message->GetStartColumn(st.context).FromMaybe(0);
368
+ append_format(buf, ":%d:%d", line, column);
369
+ } else {
370
+ set_fallback_error(st, try_catch, cause, buf);
371
+ }
270
372
  } else {
271
- fallback:
272
- v8::String::Utf8Value s(st.isolate, try_catch->Exception());
273
- const char *message = *s ? *s : "unexpected failure";
274
- if (cause == MEMORY_ERROR) message = "out of memory";
275
- if (cause == TERMINATED_ERROR) message = "terminated";
276
- snprintf(buf, sizeof(buf), "%c%s", cause, message);
373
+ v8::Local<v8::Value> t;
374
+ if (try_catch->StackTrace(st.context).ToLocal(&t)) {
375
+ v8::String::Utf8Value s(st.isolate, t);
376
+ if (*s) {
377
+ buf.push_back(static_cast<char>(cause));
378
+ append_utf8(buf, s);
379
+ } else {
380
+ set_fallback_error(st, try_catch, cause, buf);
381
+ }
382
+ } else {
383
+ set_fallback_error(st, try_catch, cause, buf);
384
+ }
277
385
  }
278
- v8::Local<v8::String> s;
279
- if (v8::String::NewFromUtf8(st.isolate, buf).ToLocal(&s)) return s;
280
- return v8::String::Empty(st.isolate);
386
+ return string_from_bytes(st.isolate, buf);
281
387
  }
282
388
 
283
389
  extern "C" void v8_global_init(void)
@@ -445,23 +551,19 @@ extern "C" void v8_attach(State *pst, const uint8_t *p, size_t n)
445
551
  v8::Local<v8::String> name;
446
552
  if (!name_v->ToString(st.context).ToLocal(&name)) goto fail;
447
553
  int32_t id;
448
- if (!id_v->Int32Value(st.context).To(&id)) goto fail;
449
- Callback *cb = new Callback{pst, id};
450
- st.callbacks.push_back(cb);
451
- v8::Local<v8::External> ext = v8::External::New(st.isolate, cb);
452
- v8::Local<v8::Function> function;
453
- if (!v8::Function::New(st.context, v8_api_callback, ext).ToLocal(&function)) goto fail;
554
+ if (!id_v->IsInt32()) goto fail;
555
+ id = id_v.As<v8::Int32>()->Value();
454
556
  // support foo.bar.baz paths
455
557
  v8::String::Utf8Value path(st.isolate, name);
456
558
  if (!*path) goto fail;
559
+ const char *p = *path;
560
+ const char *pe = p + path.length();
457
561
  v8::Local<v8::Object> obj = st.context->Global();
458
562
  v8::Local<v8::String> key;
459
- for (const char *p = *path;;) {
460
- size_t n = strcspn(p, ".");
461
- auto type = v8::NewStringType::kNormal;
462
- if (!v8::String::NewFromUtf8(st.isolate, p, type, n).ToLocal(&key)) goto fail;
463
- if (p[n] == '\0') break;
464
- p += n + 1;
563
+ for (;;) {
564
+ bool last;
565
+ if (!read_path_key(st, p, pe, &key, &last)) goto fail;
566
+ if (last) break;
465
567
  v8::Local<v8::Value> val;
466
568
  if (!obj->Get(st.context, key).ToLocal(&val)) goto fail;
467
569
  if (!val->IsObject() && !val->IsFunction()) {
@@ -470,6 +572,11 @@ extern "C" void v8_attach(State *pst, const uint8_t *p, size_t n)
470
572
  }
471
573
  obj = val.As<v8::Object>();
472
574
  }
575
+ std::unique_ptr<Callback> cb(new Callback{pst, id});
576
+ v8::Local<v8::External> ext = v8::External::New(st.isolate, cb.get());
577
+ v8::Local<v8::Function> function;
578
+ if (!v8::Function::New(st.context, v8_api_callback, ext).ToLocal(&function)) goto fail;
579
+ st.callbacks.push_back(cb.release());
473
580
  if (!obj->Set(st.context, key, function).FromMaybe(false)) goto fail;
474
581
  }
475
582
  cause = NO_ERROR;
@@ -504,14 +611,14 @@ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
504
611
  // support foo.bar.baz paths
505
612
  v8::String::Utf8Value path(st.isolate, name);
506
613
  if (!*path) goto fail;
614
+ const char *p = *path;
615
+ const char *pe = p + path.length();
507
616
  v8::Local<v8::Object> obj = st.context->Global();
508
617
  v8::Local<v8::String> key;
509
- for (const char *p = *path;;) {
510
- size_t n = strcspn(p, ".");
511
- auto type = v8::NewStringType::kNormal;
512
- if (!v8::String::NewFromUtf8(st.isolate, p, type, n).ToLocal(&key)) goto fail;
513
- if (p[n] == '\0') break;
514
- p += n + 1;
618
+ for (;;) {
619
+ bool last;
620
+ if (!read_path_key(st, p, pe, &key, &last)) goto fail;
621
+ if (last) break;
515
622
  v8::Local<v8::Value> val;
516
623
  if (!obj->Get(st.context, key).ToLocal(&val)) goto fail;
517
624
  if (!val->ToObject(st.context).ToLocal(&obj)) goto fail;
@@ -659,9 +766,22 @@ extern "C" void v8_heap_snapshot(State *pst)
659
766
  auto snapshot = st.isolate->GetHeapProfiler()->TakeHeapSnapshot();
660
767
  OutputStream os;
661
768
  snapshot->Serialize(&os, v8::HeapSnapshot::kJSON);
769
+ const_cast<v8::HeapSnapshot*>(snapshot)->Delete();
662
770
  v8_reply(st.ruby_context, os.buf.data(), os.buf.size()); // not serialized because big
663
771
  }
664
772
 
773
+ extern "C" void v8_perform_microtask_checkpoint(State *pst)
774
+ {
775
+ // Leave any termination active so the enclosing v8_call/v8_eval frame
776
+ // surfaces OOM (set by v8_gc_callback) or watchdog termination to Ruby.
777
+ State& st = *pst;
778
+ v8::TryCatch try_catch(st.isolate);
779
+ try_catch.SetVerbose(st.verbose_exceptions);
780
+ v8::HandleScope handle_scope(st.isolate);
781
+ v8::MicrotasksScope::PerformCheckpoint(st.isolate);
782
+ reply_retry(st, v8::Undefined(st.isolate));
783
+ }
784
+
665
785
  extern "C" void v8_pump_message_loop(State *pst)
666
786
  {
667
787
  State& st = *pst;
@@ -691,7 +811,7 @@ fail:
691
811
  int snapshot(bool is_warmup, bool verbose_exceptions,
692
812
  const v8::String::Utf8Value& code,
693
813
  v8::StartupData blob, v8::StartupData *result,
694
- char (*errbuf)[512])
814
+ std::vector<char> *errbuf)
695
815
  {
696
816
  // SnapshotCreator takes ownership of isolate
697
817
  v8::Isolate *isolate = v8::Isolate::Allocate();
@@ -715,7 +835,8 @@ int snapshot(bool is_warmup, bool verbose_exceptions,
715
835
  auto type = v8::NewStringType::kNormal;
716
836
  if (!v8::String::NewFromUtf8(isolate, *code, type, code.length()).ToLocal(&source)) {
717
837
  v8::String::Utf8Value s(isolate, try_catch.Exception());
718
- if (*s) snprintf(*errbuf, sizeof(*errbuf), "%c%s", cause, *s);
838
+ if (!set_error_message(*errbuf, cause, s))
839
+ set_error_message(*errbuf, cause, "unexpected failure");
719
840
  goto fail;
720
841
  }
721
842
  v8::ScriptOrigin origin(filename);
@@ -732,8 +853,16 @@ int snapshot(bool is_warmup, bool verbose_exceptions,
732
853
  v8::String::Utf8Value name(isolate, m->GetScriptResourceName());
733
854
  auto line = m->GetLineNumber(context).FromMaybe(0);
734
855
  auto column = m->GetStartColumn(context).FromMaybe(0);
735
- snprintf(*errbuf, sizeof(*errbuf), "%c%s\n%s:%d:%d",
736
- cause, *s, *name, line, column);
856
+ if (*s && *name) {
857
+ errbuf->clear();
858
+ errbuf->push_back(static_cast<char>(cause));
859
+ append_utf8(*errbuf, s);
860
+ append_literal(*errbuf, "\n");
861
+ append_utf8(*errbuf, name);
862
+ append_format(*errbuf, ":%d:%d", line, column);
863
+ } else {
864
+ set_error_message(*errbuf, cause, "unexpected failure");
865
+ }
737
866
  goto fail;
738
867
  }
739
868
  cause = INTERNAL_ERROR;
@@ -764,7 +893,7 @@ extern "C" void v8_snapshot(State *pst, const uint8_t *p, size_t n)
764
893
  v8::Local<v8::Value> result;
765
894
  v8::StartupData blob{nullptr, 0};
766
895
  int cause = INTERNAL_ERROR;
767
- char errbuf[512] = {0};
896
+ std::vector<char> errbuf;
768
897
  {
769
898
  v8::Local<v8::Value> code_v;
770
899
  if (!des.ReadValue(st.context).ToLocal(&code_v)) goto fail;
@@ -793,10 +922,8 @@ fail:
793
922
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
794
923
  if (cause) result = v8::Undefined(st.isolate);
795
924
  v8::Local<v8::Value> err;
796
- if (*errbuf) {
797
- if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) {
798
- err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error");
799
- }
925
+ if (!errbuf.empty()) {
926
+ err = string_from_bytes(st.isolate, errbuf);
800
927
  } else {
801
928
  err = to_error(st, &try_catch, cause);
802
929
  }
@@ -818,7 +945,7 @@ extern "C" void v8_warmup(State *pst, const uint8_t *p, size_t n)
818
945
  v8::Local<v8::Value> result;
819
946
  v8::StartupData blob{nullptr, 0};
820
947
  int cause = INTERNAL_ERROR;
821
- char errbuf[512] = {0};
948
+ std::vector<char> errbuf;
822
949
  {
823
950
  v8::Local<v8::Value> request_v;
824
951
  if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
@@ -865,10 +992,8 @@ fail:
865
992
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
866
993
  if (cause) result = v8::Undefined(st.isolate);
867
994
  v8::Local<v8::Value> err;
868
- if (*errbuf) {
869
- if (!v8::String::NewFromUtf8(st.isolate, errbuf).ToLocal(&err)) {
870
- err = v8::String::NewFromUtf8Literal(st.isolate, "unexpected error");
871
- }
995
+ if (!errbuf.empty()) {
996
+ err = string_from_bytes(st.isolate, errbuf);
872
997
  } else {
873
998
  err = to_error(st, &try_catch, cause);
874
999
  }
@@ -919,6 +1044,7 @@ State::~State()
919
1044
  {
920
1045
  v8::Locker locker(isolate);
921
1046
  v8::Isolate::Scope isolate_scope(isolate);
1047
+ persistent_safe_context_function.Reset();
922
1048
  persistent_safe_context.Reset();
923
1049
  persistent_context.Reset();
924
1050
  ruby_exception.Reset();
@@ -42,6 +42,7 @@ void v8_call(struct State *pst, const uint8_t *p, size_t n);
42
42
  void v8_eval(struct State *pst, const uint8_t *p, size_t n);
43
43
  void v8_heap_stats(struct State *pst);
44
44
  void v8_heap_snapshot(struct State *pst);
45
+ void v8_perform_microtask_checkpoint(struct State *pst);
45
46
  void v8_pump_message_loop(struct State *pst);
46
47
  void v8_snapshot(struct State *pst, const uint8_t *p, size_t n);
47
48
  void v8_warmup(struct State *pst, const uint8_t *p, size_t n);
@@ -278,7 +278,7 @@ static void ser_int(Ser *s, int64_t v)
278
278
  if (v > INT64_MIN/1024)
279
279
  if (v <= INT64_MAX/1024)
280
280
  return ser_num(s, v);
281
- t = v < 0 ? -v : v;
281
+ t = v < 0 ? (uint64_t)(-(v + 1)) + 1 : (uint64_t)v;
282
282
  sign = v < 0 ? -1 : 1;
283
283
  ser_bigint(s, &t, sizeof(t), sign);
284
284
  } else {
@@ -118,6 +118,24 @@ module MiniRacer
118
118
  @js_symbol_to_symbol_func = eval_in_context "(x) => { var r = x.description; return r === undefined ? 'undefined' : r }"
119
119
  @js_new_date_func = eval_in_context "(x) => { return new Date(x) }"
120
120
  @js_new_array_func = eval_in_context "(x) => { return new Array(x) }"
121
+ # looks up a (dotted) function name as properties from globalThis,
122
+ # instead of evaluating the name as source code, so that names with
123
+ # embedded NUL bytes or other invalid syntax resolve correctly
124
+ @js_lookup_call_target_func = eval_in_context <<~JS
125
+ (name) => {
126
+ let target = globalThis;
127
+ for (const key of name.split(".")) {
128
+ if (target == null) {
129
+ throw new ReferenceError(name + " is not defined");
130
+ }
131
+ target = target[key];
132
+ }
133
+ if (target === undefined) {
134
+ throw new ReferenceError(name + " is not defined");
135
+ }
136
+ return target;
137
+ }
138
+ JS
121
139
  @js_new_uint8array_func = eval_in_context "(x) => { return new Uint8Array(x) }"
122
140
  end
123
141
 
@@ -174,7 +192,7 @@ module MiniRacer
174
192
  raise RuntimeError, "TruffleRuby does not support call after stop" if @stopped
175
193
  begin
176
194
  translate do
177
- function = eval_in_context(function_name)
195
+ function = @js_lookup_call_target_func.call(convert_ruby_to_js(encode(function_name)))
178
196
  function.call(*convert_ruby_to_js(arguments))
179
197
  end
180
198
  rescue Polyglot::ForeignException => e
@@ -225,8 +243,11 @@ module MiniRacer
225
243
 
226
244
  def convert_js_to_ruby(value)
227
245
  case value
228
- when true, false, Integer, Float
246
+ when true, false, Integer
229
247
  value
248
+ when Float
249
+ # match the C extension: integral doubles convert to Ruby Integers
250
+ value.finite? && value == value.truncate ? value.to_i : value
230
251
  else
231
252
  if value.nil?
232
253
  nil
@@ -370,6 +391,10 @@ module MiniRacer
370
391
  class Platform
371
392
  def self.set_flag_as_str!(flag)
372
393
  raise TypeError, "wrong type argument #{flag.class} (should be a string)" unless flag.is_a?(String)
394
+ raise ArgumentError, "flag contains NUL byte" if flag.include?("\0")
395
+ # the C extension normalizes flags into a 256 byte "--flag" buffer
396
+ normalized = flag.start_with?("--") ? flag : "--#{flag}"
397
+ raise ArgumentError, "flag too long" if normalized.bytesize >= 256
373
398
  raise MiniRacer::PlatformAlreadyInitialized, "The platform is already initialized." if Context.instance_variable_get(:@context_initialized)
374
399
  Context.instance_variable_set(:@use_strict, true) if "--use_strict" == flag
375
400
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.21.1"
4
+ VERSION = "0.21.2"
5
5
  LIBV8_NODE_VERSION = "~> 24.12.0.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.1
4
+ version: 0.21.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -136,9 +136,9 @@ licenses:
136
136
  - MIT
137
137
  metadata:
138
138
  bug_tracker_uri: https://github.com/discourse/mini_racer/issues
139
- changelog_uri: https://github.com/discourse/mini_racer/blob/v0.21.1/CHANGELOG
140
- documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.1
141
- source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.1
139
+ changelog_uri: https://github.com/discourse/mini_racer/blob/v0.21.2/CHANGELOG
140
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.2
141
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.2
142
142
  rdoc_options: []
143
143
  require_paths:
144
144
  - lib
@@ -147,7 +147,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
147
  requirements:
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
- version: '3.1'
150
+ version: '3.3'
151
151
  required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  requirements:
153
153
  - - ">="