mini_racer 0.22.0 → 0.22.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: 7a20d79f246e2bd3f9aa86cdf19c83413d2b782b7738f0bfc13017227e2c20f7
4
- data.tar.gz: b1c6d7b75c8bbc7520f0722ed255d042e70e4e606bb627a59926523135640921
3
+ metadata.gz: 700f0533786f28a992f2b46a0790ed557c36255d8b7357aecfd3fa1bfd4a01a9
4
+ data.tar.gz: f45170bb718eeff6e2b5d6f83af6039d178f36541983ab20c31e1dbfab83f7cf
5
5
  SHA512:
6
- metadata.gz: 9c828f6078e1d6f48503bf9870cab6ced783e853decf679c0534735d7c3a9a8d6efb3c06ed7528f88f1ea8f90ddf5e2c8d50c73e89d8e7672d5b03ad7b85e3ef
7
- data.tar.gz: 62eb803f2a6aea12b7a80cc6385858631a60dadb61c2eb409e1f5ecf79cfa969f3d9350519377c0a169bf0ffd15987c06635da2d3aaa84f3017183d51b5216bb
6
+ metadata.gz: ae2e7f4e68826ce1115c933cc17835cc1d32155de483f00a3269b0669fb25198d648aa87fc3ac49b7496c23015b080ea2e9c8e57b061bd4c115716b0180ad6a7
7
+ data.tar.gz: 4d5a790f98965015cfc97138fe0e8a9e5bbfd11c0f0f67499f8780d0cdee575105f4c7deaf310b6d5937c6fe56f05b9f7548f0d998ea0658c98970fb72b4dc14
data/CHANGELOG CHANGED
@@ -1,3 +1,20 @@
1
+ - 0.22.2 - 23-10-2026
2
+ - Fix nested Symbols, functions, and Map keys aborting the process during `eval`/`call` serialization (#447)
3
+ - Fix attached callback result conversion raising inside the V8 reply path, which could hang the context; conversion errors now propagate normally
4
+ - Fix callback exceptions whose `to_s` raises or returns a non-string, including reentrant `to_s`, hanging the callback roundtrip
5
+ - Bound Ruby to JavaScript serialization depth and fix an off-by-one in the JavaScript to Ruby deserializer stack
6
+ - Fix termination state leaking from error formatting or serialization callbacks into subsequent evaluations
7
+ - Fix `Snapshot#warmup!` aborting for an empty loaded snapshot (#449)
8
+ - Fix `Snapshot#warmup!` aborting when warmup creates Intl objects (#450); snapshot creation now uses the same handle scoping as V8's warmup implementation
9
+ - Reject runtime Intl access in `Snapshot.new` scripts with `MiniRacer::SnapshotError` instead of aborting the process
10
+ - Validate loaded snapshot versions, headers, section offsets, and checksums, raising `MiniRacer::SnapshotError` for malformed or incompatible blobs instead of terminating the process (#448)
11
+ - Make `Snapshot#dump` return a copy so mutating it cannot corrupt the stored snapshot
12
+
13
+ - 0.22.1 - 27-08-2026
14
+ - Fix building with Clang 22+ and big-endian bigint serialization by making bigint serialization byte-oriented instead of relying on native `uint64_t`/`unsigned long` representations
15
+ - Fix Ruby integers at or above 512 bits being silently truncated when passed to JavaScript, and large JavaScript bigints producing an invalid internal value when returned to Ruby
16
+ - Support Ruby and JavaScript bigints up to a 16 MiB magnitude, using allocation-free conversion for common sizes and bounded dynamic storage for larger values
17
+
1
18
  - 0.22.0 - 12-08-2026
2
19
  - Add `Context#call_await` and `Context#eval_await`: like `call`/`eval` but block until a returned Promise settles and return the settled value; rejections raise `MiniRacer::RuntimeError`
3
20
  - Fix a `call` or `eval` made from a Ruby callback taking the timeout or `stop` meant for the evaluation around it, which then kept running
data/README.md CHANGED
@@ -60,6 +60,10 @@ puts context.eval("array_and_hash()")
60
60
  # => {"a" => 1, "b" => [1, {"a" => 1}]}
61
61
  ```
62
62
 
63
+ Ruby `Integer` and JavaScript `BigInt` values are converted exactly up to a
64
+ 16 MiB magnitude (about 134 million bits). Larger individual values are
65
+ rejected with a serialization error rather than truncated.
66
+
63
67
  ### Return binary data from Ruby to JavaScript
64
68
 
65
69
  Attached Ruby functions can return binary data as `Uint8Array` using `MiniRacer::Binary`:
@@ -206,6 +210,12 @@ Snapshots can come in handy for example if you want your contexts to be pre-load
206
210
  > are fixed once the snapshot has been captured. They are no longer really random
207
211
  > nor reflect the current time.
208
212
 
213
+ Snapshot scripts passed to `Snapshot.new` that read or write `Intl` are
214
+ rejected with `MiniRacer::SnapshotError`: V8 cannot serialize Intl's foreign
215
+ handles, and allowing them to reach snapshot creation aborts the process.
216
+ `Snapshot#warmup!` may use Intl because its temporary context is discarded
217
+ before the snapshot is created.
218
+
209
219
  Also note that snapshots can be warmed up, using the `warmup!` method, which allows you to call functions which are otherwise lazily compiled to get them to compile right away; any side effect of your warm up code being then dismissed. [More details on warming up here](https://github.com/electron/electron/issues/169#issuecomment-76783481), and a small example:
210
220
 
211
221
  ```ruby
@@ -238,7 +248,7 @@ context.eval("foo")
238
248
 
239
249
  Note that snapshots are architecture and V8-version specific. A snapshot created on one platform (e.g., ARM64 macOS) cannot be loaded on a different platform (e.g., x86_64 Linux). Snapshots are best used for same-machine caching or homogeneous deployment environments.
240
250
 
241
- **Security note:** Only load snapshots from trusted sources. V8 snapshots are not designed to be safely loaded from untrusted input—malformed or malicious snapshot data may cause crashes or memory corruption.
251
+ **Security note:** Only load snapshots from trusted sources. V8 snapshots are not designed to be safely loaded from untrusted input—malformed or malicious snapshot data may cause crashes or memory corruption. `Snapshot.load` performs basic version, header, section-offset, and checksum validation, but this is not a sandbox.
242
252
 
243
253
  ### Garbage collection
244
254
 
@@ -518,6 +528,25 @@ gcc >= 12.2 and Xcode >= 13 are, at the time of writing, known to work.
518
528
  * Concurrent cause .... JRuby
519
529
  * Supports execjs
520
530
 
531
+ ## Community
532
+
533
+ Libraries and integrations built with MiniRacer:
534
+
535
+ - [Humid](https://github.com/thoughtbot/humid) — JavaScript server-side rendering helpers for Rails.
536
+ - [ExecJS](https://github.com/rails/execjs) — A common interface for executing JavaScript from Ruby, with a MiniRacer runtime.
537
+ - [Opal](https://github.com/opal/opal) — A Ruby-to-JavaScript compiler with a MiniRacer CLI runner.
538
+ - [Handlebars::Engine](https://github.com/gi/handlebars-ruby) — A complete Ruby interface to Handlebars.js.
539
+ - [Minibars](https://github.com/combinaut/minibars) — A lightweight Handlebars.rb replacement built on MiniRacer.
540
+ - [Rtlcss](https://github.com/discourse/rtlcss) — Runs rtlcss from Ruby to convert stylesheets between LTR and RTL.
541
+ - [MessageFormat](https://github.com/discourse/messageformat) — Compiles MessageFormat messages from Ruby using `@messageformat/core`.
542
+ - [feelin](https://github.com/ekzo-dev/ruby-feelin) — Evaluates DMN FEEL expressions from Ruby.
543
+ - [parse-css](https://github.com/camertron/parse-css) — Parses CSS, including nested CSS, through the JavaScript parse-css library.
544
+
545
+ ### Notable users
546
+
547
+ - [Discourse](https://github.com/discourse/discourse) — A large Rails application using MiniRacer for JavaScript compilation, processing, and sandboxed evaluation.
548
+ - [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler) — Uses MiniRacer in its JavaScript build tooling.
549
+
521
550
  ## Contributing
522
551
 
523
552
  Bug reports and pull requests are welcome on GitHub at https://github.com/rubyjs/mini_racer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -62,6 +62,9 @@ static inline void rb_thread_lock_native_thread(void)
62
62
 
63
63
  #define countof(x) (sizeof(x) / sizeof(*(x)))
64
64
  #define endof(x) ((x) + countof(x))
65
+ #define BIGINT_STACK_WORDS 64
66
+ #define BIGINT_MAX_BYTES (16 * 1024 * 1024)
67
+ #define SERIALIZE_MAX_DEPTH 512
65
68
 
66
69
  // mostly RO: assigned once by platform_set_flag1 while holding |flags_mtx|,
67
70
  // from then on read-only and accessible without holding locks
@@ -136,6 +139,7 @@ typedef struct Context
136
139
  struct State *pst; // used by v8 thread
137
140
  VALUE procs; // array of js -> ruby callbacks
138
141
  VALUE exception; // pending exception or Qnil
142
+ int pending_jump_tag; // non-exception jump to replay after the roundtrip
139
143
  Buf req, res; // ruby->v8 request/response, mediated by |mtx| and |cv|
140
144
  Buf v8_req; // stable v8-side copy of a request returned by v8_roundtrip
141
145
  int res_ready; // protected by |mtx|; response may be filled before ready
@@ -287,7 +291,7 @@ static void push(DesCtx *c, VALUE v)
287
291
  {
288
292
  if (*c->err)
289
293
  return;
290
- if (c->tos == endof(c->stack)) {
294
+ if (c->tos == endof(c->stack) - 1) {
291
295
  snprintf(c->err, sizeof(c->err), "stack overflow");
292
296
  return;
293
297
  }
@@ -353,33 +357,32 @@ static void des_date(void *arg, double v)
353
357
  put(arg, rb_time_new(sec, usec));
354
358
  }
355
359
 
356
- // note: v8 stores bigints in 1's complement, ruby in 2's complement,
357
- // so we have to take additional steps to ensure correct conversion
360
+ // note: v8 stores bigints as a sign plus little-endian 64-bit magnitude words
358
361
  static void des_bigint(void *arg, const void *p, size_t n, int sign)
359
362
  {
360
363
  VALUE v;
361
- size_t i;
362
364
  DesCtx *c;
363
- unsigned long *a, t, limbs[65]; // +1 to suppress sign extension
365
+ int flags;
364
366
 
365
367
  c = arg;
366
368
  if (*c->err)
367
369
  return;
368
- if (n > sizeof(limbs) - sizeof(*limbs)) {
370
+ if (n % sizeof(uint64_t)) {
371
+ snprintf(c->err, sizeof(c->err), "bad bigint");
372
+ return;
373
+ }
374
+ if (n > BIGINT_MAX_BYTES) {
369
375
  snprintf(c->err, sizeof(c->err), "bigint too big");
370
376
  return;
371
377
  }
372
- a = limbs;
373
- t = 0;
374
- for (i = 0; i < n; a++, i += sizeof(*a)) {
375
- memcpy(a, (char *)p + i, sizeof(*a));
376
- t = *a;
377
- }
378
- if (t >> 63)
379
- *a++ = 0; // suppress sign extension
380
- v = rb_big_unpack(limbs, a-limbs);
381
- if (sign < 0)
382
- v = rb_funcall(v, rb_intern("-@"), 0);
378
+ if (n == 0) {
379
+ v = INT2FIX(0);
380
+ } else {
381
+ flags = INTEGER_PACK_LITTLE_ENDIAN;
382
+ if (sign < 0)
383
+ flags |= INTEGER_PACK_NEGATIVE;
384
+ v = rb_integer_unpack(p, n/sizeof(uint64_t), sizeof(uint64_t), 0, flags);
385
+ }
383
386
  put(c, v);
384
387
  }
385
388
 
@@ -580,15 +583,47 @@ static void add_string(Ser *s, VALUE v)
580
583
  return ser_string(s, p, n);
581
584
  }
582
585
 
583
- static int serialize1(Ser *s, VALUE refs, VALUE v)
586
+ // Keep small values allocation-free while allowing large values up to a
587
+ // deliberate per-value limit that bounds temporary conversion storage.
588
+ static int serialize_bigint(Ser *s, VALUE v)
589
+ {
590
+ uint64_t stack_words[BIGINT_STACK_WORDS];
591
+ uint64_t *words;
592
+ size_t nwords, nbytes;
593
+ int packed;
594
+
595
+ nwords = rb_absint_numwords(v, 64, NULL);
596
+ if (nwords == (size_t)-1 || nwords > BIGINT_MAX_BYTES/sizeof(*words))
597
+ return bail(&s->err, "bigint too big");
598
+ nbytes = nwords * sizeof(*words);
599
+ words = stack_words;
600
+ if (nwords > countof(stack_words)) {
601
+ words = malloc(nbytes);
602
+ if (!words)
603
+ return bail(&s->err, "out of memory");
604
+ }
605
+ packed = rb_integer_pack(v, words, nwords, sizeof(*words), 0,
606
+ INTEGER_PACK_LITTLE_ENDIAN);
607
+ if (packed < -1 || packed > 1) {
608
+ if (words != stack_words)
609
+ free(words);
610
+ return bail(&s->err, "bigint too big");
611
+ }
612
+ ser_bigint(s, words, nbytes, packed < 0 ? -1 : 1);
613
+ if (words != stack_words)
614
+ free(words);
615
+ return *s->err ? -1 : 0;
616
+ }
617
+
618
+ static int serialize1(Ser *s, VALUE refs, VALUE v, int depth)
584
619
  {
585
- unsigned long limbs[64];
586
620
  VALUE a, t, id;
587
621
  size_t i, n;
588
- int sign;
589
622
 
590
623
  if (*s->err)
591
624
  return -1;
625
+ if (depth > SERIALIZE_MAX_DEPTH)
626
+ return bail(&s->err, "max depth exceeded");
592
627
  switch (TYPE(v)) {
593
628
  case T_ARRAY:
594
629
  {
@@ -600,7 +635,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
600
635
  rb_hash_aset(refs, obj_id, LONG2FIX(i));
601
636
  ser_array_begin(s, n);
602
637
  for (i = 0; i < n; i++)
603
- if (serialize1(s, refs, rb_ary_entry(v, i)))
638
+ if (serialize1(s, refs, rb_ary_entry(v, i), depth + 1))
604
639
  return -1;
605
640
  ser_array_end(s, n);
606
641
  } else {
@@ -631,9 +666,9 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
631
666
  if (i == 2*n) {
632
667
  ser_object_begin(s);
633
668
  for (i = 0; i < 2*n; i += 2) {
634
- if (serialize1(s, refs, rb_ary_entry(a, i+0)))
669
+ if (serialize1(s, refs, rb_ary_entry(a, i+0), depth + 1))
635
670
  return -1;
636
- if (serialize1(s, refs, rb_ary_entry(a, i+1)))
671
+ if (serialize1(s, refs, rb_ary_entry(a, i+1), depth + 1))
637
672
  return -1;
638
673
  }
639
674
  ser_object_end(s, n);
@@ -670,15 +705,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
670
705
  ser_bool(s, 0);
671
706
  break;
672
707
  case T_BIGNUM:
673
- // note: v8 stores bigints in 1's complement, ruby in 2's complement,
674
- // so we have to take additional steps to ensure correct conversion
675
- memset(limbs, 0, sizeof(limbs));
676
- sign = rb_big_sign(v) ? 1 : -1;
677
- if (sign < 0)
678
- v = rb_big_mul(v, LONG2FIX(-1));
679
- rb_big_pack(v, limbs, countof(limbs));
680
- ser_bigint(s, limbs, countof(limbs), sign);
681
- break;
708
+ return serialize_bigint(s, v);
682
709
  case T_FIXNUM:
683
710
  ser_int(s, FIX2LONG(v));
684
711
  break;
@@ -725,7 +752,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
725
752
  // that will throw off the object reference count
726
753
  static int serialize(Ser *s, VALUE v)
727
754
  {
728
- return serialize1(s, rb_hash_new(), v);
755
+ return serialize1(s, rb_hash_new(), v, 0);
729
756
  }
730
757
 
731
758
  static struct timespec deadline_ms(int ms)
@@ -958,6 +985,8 @@ static VALUE deserialize1(DesCtx *d, const uint8_t *p, size_t n)
958
985
 
959
986
  if (des(&err, p, n, d))
960
987
  rb_raise(runtime_error, "%s", err);
988
+ if (*d->err)
989
+ rb_raise(runtime_error, "%s", d->err);
961
990
  if (d->tos != d->stack) // should not happen
962
991
  rb_raise(runtime_error, "parse stack not empty");
963
992
  return d->tos->a;
@@ -1000,11 +1029,54 @@ static VALUE rendezvous_callback_do(VALUE arg)
1000
1029
  return rb_funcall2(func, rb_intern("call"), RARRAY_LENINT(args), RARRAY_PTR(args));
1001
1030
  }
1002
1031
 
1032
+ struct callback_serialize_args
1033
+ {
1034
+ Ser *ser;
1035
+ VALUE value;
1036
+ };
1037
+
1038
+ static VALUE callback_serialize(VALUE arg)
1039
+ {
1040
+ struct callback_serialize_args *args;
1041
+ args = (void *)arg;
1042
+ return INT2FIX(serialize(args->ser, args->value));
1043
+ }
1044
+
1045
+ static VALUE callback_exception_to_s(VALUE exception)
1046
+ {
1047
+ VALUE message;
1048
+ message = rb_funcall(exception, rb_intern("to_s"), 0);
1049
+ StringValue(message);
1050
+ return message;
1051
+ }
1052
+
1053
+ static VALUE callback_exception_from_state(int state, int *pending_jump_tag)
1054
+ {
1055
+ VALUE exception = rb_errinfo();
1056
+ rb_set_errinfo(Qnil);
1057
+ if (RB_TYPE_P(exception, T_OBJECT) &&
1058
+ rb_obj_is_kind_of(exception, rb_eException)) {
1059
+ return exception;
1060
+ }
1061
+ // Thread#kill and other non-exception jumps leave a sentinel or internal
1062
+ // throw data in errinfo; do not hand that to rb_funcall/rb_exc_raise.
1063
+ // Replay the jump once the V8 roundtrip has fully unwound.
1064
+ if (state && pending_jump_tag && !*pending_jump_tag)
1065
+ *pending_jump_tag = state;
1066
+ return Qnil;
1067
+ }
1068
+
1069
+ static VALUE callback_internal_error(VALUE message)
1070
+ {
1071
+ return rb_exc_new_cstr(internal_error, (const char *)message);
1072
+ }
1073
+
1003
1074
  // called with |rr_mtx| and GVL held; |mtx| is unlocked
1004
1075
  // callback data is in |a->res|, serialized result goes in |a->req|
1005
1076
  static void *rendezvous_callback(void *arg)
1006
1077
  {
1007
1078
  struct rendezvous_nogvl *a;
1079
+ struct callback_serialize_args args;
1008
1080
  const char *err;
1009
1081
  Context *c;
1010
1082
  int exc;
@@ -1015,13 +1087,29 @@ static void *rendezvous_callback(void *arg)
1015
1087
  c = a->context;
1016
1088
  r = rb_protect(rendezvous_callback_do, (VALUE)a, &exc);
1017
1089
  if (exc) {
1018
- c->exception = rb_errinfo();
1019
- rb_set_errinfo(Qnil);
1090
+ c->exception = callback_exception_from_state(exc, &c->pending_jump_tag);
1020
1091
  goto fail;
1021
1092
  }
1022
1093
  ser_init1(&s, 'c'); // callback reply
1023
- if (serialize(&s, r)) { // should not happen
1024
- c->exception = rb_exc_new_cstr(internal_error, s.err);
1094
+ args.ser = &s;
1095
+ args.value = r;
1096
+ r = rb_protect(callback_serialize, (VALUE)&args, &exc);
1097
+ if (exc) {
1098
+ // A Ruby conversion hook, Check_Type(), or stack overflow escaped
1099
+ // serialize(). Keep the V8 thread waiting for a reply and turn it
1100
+ // into a normal callback exception instead.
1101
+ c->exception = callback_exception_from_state(exc, &c->pending_jump_tag);
1102
+ ser_reset(&s);
1103
+ goto fail;
1104
+ }
1105
+ if (NUM2INT(r)) {
1106
+ VALUE exception = rb_protect(callback_internal_error,
1107
+ (VALUE)s.err, &exc);
1108
+ if (exc) {
1109
+ c->exception = callback_exception_from_state(exc, &c->pending_jump_tag);
1110
+ } else {
1111
+ c->exception = exception;
1112
+ }
1025
1113
  ser_reset(&s);
1026
1114
  goto fail;
1027
1115
  }
@@ -1031,10 +1119,25 @@ out:
1031
1119
  fail:
1032
1120
  ser_init0(&s); // ruby exception pending
1033
1121
  w_byte(&s, 'e'); // send ruby error message to v8 thread
1034
- r = rb_funcall(c->exception, rb_intern("to_s"), 0);
1035
- err = StringValuePtr(r);
1036
- if (err)
1122
+ r = Qnil;
1123
+ if (!NIL_P(c->exception)) {
1124
+ VALUE original_exception = c->exception;
1125
+ r = rb_protect(callback_exception_to_s, c->exception, &exc);
1126
+ if (exc) {
1127
+ callback_exception_from_state(exc, &c->pending_jump_tag);
1128
+ r = Qnil;
1129
+ }
1130
+ // to_s can re-enter the context and consume c->exception. Keep the
1131
+ // original callback exception for the eventual Ruby-side re-raise.
1132
+ c->exception = original_exception;
1133
+ }
1134
+ if (NIL_P(r)) {
1135
+ err = "Ruby exception";
1136
+ w(&s, err, strlen(err));
1137
+ } else {
1138
+ err = RSTRING_PTR(r);
1037
1139
  w(&s, err, RSTRING_LEN(r));
1140
+ }
1038
1141
  goto out;
1039
1142
  }
1040
1143
 
@@ -1325,6 +1428,12 @@ static VALUE rendezvous1(Context *c, Buf *req, DesCtx *d)
1325
1428
  int exc;
1326
1429
 
1327
1430
  rendezvous_no_des(c, req, &res); // takes ownership of |req|
1431
+ int jump_tag = c->pending_jump_tag;
1432
+ c->pending_jump_tag = 0;
1433
+ if (jump_tag) {
1434
+ buf_reset(&res);
1435
+ rb_jump_tag(jump_tag);
1436
+ }
1328
1437
  r = c->exception;
1329
1438
  c->exception = Qnil;
1330
1439
  // if js land didn't handle exception from ruby callback, re-raise it now
@@ -2121,15 +2230,22 @@ static VALUE snapshot_dump(VALUE self)
2121
2230
  Snapshot *ss;
2122
2231
 
2123
2232
  TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss);
2124
- return ss->blob;
2233
+ // Callers may mutate the returned String; never expose the blob that
2234
+ // Context.new/snapshot_warmup later hand to V8.
2235
+ return rb_str_dup(ss->blob);
2125
2236
  }
2126
2237
 
2127
2238
  static VALUE snapshot_load(VALUE klass, VALUE blob)
2128
2239
  {
2129
2240
  Snapshot *ss;
2130
2241
  VALUE self;
2242
+ char err[128];
2131
2243
 
2132
2244
  Check_Type(blob, T_STRING);
2245
+ if (v8_snapshot_validate((const uint8_t *)RSTRING_PTR(blob),
2246
+ (size_t)RSTRING_LEN(blob), err, sizeof(err))) {
2247
+ rb_raise(snapshot_error, "%s", err);
2248
+ }
2133
2249
  self = snapshot_alloc(klass);
2134
2250
  TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss);
2135
2251
  ss->blob = rb_str_dup(blob);
@@ -276,6 +276,8 @@ bool reply(State& st, v8::Local<v8::Value> v)
276
276
  Serialized serialized(st, v);
277
277
  if (serialized.data)
278
278
  v8_reply(st.ruby_context, serialized.data, serialized.size);
279
+ else
280
+ try_catch.ReThrow();
279
281
  return serialized.data != nullptr; // exception pending if false
280
282
  }
281
283
 
@@ -750,10 +752,35 @@ fail:
750
752
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
751
753
  if (cause) result = v8::Undefined(st.isolate);
752
754
  auto err = to_error(st, &try_catch, cause);
755
+ // Error formatting can run user JS (for example Error.prepareStackTrace)
756
+ // which may call Context#stop. Do not let that termination leak into the
757
+ // reply or the next evaluation.
758
+ bool terminated_during_error = false;
759
+ if (st.terminate_requested.load() || st.isolate->IsExecutionTerminating()) {
760
+ terminated_during_error = true;
761
+ preserve_termination =
762
+ suspend_termination(st, nested, cause) || preserve_termination;
763
+ }
764
+ if (terminated_during_error) {
765
+ result = v8::Undefined(st.isolate);
766
+ std::vector<char> buf;
767
+ set_error_message(buf, cause,
768
+ cause == MEMORY_ERROR ? "out of memory" : "terminated");
769
+ err = string_from_bytes(st.isolate, buf);
770
+ }
753
771
  if (!reply(st, result, err)) {
754
772
  assert(try_catch.HasCaught());
755
773
  goto fail; // retry; can be termination exception
756
774
  }
775
+ if (!nested &&
776
+ (st.terminate_requested.load() || st.isolate->IsExecutionTerminating())) {
777
+ // A callback invoked by the serializer (for example a getter) can
778
+ // request termination after the last suspend_termination check. Do not
779
+ // leak that request into the next outermost evaluation.
780
+ st.terminate_requested.store(0);
781
+ st.isolate->CancelTerminateExecution();
782
+ st.err_reason = NO_ERROR;
783
+ }
757
784
  restore_termination(st, preserve_termination);
758
785
  }
759
786
 
@@ -818,10 +845,35 @@ fail:
818
845
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
819
846
  if (cause) result = v8::Undefined(st.isolate);
820
847
  auto err = to_error(st, &try_catch, cause);
848
+ // Error formatting can run user JS (for example Error.prepareStackTrace)
849
+ // which may call Context#stop. Do not let that termination leak into the
850
+ // reply or the next evaluation.
851
+ bool terminated_during_error = false;
852
+ if (st.terminate_requested.load() || st.isolate->IsExecutionTerminating()) {
853
+ terminated_during_error = true;
854
+ preserve_termination =
855
+ suspend_termination(st, nested, cause) || preserve_termination;
856
+ }
857
+ if (terminated_during_error) {
858
+ result = v8::Undefined(st.isolate);
859
+ std::vector<char> buf;
860
+ set_error_message(buf, cause,
861
+ cause == MEMORY_ERROR ? "out of memory" : "terminated");
862
+ err = string_from_bytes(st.isolate, buf);
863
+ }
821
864
  if (!reply(st, result, err)) {
822
865
  assert(try_catch.HasCaught());
823
866
  goto fail; // retry; can be termination exception
824
867
  }
868
+ if (!nested &&
869
+ (st.terminate_requested.load() || st.isolate->IsExecutionTerminating())) {
870
+ // A callback invoked by the serializer (for example a getter) can
871
+ // request termination after the last suspend_termination check. Do not
872
+ // leak that request into the next outermost evaluation.
873
+ st.terminate_requested.store(0);
874
+ st.isolate->CancelTerminateExecution();
875
+ st.err_reason = NO_ERROR;
876
+ }
825
877
  restore_termination(st, preserve_termination);
826
878
  }
827
879
 
@@ -936,6 +988,138 @@ fail:
936
988
  reply_retry(st, result);
937
989
  }
938
990
 
991
+ static uint32_t read_le32(const uint8_t *p)
992
+ {
993
+ return static_cast<uint32_t>(p[0])
994
+ | static_cast<uint32_t>(p[1]) << 8
995
+ | static_cast<uint32_t>(p[2]) << 16
996
+ | static_cast<uint32_t>(p[3]) << 24;
997
+ }
998
+
999
+ static bool snapshot_checksum_matches(const uint8_t *data, size_t n)
1000
+ {
1001
+ if (n < 13)
1002
+ return false;
1003
+ uint32_t expected = read_le32(data + 8);
1004
+
1005
+ // V8 uses zlib's adler32 when built with V8_USE_ZLIB, otherwise a
1006
+ // Fletcher-32 variant. Accept either so this check remains portable
1007
+ // across libv8-node builds. This is a corruption check, not a sandbox:
1008
+ // snapshots are still trusted-input-only.
1009
+ uint32_t s1 = 0, s2 = 0;
1010
+ for (size_t i = 12; i < n; i++) {
1011
+ s1 = (s1 + data[i]) % 65521;
1012
+ s2 = (s2 + s1) % 65521;
1013
+ }
1014
+ if ((s2 << 16 | s1) == expected)
1015
+ return true;
1016
+
1017
+ s1 = s2 = 0;
1018
+ for (size_t i = 12; i < n; i++) {
1019
+ s1 = (s1 + data[i]) % 65535;
1020
+ s2 = (s2 + s1) % 65535;
1021
+ }
1022
+ return (s2 << 16 | s1) == expected;
1023
+ }
1024
+
1025
+ // The header/section offsets below mirror V8 13.6's SnapshotImpl. Keep them
1026
+ // in sync with libv8-node; the fallback is intentionally limited to preventing
1027
+ // V8's fatal deserializer checks, not to sandboxing untrusted snapshots.
1028
+ extern "C" int v8_snapshot_validate(const uint8_t *data, size_t n,
1029
+ char *err, size_t errlen)
1030
+ {
1031
+ #define SNAPSHOT_INVALID(msg) \
1032
+ do { \
1033
+ if (err && errlen) \
1034
+ snprintf(err, errlen, "%s", msg); \
1035
+ return -1; \
1036
+ } while (0)
1037
+
1038
+ if (n == 0)
1039
+ return 0; // Snapshot.load("") means "use V8's built-in snapshot".
1040
+ if (n > static_cast<size_t>(std::numeric_limits<int>::max()))
1041
+ SNAPSHOT_INVALID("snapshot is too large");
1042
+ if (n < 88)
1043
+ SNAPSHOT_INVALID("snapshot is too short");
1044
+
1045
+ v8::StartupData blob{reinterpret_cast<const char*>(data),
1046
+ static_cast<int>(n)};
1047
+ if (!blob.IsValid())
1048
+ SNAPSHOT_INVALID("snapshot version mismatch");
1049
+
1050
+ uint32_t num_contexts = read_le32(data);
1051
+ uint32_t rehashability = read_le32(data + 4);
1052
+ if (rehashability > 1)
1053
+ SNAPSHOT_INVALID("snapshot header is invalid");
1054
+ if (num_contexts == 0)
1055
+ SNAPSHOT_INVALID("snapshot has no contexts");
1056
+ if (num_contexts > (n - 88) / 4)
1057
+ SNAPSHOT_INVALID("snapshot context table is invalid");
1058
+
1059
+ const size_t pointer_size = sizeof(void*);
1060
+ size_t header_size = 88 + static_cast<size_t>(num_contexts) * 4;
1061
+ size_t startup_offset =
1062
+ (header_size + pointer_size - 1) & ~(pointer_size - 1);
1063
+ if (startup_offset > n)
1064
+ SNAPSHOT_INVALID("snapshot context table is invalid");
1065
+
1066
+ uint32_t read_only_offset = read_le32(data + 80);
1067
+ uint32_t shared_heap_offset = read_le32(data + 84);
1068
+ if (read_only_offset <= startup_offset || read_only_offset > n)
1069
+ SNAPSHOT_INVALID("snapshot read-only section is invalid");
1070
+ if (shared_heap_offset <= read_only_offset || shared_heap_offset > n)
1071
+ SNAPSHOT_INVALID("snapshot shared-heap section is invalid");
1072
+
1073
+ size_t previous_offset = shared_heap_offset;
1074
+ for (uint32_t i = 0; i < num_contexts; i++) {
1075
+ uint32_t offset = read_le32(data + 88 + static_cast<size_t>(i) * 4);
1076
+ if (offset <= previous_offset || offset >= n)
1077
+ SNAPSHOT_INVALID("snapshot context section is invalid");
1078
+ previous_offset = offset;
1079
+ }
1080
+
1081
+ if (!snapshot_checksum_matches(data, n))
1082
+ SNAPSHOT_INVALID("snapshot checksum mismatch");
1083
+
1084
+ return 0;
1085
+ #undef SNAPSHOT_INVALID
1086
+ }
1087
+
1088
+ // Keep all guard state in a JavaScript closure. The setup script returns a
1089
+ // query function to C++; do not install a native callback or External global,
1090
+ // because a retained property descriptor would otherwise make CreateBlob fail
1091
+ // with "Unknown external reference".
1092
+ static const char snapshot_intl_setup_source[] = R"js(
1093
+ ;(function() {
1094
+ const original = globalThis.Intl
1095
+ let used = false
1096
+ const getter = function() {
1097
+ used = true
1098
+ return original
1099
+ }
1100
+ const setter = function(value) {
1101
+ used = true
1102
+ }
1103
+ if (!original) {
1104
+ const query = function() { return used }
1105
+ query.getter = getter
1106
+ return query
1107
+ }
1108
+ Object.defineProperty(globalThis, "Intl", {
1109
+ configurable: true,
1110
+ enumerable: false,
1111
+ get: getter,
1112
+ set: setter
1113
+ })
1114
+ const query = function() { return used }
1115
+ query.getter = getter
1116
+ return query
1117
+ })()
1118
+ )js";
1119
+
1120
+ static const char snapshot_intl_error[] =
1121
+ "Intl is not supported in Snapshot.new scripts";
1122
+
939
1123
  int snapshot(bool is_warmup, bool verbose_exceptions,
940
1124
  const v8::String::Utf8Value& code,
941
1125
  v8::StartupData blob, v8::StartupData *result,
@@ -943,40 +1127,107 @@ int snapshot(bool is_warmup, bool verbose_exceptions,
943
1127
  {
944
1128
  // SnapshotCreator takes ownership of isolate
945
1129
  v8::Isolate *isolate = v8::Isolate::Allocate();
946
- v8::StartupData *existing_blob = is_warmup ? &blob : nullptr;
1130
+ // An empty loaded snapshot means "start from V8's built-in snapshot", just
1131
+ // like Context.new. Passing a non-null StartupData with raw_size == 0 to
1132
+ // SnapshotCreator trips CHECK(!artifacts) during warmup.
1133
+ v8::StartupData *existing_blob =
1134
+ (is_warmup && blob.data && blob.raw_size > 0) ? &blob : nullptr;
947
1135
  v8::SnapshotCreator snapshot_creator(isolate, nullptr, existing_blob);
948
1136
  v8::Isolate::Scope isolate_scope(isolate);
949
- v8::HandleScope handle_scope(isolate);
950
1137
  v8::TryCatch try_catch(isolate);
951
1138
  try_catch.SetVerbose(verbose_exceptions);
952
- auto filename = is_warmup
953
- ? v8::String::NewFromUtf8Literal(isolate, "<warmup>")
954
- : v8::String::NewFromUtf8Literal(isolate, "<snapshot>");
955
1139
  auto mode = is_warmup
956
1140
  ? v8::SnapshotCreator::FunctionCodeHandling::kKeep
957
1141
  : v8::SnapshotCreator::FunctionCodeHandling::kClear;
958
1142
  int cause = INTERNAL_ERROR;
959
1143
  {
1144
+ // Keep script-created values out of the scope that is alive when
1145
+ // CreateBlob() runs. V8's own warmup implementation uses separate
1146
+ // scopes; sharing one scope leaves Intl foreign handles visible to the
1147
+ // snapshot handle checker and aborts with
1148
+ // CheckGlobalAndEternalHandles failed.
1149
+ v8::HandleScope handle_scope(isolate);
1150
+ auto filename = is_warmup
1151
+ ? v8::String::NewFromUtf8Literal(isolate, "<warmup>")
1152
+ : v8::String::NewFromUtf8Literal(isolate, "<snapshot>");
960
1153
  auto context = v8::Context::New(isolate);
961
1154
  v8::Context::Scope context_scope(context);
1155
+ auto intl_key = v8::String::NewFromUtf8Literal(isolate, "Intl");
1156
+ v8::Local<v8::Value> intl_original;
1157
+ v8::Local<v8::Function> intl_state_fn;
1158
+ v8::Local<v8::Function> intl_guard_getter;
1159
+ bool intl_guarded = false;
1160
+ if (!context->Global()->Get(context, intl_key).ToLocal(&intl_original)) {
1161
+ set_error_message(*errbuf, INTERNAL_ERROR,
1162
+ "failed to inspect snapshot Intl");
1163
+ return INTERNAL_ERROR;
1164
+ }
1165
+ if (!is_warmup && intl_original->IsObject()) {
1166
+ auto intl_setup_source = v8::String::NewFromUtf8Literal(
1167
+ isolate, snapshot_intl_setup_source);
1168
+ v8::Local<v8::Script> intl_setup_script;
1169
+ v8::Local<v8::Value> intl_state;
1170
+ if (!v8::Script::Compile(context, intl_setup_source)
1171
+ .ToLocal(&intl_setup_script) ||
1172
+ !intl_setup_script->Run(context).ToLocal(&intl_state) ||
1173
+ !intl_state->IsFunction()) {
1174
+ set_error_message(*errbuf, INTERNAL_ERROR,
1175
+ "failed to initialize snapshot Intl guard");
1176
+ return INTERNAL_ERROR;
1177
+ }
1178
+ intl_state_fn = intl_state.As<v8::Function>();
1179
+ v8::Local<v8::Value> getter;
1180
+ auto getter_key =
1181
+ v8::String::NewFromUtf8Literal(isolate, "getter");
1182
+ if (!intl_state_fn->Get(context, getter_key).ToLocal(&getter) ||
1183
+ !getter->IsFunction()) {
1184
+ set_error_message(*errbuf, INTERNAL_ERROR,
1185
+ "failed to initialize snapshot Intl guard");
1186
+ return INTERNAL_ERROR;
1187
+ }
1188
+ intl_guard_getter = getter.As<v8::Function>();
1189
+ intl_guarded = true;
1190
+ }
962
1191
  v8::Local<v8::String> source;
963
1192
  auto type = v8::NewStringType::kNormal;
964
1193
  if (!v8::String::NewFromUtf8(isolate, *code, type, code.length()).ToLocal(&source)) {
965
1194
  v8::String::Utf8Value s(isolate, try_catch.Exception());
966
1195
  if (!set_error_message(*errbuf, cause, s))
967
1196
  set_error_message(*errbuf, cause, "unexpected failure");
968
- goto fail;
1197
+ return cause;
969
1198
  }
970
1199
  v8::ScriptOrigin origin(filename);
971
1200
  v8::Local<v8::Script> script;
972
1201
  cause = PARSE_ERROR;
973
1202
  if (!v8::Script::Compile(context, source, &origin).ToLocal(&script)) {
974
- goto err;
1203
+ auto m = try_catch.Message();
1204
+ if (m.IsEmpty()) {
1205
+ set_error_message(*errbuf, cause, "unexpected failure");
1206
+ return cause;
1207
+ }
1208
+ v8::String::Utf8Value s(isolate, m->Get());
1209
+ v8::String::Utf8Value name(isolate, m->GetScriptResourceName());
1210
+ auto line = m->GetLineNumber(context).FromMaybe(0);
1211
+ auto column = m->GetStartColumn(context).FromMaybe(0);
1212
+ if (*s && *name) {
1213
+ errbuf->clear();
1214
+ errbuf->push_back(static_cast<char>(cause));
1215
+ append_utf8(*errbuf, s);
1216
+ append_literal(*errbuf, "\n");
1217
+ append_utf8(*errbuf, name);
1218
+ append_format(*errbuf, ":%d:%d", line, column);
1219
+ } else {
1220
+ set_error_message(*errbuf, cause, "unexpected failure");
1221
+ }
1222
+ return cause;
975
1223
  }
976
1224
  cause = RUNTIME_ERROR;
977
1225
  if (script->Run(context).IsEmpty()) {
978
- err:
979
1226
  auto m = try_catch.Message();
1227
+ if (m.IsEmpty()) {
1228
+ set_error_message(*errbuf, cause, "unexpected failure");
1229
+ return cause;
1230
+ }
980
1231
  v8::String::Utf8Value s(isolate, m->Get());
981
1232
  v8::String::Utf8Value name(isolate, m->GetScriptResourceName());
982
1233
  auto line = m->GetLineNumber(context).FromMaybe(0);
@@ -991,20 +1242,82 @@ int snapshot(bool is_warmup, bool verbose_exceptions,
991
1242
  } else {
992
1243
  set_error_message(*errbuf, cause, "unexpected failure");
993
1244
  }
994
- goto fail;
1245
+ return cause;
1246
+ }
1247
+ if (intl_guarded) {
1248
+ v8::Local<v8::Value> used;
1249
+ auto recv = v8::Undefined(isolate);
1250
+ if (!intl_state_fn->Call(context, recv, 0, nullptr)
1251
+ .ToLocal(&used)) {
1252
+ set_error_message(*errbuf, INTERNAL_ERROR,
1253
+ "failed to inspect snapshot Intl guard");
1254
+ return INTERNAL_ERROR;
1255
+ }
1256
+ if (used->IsTrue()) {
1257
+ set_error_message(*errbuf, RUNTIME_ERROR, snapshot_intl_error);
1258
+ return RUNTIME_ERROR;
1259
+ }
1260
+ }
1261
+ if (intl_guarded) {
1262
+ // Only replace our accessor. If the script deliberately replaced
1263
+ // or deleted Intl, leave that change in the snapshot.
1264
+ v8::Local<v8::Value> descriptor;
1265
+ if (!context->Global()
1266
+ ->GetOwnPropertyDescriptor(context, intl_key)
1267
+ .ToLocal(&descriptor)) {
1268
+ set_error_message(*errbuf, INTERNAL_ERROR,
1269
+ "failed to inspect Intl after snapshot script");
1270
+ return INTERNAL_ERROR;
1271
+ }
1272
+ bool restore_intl = false;
1273
+ if (descriptor->IsObject()) {
1274
+ auto get_key =
1275
+ v8::String::NewFromUtf8Literal(isolate, "get");
1276
+ v8::Local<v8::Value> getter_descriptor;
1277
+ if (!descriptor.As<v8::Object>()
1278
+ ->GetOwnPropertyDescriptor(context, get_key)
1279
+ .ToLocal(&getter_descriptor)) {
1280
+ set_error_message(*errbuf, INTERNAL_ERROR,
1281
+ "failed to inspect Intl after snapshot script");
1282
+ return INTERNAL_ERROR;
1283
+ }
1284
+ if (getter_descriptor->IsObject()) {
1285
+ v8::Local<v8::Value> getter;
1286
+ auto value_key =
1287
+ v8::String::NewFromUtf8Literal(isolate, "value");
1288
+ if (!getter_descriptor.As<v8::Object>()
1289
+ ->Get(context, value_key)
1290
+ .ToLocal(&getter)) {
1291
+ set_error_message(*errbuf, INTERNAL_ERROR,
1292
+ "failed to inspect Intl after snapshot script");
1293
+ return INTERNAL_ERROR;
1294
+ }
1295
+ restore_intl =
1296
+ getter->IsFunction() &&
1297
+ getter->SameValue(intl_guard_getter);
1298
+ }
1299
+ }
1300
+ if (restore_intl &&
1301
+ !context->Global()
1302
+ ->DefineOwnProperty(
1303
+ context, intl_key, intl_original,
1304
+ v8::PropertyAttribute::DontEnum)
1305
+ .FromMaybe(false)) {
1306
+ set_error_message(*errbuf, INTERNAL_ERROR,
1307
+ "failed to restore Intl after snapshot script");
1308
+ return INTERNAL_ERROR;
1309
+ }
995
1310
  }
996
- cause = INTERNAL_ERROR;
997
1311
  if (!is_warmup) snapshot_creator.SetDefaultContext(context);
998
1312
  }
999
1313
  if (is_warmup) {
1314
+ v8::HandleScope handle_scope(isolate);
1000
1315
  isolate->ContextDisposedNotification(false);
1001
1316
  auto context = v8::Context::New(isolate);
1002
1317
  snapshot_creator.SetDefaultContext(context);
1003
1318
  }
1004
1319
  *result = snapshot_creator.CreateBlob(mode);
1005
- cause = NO_ERROR;
1006
- fail:
1007
- return cause;
1320
+ return NO_ERROR;
1008
1321
  }
1009
1322
 
1010
1323
  // response is errback [result, err] array
@@ -48,6 +48,10 @@ void v8_perform_microtask_checkpoint(struct State *pst);
48
48
  void v8_pump_message_loop(struct State *pst);
49
49
  void v8_snapshot(struct State *pst, const uint8_t *p, size_t n);
50
50
  void v8_warmup(struct State *pst, const uint8_t *p, size_t n);
51
+ // Returns 0 for a valid blob (including an empty blob), otherwise nonzero and
52
+ // writes a short reason to |err|. This performs header/section/checksum checks
53
+ // before V8's fatal snapshot deserializer sees the blob.
54
+ int v8_snapshot_validate(const uint8_t *data, size_t n, char *err, size_t errlen);
51
55
  void v8_low_memory_notification(struct State *pst);
52
56
  void v8_terminate_execution(struct State *pst); // called from ruby thread
53
57
  void v8_terminate_watchdog(struct State *pst); // called from watchdog thread
@@ -243,33 +243,38 @@ static void ser_num(Ser *s, double v)
243
243
  }
244
244
  }
245
245
 
246
- // ser_bigint: |n| is in bytes, not quadwords
247
- static void ser_bigint(Ser *s, const uint64_t *p, size_t n, int sign)
246
+ // ser_bigint: |p| points to |n| bytes, interpreted as little-endian
247
+ // 64-bit words. Keep the interface byte-oriented so callers don't need to
248
+ // expose a concrete word type.
249
+ static void ser_bigint(Ser *s, const void *p, size_t n, int sign)
248
250
  {
251
+ const uint8_t *bytes;
252
+
249
253
  if (*s->err)
250
254
  return;
251
255
  if (n % 8) {
252
256
  snprintf(s->err, sizeof(s->err), "bad bigint");
253
257
  return;
254
258
  }
259
+ bytes = p;
255
260
  w_byte(s, 'Z');
256
261
  // chop off high all-zero words
257
- n /= 8;
258
- while (n--)
259
- if (p[n])
260
- break;
261
- if (n == (size_t)-1) {
262
+ while (n > 0 && bytes[n-1] == 0)
263
+ n--;
264
+ if (n == 0) {
262
265
  w_byte(s, 0); // normalized zero
263
266
  } else {
264
- n = 8*n + 8;
267
+ n = (n + 7) & ~(size_t)7;
265
268
  w_varint(s, 2*n + (sign < 0));
266
- w(s, p, n);
269
+ w(s, bytes, n);
267
270
  }
268
271
  }
269
272
 
270
273
  static void ser_int(Ser *s, int64_t v)
271
274
  {
275
+ uint8_t bytes[8];
272
276
  uint64_t t;
277
+ size_t i;
273
278
  int sign;
274
279
 
275
280
  if (*s->err)
@@ -279,8 +284,10 @@ static void ser_int(Ser *s, int64_t v)
279
284
  if (v <= INT64_MAX/1024)
280
285
  return ser_num(s, v);
281
286
  t = v < 0 ? (uint64_t)(-(v + 1)) + 1 : (uint64_t)v;
287
+ for (i = 0; i < sizeof(bytes); i++)
288
+ bytes[i] = t >> (8*i);
282
289
  sign = v < 0 ? -1 : 1;
283
- ser_bigint(s, &t, sizeof(t), sign);
290
+ ser_bigint(s, bytes, sizeof(bytes), sign);
284
291
  } else {
285
292
  w_byte(s, 'I');
286
293
  w_zigzag(s, v);
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.22.0"
4
+ VERSION = "0.22.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.22.0
4
+ version: 0.22.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -164,9 +164,9 @@ licenses:
164
164
  - MIT
165
165
  metadata:
166
166
  bug_tracker_uri: https://github.com/discourse/mini_racer/issues
167
- changelog_uri: https://github.com/discourse/mini_racer/blob/v0.22.0/CHANGELOG
168
- documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.22.0
169
- source_code_uri: https://github.com/discourse/mini_racer/tree/v0.22.0
167
+ changelog_uri: https://github.com/discourse/mini_racer/blob/v0.22.2/CHANGELOG
168
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.22.2
169
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.22.2
170
170
  rdoc_options: []
171
171
  require_paths:
172
172
  - lib