mini_racer 0.22.1 → 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: 522742f60d9062d656ffab6c1eb7ead5ceb0eabb6918b9582e9eec32f1f8a4c8
4
- data.tar.gz: 404097b19b0e243bcd995e54f2560dd8fa5097dcc94a9488b435242350bc6719
3
+ metadata.gz: 700f0533786f28a992f2b46a0790ed557c36255d8b7357aecfd3fa1bfd4a01a9
4
+ data.tar.gz: f45170bb718eeff6e2b5d6f83af6039d178f36541983ab20c31e1dbfab83f7cf
5
5
  SHA512:
6
- metadata.gz: 723b3777cf8085b76662a3f4ede9fcdd8d4d8b31c31346ed0a87af1092b6a0323579b4dd6c3ec45a3c06f6cecedda7a30b8e807e69ecceaf5c3bf2ab9ceabc07
7
- data.tar.gz: 36e1ce3f381f3d4017c10d811805320af94fd026a81eca8bdc71e6fa0cecd58a23f4668883de3cf8810ce57e41e33e9e4560810d4621b9825104a22dd2076e6b
6
+ metadata.gz: ae2e7f4e68826ce1115c933cc17835cc1d32155de483f00a3269b0669fb25198d648aa87fc3ac49b7496c23015b080ea2e9c8e57b061bd4c115716b0180ad6a7
7
+ data.tar.gz: 4d5a790f98965015cfc97138fe0e8a9e5bbfd11c0f0f67499f8780d0cdee575105f4c7deaf310b6d5937c6fe56f05b9f7548f0d998ea0658c98970fb72b4dc14
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
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
+
1
13
  - 0.22.1 - 27-08-2026
2
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
3
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
data/README.md CHANGED
@@ -210,6 +210,12 @@ Snapshots can come in handy for example if you want your contexts to be pre-load
210
210
  > are fixed once the snapshot has been captured. They are no longer really random
211
211
  > nor reflect the current time.
212
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
+
213
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:
214
220
 
215
221
  ```ruby
@@ -242,7 +248,7 @@ context.eval("foo")
242
248
 
243
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.
244
250
 
245
- **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.
246
252
 
247
253
  ### Garbage collection
248
254
 
@@ -64,6 +64,7 @@ static inline void rb_thread_lock_native_thread(void)
64
64
  #define endof(x) ((x) + countof(x))
65
65
  #define BIGINT_STACK_WORDS 64
66
66
  #define BIGINT_MAX_BYTES (16 * 1024 * 1024)
67
+ #define SERIALIZE_MAX_DEPTH 512
67
68
 
68
69
  // mostly RO: assigned once by platform_set_flag1 while holding |flags_mtx|,
69
70
  // from then on read-only and accessible without holding locks
@@ -138,6 +139,7 @@ typedef struct Context
138
139
  struct State *pst; // used by v8 thread
139
140
  VALUE procs; // array of js -> ruby callbacks
140
141
  VALUE exception; // pending exception or Qnil
142
+ int pending_jump_tag; // non-exception jump to replay after the roundtrip
141
143
  Buf req, res; // ruby->v8 request/response, mediated by |mtx| and |cv|
142
144
  Buf v8_req; // stable v8-side copy of a request returned by v8_roundtrip
143
145
  int res_ready; // protected by |mtx|; response may be filled before ready
@@ -289,7 +291,7 @@ static void push(DesCtx *c, VALUE v)
289
291
  {
290
292
  if (*c->err)
291
293
  return;
292
- if (c->tos == endof(c->stack)) {
294
+ if (c->tos == endof(c->stack) - 1) {
293
295
  snprintf(c->err, sizeof(c->err), "stack overflow");
294
296
  return;
295
297
  }
@@ -613,13 +615,15 @@ static int serialize_bigint(Ser *s, VALUE v)
613
615
  return *s->err ? -1 : 0;
614
616
  }
615
617
 
616
- static int serialize1(Ser *s, VALUE refs, VALUE v)
618
+ static int serialize1(Ser *s, VALUE refs, VALUE v, int depth)
617
619
  {
618
620
  VALUE a, t, id;
619
621
  size_t i, n;
620
622
 
621
623
  if (*s->err)
622
624
  return -1;
625
+ if (depth > SERIALIZE_MAX_DEPTH)
626
+ return bail(&s->err, "max depth exceeded");
623
627
  switch (TYPE(v)) {
624
628
  case T_ARRAY:
625
629
  {
@@ -631,7 +635,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
631
635
  rb_hash_aset(refs, obj_id, LONG2FIX(i));
632
636
  ser_array_begin(s, n);
633
637
  for (i = 0; i < n; i++)
634
- if (serialize1(s, refs, rb_ary_entry(v, i)))
638
+ if (serialize1(s, refs, rb_ary_entry(v, i), depth + 1))
635
639
  return -1;
636
640
  ser_array_end(s, n);
637
641
  } else {
@@ -662,9 +666,9 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
662
666
  if (i == 2*n) {
663
667
  ser_object_begin(s);
664
668
  for (i = 0; i < 2*n; i += 2) {
665
- if (serialize1(s, refs, rb_ary_entry(a, i+0)))
669
+ if (serialize1(s, refs, rb_ary_entry(a, i+0), depth + 1))
666
670
  return -1;
667
- if (serialize1(s, refs, rb_ary_entry(a, i+1)))
671
+ if (serialize1(s, refs, rb_ary_entry(a, i+1), depth + 1))
668
672
  return -1;
669
673
  }
670
674
  ser_object_end(s, n);
@@ -748,7 +752,7 @@ static int serialize1(Ser *s, VALUE refs, VALUE v)
748
752
  // that will throw off the object reference count
749
753
  static int serialize(Ser *s, VALUE v)
750
754
  {
751
- return serialize1(s, rb_hash_new(), v);
755
+ return serialize1(s, rb_hash_new(), v, 0);
752
756
  }
753
757
 
754
758
  static struct timespec deadline_ms(int ms)
@@ -1025,11 +1029,54 @@ static VALUE rendezvous_callback_do(VALUE arg)
1025
1029
  return rb_funcall2(func, rb_intern("call"), RARRAY_LENINT(args), RARRAY_PTR(args));
1026
1030
  }
1027
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
+
1028
1074
  // called with |rr_mtx| and GVL held; |mtx| is unlocked
1029
1075
  // callback data is in |a->res|, serialized result goes in |a->req|
1030
1076
  static void *rendezvous_callback(void *arg)
1031
1077
  {
1032
1078
  struct rendezvous_nogvl *a;
1079
+ struct callback_serialize_args args;
1033
1080
  const char *err;
1034
1081
  Context *c;
1035
1082
  int exc;
@@ -1040,13 +1087,29 @@ static void *rendezvous_callback(void *arg)
1040
1087
  c = a->context;
1041
1088
  r = rb_protect(rendezvous_callback_do, (VALUE)a, &exc);
1042
1089
  if (exc) {
1043
- c->exception = rb_errinfo();
1044
- rb_set_errinfo(Qnil);
1090
+ c->exception = callback_exception_from_state(exc, &c->pending_jump_tag);
1045
1091
  goto fail;
1046
1092
  }
1047
1093
  ser_init1(&s, 'c'); // callback reply
1048
- if (serialize(&s, r)) {
1049
- 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
+ }
1050
1113
  ser_reset(&s);
1051
1114
  goto fail;
1052
1115
  }
@@ -1056,10 +1119,25 @@ out:
1056
1119
  fail:
1057
1120
  ser_init0(&s); // ruby exception pending
1058
1121
  w_byte(&s, 'e'); // send ruby error message to v8 thread
1059
- r = rb_funcall(c->exception, rb_intern("to_s"), 0);
1060
- err = StringValuePtr(r);
1061
- 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);
1062
1139
  w(&s, err, RSTRING_LEN(r));
1140
+ }
1063
1141
  goto out;
1064
1142
  }
1065
1143
 
@@ -1350,6 +1428,12 @@ static VALUE rendezvous1(Context *c, Buf *req, DesCtx *d)
1350
1428
  int exc;
1351
1429
 
1352
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
+ }
1353
1437
  r = c->exception;
1354
1438
  c->exception = Qnil;
1355
1439
  // if js land didn't handle exception from ruby callback, re-raise it now
@@ -2146,15 +2230,22 @@ static VALUE snapshot_dump(VALUE self)
2146
2230
  Snapshot *ss;
2147
2231
 
2148
2232
  TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss);
2149
- 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);
2150
2236
  }
2151
2237
 
2152
2238
  static VALUE snapshot_load(VALUE klass, VALUE blob)
2153
2239
  {
2154
2240
  Snapshot *ss;
2155
2241
  VALUE self;
2242
+ char err[128];
2156
2243
 
2157
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
+ }
2158
2249
  self = snapshot_alloc(klass);
2159
2250
  TypedData_Get_Struct(self, Snapshot, &snapshot_type, ss);
2160
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MiniRacer
4
- VERSION = "0.22.1"
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.1
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.1/CHANGELOG
168
- documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.22.1
169
- source_code_uri: https://github.com/discourse/mini_racer/tree/v0.22.1
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