quickjs 0.21.0.rc1 → 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.
@@ -6,10 +6,29 @@
6
6
  static const char *js_get_algorithm_name(JSContext *ctx, JSValueConst j_algo)
7
7
  {
8
8
  if (JS_IsString(j_algo))
9
- return JS_ToCString(ctx, j_algo);
9
+ {
10
+ // Drained like the reads below: a conversion that cannot allocate answers
11
+ // NULL with its throw still set, and the caller reports a missing
12
+ // algorithm name over the top of it.
13
+ const char *name = JS_ToCString(ctx, j_algo);
14
+ if (name == NULL)
15
+ quickjsrb_drain_pending(ctx);
16
+ return name;
17
+ }
18
+ // A getter answers this read, so it can throw, and a throw from a bridge
19
+ // carries a host exception that only find_ruby_error takes back out of
20
+ // alive_objects. Same for the conversion below, which runs toString.
10
21
  JSValue j_name = JS_GetPropertyStr(ctx, j_algo, "name");
22
+ if (JS_IsException(j_name))
23
+ {
24
+ quickjsrb_drain_pending(ctx);
25
+ return NULL;
26
+ }
27
+
11
28
  const char *name = JS_ToCString(ctx, j_name);
12
29
  JS_FreeValue(ctx, j_name);
30
+ if (name == NULL)
31
+ quickjsrb_drain_pending(ctx);
13
32
  return name;
14
33
  }
15
34
 
@@ -42,49 +61,295 @@ static VALUE js_buffer_to_ruby_str(JSContext *ctx, JSValueConst j_val)
42
61
  }
43
62
 
44
63
  // Build a Ruby Array of strings from a JS array value.
64
+ #define QUICKJSRB_MAX_KEY_USAGES 1024
65
+
66
+ // Qnil when the list is longer than any usages list can be, which the
67
+ // callers turn into a TypeError before they create a promise to reject.
45
68
  static VALUE js_usages_to_ruby_array(JSContext *ctx, JSValueConst j_usages)
46
69
  {
70
+ // Every read here is a property of an object the guest handed in, answerable
71
+ // by a getter that can reach a bridge, so each drains what it throws for the
72
+ // reason js_algo_read gives. What the operation then does with a usages list
73
+ // that could not be read is the refusal path nothing has yet: #130.
47
74
  VALUE r_usages = rb_ary_new();
48
75
  JSValue j_len = JS_GetPropertyStr(ctx, j_usages, "length");
76
+ if (JS_IsException(j_len))
77
+ {
78
+ quickjsrb_drain_pending(ctx);
79
+ return r_usages;
80
+ }
81
+
49
82
  int32_t count = 0;
50
- JS_ToInt32(ctx, &count, j_len);
83
+ if (JS_ToInt32(ctx, &count, j_len) < 0)
84
+ {
85
+ quickjsrb_drain_pending(ctx);
86
+ count = 0;
87
+ }
51
88
  JS_FreeValue(ctx, j_len);
89
+
90
+ // The guest writes this length, and the loop below does a property read and
91
+ // a Ruby allocation per step without polling a QuickJS interrupt, so
92
+ // timeout_msec cannot end it: measured, a length of 10,000,000 runs 1.9s
93
+ // past a 100ms budget and leaves the VM out of memory, which a guest gets to
94
+ // choose by passing a number. There are eight key usages in the spec, so
95
+ // anything near this bound is not a usages list; refused rather than
96
+ // truncated, because a truncated one would be accepted as if the guest had
97
+ // asked for what is left.
98
+ // A negative length is an empty sequence, not too many entries: WebIDL's
99
+ // ToLength clamps it to 0, and main answered [] for it. JS_ToInt32 wraps, so
100
+ // 2**31 arrives here negative and means the same thing.
101
+ if (count < 0)
102
+ count = 0;
103
+ if (count > QUICKJSRB_MAX_KEY_USAGES)
104
+ return Qnil;
52
105
  for (int32_t i = 0; i < count; i++)
53
106
  {
54
107
  JSValue j_u = JS_GetPropertyUint32(ctx, j_usages, (uint32_t)i);
108
+ if (JS_IsException(j_u))
109
+ {
110
+ quickjsrb_drain_pending(ctx);
111
+ continue;
112
+ }
113
+
55
114
  const char *u_str = JS_ToCString(ctx, j_u);
56
115
  if (u_str)
57
116
  rb_ary_push(r_usages, rb_str_new_cstr(u_str));
117
+ else
118
+ quickjsrb_drain_pending(ctx);
58
119
  JS_FreeCString(ctx, u_str);
59
120
  JS_FreeValue(ctx, j_u);
60
121
  }
61
122
  return r_usages;
62
123
  }
63
124
 
125
+ // Settles with the value, or rejects with whatever stopped it being built.
126
+ // Never resolves with the sentinel: typeof reads "unknown" for it and the guest
127
+ // has no way to name, catch or discard what it was handed. Always settles, so
128
+ // no caller is left returning a promise nothing will ever resolve.
129
+ static void js_settle_or_reject(JSContext *ctx, JSValueConst *resolving_funcs, JSValue j_key)
130
+ {
131
+ JSValue j_settled;
132
+ if (JS_IsException(j_key))
133
+ {
134
+ // JS_GetException answers JS_UNINITIALIZED when nothing is set, and that
135
+ // is an internal sentinel, not a value to hand a guest. The slot can be
136
+ // empty here: a describe failure sets its throw, and a later step that
137
+ // takes a throw of its own unconditionally clears it on the way past.
138
+ if (!JS_HasException(ctx))
139
+ JS_ThrowInternalError(ctx, "quickjs: the operation failed without saying why");
140
+ JSValue j_thrown = JS_GetException(ctx);
141
+ j_settled = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1, (JSValueConst *)&j_thrown);
142
+ JS_FreeValue(ctx, j_thrown);
143
+ }
144
+ else
145
+ {
146
+ j_settled = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, (JSValueConst *)&j_key);
147
+ JS_FreeValue(ctx, j_key);
148
+ }
149
+ JS_FreeValue(ctx, j_settled);
150
+ }
151
+
152
+ // A rejected promise carrying a TypeError. These entry points are specified to
153
+ // always return a promise, so a refusal decided before the operation starts
154
+ // still has to arrive as a rejection rather than as a throw at the call: a
155
+ // guest writing importKey(...).catch(h) would otherwise never reach h.
156
+ static JSValue js_rejected_type_error(JSContext *ctx, const char *message)
157
+ {
158
+ JSValue resolving_funcs[2];
159
+ JSValue promise = JS_NewPromiseCapability(ctx, resolving_funcs);
160
+ if (JS_IsException(promise))
161
+ return JS_EXCEPTION;
162
+
163
+ JS_ThrowTypeError(ctx, "%s", message);
164
+ JSValue j_thrown = JS_GetException(ctx);
165
+ JSValue j_settled = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1, (JSValueConst *)&j_thrown);
166
+ JS_FreeValue(ctx, j_thrown);
167
+ JS_FreeValue(ctx, j_settled);
168
+ JS_FreeValue(ctx, resolving_funcs[0]);
169
+ JS_FreeValue(ctx, resolving_funcs[1]);
170
+ return promise;
171
+ }
172
+
64
173
  // Reject a promise with a plain JS Error built from Ruby exception info.
65
174
  static void js_reject_with_ruby_error(JSContext *ctx, JSValueConst *resolving_funcs)
66
175
  {
67
176
  VALUE r_error = rb_errinfo();
68
177
  rb_set_errinfo(Qnil);
178
+ // Read and converted before anything is allocated, which is the ordering
179
+ // j_error_from_ruby_error follows: #message is host code and can raise
180
+ // anything, StringValueCStr raises on a non-String and on an embedded NUL,
181
+ // and a raise once j_err exists would longjmp out of a JSCFunction past it
182
+ // and past the promise capability the caller is holding.
69
183
  VALUE r_message = rb_funcall(r_error, rb_intern("message"), 0);
184
+ StringValueCStr(r_message);
185
+
70
186
  JSValue j_err = JS_NewError(ctx);
71
- JS_SetPropertyStr(ctx, j_err, "message", JS_NewString(ctx, StringValueCStr(r_message)));
187
+ if (JS_IsException(j_err))
188
+ {
189
+ // Settling with the sentinel would hand the guest a value it cannot name,
190
+ // which is what js_quickjsrb_call_global refuses to do for the same reason.
191
+ // The runtime's own throw is the honest answer, and it is already pending.
192
+ // Nothing left to describe the failure with, so the promise is rejected
193
+ // with the runtime's own throw rather than left for a guest that is still
194
+ // awaiting it, and rather than carried out of here to be attributed to
195
+ // whatever asks for an exception next.
196
+ JS_FreeValue(ctx, j_err);
197
+ js_settle_or_reject(ctx, resolving_funcs, JS_EXCEPTION);
198
+ return;
199
+ }
200
+ // Defined, not assigned, for the reason j_error_from_ruby_error gives: an
201
+ // accessor on Error.prototype absorbs the write, and the guest reads whatever
202
+ // that getter says instead of why its call was rejected. Built first and
203
+ // checked, so the sentinel is never what the error says its message is.
204
+ JSValue j_message = JS_NewString(ctx, StringValueCStr(r_message));
205
+ if (JS_IsException(j_message))
206
+ {
207
+ JS_FreeValue(ctx, j_message);
208
+ JS_FreeValue(ctx, j_err);
209
+ js_settle_or_reject(ctx, resolving_funcs, JS_EXCEPTION);
210
+ return;
211
+ }
212
+ if (JS_DefinePropertyValueStr(ctx, j_err, "message", j_message,
213
+ JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE) < 0)
214
+ {
215
+ // An Error with no own message is one Error.prototype answers for, which
216
+ // is what defining it was for, and the define's throw would be left to
217
+ // land on an unrelated evaluation.
218
+ JS_FreeValue(ctx, j_err);
219
+ js_settle_or_reject(ctx, resolving_funcs, JS_EXCEPTION);
220
+ return;
221
+ }
72
222
  JSValue ret = JS_Call(ctx, resolving_funcs[1], JS_UNDEFINED, 1, (JSValueConst *)&j_err);
73
223
  JS_FreeValue(ctx, j_err);
74
224
  JS_FreeValue(ctx, ret);
75
225
  }
76
226
 
227
+ // Quickjs::CryptoKey, or Qnil while the Ruby layer that defines it has not been
228
+ // loaded — requiring the extension alone is enough to reach the callers below.
229
+ // Resolved without rb_const_get's raise, because these run inside QuickJS
230
+ // native callbacks with no rb_protect between them and the interpreter: a
231
+ // NameError there longjmps through QuickJS's own frames, past the JS values
232
+ // they still hold.
233
+ //
234
+ // Looked up per call rather than cached in a static. A class held only by a
235
+ // constant table is movable, so a cached VALUE survives GC.compact as an
236
+ // address the collector has since handed to something else — which would name
237
+ // the wrong class, or a non-class that makes rb_obj_is_kind_of raise the very
238
+ // unprotected raise this function exists to avoid. Two constant lookups are
239
+ // nothing beside the OpenSSL work every caller goes on to do.
240
+ static VALUE r_crypto_key_class(void)
241
+ {
242
+ // Gated too, though Init_quickjsrb defines it before any of this can run.
243
+ // Not for the promise, which every caller creates after resolving the key,
244
+ // but because a raise out of a JSCFunction unwinds past QuickJS's own call
245
+ // frame, which is what this function exists to answer instead of.
246
+ if (!rb_const_defined_at(rb_cObject, rb_intern("Quickjs")))
247
+ return Qnil;
248
+ // Defined answers true for an autoload here too, and rb_const_get would run
249
+ // its body from this callback, which is the raise being avoided.
250
+ if (!NIL_P(rb_autoload_p(rb_cObject, rb_intern("Quickjs"))))
251
+ return Qnil;
252
+
253
+ VALUE r_quickjs = rb_const_get(rb_cObject, rb_intern("Quickjs"));
254
+ // Same reason the inner one is type-checked: rb_const_defined_at reads a
255
+ // constant table off its receiver, so a non-module bound to the name is
256
+ // worse than the NameError being avoided.
257
+ if (!RB_TYPE_P(r_quickjs, T_MODULE))
258
+ return Qnil;
259
+ if (!rb_const_defined_at(r_quickjs, rb_intern("CryptoKey")))
260
+ return Qnil;
261
+ // Defined answers true for a registered autoload too, and rb_const_get would
262
+ // then run the autoload body here, which is a raise this function cannot
263
+ // afford for the reason above. Nobody autoloads this constant, but the claim
264
+ // has to hold rather than happen to.
265
+ if (!NIL_P(rb_autoload_p(r_quickjs, rb_intern("CryptoKey"))))
266
+ return Qnil;
267
+
268
+ VALUE r_class = rb_const_get(r_quickjs, rb_intern("CryptoKey"));
269
+ // Defined is not the same as a class. rb_obj_is_kind_of takes a class or a
270
+ // module and raises TypeError on anything else, so a String bound to the
271
+ // name would make the unprotected raise above; a Module would not raise but
272
+ // is not what a key is an instance of either. One predicate covers both.
273
+ return RB_TYPE_P(r_class, T_CLASS) ? r_class : Qnil;
274
+ }
275
+
77
276
  // Find Ruby CryptoKey from a JS CryptoKey object via rb_object_id handle.
78
277
  static VALUE r_find_alive_crypto_key(JSContext *ctx, JSValueConst j_key)
79
278
  {
80
- JSValue j_handle = JS_GetPropertyStr(ctx, j_key, "rb_object_id");
279
+ // Both of the reads below can run the guest's own code on a value it chose:
280
+ // a getter for the property, a valueOf for the conversion, either of which
281
+ // can reach a Ruby bridge. Nothing here can report what that raises, since
282
+ // this runs after its caller's rb_protect has returned, but the exception it
283
+ // parked in alive_objects has to come back out: find_ruby_error is the only
284
+ // thing that takes one out, so leaving it would pin one per call, at a rate
285
+ // the guest picks.
286
+ // Own data only, like the other two readers. Through the prototype chain an
287
+ // accessor on Object.prototype answers this, and then `{}` signs with a key
288
+ // it never carried a handle for, including one generated extractable: false.
289
+ JSValue j_handle = JS_UNDEFINED;
290
+ if (!j_read_own_handle(ctx, j_key, &j_handle))
291
+ {
292
+ // A Proxy trap can answer the descriptor read and throw, and what it throws
293
+ // can be a bridge that parked a host exception on its way out.
294
+ if (JS_IsException(j_handle))
295
+ quickjsrb_drain_pending(ctx);
296
+ return Qnil;
297
+ }
298
+
299
+ // Only a number is a handle. Asking JS_ToInt64 for one is what runs valueOf,
300
+ // and the answer for anything else was never going to be a handle anyway.
301
+ int32_t tag = JS_VALUE_GET_NORM_TAG(j_handle);
302
+ if (tag != JS_TAG_INT && tag != JS_TAG_FLOAT64)
303
+ {
304
+ JS_FreeValue(ctx, j_handle);
305
+ return Qnil;
306
+ }
307
+
81
308
  int64_t handle = 0;
82
309
  JS_ToInt64(ctx, &handle, j_handle);
83
310
  JS_FreeValue(ctx, j_handle);
84
311
  if (handle <= 0)
85
312
  return Qnil;
86
313
  VMData *data = JS_GetContextOpaque(ctx);
87
- return rb_hash_aref(data->alive_objects, LONG2NUM(handle));
314
+ VALUE r_key = rb_hash_aref(data->alive_objects, LL2NUM(handle));
315
+ // Before the class lookup, which is otherwise paid on the path a guest can
316
+ // repeat for free: sign('HMAC', {}, buf) reaches no OpenSSL work to dwarf it.
317
+ if (NIL_P(r_key))
318
+ return Qnil;
319
+ // The handle is read off whatever the guest passed as the key, and
320
+ // alive_objects also holds bridged exceptions and the Ruby object behind a
321
+ // File proxy. Without this an object carrying another entry's id reaches the
322
+ // operations below as if it were a key, and the caller is told about a
323
+ // method missing on a File rather than about an invalid CryptoKey.
324
+ VALUE r_key_class = r_crypto_key_class();
325
+ if (NIL_P(r_key_class) || !rb_obj_is_kind_of(r_key, r_key_class))
326
+ return Qnil;
327
+ return r_key;
328
+ }
329
+
330
+ // Every read below is a property of an object the guest handed in, so a getter
331
+ // answers it and may throw. The block that follows each read already skips an
332
+ // exception, but skipping is not enough: a throw from a bridge carries a host
333
+ // exception that find_ruby_error is the only thing to take back out of
334
+ // alive_objects, so a skipped one pins it for the life of the VM, once per
335
+ // read, at a rate the guest picks.
336
+ //
337
+ // The read only. What each block then does with the value, JS_ToInt32 on a
338
+ // length, JS_ToCString on a curve name, a buffer conversion on an iv, runs the
339
+ // guest's valueOf or toString and can throw for the same reason, and those
340
+ // returns are unchecked here as they were before. #130.
341
+ static JSValue js_algo_read(JSContext *ctx, JSValueConst j_algo, const char *name)
342
+ {
343
+ JSValue j_value = JS_GetPropertyStr(ctx, j_algo, name);
344
+ if (JS_IsException(j_value))
345
+ // Taken, not put back. Putting it back and carrying on would leave it
346
+ // pending for the next thing that asks, and carrying on is what this does:
347
+ // the block below skips the property and the operation runs without it. So
348
+ // the guest's own error is lost here, replaced by whatever the call fails
349
+ // with next. Both halves want the same fix, a way for this function to
350
+ // refuse, which its ten callers do not have yet. #130.
351
+ quickjsrb_drain_pending(ctx);
352
+ return j_value;
88
353
  }
89
354
 
90
355
  // Build a comprehensive Ruby Hash (symbol keys) from a JS algorithm object.
@@ -103,7 +368,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
103
368
  if (JS_IsString(j_algo))
104
369
  return r_hash;
105
370
 
106
- JSValue j_length = JS_GetPropertyStr(ctx, j_algo, "length");
371
+ JSValue j_length = js_algo_read(ctx, j_algo, "length");
107
372
  if (!JS_IsUndefined(j_length) && !JS_IsException(j_length))
108
373
  {
109
374
  int32_t length = 0;
@@ -112,7 +377,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
112
377
  }
113
378
  JS_FreeValue(ctx, j_length);
114
379
 
115
- JSValue j_named_curve = JS_GetPropertyStr(ctx, j_algo, "namedCurve");
380
+ JSValue j_named_curve = js_algo_read(ctx, j_algo, "namedCurve");
116
381
  if (!JS_IsUndefined(j_named_curve) && !JS_IsException(j_named_curve))
117
382
  {
118
383
  const char *nc = JS_ToCString(ctx, j_named_curve);
@@ -124,7 +389,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
124
389
  }
125
390
  JS_FreeValue(ctx, j_named_curve);
126
391
 
127
- JSValue j_hash = JS_GetPropertyStr(ctx, j_algo, "hash");
392
+ JSValue j_hash = js_algo_read(ctx, j_algo, "hash");
128
393
  if (!JS_IsUndefined(j_hash) && !JS_IsException(j_hash))
129
394
  {
130
395
  const char *hash_name = js_get_algorithm_name(ctx, j_hash);
@@ -136,7 +401,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
136
401
  }
137
402
  JS_FreeValue(ctx, j_hash);
138
403
 
139
- JSValue j_modulus_length = JS_GetPropertyStr(ctx, j_algo, "modulusLength");
404
+ JSValue j_modulus_length = js_algo_read(ctx, j_algo, "modulusLength");
140
405
  if (!JS_IsUndefined(j_modulus_length) && !JS_IsException(j_modulus_length))
141
406
  {
142
407
  int32_t mod_len = 0;
@@ -145,7 +410,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
145
410
  }
146
411
  JS_FreeValue(ctx, j_modulus_length);
147
412
 
148
- JSValue j_pub_exp = JS_GetPropertyStr(ctx, j_algo, "publicExponent");
413
+ JSValue j_pub_exp = js_algo_read(ctx, j_algo, "publicExponent");
149
414
  if (!JS_IsUndefined(j_pub_exp) && !JS_IsException(j_pub_exp))
150
415
  {
151
416
  VALUE r_pe = js_buffer_to_ruby_str(ctx, j_pub_exp);
@@ -154,7 +419,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
154
419
  }
155
420
  JS_FreeValue(ctx, j_pub_exp);
156
421
 
157
- JSValue j_iv = JS_GetPropertyStr(ctx, j_algo, "iv");
422
+ JSValue j_iv = js_algo_read(ctx, j_algo, "iv");
158
423
  if (!JS_IsUndefined(j_iv) && !JS_IsException(j_iv))
159
424
  {
160
425
  VALUE r_iv = js_buffer_to_ruby_str(ctx, j_iv);
@@ -163,7 +428,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
163
428
  }
164
429
  JS_FreeValue(ctx, j_iv);
165
430
 
166
- JSValue j_tag_length = JS_GetPropertyStr(ctx, j_algo, "tagLength");
431
+ JSValue j_tag_length = js_algo_read(ctx, j_algo, "tagLength");
167
432
  if (!JS_IsUndefined(j_tag_length) && !JS_IsException(j_tag_length))
168
433
  {
169
434
  int32_t tag_length = 0;
@@ -172,7 +437,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
172
437
  }
173
438
  JS_FreeValue(ctx, j_tag_length);
174
439
 
175
- JSValue j_additional_data = JS_GetPropertyStr(ctx, j_algo, "additionalData");
440
+ JSValue j_additional_data = js_algo_read(ctx, j_algo, "additionalData");
176
441
  if (!JS_IsUndefined(j_additional_data) && !JS_IsException(j_additional_data))
177
442
  {
178
443
  VALUE r_ad = js_buffer_to_ruby_str(ctx, j_additional_data);
@@ -181,7 +446,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
181
446
  }
182
447
  JS_FreeValue(ctx, j_additional_data);
183
448
 
184
- JSValue j_counter = JS_GetPropertyStr(ctx, j_algo, "counter");
449
+ JSValue j_counter = js_algo_read(ctx, j_algo, "counter");
185
450
  if (!JS_IsUndefined(j_counter) && !JS_IsException(j_counter))
186
451
  {
187
452
  VALUE r_counter = js_buffer_to_ruby_str(ctx, j_counter);
@@ -190,7 +455,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
190
455
  }
191
456
  JS_FreeValue(ctx, j_counter);
192
457
 
193
- JSValue j_salt = JS_GetPropertyStr(ctx, j_algo, "salt");
458
+ JSValue j_salt = js_algo_read(ctx, j_algo, "salt");
194
459
  if (!JS_IsUndefined(j_salt) && !JS_IsException(j_salt))
195
460
  {
196
461
  VALUE r_salt = js_buffer_to_ruby_str(ctx, j_salt);
@@ -199,7 +464,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
199
464
  }
200
465
  JS_FreeValue(ctx, j_salt);
201
466
 
202
- JSValue j_iterations = JS_GetPropertyStr(ctx, j_algo, "iterations");
467
+ JSValue j_iterations = js_algo_read(ctx, j_algo, "iterations");
203
468
  if (!JS_IsUndefined(j_iterations) && !JS_IsException(j_iterations))
204
469
  {
205
470
  int32_t iterations = 0;
@@ -208,7 +473,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
208
473
  }
209
474
  JS_FreeValue(ctx, j_iterations);
210
475
 
211
- JSValue j_info = JS_GetPropertyStr(ctx, j_algo, "info");
476
+ JSValue j_info = js_algo_read(ctx, j_algo, "info");
212
477
  if (!JS_IsUndefined(j_info) && !JS_IsException(j_info))
213
478
  {
214
479
  VALUE r_info = js_buffer_to_ruby_str(ctx, j_info);
@@ -217,7 +482,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
217
482
  }
218
483
  JS_FreeValue(ctx, j_info);
219
484
 
220
- JSValue j_salt_length = JS_GetPropertyStr(ctx, j_algo, "saltLength");
485
+ JSValue j_salt_length = js_algo_read(ctx, j_algo, "saltLength");
221
486
  if (!JS_IsUndefined(j_salt_length) && !JS_IsException(j_salt_length))
222
487
  {
223
488
  int32_t salt_length = 0;
@@ -226,7 +491,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
226
491
  }
227
492
  JS_FreeValue(ctx, j_salt_length);
228
493
 
229
- JSValue j_label = JS_GetPropertyStr(ctx, j_algo, "label");
494
+ JSValue j_label = js_algo_read(ctx, j_algo, "label");
230
495
  if (!JS_IsUndefined(j_label) && !JS_IsException(j_label))
231
496
  {
232
497
  VALUE r_label = js_buffer_to_ruby_str(ctx, j_label);
@@ -235,7 +500,7 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
235
500
  }
236
501
  JS_FreeValue(ctx, j_label);
237
502
 
238
- JSValue j_public = JS_GetPropertyStr(ctx, j_algo, "public");
503
+ JSValue j_public = js_algo_read(ctx, j_algo, "public");
239
504
  if (!JS_IsUndefined(j_public) && !JS_IsException(j_public) && JS_IsObject(j_public))
240
505
  {
241
506
  VALUE r_public = r_find_alive_crypto_key(ctx, j_public);
@@ -247,77 +512,297 @@ static VALUE js_algo_to_ruby_hash(JSContext *ctx, JSValueConst j_algo)
247
512
  return r_hash;
248
513
  }
249
514
 
250
- // Build a JS CryptoKey plain object from a Ruby Quickjs::CryptoKey.
251
- // Stores the Ruby object in alive_objects; sets rb_object_id as non-enumerable.
252
- static JSValue js_crypto_key_to_js(JSContext *ctx, VALUE r_key)
515
+
516
+ // Builds the string and defines it, reporting whether it got that far, so a
517
+ // caller can throw away a half-described object rather than define the
518
+ // allocation-failure sentinel as an ordinary property value. Defined, not
519
+ // assigned, for the reason js_crypto_key_to_js gives.
520
+ static bool j_define_str(JSContext *ctx, JSValue j_obj, const char *prop, VALUE r_str)
521
+ {
522
+ JSValue j_val = JS_NewString(ctx, StringValueCStr(r_str));
523
+ if (JS_IsException(j_val))
524
+ {
525
+ JS_FreeValue(ctx, j_val);
526
+ return false;
527
+ }
528
+ return JS_DefinePropertyValueStr(ctx, j_obj, prop, j_val, JS_PROP_C_W_E) >= 0;
529
+ }
530
+
531
+ // Build a JS CryptoKey plain object from a Ruby Quickjs::CryptoKey. Stores the
532
+ // Ruby object in alive_objects; sets rb_object_id as non-enumerable.
533
+ //
534
+ // registered_here, when given, says whether this call is the one that put the
535
+ // key in alive_objects, so a caller that has to throw the key away can take
536
+ // exactly the row it caused and not one an earlier crossing still relies on.
537
+ static JSValue js_crypto_key_to_js(JSContext *ctx, VALUE r_key, bool *registered_here_out)
253
538
  {
254
539
  VMData *data = JS_GetContextOpaque(ctx);
255
- VALUE r_object_id = rb_funcall(r_key, rb_intern("object_id"), 0);
256
- rb_hash_aset(data->alive_objects, r_object_id, r_key);
257
- int64_t handle = NUM2LONG(r_object_id);
258
540
 
541
+ // Nothing is registered until the key is fully described, which is the
542
+ // ordering j_error_from_ruby_error follows and the only version of it that
543
+ // holds: the reads below raise, and so does describing them, since
544
+ // StringValueCStr rejects a non-String and a NUL, and NUM2INT rejects a
545
+ // non-Integer. A raise out of a JSCFunction after registering would leave a
546
+ // row anchoring this key for the life of the VM with nothing able to reach
547
+ // it. With the draw last, there is nothing to leave.
259
548
  VALUE r_type = rb_funcall(r_key, rb_intern("type"), 0);
260
549
  VALUE r_extractable = rb_funcall(r_key, rb_intern("extractable"), 0);
261
550
  VALUE r_algorithm = rb_funcall(r_key, rb_intern("algorithm"), 0);
262
551
  VALUE r_usages = rb_funcall(r_key, rb_intern("usages"), 0);
263
552
 
553
+ VALUE r_algo_name = rb_hash_aref(r_algorithm, rb_str_new_cstr("name"));
554
+ VALUE r_algo_length = rb_hash_aref(r_algorithm, rb_str_new_cstr("length"));
555
+ VALUE r_algo_named_curve = rb_hash_aref(r_algorithm, rb_str_new_cstr("namedCurve"));
556
+ VALUE r_algo_hash = rb_hash_aref(r_algorithm, rb_str_new_cstr("hash"));
557
+ VALUE r_algo_modulus_length = rb_hash_aref(r_algorithm, rb_str_new_cstr("modulusLength"));
558
+ VALUE r_algo_pub_exp = rb_hash_aref(r_algorithm, rb_str_new_cstr("publicExponent"));
559
+ long usages_len = RARRAY_LEN(r_usages);
560
+
561
+ // Touched for their conversions, not their values: StringValueCStr raises on
562
+ // a non-String and on an embedded NUL, NUM2INT on a non-Integer. Doing that
563
+ // here means the describing below cannot raise, which is what lets the draw
564
+ // sit between the two. Both orderings cost something otherwise: draw first
565
+ // and a raise from describing orphans a table row that anchors this key, with
566
+ // its private material, for the life of the VM; draw last and a raise from
567
+ // the draw leaves j_key, and the promise capability the caller is holding,
568
+ // unreleased. With nothing between them able to raise, neither is reachable.
569
+ StringValueCStr(r_type);
570
+ StringValueCStr(r_algo_name);
571
+ if (!NIL_P(r_algo_length))
572
+ NUM2INT(r_algo_length);
573
+ if (!NIL_P(r_algo_named_curve))
574
+ StringValueCStr(r_algo_named_curve);
575
+ if (!NIL_P(r_algo_hash))
576
+ StringValueCStr(r_algo_hash);
577
+ if (!NIL_P(r_algo_modulus_length))
578
+ NUM2INT(r_algo_modulus_length);
579
+ if (!NIL_P(r_algo_pub_exp))
580
+ StringValue(r_algo_pub_exp);
581
+ // Into a list of our own, because StringValueCStr converts the local it is
582
+ // given and rb_ary_entry hands back a temporary: touching that would leave
583
+ // the caller's array holding the unconverted member, and the describing loop
584
+ // would convert it a second time, after the draw and with j_key allocated,
585
+ // which is the raise the ordering above exists to rule out.
586
+ VALUE r_usage_strings = rb_ary_new_capa(usages_len);
587
+ for (long i = 0; i < usages_len; i++)
588
+ {
589
+ VALUE r_usage = rb_ary_entry(r_usages, i);
590
+ StringValueCStr(r_usage);
591
+ rb_ary_push(r_usage_strings, r_usage);
592
+ }
593
+
594
+ // Taken here, in the same raise-safe stretch, so the rollback paths below can
595
+ // undo a registration without dispatching from inside a JSCFunction.
596
+ VALUE r_key_object_id = rb_obj_id(r_key);
597
+
598
+ // Drawn here, with everything that can raise behind it and nothing allocated
599
+ // in front of it.
600
+ bool registered_here = false;
601
+ VALUE r_object_id = alive_objects_register(data, r_key, &registered_here);
602
+ if (registered_here_out != NULL)
603
+ *registered_here_out = registered_here;
604
+ if (NIL_P(r_object_id))
605
+ return JS_ThrowInternalError(ctx, "quickjs: could not publish a handle for a CryptoKey");
606
+
607
+ // Checked, like every value that becomes a property of the key: passing the
608
+ // sentinel to JS_DefinePropertyValue* stores it as an ordinary value, which
609
+ // is the shape the rest of this branch refuses.
264
610
  JSValue j_key = JS_NewObject(ctx);
611
+ if (JS_IsException(j_key))
612
+ {
613
+ if (registered_here)
614
+ {
615
+ alive_objects_unregister(data, r_object_id, r_key_object_id);
616
+ // The claim goes with the row: a caller told the row is theirs would ask
617
+ // alive_objects_forget for one that is already gone.
618
+ registered_here = false;
619
+ if (registered_here_out != NULL)
620
+ *registered_here_out = false;
621
+ }
622
+ return j_key;
623
+ }
265
624
 
266
- JS_SetPropertyStr(ctx, j_key, "type", JS_NewString(ctx, StringValueCStr(r_type)));
267
- JS_SetPropertyStr(ctx, j_key, "extractable", JS_NewBool(ctx, RTEST(r_extractable)));
625
+ bool described = j_define_str(ctx, j_key, "type", r_type);
626
+ described = JS_DefinePropertyValueStr(ctx, j_key, "extractable", JS_NewBool(ctx, RTEST(r_extractable)),
627
+ JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE) >= 0 && described;
268
628
 
629
+ // Every one of these is defined rather than assigned: an accessor a guest
630
+ // puts on Object.prototype otherwise absorbs the write, and the key ends up
631
+ // describing itself with whatever that getter says, including reporting
632
+ // extractable: true for a key generated without it.
269
633
  JSValue j_algo = JS_NewObject(ctx);
634
+ if (JS_IsException(j_algo))
635
+ {
636
+ if (registered_here)
637
+ {
638
+ alive_objects_unregister(data, r_object_id, r_key_object_id);
639
+ // The claim goes with the row: a caller told the row is theirs would ask
640
+ // alive_objects_forget for one that is already gone.
641
+ registered_here = false;
642
+ if (registered_here_out != NULL)
643
+ *registered_here_out = false;
644
+ }
645
+ JS_FreeValue(ctx, j_key);
646
+ return j_algo;
647
+ }
270
648
 
271
- VALUE r_algo_name = rb_hash_aref(r_algorithm, rb_str_new_cstr("name"));
272
- JS_SetPropertyStr(ctx, j_algo, "name", JS_NewString(ctx, StringValueCStr(r_algo_name)));
649
+ described = j_define_str(ctx, j_algo, "name", r_algo_name) && described;
273
650
 
274
- VALUE r_algo_length = rb_hash_aref(r_algorithm, rb_str_new_cstr("length"));
275
651
  if (!NIL_P(r_algo_length))
276
- JS_SetPropertyStr(ctx, j_algo, "length", JS_NewInt32(ctx, NUM2INT(r_algo_length)));
652
+ described = JS_DefinePropertyValueStr(ctx, j_algo, "length", JS_NewInt32(ctx, NUM2INT(r_algo_length)),
653
+ JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE) >= 0 && described;
277
654
 
278
- VALUE r_algo_named_curve = rb_hash_aref(r_algorithm, rb_str_new_cstr("namedCurve"));
279
655
  if (!NIL_P(r_algo_named_curve))
280
- JS_SetPropertyStr(ctx, j_algo, "namedCurve", JS_NewString(ctx, StringValueCStr(r_algo_named_curve)));
656
+ described = j_define_str(ctx, j_algo, "namedCurve", r_algo_named_curve) && described;
281
657
 
282
- VALUE r_algo_hash = rb_hash_aref(r_algorithm, rb_str_new_cstr("hash"));
283
658
  if (!NIL_P(r_algo_hash))
284
659
  {
285
660
  JSValue j_hash_obj = JS_NewObject(ctx);
286
- JS_SetPropertyStr(ctx, j_hash_obj, "name", JS_NewString(ctx, StringValueCStr(r_algo_hash)));
287
- JS_SetPropertyStr(ctx, j_algo, "hash", j_hash_obj);
661
+ if (JS_IsException(j_hash_obj))
662
+ {
663
+ if (registered_here)
664
+ {
665
+ alive_objects_unregister(data, r_object_id, r_key_object_id);
666
+ registered_here = false;
667
+ if (registered_here_out != NULL)
668
+ *registered_here_out = false;
669
+ }
670
+ JS_FreeValue(ctx, j_algo);
671
+ JS_FreeValue(ctx, j_key);
672
+ return j_hash_obj;
673
+ }
674
+ described = j_define_str(ctx, j_hash_obj, "name", r_algo_hash) && described;
675
+ described = JS_DefinePropertyValueStr(ctx, j_algo, "hash", j_hash_obj,
676
+ JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE) >= 0 && described;
288
677
  }
289
678
 
290
- VALUE r_algo_modulus_length = rb_hash_aref(r_algorithm, rb_str_new_cstr("modulusLength"));
291
679
  if (!NIL_P(r_algo_modulus_length))
292
- JS_SetPropertyStr(ctx, j_algo, "modulusLength", JS_NewInt32(ctx, NUM2INT(r_algo_modulus_length)));
680
+ described = JS_DefinePropertyValueStr(ctx, j_algo, "modulusLength", JS_NewInt32(ctx, NUM2INT(r_algo_modulus_length)),
681
+ JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE) >= 0 && described;
293
682
 
294
- VALUE r_algo_pub_exp = rb_hash_aref(r_algorithm, rb_str_new_cstr("publicExponent"));
295
683
  if (!NIL_P(r_algo_pub_exp))
296
684
  {
685
+ // Built through JS_NewTypedArray rather than by calling globalThis's
686
+ // Uint8Array. That constructor is the guest's to delete or replace, and a
687
+ // replacement can throw anything it likes, including a Ruby exception from
688
+ // a bridge: nothing here can report it, because this runs after its
689
+ // caller's rb_protect has returned and every caller stores the result
690
+ // unchecked. Not reading the global removes the question rather than
691
+ // answering it. What remains is the runtime's own allocation failing, and
692
+ // both branches below take that rather than leave it: no caller of this
693
+ // function returns JS_EXCEPTION, so a throw left set would be attributed to
694
+ // whatever evaluation next asked for one. The cost is that a key can come
695
+ // back describing itself without publicExponent, with the out-of-memory
696
+ // reported nowhere. #129.
297
697
  JSValue j_pe_buf = JS_NewArrayBufferCopy(ctx,
298
698
  (const uint8_t *)RSTRING_PTR(r_algo_pub_exp),
299
699
  RSTRING_LEN(r_algo_pub_exp));
300
- JSValue j_uint8 = JS_GetPropertyStr(ctx, JS_GetGlobalObject(ctx), "Uint8Array");
301
- JSValue j_pe = JS_CallConstructor(ctx, j_uint8, 1, (JSValueConst *)&j_pe_buf);
302
- JS_FreeValue(ctx, j_uint8);
303
- JS_FreeValue(ctx, j_pe_buf);
304
- JS_SetPropertyStr(ctx, j_algo, "publicExponent", j_pe);
700
+ if (JS_IsException(j_pe_buf))
701
+ {
702
+ // Taken here for the reason the branch below gives: the key is still
703
+ // handed over without a publicExponent, so leaving this set would
704
+ // attribute the allocation failure to whatever asked next.
705
+ JS_FreeValue(ctx, JS_GetException(ctx));
706
+ }
707
+ else
708
+ {
709
+ JSValue j_args[3] = {j_pe_buf, JS_NewInt32(ctx, 0), JS_NewInt64(ctx, RSTRING_LEN(r_algo_pub_exp))};
710
+ JSValue j_pe = JS_NewTypedArray(ctx, 3, (JSValueConst *)j_args, JS_TYPED_ARRAY_UINT8);
711
+ JS_FreeValue(ctx, j_pe_buf);
712
+ if (JS_IsException(j_pe))
713
+ // Taken and lost, because the key is still described well enough to
714
+ // hand over and a throw left here would surface at whatever calls
715
+ // JS_GetException next. It can be a throw an earlier describe set, so
716
+ // js_settle_or_reject no longer assumes the slot is full. The only way
717
+ // to get here is the runtime's own allocation failing, which the next
718
+ // one will fail at too.
719
+ JS_FreeValue(ctx, JS_GetException(ctx));
720
+ else
721
+ described = JS_DefinePropertyValueStr(ctx, j_algo, "publicExponent", j_pe,
722
+ JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE) >= 0 && described;
723
+ }
305
724
  }
306
725
 
307
- JS_SetPropertyStr(ctx, j_key, "algorithm", j_algo);
726
+ described = JS_DefinePropertyValueStr(ctx, j_key, "algorithm", j_algo,
727
+ JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE) >= 0 && described;
308
728
 
309
- long usages_len = RARRAY_LEN(r_usages);
310
729
  JSValue j_usages = JS_NewArray(ctx);
730
+ if (JS_IsException(j_usages))
731
+ {
732
+ if (registered_here)
733
+ {
734
+ alive_objects_unregister(data, r_object_id, r_key_object_id);
735
+ // The claim goes with the row: a caller told the row is theirs would ask
736
+ // alive_objects_forget for one that is already gone.
737
+ registered_here = false;
738
+ if (registered_here_out != NULL)
739
+ *registered_here_out = false;
740
+ }
741
+ JS_FreeValue(ctx, j_key);
742
+ return j_usages;
743
+ }
311
744
  for (long i = 0; i < usages_len; i++)
312
745
  {
313
- VALUE r_usage = rb_ary_entry(r_usages, i);
314
- JS_SetPropertyUint32(ctx, j_usages, (uint32_t)i, JS_NewString(ctx, StringValueCStr(r_usage)));
746
+ VALUE r_usage = rb_ary_entry(r_usage_strings, i);
747
+ // Defined, like every other property of the key: an index property on
748
+ // Array.prototype takes the fast array path away, and the element write
749
+ // then walks the chain into the guest's setter like any named one would.
750
+ // StringValueCStr cannot raise here: it is a String already, converted
751
+ // above, before anything was drawn or allocated.
752
+ JSValue j_usage = JS_NewString(ctx, StringValueCStr(r_usage));
753
+ if (JS_IsException(j_usage))
754
+ {
755
+ JS_FreeValue(ctx, j_usage);
756
+ described = false;
757
+ }
758
+ else
759
+ {
760
+ described = JS_DefinePropertyValueUint32(ctx, j_usages, (uint32_t)i, j_usage, JS_PROP_C_W_E) >= 0 && described;
761
+ }
315
762
  }
316
- JS_SetPropertyStr(ctx, j_key, "usages", j_usages);
763
+ described = JS_DefinePropertyValueStr(ctx, j_key, "usages", j_usages,
764
+ JS_PROP_WRITABLE | JS_PROP_ENUMERABLE | JS_PROP_CONFIGURABLE) >= 0 && described;
317
765
 
318
- JS_DefinePropertyValueStr(ctx, j_key, "rb_object_id",
319
- JS_NewInt64(ctx, handle),
320
- JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE);
766
+ if (!described)
767
+ {
768
+ // A key that cannot say what it is, is not one to hand over: the runtime's
769
+ // own throw is already pending from whichever allocation failed. The row
770
+ // goes with it, since the draw is behind us now: left there it would anchor
771
+ // this key, key_data included, for the life of the VM with nothing in JS
772
+ // able to reach it. Only what this call made, because the same key crossing
773
+ // twice reuses its handle and the earlier crossing still relies on that row.
774
+ if (registered_here)
775
+ {
776
+ alive_objects_unregister(data, r_object_id, r_key_object_id);
777
+ // The claim goes with the row: a caller told the row is theirs would ask
778
+ // alive_objects_forget for one that is already gone.
779
+ registered_here = false;
780
+ if (registered_here_out != NULL)
781
+ *registered_here_out = false;
782
+ }
783
+ JS_FreeValue(ctx, j_key);
784
+ return JS_EXCEPTION;
785
+ }
786
+
787
+ // A key that looks like a key and carries no handle is worse than no key:
788
+ // every later sign or exportKey on it fails as an invalid key while the row
789
+ // keeps anchoring the Ruby object.
790
+ if (JS_DefinePropertyValueStr(ctx, j_key, "rb_object_id",
791
+ JS_NewInt64(ctx, NUM2LL(r_object_id)),
792
+ JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE) < 0)
793
+ {
794
+ // Only what this call put there: the same key object crossing twice reuses
795
+ // its handle, and taking that row would unanchor the key the earlier
796
+ // crossing handed the guest.
797
+ if (registered_here)
798
+ {
799
+ alive_objects_unregister(data, r_object_id, r_key_object_id);
800
+ if (registered_here_out != NULL)
801
+ *registered_here_out = false;
802
+ }
803
+ JS_FreeValue(ctx, j_key);
804
+ return JS_EXCEPTION;
805
+ }
321
806
 
322
807
  return j_key;
323
808
  }
@@ -389,6 +874,8 @@ static JSValue js_subtle_generate_key(JSContext *ctx, JSValueConst this_val, int
389
874
  VALUE r_algo_hash = js_algo_to_ruby_hash(ctx, argv[0]);
390
875
  VALUE r_extractable = JS_ToBool(ctx, argv[1]) ? Qtrue : Qfalse;
391
876
  VALUE r_usages = js_usages_to_ruby_array(ctx, argv[2]);
877
+ if (NIL_P(r_usages))
878
+ return js_rejected_type_error(ctx, "SubtleCrypto: keyUsages has more entries than a key can have");
392
879
 
393
880
  JSValue promise, resolving_funcs[2];
394
881
  promise = JS_NewPromiseCapability(ctx, resolving_funcs);
@@ -407,21 +894,45 @@ static JSValue js_subtle_generate_key(JSContext *ctx, JSValueConst this_val, int
407
894
  {
408
895
  VALUE r_priv = rb_hash_aref(r_result, ID2SYM(rb_intern("private_key")));
409
896
  VALUE r_pub = rb_hash_aref(r_result, ID2SYM(rb_intern("public_key")));
897
+ bool priv_registered_here = false;
898
+ bool pub_registered_here = false;
899
+ JSValue j_priv = js_crypto_key_to_js(ctx, r_priv, &priv_registered_here);
900
+ JSValue j_pub = js_crypto_key_to_js(ctx, r_pub, &pub_registered_here);
410
901
  JSValue j_pair = JS_NewObject(ctx);
411
- JSValue j_priv = js_crypto_key_to_js(ctx, r_priv);
412
- JSValue j_pub = js_crypto_key_to_js(ctx, r_pub);
413
- JS_SetPropertyStr(ctx, j_pair, "privateKey", j_priv);
414
- JS_SetPropertyStr(ctx, j_pair, "publicKey", j_pub);
415
- JSValue ret = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, (JSValueConst *)&j_pair);
416
- JS_FreeValue(ctx, j_pair);
417
- JS_FreeValue(ctx, ret);
902
+ bool built = !JS_IsException(j_priv) && !JS_IsException(j_pub) && !JS_IsException(j_pair);
903
+ if (built)
904
+ {
905
+ built = JS_DefinePropertyValueStr(ctx, j_pair, "privateKey", j_priv, JS_PROP_C_W_E) >= 0;
906
+ built = JS_DefinePropertyValueStr(ctx, j_pair, "publicKey", j_pub, JS_PROP_C_W_E) >= 0 && built;
907
+ }
908
+ else
909
+ {
910
+ // Half a pair is not a pair, and the sentinel is not a value to hand on.
911
+ JS_FreeValue(ctx, j_priv);
912
+ JS_FreeValue(ctx, j_pub);
913
+ }
914
+
915
+ if (built)
916
+ {
917
+ js_settle_or_reject(ctx, resolving_funcs, j_pair);
918
+ }
919
+ else
920
+ {
921
+ // Whichever key did get built is being dropped here, so its row goes with
922
+ // it. Left behind, it would anchor a Ruby CryptoKey, private material
923
+ // included, for the life of the VM with nothing in JS able to reach it.
924
+ VMData *data = JS_GetContextOpaque(ctx);
925
+ if (priv_registered_here)
926
+ alive_objects_forget(data, r_priv);
927
+ if (pub_registered_here)
928
+ alive_objects_forget(data, r_pub);
929
+ JS_FreeValue(ctx, j_pair);
930
+ js_settle_or_reject(ctx, resolving_funcs, JS_EXCEPTION);
931
+ }
418
932
  }
419
933
  else
420
934
  {
421
- JSValue j_result = js_crypto_key_to_js(ctx, r_result);
422
- JSValue ret = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, (JSValueConst *)&j_result);
423
- JS_FreeValue(ctx, j_result);
424
- JS_FreeValue(ctx, ret);
935
+ js_settle_or_reject(ctx, resolving_funcs, js_crypto_key_to_js(ctx, r_result, NULL));
425
936
  }
426
937
 
427
938
  JS_FreeValue(ctx, resolving_funcs[0]);
@@ -457,6 +968,8 @@ static JSValue js_subtle_import_key(JSContext *ctx, JSValueConst this_val, int a
457
968
  VALUE r_algo_hash = js_algo_to_ruby_hash(ctx, argv[2]);
458
969
  VALUE r_extractable = JS_ToBool(ctx, argv[3]) ? Qtrue : Qfalse;
459
970
  VALUE r_usages = js_usages_to_ruby_array(ctx, argv[4]);
971
+ if (NIL_P(r_usages))
972
+ return js_rejected_type_error(ctx, "SubtleCrypto: keyUsages has more entries than a key can have");
460
973
 
461
974
  JSValue promise, resolving_funcs[2];
462
975
  promise = JS_NewPromiseCapability(ctx, resolving_funcs);
@@ -473,10 +986,7 @@ static JSValue js_subtle_import_key(JSContext *ctx, JSValueConst this_val, int a
473
986
  }
474
987
  else
475
988
  {
476
- JSValue j_result = js_crypto_key_to_js(ctx, r_result);
477
- JSValue ret = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, (JSValueConst *)&j_result);
478
- JS_FreeValue(ctx, j_result);
479
- JS_FreeValue(ctx, ret);
989
+ js_settle_or_reject(ctx, resolving_funcs, js_crypto_key_to_js(ctx, r_result, NULL));
480
990
  }
481
991
 
482
992
  JS_FreeValue(ctx, resolving_funcs[0]);
@@ -809,6 +1319,8 @@ static JSValue js_subtle_derive_key(JSContext *ctx, JSValueConst this_val, int a
809
1319
  VALUE r_derived_algo_hash = js_algo_to_ruby_hash(ctx, argv[2]);
810
1320
  VALUE r_extractable = JS_ToBool(ctx, argv[3]) ? Qtrue : Qfalse;
811
1321
  VALUE r_usages = js_usages_to_ruby_array(ctx, argv[4]);
1322
+ if (NIL_P(r_usages))
1323
+ return js_rejected_type_error(ctx, "SubtleCrypto: keyUsages has more entries than a key can have");
812
1324
 
813
1325
  JSValue promise, resolving_funcs[2];
814
1326
  promise = JS_NewPromiseCapability(ctx, resolving_funcs);
@@ -825,10 +1337,7 @@ static JSValue js_subtle_derive_key(JSContext *ctx, JSValueConst this_val, int a
825
1337
  }
826
1338
  else
827
1339
  {
828
- JSValue j_result = js_crypto_key_to_js(ctx, r_result);
829
- JSValue ret = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, (JSValueConst *)&j_result);
830
- JS_FreeValue(ctx, j_result);
831
- JS_FreeValue(ctx, ret);
1340
+ js_settle_or_reject(ctx, resolving_funcs, js_crypto_key_to_js(ctx, r_result, NULL));
832
1341
  }
833
1342
 
834
1343
  JS_FreeValue(ctx, resolving_funcs[0]);
@@ -934,6 +1443,8 @@ static JSValue js_subtle_unwrap_key(JSContext *ctx, JSValueConst this_val, int a
934
1443
  VALUE r_unwrapped_algo_hash = js_algo_to_ruby_hash(ctx, argv[4]);
935
1444
  VALUE r_extractable = JS_ToBool(ctx, argv[5]) ? Qtrue : Qfalse;
936
1445
  VALUE r_usages = js_usages_to_ruby_array(ctx, argv[6]);
1446
+ if (NIL_P(r_usages))
1447
+ return js_rejected_type_error(ctx, "SubtleCrypto: keyUsages has more entries than a key can have");
937
1448
 
938
1449
  JSValue promise, resolving_funcs[2];
939
1450
  promise = JS_NewPromiseCapability(ctx, resolving_funcs);
@@ -959,10 +1470,7 @@ static JSValue js_subtle_unwrap_key(JSContext *ctx, JSValueConst this_val, int a
959
1470
  }
960
1471
  else
961
1472
  {
962
- JSValue j_result = js_crypto_key_to_js(ctx, r_result);
963
- JSValue ret = JS_Call(ctx, resolving_funcs[0], JS_UNDEFINED, 1, (JSValueConst *)&j_result);
964
- JS_FreeValue(ctx, j_result);
965
- JS_FreeValue(ctx, ret);
1473
+ js_settle_or_reject(ctx, resolving_funcs, js_crypto_key_to_js(ctx, r_result, NULL));
966
1474
  }
967
1475
 
968
1476
  JS_FreeValue(ctx, resolving_funcs[0]);