mini_racer 0.21.1 → 0.21.3

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: 3e370b2c6cdb60e5081d5f97840cff28489119f0225a3c9a74ed7f6859caf40e
4
+ data.tar.gz: 7405ed87278cbaa1d155bc72f8c99d8e8c744df743252eb4128b9ae9ff899d07
5
5
  SHA512:
6
- metadata.gz: d826ca742dbe6580f92faebf11c827795536c55596c279fa070dd38836d03b7dcecc0b43fe254b6d95590bea1a24d8ec168f9d5008b668ba488a5aff7b6e3aea
7
- data.tar.gz: 985effeb709d8dfc1481c679dfdc86a3930a0d84de6d3b6d9d154d6801a6766af76ffecd468e22d8b20b68952e02b756ab0ec11b1bc3c93736b078d046719798
6
+ metadata.gz: 3c9f53c6a9052cca6a5129d023ecece59613c40c17f4c7666c728f8f6327200e34d707557ea4dcd9b626aed332b327c6c3e3f90e365c630adbec0b2d02c4b1c1
7
+ data.tar.gz: 4dc0348a006f7620c4c9688c0b877f2bd9500b7ece37fba4fa6e868006af1517e4279537693fd87b2f18c2b0602067084e2332e2bf9fa862504ebaab4ddbbcfc
data/CHANGELOG CHANGED
@@ -1,3 +1,17 @@
1
+ - 0.21.3 - 18-06-2026
2
+ - Fix `:single_threaded` contexts inherited across `fork` by recovering idle reusable native runners in the child process without falling back to per-dispatch native thread spawning
3
+ - Avoid intermittent heap corruption during `:single_threaded` context finalization, especially when forked children exit normally after touching inherited contexts
4
+ - Avoid finalizer hangs when a forked child garbage-collects a non-idle inherited `:single_threaded` context
5
+ - Allow Ruby thread interrupts, process shutdown, and cross-thread `Context#dispose` to terminate busy `:single_threaded` JavaScript execution instead of hanging
6
+ - Make `Context#dispose` while an attached Ruby callback is active either terminate safely or raise instead of deadlocking
7
+
8
+ - 0.21.2 - 11-06-2026
9
+ - Add `Context#perform_microtask_checkpoint` to synchronously drain the V8 microtask queue, useful for spec-compliant `dispatchEvent` sequencing inside Ruby callbacks
10
+ - Fix native memory leaks in `Context#heap_snapshot`/`Context#write_heap_snapshot`; thanks to Pranjali Thakur from depthfirst.com
11
+ - Fix large integral JavaScript numbers wrapping to negative Ruby integers; thanks to Pranjali Thakur from depthfirst.com
12
+ - Fix Ruby callback exceptions with embedded NUL bytes permanently deadlocking a context; thanks to Pranjali Thakur from depthfirst.com
13
+ - Preserve embedded NUL bytes in JavaScript exception messages and attached function names, and reject unsafe V8 flags containing NUL bytes or overly long values
14
+
1
15
  - 0.21.1 - 25-05-2026
2
16
  - Run `:single_threaded` V8 dispatches on a reusable mini_racer-owned native thread so V8 does not execute on Ruby-owned threads
3
17
  - 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.
@@ -5,6 +5,7 @@
5
5
  #include <string.h>
6
6
  #include <pthread.h>
7
7
  #include <unistd.h>
8
+ #include <errno.h>
8
9
  #include <math.h>
9
10
 
10
11
  #if defined(__linux__) && !defined(__GLIBC__)
@@ -159,6 +160,7 @@ typedef struct Snapshot {
159
160
  } Snapshot;
160
161
 
161
162
  static void context_destroy(Context *c);
163
+ static void context_abandon(Context *c);
162
164
  static void context_free(void *arg);
163
165
  static void context_mark(void *arg);
164
166
  static size_t context_size(const void *arg);
@@ -227,6 +229,7 @@ struct rendezvous_nogvl
227
229
  {
228
230
  Context *context;
229
231
  Buf *req, *res;
232
+ atomic_int active;
230
233
  };
231
234
 
232
235
  struct rendezvous_des
@@ -316,13 +319,19 @@ static void des_bool(void *arg, int v)
316
319
 
317
320
  static void des_int(void *arg, int64_t v)
318
321
  {
319
- put(arg, LONG2FIX(v));
322
+ put(arg, LL2NUM((LONG_LONG)v));
320
323
  }
321
324
 
322
325
  static void des_num(void *arg, double v)
323
326
  {
324
- if (isfinite(v) && v == trunc(v) && v >= INT64_MIN && v <= INT64_MAX) {
325
- put(arg, LONG2FIX(v));
327
+ if (isfinite(v) && v == trunc(v)) {
328
+ // INT64_MAX is not exactly representable as a double: it rounds up to
329
+ // 2^63, which would let 2^63 through and make the cast undefined.
330
+ if (v >= -0x1p63 && v < 0x1p63) {
331
+ put(arg, LL2NUM((LONG_LONG)v));
332
+ } else {
333
+ put(arg, rb_dbl2big(v));
334
+ }
326
335
  } else {
327
336
  put(arg, DBL2NUM(v));
328
337
  }
@@ -364,19 +373,8 @@ static void des_bigint(void *arg, const void *p, size_t n, int sign)
364
373
  if (t >> 63)
365
374
  *a++ = 0; // suppress sign extension
366
375
  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
- }
376
+ if (sign < 0)
377
+ v = rb_funcall(v, rb_intern("-@"), 0);
380
378
  put(c, v);
381
379
  }
382
380
 
@@ -810,6 +808,7 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
810
808
  case 'C': return v8_timedwait(c, p+1, n-1, v8_call);
811
809
  case 'E': return v8_timedwait(c, p+1, n-1, v8_eval);
812
810
  case 'H': return v8_heap_snapshot(c->pst);
811
+ case 'M': return v8_perform_microtask_checkpoint(c->pst);
813
812
  case 'P': return v8_pump_message_loop(c->pst);
814
813
  case 'S': return v8_heap_stats(c->pst);
815
814
  case 'T': return v8_snapshot(c->pst, p+1, n-1);
@@ -874,8 +873,14 @@ void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
874
873
  {
875
874
  buf_reset(&c->req);
876
875
  pthread_cond_signal(&c->cv);
877
- while (!c->req.len)
876
+ while (!c->req.len && !atomic_load(&c->quit))
878
877
  pthread_cond_wait(&c->cv, &c->mtx);
878
+ if (!c->req.len && atomic_load(&c->quit)) {
879
+ static const uint8_t disposed[] = "edisposed context";
880
+ *p = disposed;
881
+ *n = sizeof(disposed) - 1;
882
+ return;
883
+ }
879
884
  buf_reset(&c->res);
880
885
  *p = c->req.buf;
881
886
  *n = c->req.len;
@@ -936,6 +941,7 @@ static VALUE rendezvous_callback_do(VALUE arg)
936
941
  Context *c;
937
942
  DesCtx d;
938
943
  Buf *b;
944
+ long id;
939
945
 
940
946
  a = (void *)arg;
941
947
  b = a->res;
@@ -945,7 +951,12 @@ static VALUE rendezvous_callback_do(VALUE arg)
945
951
  DesCtx_init(&d);
946
952
  args = deserialize1(&d, b->buf+1, b->len-1); // skip 'c' marker
947
953
  func = rb_ary_pop(args); // callback id
948
- func = rb_ary_entry(c->procs, FIX2LONG(func));
954
+ if (!RB_INTEGER_TYPE_P(func))
955
+ rb_raise(runtime_error, "bad callback id");
956
+ id = NUM2LONG(func);
957
+ if (id < 0 || id >= RARRAY_LEN(c->procs))
958
+ rb_raise(runtime_error, "bad callback id");
959
+ func = rb_ary_entry(c->procs, id);
949
960
  return rb_funcall2(func, rb_intern("call"), RARRAY_LENINT(args), RARRAY_PTR(args));
950
961
  }
951
962
 
@@ -981,9 +992,9 @@ fail:
981
992
  ser_init0(&s); // ruby exception pending
982
993
  w_byte(&s, 'e'); // send ruby error message to v8 thread
983
994
  r = rb_funcall(c->exception, rb_intern("to_s"), 0);
984
- err = StringValueCStr(r);
995
+ err = StringValuePtr(r);
985
996
  if (err)
986
- w(&s, err, strlen(err));
997
+ w(&s, err, RSTRING_LEN(r));
987
998
  goto out;
988
999
  }
989
1000
 
@@ -1005,6 +1016,33 @@ static void *single_threaded_runner(void *arg)
1005
1016
  return NULL;
1006
1017
  }
1007
1018
 
1019
+ static int single_threaded_recover_after_fork(Context *c)
1020
+ {
1021
+ pthread_condattr_t cattr;
1022
+ pid_t pid;
1023
+ int r;
1024
+
1025
+ pid = getpid();
1026
+ if (!c->single_threaded_thr_started || c->single_threaded_pid == pid)
1027
+ return 0;
1028
+ if (c->depth || c->req.len || c->res.len)
1029
+ return EBUSY;
1030
+
1031
+ if ((r = pthread_condattr_init(&cattr)))
1032
+ return r;
1033
+ #ifndef __APPLE__
1034
+ pthread_condattr_setclock(&cattr, CLOCK_MONOTONIC);
1035
+ #endif
1036
+ r = pthread_cond_init(&c->cv, &cattr);
1037
+ pthread_condattr_destroy(&cattr);
1038
+ if (r)
1039
+ return r;
1040
+
1041
+ c->single_threaded_thr_started = 0;
1042
+ c->single_threaded_pid = pid;
1043
+ return 0;
1044
+ }
1045
+
1008
1046
  static int single_threaded_runner_start(Context *c)
1009
1047
  {
1010
1048
  pid_t pid;
@@ -1029,7 +1067,10 @@ static inline void *rendezvous_nogvl(void *arg)
1029
1067
 
1030
1068
  a = arg;
1031
1069
  c = a->context;
1070
+ if (single_threaded && (r = single_threaded_recover_after_fork(c)))
1071
+ return (void *)(intptr_t)r;
1032
1072
  pthread_mutex_lock(&c->rr_mtx);
1073
+ atomic_store(&a->active, 1);
1033
1074
  if (c->depth > 0 && c->depth%50 == 0) { // TODO stop steep recursion
1034
1075
  fprintf(stderr, "mini_racer: deep js->ruby->js recursion, depth=%d\n", c->depth);
1035
1076
  fflush(stderr);
@@ -1037,6 +1078,14 @@ static inline void *rendezvous_nogvl(void *arg)
1037
1078
  c->depth++;
1038
1079
  next:
1039
1080
  pthread_mutex_lock(&c->mtx);
1081
+ if (atomic_load(&c->quit)) {
1082
+ buf_reset(a->req);
1083
+ pthread_mutex_unlock(&c->mtx);
1084
+ c->depth--;
1085
+ atomic_store(&a->active, 0);
1086
+ pthread_mutex_unlock(&c->rr_mtx);
1087
+ return (void *)(intptr_t)ECANCELED;
1088
+ }
1040
1089
  assert(c->req.len == 0);
1041
1090
  assert(c->res.len == 0);
1042
1091
  buf_move(a->req, &c->req); // v8 thread takes ownership of req
@@ -1046,6 +1095,7 @@ next:
1046
1095
  buf_move(&c->req, a->req);
1047
1096
  pthread_mutex_unlock(&c->mtx);
1048
1097
  c->depth--;
1098
+ atomic_store(&a->active, 0);
1049
1099
  pthread_mutex_unlock(&c->rr_mtx);
1050
1100
  return (void *)(intptr_t)r;
1051
1101
  }
@@ -1056,29 +1106,66 @@ next:
1056
1106
  do pthread_cond_wait(&c->cv, &c->mtx); while (!c->res.len);
1057
1107
  }
1058
1108
  buf_move(&c->res, a->res);
1109
+ pthread_cond_broadcast(&c->cv);
1059
1110
  pthread_mutex_unlock(&c->mtx);
1060
1111
  if (*a->res->buf == 'c') { // js -> ruby callback?
1061
1112
  rb_thread_call_with_gvl(rendezvous_callback, a);
1062
1113
  buf_reset(a->res);
1114
+ if (atomic_load(&c->quit)) {
1115
+ buf_reset(a->req);
1116
+ c->depth--;
1117
+ atomic_store(&a->active, 0);
1118
+ pthread_mutex_unlock(&c->rr_mtx);
1119
+ return (void *)(intptr_t)ECANCELED;
1120
+ }
1063
1121
  goto next;
1064
1122
  }
1065
1123
  c->depth--;
1124
+ atomic_store(&a->active, 0);
1066
1125
  pthread_mutex_unlock(&c->rr_mtx);
1067
1126
  return NULL;
1068
1127
  }
1069
1128
 
1129
+ static void rendezvous_ubf(void *arg)
1130
+ {
1131
+ struct rendezvous_nogvl *a;
1132
+ Context *c;
1133
+
1134
+ a = arg;
1135
+ if (!atomic_load(&a->active))
1136
+ return;
1137
+ c = a->context;
1138
+ if (c->pst)
1139
+ v8_terminate_execution(c->pst);
1140
+ pthread_cond_broadcast(&c->cv);
1141
+ }
1142
+
1143
+ static void terminate_ubf(void *arg)
1144
+ {
1145
+ Context *c;
1146
+
1147
+ c = arg;
1148
+ if (c->pst)
1149
+ v8_terminate_execution(c->pst);
1150
+ pthread_cond_broadcast(&c->cv);
1151
+ }
1152
+
1070
1153
  static void rendezvous_no_des(Context *c, Buf *req, Buf *res)
1071
1154
  {
1072
1155
  void *r;
1156
+ struct rendezvous_nogvl a;
1073
1157
 
1074
1158
  if (atomic_load(&c->quit)) {
1075
1159
  buf_reset(req);
1076
1160
  rb_raise(context_disposed_error, "disposed context");
1077
1161
  }
1078
- r = rb_nogvl(rendezvous_nogvl, &(struct rendezvous_nogvl){c, req, res},
1079
- NULL, NULL, 0);
1162
+ a.context = c;
1163
+ a.req = req;
1164
+ a.res = res;
1165
+ atomic_init(&a.active, 0);
1166
+ r = rb_nogvl(rendezvous_nogvl, &a, rendezvous_ubf, &a, 0);
1080
1167
  if (r)
1081
- rb_raise(runtime_error, "pthread_create: %s", strerror((int)(intptr_t)r));
1168
+ rb_raise(runtime_error, "single-threaded runner: %s", strerror((int)(intptr_t)r));
1082
1169
  }
1083
1170
 
1084
1171
  // send request to & receive reply from v8 thread; takes ownership of |req|
@@ -1094,7 +1181,8 @@ static VALUE rendezvous1(Context *c, Buf *req, DesCtx *d)
1094
1181
  c->exception = Qnil;
1095
1182
  // if js land didn't handle exception from ruby callback, re-raise it now
1096
1183
  if (res.len == 1 && *res.buf == 'e') {
1097
- assert(!NIL_P(r));
1184
+ if (NIL_P(r))
1185
+ rb_raise(context_disposed_error, "disposed context");
1098
1186
  rb_exc_raise(r);
1099
1187
  }
1100
1188
  r = rb_protect(deserialize, (VALUE)&(struct rendezvous_des){d, &res}, &exc);
@@ -1115,16 +1203,35 @@ static VALUE rendezvous(Context *c, Buf *req)
1115
1203
  return rendezvous1(c, req, &d);
1116
1204
  }
1117
1205
 
1206
+ static void raise_exception_with_message(VALUE klass, VALUE e)
1207
+ {
1208
+ long n;
1209
+ VALUE message;
1210
+
1211
+ if (NIL_P(e))
1212
+ return;
1213
+ e = StringValue(e);
1214
+ n = RSTRING_LEN(e);
1215
+ if (n == 0 || RSTRING_PTR(e)[0] == NO_ERROR)
1216
+ return;
1217
+ message = rb_str_subseq(e, 1, n - 1);
1218
+ rb_exc_raise(rb_exc_new_str(klass, message));
1219
+ }
1220
+
1118
1221
  static void handle_exception(VALUE e)
1119
1222
  {
1120
1223
  const char *s;
1121
1224
  VALUE klass;
1225
+ long n;
1122
1226
 
1123
1227
  if (NIL_P(e))
1124
1228
  return;
1125
1229
  e = StringValue(e);
1126
- s = StringValueCStr(e);
1127
- switch (*s) {
1230
+ n = RSTRING_LEN(e);
1231
+ if (n == 0)
1232
+ return;
1233
+ s = RSTRING_PTR(e);
1234
+ switch ((unsigned char)*s) {
1128
1235
  case NO_ERROR:
1129
1236
  return;
1130
1237
  case INTERNAL_ERROR:
@@ -1143,9 +1250,9 @@ static void handle_exception(VALUE e)
1143
1250
  klass = terminated_error;
1144
1251
  break;
1145
1252
  default:
1146
- rb_raise(internal_error, "bad error class %02x", *s);
1253
+ rb_raise(internal_error, "bad error class %02x", (unsigned char)*s);
1147
1254
  }
1148
- rb_enc_raise(rb_enc_get(e), klass, "%s", s+1);
1255
+ raise_exception_with_message(klass, e);
1149
1256
  }
1150
1257
 
1151
1258
  static VALUE context_alloc(VALUE klass)
@@ -1230,11 +1337,19 @@ fail0:
1230
1337
  return Qnil; // pacify compiler
1231
1338
  }
1232
1339
 
1233
- static void *context_free_thread_do(void *arg)
1340
+ static void *context_free_do(void *arg)
1234
1341
  {
1235
1342
  Context *c;
1236
1343
 
1237
1344
  c = arg;
1345
+ if (single_threaded && single_threaded_recover_after_fork(c)) {
1346
+ // The child forked while this inherited context was not idle. There is
1347
+ // no live runner thread to join and the inherited V8/pthread state is
1348
+ // not safe to tear down. A finalizer must not hang here; let the OS
1349
+ // reclaim the abandoned V8 state when the child exits.
1350
+ context_abandon(c);
1351
+ return NULL;
1352
+ }
1238
1353
  if (single_threaded && c->single_threaded_thr_started && c->single_threaded_pid == getpid()) {
1239
1354
  pthread_mutex_lock(&c->mtx);
1240
1355
  atomic_store(&c->quit, 2);
@@ -1249,31 +1364,16 @@ static void *context_free_thread_do(void *arg)
1249
1364
  return NULL;
1250
1365
  }
1251
1366
 
1252
- static void context_free_thread(Context *c)
1253
- {
1254
- pthread_t thr;
1255
- int r;
1256
-
1257
- // dispose on another thread so we don't block when trying to
1258
- // enter an isolate that's in a stuck state; that *should* be
1259
- // impossible but apparently it happened regularly before the
1260
- // rewrite and I'm carrying it over out of an abundance of caution
1261
- if ((r = pthread_create(&thr, NULL, context_free_thread_do, c))) {
1262
- fprintf(stderr, "mini_racer: pthread_create: %s", strerror(r));
1263
- fflush(stderr);
1264
- context_free_thread_do(c);
1265
- } else {
1266
- pthread_detach(thr);
1267
- }
1268
- }
1269
-
1270
1367
  static void context_free(void *arg)
1271
1368
  {
1272
1369
  Context *c;
1273
1370
 
1274
1371
  c = arg;
1275
1372
  if (single_threaded) {
1276
- context_free_thread(c);
1373
+ // Free synchronously. A detached cleanup thread can race normal Ruby
1374
+ // process shutdown and trip glibc malloc corruption checks while V8 is
1375
+ // tearing down single-threaded contexts.
1376
+ context_free_do(c);
1277
1377
  } else {
1278
1378
  pthread_mutex_lock(&c->mtx);
1279
1379
  c->quit = 2; // 2 = v8 thread frees
@@ -1282,6 +1382,14 @@ static void context_free(void *arg)
1282
1382
  }
1283
1383
  }
1284
1384
 
1385
+ static void context_abandon(Context *c)
1386
+ {
1387
+ buf_reset(&c->snapshot);
1388
+ buf_reset(&c->req);
1389
+ buf_reset(&c->res);
1390
+ ruby_xfree(c);
1391
+ }
1392
+
1285
1393
  static void context_destroy(Context *c)
1286
1394
  {
1287
1395
  pthread_mutex_unlock(&c->mtx);
@@ -1317,13 +1425,17 @@ static VALUE context_attach(VALUE self, VALUE name, VALUE proc)
1317
1425
  Context *c;
1318
1426
  VALUE e;
1319
1427
  Ser s;
1428
+ long id;
1320
1429
 
1321
1430
  TypedData_Get_Struct(self, Context, &context_type, c);
1431
+ id = RARRAY_LEN(c->procs);
1432
+ if (id > INT32_MAX)
1433
+ rb_raise(runtime_error, "too many callbacks");
1322
1434
  // request is (A)ttach, [name, id] array
1323
1435
  ser_init1(&s, 'A');
1324
1436
  ser_array_begin(&s, 2);
1325
1437
  add_string(&s, name);
1326
- ser_int(&s, RARRAY_LENINT(c->procs));
1438
+ ser_int(&s, id);
1327
1439
  ser_array_end(&s, 2);
1328
1440
  rb_ary_push(c->procs, proc);
1329
1441
  // response is an exception or undefined
@@ -1335,8 +1447,25 @@ static VALUE context_attach(VALUE self, VALUE name, VALUE proc)
1335
1447
  static void *context_dispose_do(void *arg)
1336
1448
  {
1337
1449
  Context *c;
1450
+ int r;
1338
1451
 
1339
1452
  c = arg;
1453
+ if (single_threaded) {
1454
+ if ((r = single_threaded_recover_after_fork(c)))
1455
+ return (void *)(intptr_t)r;
1456
+ }
1457
+ if (c->depth > 0) {
1458
+ r = pthread_mutex_trylock(&c->rr_mtx);
1459
+ if (!r) {
1460
+ pthread_mutex_unlock(&c->rr_mtx);
1461
+ return (void *)(intptr_t)EBUSY;
1462
+ }
1463
+ if (r != EBUSY)
1464
+ return (void *)(intptr_t)r;
1465
+ if (c->pst)
1466
+ v8_terminate_execution(c->pst);
1467
+ pthread_cond_broadcast(&c->cv);
1468
+ }
1340
1469
  if (single_threaded) {
1341
1470
  pthread_mutex_lock(&c->mtx);
1342
1471
  while (c->req.len || c->res.len)
@@ -1364,9 +1493,12 @@ static void *context_dispose_do(void *arg)
1364
1493
  static VALUE context_dispose(VALUE self)
1365
1494
  {
1366
1495
  Context *c;
1496
+ void *r;
1367
1497
 
1368
1498
  TypedData_Get_Struct(self, Context, &context_type, c);
1369
- rb_thread_call_without_gvl(context_dispose_do, c, NULL, NULL);
1499
+ r = rb_thread_call_without_gvl(context_dispose_do, c, terminate_ubf, c);
1500
+ if (r)
1501
+ rb_raise(runtime_error, "context dispose: %s", strerror((int)(intptr_t)r));
1370
1502
  return Qnil;
1371
1503
  }
1372
1504
 
@@ -1457,6 +1589,20 @@ static VALUE context_heap_stats(VALUE self)
1457
1589
  return h;
1458
1590
  }
1459
1591
 
1592
+ static VALUE buf_reset_ensure(VALUE arg)
1593
+ {
1594
+ buf_reset((Buf *)arg);
1595
+ return Qnil;
1596
+ }
1597
+
1598
+ static VALUE heap_snapshot_to_str(VALUE arg)
1599
+ {
1600
+ Buf *res;
1601
+
1602
+ res = (Buf *)arg;
1603
+ return rb_utf8_str_new((char *)res->buf, res->len);
1604
+ }
1605
+
1460
1606
  static VALUE context_heap_snapshot(VALUE self)
1461
1607
  {
1462
1608
  Buf req, res;
@@ -1466,7 +1612,19 @@ static VALUE context_heap_snapshot(VALUE self)
1466
1612
  buf_init(&req);
1467
1613
  buf_putc(&req, 'H'); // (H)eap snapshot, returns plain bytes
1468
1614
  rendezvous_no_des(c, &req, &res); // takes ownership of |req|
1469
- return rb_utf8_str_new((char *)res.buf, res.len);
1615
+ return rb_ensure(heap_snapshot_to_str, (VALUE)&res,
1616
+ buf_reset_ensure, (VALUE)&res);
1617
+ }
1618
+
1619
+ static VALUE context_perform_microtask_checkpoint(VALUE self)
1620
+ {
1621
+ Context *c;
1622
+ Buf b;
1623
+
1624
+ TypedData_Get_Struct(self, Context, &context_type, c);
1625
+ buf_init(&b);
1626
+ buf_putc(&b, 'M'); // (M)icrotask checkpoint, returns nil
1627
+ return rendezvous(c, &b); // takes ownership of |b|
1470
1628
  }
1471
1629
 
1472
1630
  static VALUE context_pump_message_loop(VALUE self)
@@ -1489,12 +1647,14 @@ static VALUE context_low_memory_notification(VALUE self)
1489
1647
  buf_init(&req);
1490
1648
  buf_putc(&req, 'L'); // (L)ow memory notification, returns nothing
1491
1649
  rendezvous_no_des(c, &req, &res); // takes ownership of |req|
1650
+ buf_reset(&res);
1492
1651
  return Qnil;
1493
1652
  }
1494
1653
 
1495
1654
  static int platform_set_flag1(VALUE k, VALUE v)
1496
1655
  {
1497
- char *p, *q, buf[256];
1656
+ char *p, *q, *r, buf[256];
1657
+ long pn, vn, len;
1498
1658
  int ok;
1499
1659
 
1500
1660
  k = rb_funcall(k, rb_intern("to_s"), 0);
@@ -1504,12 +1664,40 @@ static int platform_set_flag1(VALUE k, VALUE v)
1504
1664
  Check_Type(v, T_STRING);
1505
1665
  }
1506
1666
  p = RSTRING_PTR(k);
1507
- if (!strncmp(p, "--", 2))
1667
+ pn = RSTRING_LEN(k);
1668
+ if (memchr(p, '\0', pn))
1669
+ rb_raise(rb_eArgError, "flag contains NUL byte");
1670
+ if (pn >= 2 && p[0] == '-' && p[1] == '-') {
1508
1671
  p += 2;
1672
+ pn -= 2;
1673
+ }
1509
1674
  if (NIL_P(v)) {
1510
- snprintf(buf, sizeof(buf), "--%s", p);
1675
+ len = 2 + pn;
1676
+ if (len >= (long)sizeof(buf))
1677
+ rb_raise(rb_eArgError, "flag too long");
1678
+ q = buf;
1679
+ *q++ = '-';
1680
+ *q++ = '-';
1681
+ memcpy(q, p, pn);
1682
+ q += pn;
1683
+ *q = '\0';
1511
1684
  } else {
1512
- snprintf(buf, sizeof(buf), "--%s=%s", p, RSTRING_PTR(v));
1685
+ q = RSTRING_PTR(v);
1686
+ vn = RSTRING_LEN(v);
1687
+ if (memchr(q, '\0', vn))
1688
+ rb_raise(rb_eArgError, "flag contains NUL byte");
1689
+ len = 3 + pn + vn;
1690
+ if (len >= (long)sizeof(buf))
1691
+ rb_raise(rb_eArgError, "flag too long");
1692
+ r = buf;
1693
+ *r++ = '-';
1694
+ *r++ = '-';
1695
+ memcpy(r, p, pn);
1696
+ r += pn;
1697
+ *r++ = '=';
1698
+ memcpy(r, q, vn);
1699
+ r += vn;
1700
+ *r = '\0';
1513
1701
  }
1514
1702
  p = buf;
1515
1703
  pthread_mutex_lock(&flags_mtx);
@@ -1722,8 +1910,7 @@ static VALUE snapshot_initialize(int argc, VALUE *argv, VALUE self)
1722
1910
  a = rendezvous1(c, &s.b, &d);
1723
1911
  e = rb_ary_pop(a);
1724
1912
  context_dispose(cv);
1725
- if (*RSTRING_PTR(e))
1726
- rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1);
1913
+ raise_exception_with_message(snapshot_error, e);
1727
1914
  ss->blob = rb_ary_pop(a);
1728
1915
  return Qnil;
1729
1916
  }
@@ -1753,8 +1940,7 @@ static VALUE snapshot_warmup(VALUE self, VALUE arg)
1753
1940
  a = rendezvous1(c, &s.b, &d);
1754
1941
  e = rb_ary_pop(a);
1755
1942
  context_dispose(cv);
1756
- if (*RSTRING_PTR(e))
1757
- rb_raise(snapshot_error, "%s", RSTRING_PTR(e)+1);
1943
+ raise_exception_with_message(snapshot_error, e);
1758
1944
  ss->blob = rb_ary_pop(a);
1759
1945
  return self;
1760
1946
  }
@@ -1824,6 +2010,7 @@ void Init_mini_racer_extension(void)
1824
2010
  rb_define_method(c, "eval", context_eval, -1);
1825
2011
  rb_define_method(c, "heap_stats", context_heap_stats, 0);
1826
2012
  rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0);
2013
+ rb_define_method(c, "perform_microtask_checkpoint", context_perform_microtask_checkpoint, 0);
1827
2014
  rb_define_method(c, "pump_message_loop", context_pump_message_loop, 0);
1828
2015
  rb_define_method(c, "low_memory_notification", context_low_memory_notification, 0);
1829
2016
  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.3"
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.3
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.3/CHANGELOG
140
+ documentation_uri: https://www.rubydoc.info/gems/mini_racer/0.21.3
141
+ source_code_uri: https://github.com/discourse/mini_racer/tree/v0.21.3
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
  - - ">="