quickjs 0.21.0 → 0.22.0
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 +4 -4
- data/README.md +237 -7
- data/ext/quickjsrb/quickjsrb.c +3113 -556
- data/ext/quickjsrb/quickjsrb.h +401 -6
- data/ext/quickjsrb/quickjsrb_crypto_subtle.c +584 -76
- data/ext/quickjsrb/quickjsrb_file.c +253 -48
- data/lib/quickjs/crypto_key.rb +13 -0
- data/lib/quickjs/importable.rb +80 -0
- data/lib/quickjs/modules.rb +98 -0
- data/lib/quickjs/polyfills.rb +15 -1
- data/lib/quickjs/variables.rb +890 -0
- data/lib/quickjs/version.rb +1 -1
- data/lib/quickjs.rb +3 -0
- data/polyfills/package-lock.json +2 -2
- data/polyfills/package.json +1 -1
- data/sig/quickjs.rbs +20 -3
- metadata +4 -2
- data/ext/quickjsrb/quickjs/repl.c +0 -2057
data/ext/quickjsrb/quickjsrb.h
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
250
|
-
|
|
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)
|