quickjs 0.21.0 → 0.22.0.rc1

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.
@@ -9,6 +9,8 @@
9
9
  #include "quickjs-libc.h"
10
10
  #include "cutils.h"
11
11
 
12
+ #include <pthread.h>
13
+ #include <stdarg.h>
12
14
  #include <stdint.h>
13
15
  #include <stdio.h>
14
16
  #include <string.h>
@@ -46,6 +48,55 @@ typedef struct EvalTime
46
48
  struct timespec started_at;
47
49
  } EvalTime;
48
50
 
51
+ static int64_t eval_elapsed_ms(const EvalTime *eval_time)
52
+ {
53
+ struct timespec now;
54
+ clock_gettime(CLOCK_MONOTONIC, &now);
55
+ return (int64_t)(now.tv_sec - eval_time->started_at.tv_sec) * 1000
56
+ + (now.tv_nsec - eval_time->started_at.tv_nsec) / 1000000;
57
+ }
58
+
59
+ // A rejected promise and its reason, converted when it was rejected.
60
+ // error_handle names the bridged exception the reason was, if any: peeked
61
+ // rather than taken, so it is taken only if this is reported.
62
+ typedef struct RejectionEntry
63
+ {
64
+ JSValue promise;
65
+ VALUE reason;
66
+ int64_t error_handle;
67
+ } RejectionEntry;
68
+
69
+ // Entries in insertion order, promise JS_UNDEFINED once removed, with a
70
+ // pointer-keyed open-addressing index (2 * cap slots: 0 empty, UINT32_MAX
71
+ // removed, else item + 1) so removal stays O(1). Zeroed when empty.
72
+ typedef struct RejectionList
73
+ {
74
+ RejectionEntry *items;
75
+ uint32_t *index;
76
+ uint32_t count;
77
+ uint32_t cap;
78
+ uint32_t live;
79
+ } RejectionList;
80
+
81
+ static void rejection_list_free(JSContext *ctx, RejectionList *list)
82
+ {
83
+ for (uint32_t i = 0; i < list->count; i++)
84
+ JS_FreeValue(ctx, list->items[i].promise);
85
+ js_free(ctx, list->items);
86
+ js_free(ctx, list->index);
87
+ memset(list, 0, sizeof(*list));
88
+ }
89
+
90
+ // Pinned: the list lives in js_malloc'd memory the GC cannot update.
91
+ static void rejection_list_mark(const RejectionList *list)
92
+ {
93
+ if (list == NULL)
94
+ return;
95
+ for (uint32_t i = 0; i < list->count; i++)
96
+ if (!JS_IsUndefined(list->items[i].promise))
97
+ rb_gc_mark(list->items[i].reason);
98
+ }
99
+
49
100
  typedef struct VMData
50
101
  {
51
102
  struct JSContext *context;
@@ -53,8 +104,17 @@ typedef struct VMData
53
104
  struct EvalTime *eval_time;
54
105
  VALUE log_listener;
55
106
  VALUE alive_objects;
107
+ // object_id -> handle, so an object bridged twice keeps one entry. Private:
108
+ // the guest is told the handle, never this key.
109
+ VALUE alive_handles;
56
110
  VALUE module_loader;
57
111
  VALUE on_unhandled_rejection;
112
+ // Rejected promises not yet handled, held until the checkpoint ends.
113
+ RejectionList pending_rejections;
114
+ // The batch being notified; NULL outside notification.
115
+ RejectionList *notifying_rejections;
116
+ // How many may stay pending past an entry that leaves jobs queued.
117
+ uint32_t max_pending_rejections;
58
118
  // Memoize (specifier, importer) → canonical so the user's loader Proc
59
119
  // runs at most once per distinct pair across the VM's lifetime. Without
60
120
  // this, QuickJS calls normalize on every import statement — including
@@ -64,13 +124,42 @@ typedef struct VMData
64
124
  // JS_Eval that follows. Keyed by canonical; populated when the user's
65
125
  // loader returns either a raw source String or `{ code:, as: }`.
66
126
  VALUE module_source_cache;
127
+ // Set (name → Qtrue) of modules read into this context by
128
+ // _preload_module_bytecode. QuickJS calls normalize *before* it looks in
129
+ // ctx->loaded_modules, so without this the normalize hook would ask the
130
+ // user's loader for a module that is already loaded — and raise
131
+ // ReferenceError when the loader doesn't know the name.
132
+ VALUE preloaded_module_names;
67
133
  JSValue j_file_proxy_creator;
134
+ // Captured with it, at init, so slice does not build its result by calling
135
+ // a constructor the guest has had a chance to replace.
136
+ JSValue j_blob_ctor;
68
137
  // Once the runtime has hit JS-level "out of memory", the QuickJS heap is in
69
138
  // a fragile state where further evaluation can trigger a use-after-free in
70
139
  // the parser-error-during-OOM cascade (segfault inside js_shape_hash_unlink).
71
140
  // Trip this flag so subsequent eval_code/call calls refuse cleanly with a
72
141
  // Ruby exception instead of risking a process crash.
73
142
  bool oom_poisoned;
143
+ // Latched when the handle source stops giving out distinct values. Like
144
+ // oom_poisoned, it is reported from the next entry point rather than from
145
+ // wherever it was noticed, because that place is inside a JSCFunction.
146
+ bool handle_source_broken;
147
+ // What was raised at the registrar, when something was. rb_protect cannot
148
+ // tell a stubbed source apart from an Interrupt, a Timeout::Error or any
149
+ // other Thread#raise landing in that window, and discarding one of those
150
+ // loses it entirely. Kept instead, and re-raised from the next entry point,
151
+ // where there is no QuickJS frame to unwind through.
152
+ VALUE r_registrar_error;
153
+ // The class id a Proxy reports in this runtime, learned at init from a Proxy
154
+ // this extension builds, before any guest code can run. Read from the guest's
155
+ // own Proxy instead and a script that reassigns or deletes that global
156
+ // decides what the readers treat as a Proxy.
157
+ JSClassID proxy_class_id;
158
+ // Whether interrupt_handler is currently installed. started_at belongs to
159
+ // whichever evaluation armed it, so without this the elapsed time is read
160
+ // off a stale clock — an unbudgeted polyfill load disarms deliberately and
161
+ // would otherwise look infinitely overdue.
162
+ bool eval_timer_armed;
74
163
  // Set by VM#dispose! to release the multi-MB JS heap before Ruby GC sees
75
164
  // enough pressure to collect the wrapper. Doubles as a double-free guard
76
165
  // for the dfree handler.
@@ -93,6 +182,18 @@ typedef struct VMData
93
182
  // mid-eval. Only mutated while holding the GVL, so plain int accesses
94
183
  // are race-free.
95
184
  int evals_in_flight;
185
+ // The Ruby thread that opened the outermost in-flight JS entry, or Qnil
186
+ // when evals_in_flight is 0. A second thread entering while this is set
187
+ // to someone else is the "one VM, one thread at a time" rule being
188
+ // broken, and QuickJS contexts have no internal locking, so it is refused
189
+ // (ThreadError) rather than left to corrupt the heap.
190
+ //
191
+ // Keyed on the owner rather than the counter because same-thread nesting
192
+ // is legitimate and common: an on_log listener or a define_function proc
193
+ // re-entering the VM elevates evals_in_flight without any concurrency.
194
+ // Comparing the owner lets that through and catches only the real thing.
195
+ // Only mutated while holding the GVL.
196
+ VALUE owner_thread;
96
197
  // Number of GVL-release regions currently open on this VM (a subset of
97
198
  // evals_in_flight). Bridge-registration APIs (define_function,
98
199
  // module_loader=, on_unhandled_rejection) refuse (ThreadError) while
@@ -102,6 +203,17 @@ typedef struct VMData
102
203
  // the GVL re-acquired — would hand the still-released JS a path into
103
204
  // Ruby APIs without the GVL. Only mutated while holding the GVL.
104
205
  int gvl_release_regions;
206
+ // The max_stack_size the caller asked for, kept so rebase_stack_limit can
207
+ // re-apply it on each outermost entry: the budget is re-derived per entry
208
+ // from the running thread's real headroom, and the request is the ceiling
209
+ // it clamps down from. Zero keeps QuickJS's documented "no limit".
210
+ size_t requested_max_stack_size;
211
+ // Whether initialize ran. Quickjs::VM.allocate is public on every Ruby
212
+ // class, and an object allocated but never initialized still reaches
213
+ // vm_free, which used to hand js_std_free_handlers a runtime whose
214
+ // handlers js_std_init_handlers had never set up. That segfaulted at
215
+ // process exit. Teardown consults this instead of assuming.
216
+ bool std_handlers_installed;
105
217
  // Latched by quickjsrb_new_ruby_bridge whenever a C function that calls
106
218
  // into Ruby synchronously (rb_funcall & friends) WITHOUT honoring
107
219
  // gvl_released_js is installed into this context. While true,
@@ -129,11 +241,12 @@ static inline JSValue quickjsrb_new_ruby_bridge(JSContext *ctx, JSCFunction *fun
129
241
  return JS_NewCFunction(ctx, func, name, length);
130
242
  }
131
243
 
132
- static void vm_teardown_context(JSContext *ctx)
244
+ static void vm_teardown_context(JSContext *ctx, bool std_handlers_installed)
133
245
  {
134
246
  JSRuntime *runtime = JS_GetRuntime(ctx);
135
247
  JS_SetInterruptHandler(runtime, NULL, NULL);
136
- js_std_free_handlers(runtime);
248
+ if (std_handlers_installed)
249
+ js_std_free_handlers(runtime);
137
250
  JS_FreeContext(ctx);
138
251
  JS_FreeRuntime(runtime);
139
252
  }
@@ -147,8 +260,12 @@ static void vm_free(void *ptr)
147
260
  {
148
261
  if (!JS_IsUndefined(data->j_file_proxy_creator))
149
262
  JS_FreeValue(data->context, data->j_file_proxy_creator);
263
+ if (!JS_IsUndefined(data->j_blob_ctor))
264
+ JS_FreeValue(data->context, data->j_blob_ctor);
150
265
 
151
- vm_teardown_context(data->context);
266
+ rejection_list_free(data->context, &data->pending_rejections);
267
+
268
+ vm_teardown_context(data->context, data->std_handlers_installed);
152
269
  }
153
270
 
154
271
  xfree(ptr);
@@ -165,10 +282,19 @@ static void vm_mark(void *ptr)
165
282
  rb_gc_mark_movable(data->defined_functions);
166
283
  rb_gc_mark_movable(data->log_listener);
167
284
  rb_gc_mark_movable(data->alive_objects);
285
+ rb_gc_mark_movable(data->alive_handles);
286
+ rb_gc_mark_movable(data->r_registrar_error);
168
287
  rb_gc_mark_movable(data->module_loader);
169
288
  rb_gc_mark_movable(data->on_unhandled_rejection);
170
289
  rb_gc_mark_movable(data->module_resolution_cache);
171
290
  rb_gc_mark_movable(data->module_source_cache);
291
+ rb_gc_mark_movable(data->preloaded_module_names);
292
+ // Pinned rather than movable: it is compared by identity against
293
+ // rb_thread_current(), and it is only ever set for the duration of a JS
294
+ // entry, so there is nothing to gain from letting it move.
295
+ rb_gc_mark(data->owner_thread);
296
+ rejection_list_mark(&data->pending_rejections);
297
+ rejection_list_mark(data->notifying_rejections);
172
298
  }
173
299
 
174
300
  static void vm_compact(void *ptr)
@@ -177,10 +303,13 @@ static void vm_compact(void *ptr)
177
303
  data->defined_functions = rb_gc_location(data->defined_functions);
178
304
  data->log_listener = rb_gc_location(data->log_listener);
179
305
  data->alive_objects = rb_gc_location(data->alive_objects);
306
+ data->alive_handles = rb_gc_location(data->alive_handles);
307
+ data->r_registrar_error = rb_gc_location(data->r_registrar_error);
180
308
  data->module_loader = rb_gc_location(data->module_loader);
181
309
  data->on_unhandled_rejection = rb_gc_location(data->on_unhandled_rejection);
182
310
  data->module_resolution_cache = rb_gc_location(data->module_resolution_cache);
183
311
  data->module_source_cache = rb_gc_location(data->module_source_cache);
312
+ data->preloaded_module_names = rb_gc_location(data->preloaded_module_names);
184
313
  }
185
314
 
186
315
  static const rb_data_type_t vm_type = {
@@ -213,15 +342,26 @@ static VALUE vm_alloc(VALUE r_self)
213
342
  data->defined_functions = rb_hash_new();
214
343
  data->log_listener = Qnil;
215
344
  data->alive_objects = rb_hash_new();
345
+ data->alive_handles = rb_hash_new();
216
346
  data->module_loader = Qnil;
217
347
  data->on_unhandled_rejection = Qnil;
348
+ data->notifying_rejections = NULL;
218
349
  data->module_resolution_cache = rb_hash_new();
219
350
  data->module_source_cache = rb_hash_new();
351
+ data->preloaded_module_names = rb_hash_new();
220
352
  data->j_file_proxy_creator = JS_UNDEFINED;
353
+ data->j_blob_ctor = JS_UNDEFINED;
354
+ data->proxy_class_id = 0;
221
355
  data->oom_poisoned = false;
356
+ data->handle_source_broken = false;
357
+ data->r_registrar_error = Qnil;
358
+ data->eval_timer_armed = false;
222
359
  data->disposed = false;
223
360
  data->gvl_released_js = false;
224
361
  data->evals_in_flight = 0;
362
+ data->owner_thread = Qnil;
363
+ data->requested_max_stack_size = 0;
364
+ data->std_handlers_installed = false;
225
365
  data->gvl_release_regions = 0;
226
366
  data->has_native_ruby_bridge = false;
227
367
 
@@ -240,14 +380,269 @@ static VALUE vm_alloc(VALUE r_self)
240
380
 
241
381
  // Utils
242
382
 
243
- static char *random_string()
383
+ // Derived, not two independent numbers: snprintf truncates silently, so a
384
+ // buffer sized apart from the requested length would quietly hand back a
385
+ // shorter name than asked for the moment anyone raised the entropy.
386
+ #define QUICKJSRB_GENERATED_NAME_LEN 12
387
+ #define QUICKJSRB_GENERATED_NAME_SIZE (QUICKJSRB_GENERATED_NAME_LEN + 1)
388
+
389
+ // The handle a bridged object is published to the guest under. It was the
390
+ // Ruby object_id, which is a small sequential integer, and nothing ever leaves
391
+ // alive_objects — so a script could walk 1..4000 and be handed back objects it
392
+ // had never held: a File to read, a CryptoKey to sign with after every JS
393
+ // reference to it was gone. Drawn from 2^48 instead, which stays exact in a JS
394
+ // double and is not a space to walk. Collisions are re-drawn rather than
395
+ // trusted to be unlikely, since one would hand two subsystems the same entry.
396
+ #define QUICKJSRB_HANDLE_BITS 48
397
+ #define QUICKJSRB_HANDLE_DRAW_LIMIT 16
398
+
399
+ // Reads rb_object_id as an own data property, never through the prototype
400
+ // chain and never through a getter, and answers whether there was one. All
401
+ // three writers define it that way, so nothing legitimate is missed, and an
402
+ // accessor a guest puts on Object.prototype otherwise answers for every object
403
+ // that crosses back: one line of setup and a returned Hash becomes whichever
404
+ // host File or CryptoKey the guest already holds a handle for.
405
+ static inline bool j_read_own_handle(JSContext *ctx, JSValueConst j_val, JSValue *j_handle_out)
406
+ {
407
+ JSAtom atom = JS_NewAtom(ctx, "rb_object_id");
408
+ JSPropertyDescriptor desc;
409
+ int found = JS_GetOwnProperty(ctx, &desc, j_val, atom);
410
+ JS_FreeAtom(ctx, atom);
411
+ if (found < 0)
412
+ {
413
+ // A Proxy trap answered and threw. The callers drain it.
414
+ *j_handle_out = JS_EXCEPTION;
415
+ return false;
416
+ }
417
+ if (found == 0)
418
+ return false;
419
+ if ((desc.flags & JS_PROP_TMASK) != JS_PROP_NORMAL)
420
+ {
421
+ JS_FreeValue(ctx, desc.value);
422
+ JS_FreeValue(ctx, desc.getter);
423
+ JS_FreeValue(ctx, desc.setter);
424
+ return false;
425
+ }
426
+ JS_FreeValue(ctx, desc.getter);
427
+ JS_FreeValue(ctx, desc.setter);
428
+ *j_handle_out = desc.value;
429
+ return true;
430
+ }
431
+
432
+ // Defined in quickjsrb.c. Declared here because the crypto reader needs it to
433
+ // give back an exception a guest's getter parked on its way through.
434
+ VALUE find_ruby_error(JSContext *ctx, JSValue j_error);
435
+ // Also in quickjsrb.c: whether the evaluation has outrun the budget it was
436
+ // armed with. Anything that hands a throw back to the guest has to ask, or a
437
+ // deadline that fired inside a getter becomes catchable.
438
+ bool eval_budget_lapsed_now(JSContext *ctx);
439
+ // Also in quickjsrb.c: takes a throw a reader is about to drop, after giving
440
+ // find_ruby_error the chance to unpark what it carries.
441
+ void quickjsrb_drain_pending(JSContext *ctx);
442
+
443
+ // The source the handle is drawn from, resolved once at Init and pinned rather
444
+ // than looked up per registration: the registrar runs inside QuickJS native
445
+ // callbacks with no rb_protect between them and the interpreter, which is the
446
+ // hazard r_crypto_key_class is written around, and a constant lookup is exactly
447
+ // the raise that hazard is about. The limit is 2^48 - 1 so the draw lands in
448
+ // 1 .. 2^48 - 1, because every reader treats a zero handle as "no entry" and a
449
+ // zero draw would strand the object.
450
+ //
451
+ // Narrowed rather than closed: the draw still calls into Ruby, so a
452
+ // NoMemoryError or an entropy failure raises from the same place. Pre-resolving
453
+ // takes the reachable cause off that path; the rest goes with the table, in
454
+ // #114.
455
+ //
456
+ // Defined in quickjsrb.c rather than here, because a static in a header gives
457
+ // every translation unit its own copy and only the one Init touched would be
458
+ // set.
459
+ extern VALUE quickjsrb_secure_random;
460
+ extern VALUE quickjsrb_handle_limit;
461
+ void quickjsrb_init_handle_source(void);
462
+
463
+ // Both rows go together: alive_handles is only a way back to the handle, and a
464
+ // handle left in alive_objects is what anchors the object for the life of the
465
+ // VM.
466
+ // Takes the handle rather than deriving it, so nothing here dispatches: these
467
+ // run on unwind paths inside JSCFunctions, where a raise is #134, and the
468
+ // registration side is under rb_protect for exactly that reason. rb_hash_delete
469
+ // on an existing key allocates nothing.
470
+ static inline void alive_objects_unregister(VMData *data, VALUE r_handle, VALUE r_object_id)
471
+ {
472
+ rb_hash_delete(data->alive_objects, r_handle);
473
+ rb_hash_delete(data->alive_handles, r_object_id);
474
+ }
475
+
476
+ // Same, for a caller that has the object but not the handle it was given. A
477
+ // miss is not an error: the object may never have been registered.
478
+ static inline void alive_objects_forget(VMData *data, VALUE r_object)
479
+ {
480
+ VALUE r_object_id = rb_obj_id(r_object);
481
+ VALUE r_handle = rb_hash_lookup2(data->alive_handles, r_object_id, Qnil);
482
+ if (!NIL_P(r_handle))
483
+ alive_objects_unregister(data, r_handle, r_object_id);
484
+ }
485
+
486
+ struct alive_register_work
487
+ {
488
+ VMData *data;
489
+ VALUE r_object;
490
+ bool created;
491
+ };
492
+
493
+ static VALUE alive_objects_register_body(VALUE r_work)
494
+ {
495
+ struct alive_register_work *work = (struct alive_register_work *)r_work;
496
+ VMData *data = work->data;
497
+ VALUE r_object = work->r_object;
498
+ bool *created = &work->created;
499
+
500
+ // One entry per object, not per crossing. The old handle was the object_id,
501
+ // so re-bridging the same File or exception overwrote its own row; drawing a
502
+ // fresh handle every time would instead add one, and nothing is ever removed,
503
+ // so `for (i = 0; i < 200000; i++) f()` would grow the table by 200000.
504
+ // rb_obj_id, not the method: a subclass can override object_id, and the two
505
+ // sides of this table have to agree on the key find_ruby_error will use.
506
+ VALUE r_object_id = rb_obj_id(r_object);
507
+ VALUE r_known = rb_hash_lookup2(data->alive_handles, r_object_id, Qnil);
508
+ // Reused only when the row still holds this object. Asking merely whether
509
+ // the handle is occupied would hand back one that a later draw had given to
510
+ // something else, since an object_id is only unique among live objects.
511
+ if (!NIL_P(r_known) && rb_hash_lookup2(data->alive_objects, r_known, Qnil) == r_object)
512
+ {
513
+ if (created != NULL)
514
+ *created = false;
515
+ return r_known;
516
+ }
517
+
518
+ // Bounded, because nothing can interrupt a C loop: no QuickJS interrupt is
519
+ // polled inside it, so timeout_msec cannot end it and only an outer
520
+ // Timeout.timeout would, by longjmping out of here through the JSCFunction
521
+ // frames this file warns about elsewhere. A host whose suite stubs the
522
+ // source flat (`SecureRandom.random_number` returning a constant is an
523
+ // ordinary test-double line) would otherwise spin forever on the second
524
+ // object it bridges. Sixteen draws against a 2^48 range only run out when
525
+ // the source is not random, so say that rather than degrade to a sequence a
526
+ // guest could walk.
527
+ //
528
+ // Refusing rather than raising also settles what the old note here worried
529
+ // about: there is no longjmp out of js_crypto_key_to_js past a promise
530
+ // capability it never allocated, because there is no longjmp at all.
531
+ VALUE r_handle;
532
+ int attempts = 0;
533
+ do
534
+ {
535
+ if (++attempts > QUICKJSRB_HANDLE_DRAW_LIMIT)
536
+ {
537
+ // Latched and handed back as a refusal, never raised from here. This runs
538
+ // inside a JSCFunction, and a longjmp out of one leaves the runtime's
539
+ // frame chain pointing at C stack that is gone: the next Error the guest
540
+ // builds walks it in build_backtrace and takes the process with it. The
541
+ // next entry point reports this instead, the way an out-of-memory is.
542
+ data->handle_source_broken = true;
543
+ if (created != NULL)
544
+ *created = false;
545
+ return Qnil;
546
+ }
547
+ r_handle = rb_funcall(quickjsrb_secure_random, rb_intern("random_number"), 1, quickjsrb_handle_limit);
548
+
549
+ // The draw is checked into an int64_t before anything uses it as a number.
550
+ // The bound above only catches a source that keeps returning the same
551
+ // value, and everything else a source can return raises here, inside the
552
+ // loop, before the bound is ever consulted: NUM2LL raises RangeError on a
553
+ // Bignum, dispatching + raises NoMethodError on nil, and a raise out of a
554
+ // JSCFunction is #134. rb_integer_pack answers the sign and reports a
555
+ // value too wide to fit rather than raising about it, and it does not
556
+ // dispatch, which a source could also answer for.
557
+ //
558
+ // Out of range is refused rather than clamped: a negative draw is stored
559
+ // under a key every reader treats as "no entry", so the object is anchored
560
+ // for the life of the VM and the exception it was bridging comes back as a
561
+ // generic Quickjs::RuntimeError instead of itself.
562
+ int64_t drawn = 0;
563
+ bool usable = RB_INTEGER_TYPE_P(r_handle);
564
+ if (usable)
565
+ {
566
+ // Asked only of an Integer: rb_integer_pack raises on anything else,
567
+ // which is the raise this check exists to avoid.
568
+ int sign = rb_integer_pack(r_handle, &drawn, 1, sizeof(int64_t), 0,
569
+ INTEGER_PACK_NATIVE_BYTE_ORDER | INTEGER_PACK_2COMP);
570
+ usable = sign >= 0 && sign <= 1 && drawn >= 0 && drawn < ((int64_t)1 << QUICKJSRB_HANDLE_BITS);
571
+ }
572
+ if (!usable)
573
+ {
574
+ data->handle_source_broken = true;
575
+ if (created != NULL)
576
+ *created = false;
577
+ return Qnil;
578
+ }
579
+ r_handle = LL2NUM(drawn + 1);
580
+ } while (!NIL_P(rb_hash_lookup2(data->alive_objects, r_handle, Qnil)));
581
+
582
+ rb_hash_aset(data->alive_objects, r_handle, r_object);
583
+ rb_hash_aset(data->alive_handles, r_object_id, r_handle);
584
+ if (created != NULL)
585
+ *created = true;
586
+ return r_handle;
587
+ }
588
+
589
+ // Answers the handle, and says through `created` whether this call is the one
590
+ // that made the row: a caller that has to undo the registration must not undo
591
+ // a row an earlier crossing of the same object is still relying on. Answers
592
+ // Qnil, having latched handle_source_broken, when it cannot draw one.
593
+ //
594
+ // Protected, because the work it wraps runs inside a JSCFunction and a raise out
595
+ // of one leaves QuickJS's frame chain pointing at C stack that is gone: the
596
+ // next Error the guest builds walks it and takes the process with it (#134).
597
+ // The draw is a Ruby dispatch this branch introduced, so it is this branch's
598
+ // job not to open that door: a strict test double raises on an unexpected
599
+ // invocation as readily as a loose one returns a constant, and rb_hash_aset can
600
+ // raise NoMemoryError on the same path. A raise is treated as an unusable draw,
601
+ // which is what it is.
602
+ static inline VALUE alive_objects_register(VMData *data, VALUE r_object, bool *created)
603
+ {
604
+ struct alive_register_work work = {data, r_object, false};
605
+ int state = 0;
606
+ VALUE r_handle = rb_protect(alive_objects_register_body, (VALUE)&work, &state);
607
+ if (state)
608
+ {
609
+ // Kept, not discarded. rb_protect catches everything, and most of what it
610
+ // can catch here is not a broken handle source: an Interrupt from Ctrl-C, a
611
+ // Timeout::Error from the host's own Timeout.timeout, anything a
612
+ // Thread#raise lands in this window. Swallowing one of those loses it with
613
+ // no trace and then blames a test double that does not exist. It is
614
+ // re-raised from the next entry point instead, with its own class and
615
+ // message, because here there is a QuickJS frame to unwind through and
616
+ // that is #134.
617
+ //
618
+ // Not latched, either: a Timeout::Error that arrived once says nothing
619
+ // about the handle source, and condemning the VM for it would be the wrong
620
+ // diagnosis twice over. The latch is for a source that answers badly, which
621
+ // is decided inside the body and does not come through here.
622
+ data->r_registrar_error = rb_errinfo();
623
+ rb_set_errinfo(Qnil);
624
+ if (created != NULL)
625
+ *created = false;
626
+ return Qnil;
627
+ }
628
+ if (created != NULL)
629
+ *created = work.created;
630
+ return r_handle;
631
+ }
632
+
633
+ static void random_filename(char buf[QUICKJSRB_GENERATED_NAME_SIZE])
244
634
  {
245
635
  VALUE r_rand = rb_funcall(
246
636
  rb_const_get(rb_cClass, rb_intern("SecureRandom")),
247
637
  rb_intern("alphanumeric"),
248
638
  1,
249
- INT2NUM(12));
250
- return StringValueCStr(r_rand);
639
+ INT2NUM(QUICKJSRB_GENERATED_NAME_LEN));
640
+ // Copy the bytes out rather than handing back StringValueCStr(r_rand):
641
+ // nothing else references the generated String, so a GC anywhere in the
642
+ // caller's remaining work frees it and leaves the caller formatting bytes
643
+ // from a recycled slot.
644
+ snprintf(buf, QUICKJSRB_GENERATED_NAME_SIZE, "%s", StringValueCStr(r_rand));
645
+ RB_GC_GUARD(r_rand);
251
646
  }
252
647
 
253
648
  static bool is_native_error_name(const char *error_name)