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.
@@ -0,0 +1,890 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Quickjs
6
+ # Ruby values reach JS as a declaration rather than a property assignment,
7
+ # so the caller picks the binding form and JS's own rules apply unchanged:
8
+ # `const` is read-only and refuses redeclaration, `var` is the only form
9
+ # that lands on `globalThis`. Nothing here invents semantics QuickJS does
10
+ # not already have, which is what keeps a colliding `let` in user code a
11
+ # loud SyntaxError instead of a silent shadow.
12
+ #
13
+ # Defined directly on VM rather than through a prepended module, since none
14
+ # of these wrap an existing method.
15
+ class VM
16
+ # `$` and `_` are ordinary identifier characters in JS. Anything outside
17
+ # this set would be concatenated straight into source we evaluate, so
18
+ # this check is what stops a name from smuggling in arbitrary JS.
19
+ NAME_PATTERN = /\A[A-Za-z_$][A-Za-z0-9_$]*\z/
20
+
21
+ # The keyword is interpolated too, and a Symbol interpolates through
22
+ # Symbol#to_s exactly as a name would. These are ours rather than the
23
+ # caller's, but "ours" is not the property that matters: what reaches the
24
+ # source has to be a String that answers for itself.
25
+ JS_KEYWORDS = { const: "const", let: "let", var: "var" }.freeze
26
+
27
+ # The full spec list. QuickJS refuses some of these itself, but not all:
28
+ # a declaration eval is sloppy mode, so `var static = 1` and the other
29
+ # strict-mode-only words evaluate cleanly there. Rejecting the whole list
30
+ # keeps the answer the same whichever word it was.
31
+ RESERVED_WORDS = %w[
32
+ await break case catch class const continue debugger default delete do
33
+ else enum export extends false finally for function if implements import
34
+ in instanceof interface let new null package private protected public
35
+ return static super switch this throw true try typeof var void while
36
+ with yield
37
+ ].freeze
38
+
39
+ private_constant :NAME_PATTERN, :RESERVED_WORDS, :JS_KEYWORDS
40
+
41
+ def define_const(name, value)
42
+ _define_variable(:const, name, value)
43
+ end
44
+
45
+ def define_let(name, value)
46
+ _define_variable(:let, name, value)
47
+ end
48
+
49
+ def define_var(name, value)
50
+ _define_variable(:var, name, value)
51
+ end
52
+
53
+ private
54
+
55
+ def _define_variable(kind, name, value)
56
+ # The validated String itself, all the way to the interpolation. Handing
57
+ # back a Symbol and interpolating that put a dispatch between the check
58
+ # and the source: interpolating a Symbol calls Symbol#to_s, so the bytes
59
+ # that were matched against the pattern were thrown away and the name was
60
+ # asked for again afterwards. A plain String interpolates as itself.
61
+ budget = _js_source_budget
62
+ key = _validate_variable_name(name, budget)
63
+ # The per-container check bounds the expanding case; this catches the
64
+ # flat one, a single enormous String or a very wide container, where no
65
+ # inner container ever crosses the line on its own.
66
+ #
67
+ # Depth is left to the stack rather than to a constant. What a walk
68
+ # costs depends on the stack it runs on, and a Ruby thread's is a
69
+ # fraction of the main thread's, so the same 900 levels are fine on one
70
+ # and fatal on the other. SystemStackError is not a StandardError
71
+ # either, so a caller's `rescue => e` would miss it and the thread would
72
+ # go down.
73
+ literal =
74
+ begin
75
+ ValueLiteral._within_budget(ValueLiteral._js_literal(value, nil, budget), budget)
76
+ rescue ::SystemStackError
77
+ raise ::ArgumentError, "value nests too deeply to convert on this thread's stack"
78
+ end
79
+
80
+ declared = (@_defined_variables ||= {})
81
+ existing = declared[key]
82
+ # Cleared before any of the branches below can read it. It is one slot on
83
+ # the VM, and the branches that consult it sit in rescues that a probe
84
+ # ahead of _declare can also reach: _refuse_unusable_global evaluates JS
85
+ # and can raise the same classes. Left standing, an outcome from the
86
+ # previous define would answer for a declaration this one never ran.
87
+ @_last_declare_outcome = nil
88
+
89
+ if existing == :interrupted
90
+ raise ::ArgumentError,
91
+ "#{key} was left uninitialized by a declaration this VM interrupted, so the name is " \
92
+ "unusable for the life of the VM"
93
+ elsif existing.nil?
94
+ _refuse_unusable_global(key) if kind == :var
95
+ begin
96
+ _declare(declared, kind, key, literal)
97
+ rescue Quickjs::InterruptedError
98
+ raise
99
+ rescue Quickjs::SyntaxError
100
+ # JavaScript refused the declaration, and a fresh define used to pass
101
+ # that on as the engine's own redeclaration error while the same
102
+ # situation with a record present answered with an ArgumentError
103
+ # saying why. Two classes for one situation, and the README teaches
104
+ # the ArgumentError.
105
+ raise unless _declare_refused?
106
+
107
+ # Which refusal it was depends on the form. A non-configurable global
108
+ # blocks a lexical declaration and does not block a var: measured, a
109
+ # var of that name is accepted and the assignment reaches the global.
110
+ # So for a var the only thing left is a lexical binding the guest
111
+ # declared, and diagnosing the descriptor there would name a cause
112
+ # that cannot be the one.
113
+ _refuse_restricted_global(key, kind) unless kind == :var
114
+
115
+ raise ::ArgumentError,
116
+ "#{key} is already declared in this VM by JavaScript that ran earlier, and " \
117
+ "JavaScript does not allow redeclaring it"
118
+ end
119
+ elsif existing != kind
120
+ # A record can be ahead of a declaration that never ran, so refusing a
121
+ # change of form on the record's word alone would go on refusing a
122
+ # legitimate one for the life of the VM. The same correction the two
123
+ # branches below make: ask, and if the other form is not there, the
124
+ # caller gets the one they asked for.
125
+ #
126
+ # Every cross-form redeclaration is a parse error in JavaScript, so the
127
+ # declaration answers this the same way it answers the const case.
128
+ unless _live_lexical?(key)
129
+ begin
130
+ _refuse_unusable_global(key) if kind == :var
131
+ return _declare(declared, kind, key, literal)
132
+ rescue Quickjs::InterruptedError
133
+ raise
134
+ rescue Quickjs::SyntaxError
135
+ # The other form really is declared, so say so below. Only when the
136
+ # declaration is what refused: this same class arriving from outside
137
+ # while it ran would revert the record for a binding the VM now has,
138
+ # and the next define would meet the bare redeclaration error.
139
+ raise unless _declare_refused?
140
+
141
+ declared[key] = existing
142
+ rescue Quickjs::RuntimeError
143
+ declared[key] = existing if _declare_refused?
144
+ raise
145
+ end
146
+ end
147
+
148
+ raise ::ArgumentError,
149
+ "#{key} is already defined as a #{existing}; it cannot be redefined as a #{kind}"
150
+ elsif kind == :const
151
+ # Same question, and the same reason not to ask it by provoking a parse
152
+ # error: a refusal is not a place to emit a console.error the caller
153
+ # never wrote. If the name reads as something no `globalThis` property
154
+ # could account for, the const is there and that is the answer.
155
+ if _live_lexical?(key)
156
+ raise ::ArgumentError,
157
+ "#{key} is already defined as a const; a const cannot be redefined"
158
+ end
159
+
160
+ # Otherwise the declaration itself is the question. Redeclaring a live
161
+ # const is a parse error, thrown before anything runs, so this asks
162
+ # without running any of the guest's code and without writing anything.
163
+ # Succeeding means the registry was ahead of a declaration that never
164
+ # happened, and the const the caller asked for is now there.
165
+ begin
166
+ return _declare(declared, kind, key, literal)
167
+ rescue Quickjs::InterruptedError
168
+ # _declare already recorded what the VM is actually left holding, and
169
+ # that is more useful to the next caller than being told they
170
+ # redefined something.
171
+ raise
172
+ rescue Quickjs::SyntaxError
173
+ # The redeclaration this was asking about, and only when the
174
+ # declaration is what refused: the same class raised at us from
175
+ # outside says nothing about whether the const was already there, and
176
+ # answering "already defined" for it would report a collision the
177
+ # caller never hit.
178
+ raise unless _declare_refused?
179
+
180
+ declared[key] = :const
181
+ raise ::ArgumentError,
182
+ "#{key} is already defined as a const; a const cannot be redefined"
183
+ rescue Quickjs::RuntimeError
184
+ # Something else went wrong in the VM: out of memory, or a guest that
185
+ # renamed the error class this was looking for. _declare took the
186
+ # record out on the way past and the const may well still be there, so
187
+ # put it back, but let the real error through. Reporting an
188
+ # out-of-memory as a redefinition would tell the caller they made a
189
+ # mistake while the VM is dead.
190
+ declared[key] = :const
191
+ raise
192
+ end
193
+ else
194
+ # Redeclaring a live `let` is a parse error, so re-defining one assigns
195
+ # to it instead. A `var` we declared ourselves can have been swapped for
196
+ # an accessor since, so the same check applies to the assignment.
197
+ _refuse_unusable_global(key) if kind == :var
198
+
199
+ # The bare declaration first, which is what makes the assignment safe.
200
+ # Assigning to a name that has no binding writes `globalThis` instead:
201
+ # sloppy mode invents the property, and strict mode was no answer
202
+ # either, since it only refuses to invent one and will happily write a
203
+ # global that already exists. Either way a `let` ended up on
204
+ # `globalThis`, which is the one thing this form promises does not
205
+ # happen, and it stayed there for every later define.
206
+ #
207
+ # A live binding refuses the declaration, at parse time, with nothing
208
+ # evaluated and nothing changed. An absent one gets the binding it was
209
+ # missing. Both leave something for the assignment to find: a lexical
210
+ # binding for a `let`, which shadows any `globalThis` property of the
211
+ # same name, and the global property itself for a `var`.
212
+ #
213
+ # Provoking that parse error is not free. Rendering a JS exception
214
+ # announces it to `on_log` first, so a question asked internally came
215
+ # out of the VM as a console.error the caller never wrote, once per
216
+ # redefine, on a path that succeeded. So ask a question that cannot
217
+ # throw first, and only fall back to the declaration when its answer is
218
+ # not decisive.
219
+ #
220
+ # Decisive means: the name reads as something, and no `globalThis`
221
+ # property could be what it read. That is a lexical binding, which is
222
+ # what makes the assignment safe. `in` tests for the property without
223
+ # reading it, and it is tested first, so a guest accessor's getter does
224
+ # not run. A `has` trap on a prototype the guest installed does, which
225
+ # is the tier this file already says it cannot defend. Redeclaring a `var` is
226
+ # legal, so that form never provokes anything and skips this.
227
+ if kind == :var
228
+ # Redeclaring a var is legal, so this provokes nothing and needs no
229
+ # asking, and a guest lexical of the same name refusing it is a real
230
+ # collision rather than noise. Swallowing that let the assignment
231
+ # below write the guest's binding and report success for a define_var
232
+ # that never reached globalThis at all. It is not skipped, though: without it the assignment walks
233
+ # the prototype chain, and a setter inherited from Object.prototype
234
+ # takes the value while the guard above, which asks only about own
235
+ # properties, reports a clean global.
236
+ eval_code("var #{key};")
237
+ elsif !_live_lexical?(key)
238
+ begin
239
+ eval_code("#{JS_KEYWORDS.fetch(kind)} #{key};")
240
+ # It just made the binding, so say so. Only _declare recorded this,
241
+ # and the two are not the same moment: a guest can fix a global of
242
+ # this name afterwards, and then a later redefine, unable to tell
243
+ # that the binding is ours, refuses a name this line already gave
244
+ # the caller.
245
+ _confirmed[key] = kind
246
+ rescue Quickjs::SyntaxError
247
+ # Refused because the binding is already there, which is the
248
+ # ordinary case and leaves the assignment something to find. Or
249
+ # because JavaScript will never accept this declaration at all,
250
+ # which leaves it nothing, and then the assignment writes the
251
+ # global instead.
252
+ # A record we confirmed means our own binding is there and this is an
253
+ # ordinary redeclaration of it. Only an unconfirmed one can be sitting
254
+ # over a name JavaScript will never hand over.
255
+ _refuse_restricted_global(key, kind) unless _confirmed[key] == kind
256
+ end
257
+ end
258
+
259
+ # `void` so the statement has no completion value. An assignment
260
+ # expression evaluates to what was assigned, and eval_code converts
261
+ # whatever the statement produced back into Ruby, so this used to
262
+ # rebuild the whole value as Ruby objects and drop them.
263
+ #
264
+ # Sloppy, like the declaration above. Strict mode refuses `eval` and
265
+ # `arguments` as assignment targets, which the declaration accepts, so a
266
+ # strict assignment made those two names definable once and broken
267
+ # afterwards.
268
+ _eval_generated("void (#{key} = #{literal});")
269
+ end
270
+
271
+ # Only here, where a patched String#to_sym can decide nothing except what
272
+ # the caller is handed back.
273
+ key.to_sym
274
+ end
275
+
276
+ # memory_usage walks the whole JS heap, and the limit it reports is fixed
277
+ # when the VM is built, so reading it per call made every define cost a
278
+ # heap walk: single-digit microseconds on an empty VM against milliseconds on
279
+ # one holding a few hundred thousand objects, on this machine.
280
+ #
281
+ # JSMemoryUsage.malloc_limit is an int64_t, so a limit at or above 2**63
282
+ # comes back negative. Anything that is not a usable size means no budget,
283
+ # rather than a budget of minus nine quintillion.
284
+ def _js_source_budget
285
+ return @_js_source_budget if defined?(@_js_source_budget)
286
+
287
+ limit = memory_usage[:malloc_limit]
288
+ @_js_source_budget = limit.is_a?(::Integer) && limit.positive? ? limit : nil
289
+ end
290
+
291
+ # Every evaluation of a generated value literal goes through here, so that
292
+ # the translation below cannot be given to one caller and forgotten on
293
+ # another. It was, once: a fresh name refused with ArgumentError while a
294
+ # redefine of an existing one raised a raw Quickjs::SyntaxError, in the same
295
+ # band, for the same value.
296
+ #
297
+ # The probes and the bare declarations do not come through here, and should
298
+ # not: they are a few lines each, they cannot plausibly overflow a stack the
299
+ # literal did not, and a stack overflow in one of them would not mean the
300
+ # value nests too deeply.
301
+ #
302
+ # The Ruby walk is not the only stack this runs out of. On a Ruby thread,
303
+ # whose stack is a fraction of the main one, QuickJS clamps its own limit to
304
+ # the same headroom and the parser gives out first, where the walk still had
305
+ # room. A caller told to rescue ArgumentError got a Quickjs::SyntaxError
306
+ # instead, and on a worker thread that is fatal.
307
+ #
308
+ # Matching the message is safe in this one place, because the source being
309
+ # parsed is the one this file just built and has no call in it, so a parse
310
+ # error about it comes from the engine and describes our own literal.
311
+ def _eval_generated(source)
312
+ eval_code(source)
313
+ rescue Quickjs::SyntaxError => e
314
+ raise unless e.message.include?("stack overflow")
315
+
316
+ raise ::ArgumentError, "value nests too deeply to convert on this thread's stack"
317
+ end
318
+
319
+ # The declaration is its own eval so the caller's source is never rewritten.
320
+ # Prepending to it would shift every line number in a backtrace away from the
321
+ # code the caller actually wrote.
322
+ #
323
+ # The registry entry goes in before the eval rather than after it. Nothing
324
+ # can be relied on to run in between: eval_code is a C call, so an exception
325
+ # raised at the first Ruby checkpoint after it returns lands exactly in the
326
+ # gap between a binding the VM now has and the line that would say so.
327
+ # Timeout and Thread#raise are held off by the mask below, but a raise from a
328
+ # trap handler is not, and a server that does `trap("TERM") { raise Shutdown
329
+ # }` reaches it four times in five. That left the binding live and correct in
330
+ # JS, absent from the registry, and reported as a redeclaration nobody wrote.
331
+ #
332
+ # Recording first inverts which way the window can be wrong, and the two
333
+ # branches that act on an existing record ask the VM rather than believing
334
+ # it, so a record that ran ahead of its declaration corrects itself on the
335
+ # next define instead of standing for the life of the VM.
336
+ def _declare(declared, kind, key, literal)
337
+ # Both records as they stood, so an attempt that did not declare anything
338
+ # can put them back. A failed declaration cannot have removed a binding an
339
+ # earlier one made, and clearing the records as though it had made the
340
+ # next define refuse a name whose binding was live the whole time.
341
+ was_declared = declared[key]
342
+ was_confirmed = _confirmed[key]
343
+ # The entry goes in before the eval: nothing can be relied on to run
344
+ # between the VM having the binding and the line that would say so.
345
+ declared[key] = kind
346
+ # What happened, rather than what was raised. The record standing is the
347
+ # default on purpose: it is written before the eval because nothing can be
348
+ # relied on to run between the VM having the binding and the line that
349
+ # would say so, and an exception that merely arrives during the call, like
350
+ # a trap handler's raise, must not undo that.
351
+ outcome = :stands
352
+ begin
353
+ ::Thread.handle_interrupt(::Exception => :never) do
354
+ _eval_generated("#{JS_KEYWORDS.fetch(kind)} #{key} = #{literal};")
355
+ outcome = :declared
356
+ end
357
+ rescue Quickjs::InterruptedError
358
+ outcome = :interrupted if outcome == :stands
359
+ raise
360
+ rescue ::ArgumentError, ::ThreadError, ::NoMemoryError, Quickjs::RuntimeError
361
+ # Raised by the call, and each of them means the declaration did not
362
+ # take: a parse error, the stack overflow the evaluator translates, the
363
+ # VM refusing a second thread, or running out of memory. Listing them is
364
+ # what separates them from an exception that only arrived while the call
365
+ # was running, which says nothing about whether the declaration ran.
366
+ #
367
+ # Only when the outcome is still what it started as. The mask above
368
+ # defers an interrupt until its block exits, which is inside this begin,
369
+ # so an exception of one of these classes delivered from outside arrives
370
+ # having already let the declaration finish. Reading its class alone
371
+ # said the declaration had not taken and rolled the record back for a
372
+ # binding the VM has, which bricks the name: the next define meets the
373
+ # bare redeclaration error that recording before the eval exists to
374
+ # prevent. Reproduced 30/30 with Timeout.timeout(n, ArgumentError); the
375
+ # default Timeout::Error only missed it by not being in this list.
376
+ outcome = :failed if outcome == :stands
377
+ raise
378
+ ensure
379
+ case outcome
380
+ when :declared
381
+ # The declaration ran, so a global lexical binding exists and
382
+ # JavaScript never takes one away. Recorded by form as well as name, so
383
+ # a later define of a different form cannot inherit this one's claim.
384
+ _confirmed[key] = kind
385
+ when :interrupted
386
+ # QuickJS created the binding and the interrupt landed before it was
387
+ # initialized, so a let or const name stays in the temporal dead zone
388
+ # for the life of the VM: reading it, assigning to it and redeclaring
389
+ # it all raise. Nothing here can undo that, so the next define is told
390
+ # why rather than being left to report a redeclaration. A var is left
391
+ # alone, since redeclaring one is legal JS and works.
392
+ declared[key] = kind == :var ? :var : :interrupted
393
+ was_confirmed.nil? ? _confirmed.delete(key) : _confirmed[key] = was_confirmed
394
+ when :failed
395
+ # Nothing was declared, so both records go back to what they were. A
396
+ # failed declaration cannot have removed a binding an earlier one made.
397
+ was_declared.nil? ? declared.delete(key) : declared[key] = was_declared
398
+ was_confirmed.nil? ? _confirmed.delete(key) : _confirmed[key] = was_confirmed
399
+ else
400
+ # Whether it ran is unknown, so the record stands and the confirmation
401
+ # does not. The next define asks the VM rather than believing either.
402
+ was_confirmed.nil? ? _confirmed.delete(key) : _confirmed[key] = was_confirmed
403
+ end
404
+ # Left where the callers can read it, for the same reason the rescue
405
+ # above checks the outcome rather than the class: they wrap this call in
406
+ # rescues that decided what happened from the exception alone.
407
+ @_last_declare_outcome = outcome
408
+ end
409
+
410
+ key.to_sym
411
+ end
412
+
413
+ # What the last _declare decided about its own declaration, for the callers
414
+ # that have to tell "the VM refused this" from "something was raised at us
415
+ # while it ran".
416
+ def _declare_refused?
417
+ @_last_declare_outcome == :failed
418
+ end
419
+
420
+ def _confirmed
421
+ @_confirmed_variables ||= {}
422
+ end
423
+
424
+ # Whether the name resolves to a lexical binding: no property of the global
425
+ # object could account for it, and it reads as something. Not decisive in
426
+ # both directions, and does not have to be. A false answer only means the
427
+ # caller pays for the declaration that asks properly.
428
+ def _live_lexical?(key)
429
+ eval_code(<<~JS) == true
430
+ (() => (function () {
431
+ // The real global object, not the `globalThis` property, which is
432
+ // writable and can be pointed at a decoy: this answered about the
433
+ // decoy while the guard below answered about the real one, and a
434
+ // define_let ended up on globalThis. `false` when it is not an object
435
+ // at all, which asks by declaration instead.
436
+ const g = (function () { return this })();
437
+ if (typeof g !== 'object' || g === null) return false;
438
+ return !(#{::JSON.generate(::String.new(key))} in g);
439
+ // The caller's name is read outside this function on purpose. Read
440
+ // inside it, `typeof <name>` resolves anything declared here first, so
441
+ // a caller who named their variable after one of these locals was told
442
+ // it was live on a VM that had never heard of it.
443
+ })() && typeof #{key} !== 'undefined')()
444
+ JS
445
+ rescue Quickjs::InterruptedError
446
+ # The evaluation is over, and saying so is more use than an answer.
447
+ raise
448
+ rescue Quickjs::RuntimeError
449
+ # Not decisive is a safe answer, and the branches that ask fall back to
450
+ # asking by declaration. Anything thrown out of a question the caller did
451
+ # not ask would be theirs to make sense of.
452
+ false
453
+ end
454
+
455
+ # A non-configurable property of the global object makes a lexical
456
+ # declaration of that name impossible for the life of the VM: JavaScript
457
+ # refuses `let x;` outright rather than shadowing it. A guest's top-level
458
+ # `var` creates exactly that shape, so this is not exotic.
459
+ #
460
+ # Letting that refusal pass leaves no lexical binding for the assignment to
461
+ # find, and it writes the global instead. A define_let that puts its value
462
+ # on globalThis is the one thing this form promises does not happen, so the
463
+ # caller is told rather than being given a success it did not get.
464
+ #
465
+ # Anything but a clean false refuses, since not being able to ask is not a
466
+ # reason to go ahead.
467
+ def _refuse_restricted_global(key, kind)
468
+ restricted =
469
+ begin
470
+ eval_code(<<~JS)
471
+ (() => {
472
+ const root = (function () { return this })();
473
+ const d = Object.getOwnPropertyDescriptor(root, #{::JSON.generate(::String.new(key))});
474
+ if (!d) return false;
475
+ Object.setPrototypeOf(d, null);
476
+ return !d.configurable;
477
+ })()
478
+ JS
479
+ rescue Quickjs::InterruptedError
480
+ raise
481
+ rescue Quickjs::RuntimeError => err
482
+ # As above: the engine failing is not a fact about this name.
483
+ raise if err.js_name.nil? || err.js_name == "InternalError"
484
+
485
+ true
486
+ end
487
+ return if restricted == false
488
+
489
+ raise ::ArgumentError,
490
+ "cannot define #{kind} #{key}: globalThis.#{key} cannot be configured away, so JavaScript " \
491
+ "will not accept a #{kind} of that name and the value would be written to that global instead"
492
+ end
493
+
494
+ # `var` is the only form that lands on `globalThis`, so it is the only one a
495
+ # property already sitting there can intercept. An accessor takes the value
496
+ # in its setter and hands JS back whatever its getter likes; a non-writable
497
+ # data property swallows the assignment silently, because a declaration eval
498
+ # is sloppy mode. Either way the caller was told the define succeeded when it
499
+ # did not happen.
500
+ #
501
+ # This refuses a global that is already unusable. It is not a security
502
+ # boundary, and is deliberately not described as one: it asks the VM through
503
+ # JS, and code that has already run there can replace
504
+ # Object.getOwnPropertyDescriptor as easily as it can install the accessor. A
505
+ # VM that has evaluated untrusted JavaScript owns its own environment, and
506
+ # there is no way to hand a value into it unobserved. Define before running
507
+ # code you do not control.
508
+ def _refuse_unusable_global(key)
509
+ state = eval_code(<<~JS)
510
+ (() => {
511
+ // The real global object, not the `globalThis` property, which is
512
+ // writable and can be pointed at a decoy. That is reachable from Ruby
513
+ // alone with define_var(:globalThis, 1): the lookup below would then
514
+ // answer about the decoy and report a clean global while the value
515
+ // went to whatever the real one still had sitting there. A sloppy
516
+ // function called with no receiver gets the global object itself.
517
+ const root = (function () { return this })();
518
+ if (typeof root !== 'object' || root === null) return 'broken';
519
+ const d = Object.getOwnPropertyDescriptor(root, #{::JSON.generate(::String.new(key))});
520
+ // Extensibility only decides whether a property can be added, so it
521
+ // is asked about only when there is none. A sealed global with the
522
+ // property already on it takes the declaration quite happily, and
523
+ // refusing that told the caller something untrue about their VM.
524
+ if (!d) return Object.isExtensible(root) ? 'absent' : 'sealed';
525
+ // Own keys only, and read without asking anything that can be
526
+ // replaced. Reading `get` off a descriptor walks the prototype chain,
527
+ // so a stray Object.prototype.get made every var look like an
528
+ // accessor; going through hasOwnProperty instead only moved the
529
+ // problem, since that is a method a guest can replace, and `writable`
530
+ // was still being read through the chain. Two gadgets together made
531
+ // an accessor answer `writable` and its setter took the value.
532
+ // Cutting the descriptor loose from its prototype settles all of it.
533
+ Object.setPrototypeOf(d, null);
534
+ if ('get' in d || 'set' in d) return 'accessor';
535
+ return d.writable ? 'writable' : 'readonly';
536
+ })()
537
+ JS
538
+
539
+ case state
540
+ when 'absent', 'writable'
541
+ nil
542
+ when 'accessor'
543
+ raise ::ArgumentError,
544
+ "cannot define var #{key}: globalThis.#{key} is an accessor, so the value would go to its " \
545
+ "setter rather than to the variable"
546
+ when 'readonly'
547
+ raise ::ArgumentError,
548
+ "cannot define var #{key}: globalThis.#{key} is not writable, so the assignment would be discarded"
549
+ when 'sealed'
550
+ raise ::ArgumentError,
551
+ "cannot define var #{key}: this VM's globalThis is not extensible, so the declaration " \
552
+ "would be refused"
553
+ else
554
+ # The probe reads globalThis and Object, so a caller who has already
555
+ # replaced either of those has taken the check away from itself. That
556
+ # is reachable from Ruby alone: define_var(:globalThis, 1) makes every
557
+ # later probe answer for a Number and report a clean global, and
558
+ # define_var(:Object, 1) makes it throw. Refusing an answer we do not
559
+ # recognise turns both into a refusal rather than into a define that
560
+ # reports success it did not have.
561
+ raise ::ArgumentError,
562
+ "cannot define var #{key}: this VM cannot be asked about its globals, so whether the " \
563
+ "assignment would take effect is unknown. Replacing globalThis or Object does this"
564
+ end
565
+ rescue Quickjs::InterruptedError
566
+ raise
567
+ rescue Quickjs::RuntimeError => e
568
+ # Only an error the guest can have thrown becomes an ArgumentError about
569
+ # their globals. The engine names its own failures InternalError, which is
570
+ # out of memory and stack overflow, and names nothing at all once the VM
571
+ # has latched as poisoned or been disposed.
572
+ #
573
+ # Two earlier versions of this got the split wrong. Asking whether the
574
+ # error carried a name missed the first out-of-memory, which carries one.
575
+ # Asking whether it arrived as a subclass missed everything the guest
576
+ # throws that is not one of the seven names the C layer maps, `new Error`
577
+ # among them, which is the most ordinary throw there is.
578
+ #
579
+ # A guest can name its own error InternalError and be re-raised rather
580
+ # than explained. That is the safe direction: the caller sees what was
581
+ # actually thrown.
582
+ raise if e.js_name.nil? || e.js_name == "InternalError"
583
+
584
+ raise ::ArgumentError,
585
+ "cannot define var #{key}: asking this VM about its globals failed with #{e.class}. " \
586
+ "Replacing Object, or any of the methods this asks it for, does this"
587
+ end
588
+
589
+ def _validate_variable_name(name, budget)
590
+ # `===` rather than `name.is_a?`, which the object answers for itself.
591
+ unless ::String === name || ::Symbol === name
592
+ raise ::TypeError, "variable's name should be a Symbol or a String, got #{name.class}"
593
+ end
594
+
595
+ # A copy of the bytes, for the same reason the value path takes one.
596
+ # `to_s` and `to_sym` are both dispatched on the caller's object, so a
597
+ # String subclass could satisfy the pattern below with one and hand a
598
+ # different name to the interpolation with the other. Everything after
599
+ # this line works on a plain String that answers only for itself.
600
+ str = ::String === name ? ::String.new(name) : ::String.new(name.to_s)
601
+
602
+ # Measured against the same budget as a value, since it goes into the same
603
+ # source. Without this the one piece of the generated JavaScript that was
604
+ # not bounded was the name: an oversized value was refused and left the VM
605
+ # healthy, and an oversized name reached the eval and took the VM with it.
606
+ if budget && str.bytesize > budget
607
+ raise ::ArgumentError,
608
+ "the variable's name is longer than #{budget} bytes, which is this VM's memory_limit"
609
+ end
610
+
611
+ unless NAME_PATTERN.match?(str)
612
+ raise ::ArgumentError, "#{str.inspect} is not a valid JavaScript identifier"
613
+ end
614
+ if RESERVED_WORDS.include?(str)
615
+ raise ::ArgumentError, "#{str.inspect} is a reserved word in JavaScript"
616
+ end
617
+
618
+ str
619
+ end
620
+ end
621
+
622
+ # These build the JavaScript source for a value. They are an implementation
623
+ # detail of the define_* methods and not something to call directly, so they
624
+ # live behind a private constant rather than sitting on Quickjs itself.
625
+ module ValueLiteral
626
+
627
+ # Serialized here rather than through the C converter, which falls back to
628
+ # `#inspect` for anything it doesn't recognize and would silently turn a
629
+ # Time into a string. Defining a variable is an input path, so an
630
+ # unsupported value is a caller mistake worth raising on.
631
+ def self._js_literal(value, seen = nil, budget = nil)
632
+ case value
633
+ when nil then "null"
634
+ when true then "true"
635
+ when false then "false"
636
+ # Neither to_json nor to_s: both are dispatched on the caller's object, so
637
+ # a subclass overriding either would decide what goes into the source, and
638
+ # a gem replacing String#to_json process-wide needs no hostile value at all.
639
+ # String.new copies the bytes without asking the object anything, and
640
+ # JSON.generate does the escaping. That is not out of a monkey patch's
641
+ # reach, and nothing in Ruby is: JSON.generate, String#initialize,
642
+ # Array#join and format are all replaceable, and a process that has done
643
+ # that has bigger problems than this method. What holds is narrower and
644
+ # is the part that matters: no method dispatched on the caller's value
645
+ # decides these bytes.
646
+ when ::String then _js_string_literal(value, budget)
647
+ when ::Integer then _js_integer_literal(value, budget)
648
+ when ::Float then _js_float_literal(value)
649
+ when ::Symbol then _js_symbol_literal(value, budget)
650
+ when ::Array, ::Hash then _js_container_literal(value, seen, budget)
651
+ else
652
+ raise ::TypeError, "#{value.class} cannot be converted to a JavaScript value"
653
+ end
654
+ end
655
+
656
+ # Measured before the copy rather than after the literal exists. The
657
+ # per-container check bounds a structure that expands as it nests, but a
658
+ # single enormous String is one value, so nothing checked it until it had
659
+ # been copied once and escaped once. A caller holding 300MB got 900MB.
660
+ #
661
+ # bytesize through an unbound method, since a subclass answering that one
662
+ # low is exactly how a value would get past a cheap check into an
663
+ # expensive copy.
664
+ BYTESIZE = ::String.instance_method(:bytesize)
665
+
666
+ # The same move for the two numeric conversions. format looked like it read
667
+ # the number rather than asking it, but Kernel#format is itself replaceable
668
+ # and fails open when it is: it answers with whatever the patch returns and
669
+ # that goes straight into the source. An unbound Integer#to_s and Float#to_s
670
+ # cannot be redirected after they are taken here, and they write the
671
+ # shortest form that reads back as the same number rather than the widest.
672
+ INTEGER_TO_S = ::Integer.instance_method(:to_s)
673
+ BIT_LENGTH = ::Integer.instance_method(:bit_length)
674
+ FLOAT_TO_S = ::Float.instance_method(:to_s)
675
+
676
+ def self._js_string_literal(value, budget)
677
+ _over_budget!(budget) if budget && BYTESIZE.bind_call(value) > budget
678
+
679
+ ::JSON.generate(::String.new(value))
680
+ end
681
+
682
+ # A bignum costs its digits to render, and a decimal digit costs about 3.32
683
+ # bits, so four bits per digit is more than any number needs: anything past
684
+ # four times the budget in bits has more digits than the budget allows. Deliberately loose: this refuses only what is
685
+ # certainly too large, and the finished literal is measured as before.
686
+ def self._js_integer_literal(value, budget)
687
+ _over_budget!(budget) if budget && BIT_LENGTH.bind_call(value) / 4 > budget
688
+
689
+ INTEGER_TO_S.bind_call(value)
690
+ end
691
+
692
+ # `undefined`, `NaN` and `Infinity` are not literals in JavaScript, they are
693
+ # properties of the global object, and QuickJS lets a guest redefine them
694
+ # with defineProperty even though the spec says it should not. Writing them
695
+ # by name handed the guest the value: a const of Value::UNDEFINED came back
696
+ # as whatever the guest had put there, at any depth, and came back to Ruby
697
+ # that way too. That is the one thing const and let are supposed to be safe
698
+ # from, since neither touches globalThis.
699
+ #
700
+ # The declaration forms are refused by QuickJS, which is what made this look
701
+ # settled: `let NaN = 5` is a SyntaxError. defineProperty is not.
702
+ #
703
+ # Operators and numeric literals resolve through no scope chain, so nothing
704
+ # the guest can reach decides what these mean.
705
+ JS_SPELLED_FLOATS = {
706
+ "Infinity" => "(1/0)",
707
+ "-Infinity" => "(-1/0)",
708
+ "NaN" => "(0/0)",
709
+ }.freeze
710
+
711
+ # nan?, infinite? and positive? were each dispatched on the caller's Float
712
+ # to pick between three fixed strings, which is a branch the value decided.
713
+ # They were also unnecessary: Float#to_s already picks those three out.
714
+ def self._js_float_literal(value)
715
+ spelled = FLOAT_TO_S.bind_call(value)
716
+ JS_SPELLED_FLOATS.fetch(spelled, spelled)
717
+ end
718
+
719
+ # `Quickjs::Value::UNDEFINED` and `NAN` are plain Symbols, so they have to
720
+ # be recognized before the generic Symbol case. Every other Symbol becomes
721
+ # a string, matching how the C converter treats Symbols returned from a
722
+ # `define_function` block.
723
+ # Spelled with operators for the same reason as the floats above.
724
+ JS_SPELLED_SYMBOLS = { Value::UNDEFINED => "(void 0)", Value::NAN => "(0/0)" }
725
+ .compare_by_identity.freeze
726
+
727
+ def self._js_symbol_literal(value, budget = nil)
728
+ # Looked up by identity rather than compared with ==, which a Symbol
729
+ # answers for itself and could use to make any Symbol undefined.
730
+ spelled = JS_SPELLED_SYMBOLS[value]
731
+ return spelled if spelled
732
+
733
+ # A Symbol has no bytes to measure until to_s makes some, so the copy is
734
+ # unavoidable. Measuring it before escaping still skips the expensive
735
+ # half, since escaping walks and rewrites every byte.
736
+ copy = ::String.new(value.to_s)
737
+ _over_budget!(budget) if budget && copy.bytesize > budget
738
+
739
+ ::JSON.generate(copy)
740
+ end
741
+
742
+ # `seen` is threaded through by identity: a structure that contains itself
743
+ # would otherwise recurse until the stack gives out. Entries are removed on
744
+ # the way out so a value appearing twice as siblings stays legal.
745
+ #
746
+ # compare_by_identity rather than keying on value.object_id, which the
747
+ # value answers for itself. A Hash subclass returning a constant object_id
748
+ # had every nested instance of it called circular; one returning a fresh
749
+ # id each time was never seen twice and walked until the stack guard
750
+ # caught it.
751
+ def self._js_container_literal(value, seen, budget = nil)
752
+ seen ||= {}.compare_by_identity
753
+ if seen.key?(value)
754
+ raise ::ArgumentError, "#{value.class} contains a circular reference and cannot be converted"
755
+ end
756
+ seen[value] = true
757
+
758
+ begin
759
+ # Appended to one String rather than collected and joined. The pieces
760
+ # were retained until the end, and a Ruby String costs about forty
761
+ # bytes of object for the two bytes of budget a small element spends,
762
+ # so a wide container of tiny values reached twenty-seven times
763
+ # memory_limit in host memory before the check fired. Appending keeps
764
+ # one piece alive at a time.
765
+ used = 2
766
+ separator = ""
767
+ if ::Array === value
768
+ out = +"["
769
+ value.each do |v|
770
+ piece = _js_literal(v, seen, budget)
771
+ out << separator << piece
772
+ separator = ","
773
+ used += piece.bytesize + 1
774
+ _over_budget!(budget) if budget && used > budget
775
+ end
776
+ out << "]"
777
+ else
778
+ out = +"{"
779
+ value.each do |k, v|
780
+ # Appended in three pieces rather than interpolated into one. The
781
+ # interpolation built a second full copy of the value's literal
782
+ # beside the one it had just made, so a deeply nested Hash held both
783
+ # at every level and cost about half again what the Array shape
784
+ # costs, which is the opposite of what appending is here for.
785
+ key_piece = _js_object_key(k, budget)
786
+ value_piece = _js_literal(v, seen, budget)
787
+ out << separator << key_piece << ":" << value_piece
788
+ separator = ","
789
+ used += key_piece.bytesize + value_piece.bytesize + 2
790
+ _over_budget!(budget) if budget && used > budget
791
+ end
792
+ out << "}"
793
+ end
794
+ ensure
795
+ seen.delete(value)
796
+ end
797
+ end
798
+
799
+ # A structure that reaches the same object twice is written out twice, so a
800
+ # graph with shared branches expands rather than being shared: each level
801
+ # that references the level below twice doubles the output, and twenty-five
802
+ # of those is a third of a gigabyte from a handful of Ruby objects. YAML
803
+ # aliases and Marshal round-trips produce exactly that shape without anyone
804
+ # meaning to.
805
+ #
806
+ # The bound is the VM's own memory_limit rather than a number invented here.
807
+ # A source larger than the whole JS heap budget cannot be evaluated by that
808
+ # VM under any circumstances, so this refuses work that provably cannot
809
+ # succeed, and the message names the option to change. It is a ceiling and
810
+ # not a prediction: object-heavy literals cost many times their source size
811
+ # once parsed, so a source well under the limit can still exhaust the VM.
812
+ #
813
+ # Counted as each element is produced rather than after a container is
814
+ # built, which is the difference between bounding the source and bounding
815
+ # the host. Checking a finished container still lets `map` and `join`
816
+ # materialise the whole thing first, so a single wide container walks past
817
+ # any budget: three hundred references to one twenty-thousand-element array
818
+ # reached 103MB of Ruby against a 4MB limit that never fired in time.
819
+ #
820
+ # It is still a multiple of the limit rather than the limit. Nesting builds
821
+ # each inner literal in full before the level above can check it, so the
822
+ # doubling shape measured about eight times a 4MB limit and four and a half
823
+ # times a 32MB one, falling as the limit rises, against twice the limit for
824
+ # the wide shape above. Numbers from this machine, and the two that were
825
+ # quoted here before both went stale, so treat them as an order of
826
+ # magnitude rather than a promise. What it is no longer is proportional to
827
+ # the structure: the same value with no check at all reaches a third of a
828
+ # gigabyte at twenty-five levels and ten gigabytes at thirty.
829
+ def self._within_budget(literal, budget)
830
+ return literal if budget.nil? || literal.bytesize <= budget
831
+
832
+ _over_budget!(budget)
833
+ end
834
+
835
+ def self._over_budget!(budget)
836
+ raise ::ArgumentError,
837
+ "value serializes to more than #{budget} bytes of JavaScript, which is this VM's memory_limit; " \
838
+ "it cannot be evaluated. Shared sub-structures are written out once per occurrence, so a value " \
839
+ "with repeated branches expands"
840
+ end
841
+
842
+ # JS object keys are strings regardless, so an Integer key is unambiguous.
843
+ # Anything else would rely on `#to_s` producing something meaningful, which
844
+ # is the silent coercion this converter exists to avoid.
845
+ # `budget` for the same reason the value path takes one: a key is a value
846
+ # the caller supplies, and it was being copied and escaped before anything
847
+ # looked at its size. A 300MB String cost nothing as a value and 900MB as a
848
+ # key.
849
+ def self._js_object_key(key, budget = nil)
850
+ literal = case key
851
+ when ::String
852
+ _over_budget!(budget) if budget && BYTESIZE.bind_call(key) > budget
853
+ ::JSON.generate(::String.new(key))
854
+ when ::Integer
855
+ # Read, not asked to describe itself, as on the value path.
856
+ _over_budget!(budget) if budget && BIT_LENGTH.bind_call(key) / 4 > budget
857
+ ::JSON.generate(INTEGER_TO_S.bind_call(key))
858
+ when ::Symbol
859
+ # A Symbol has no bytes to copy, so this one asks. What it
860
+ # answers is still escaped here, so the worst a patched
861
+ # Symbol#to_s buys is a different key, not a different
862
+ # meaning.
863
+ copy = ::String.new(key.to_s)
864
+ _over_budget!(budget) if budget && copy.bytesize > budget
865
+ ::JSON.generate(copy)
866
+ else
867
+ raise ::TypeError, "#{key.class} cannot be used as a JavaScript object key"
868
+ end
869
+
870
+ # In an object literal `__proto__: v` sets the prototype rather than
871
+ # defining a property, and quoting the key does not opt out of that. A Hash
872
+ # key of that name would otherwise vanish: the entry becomes the object's
873
+ # prototype, `Object.keys` never lists it, and the value round-trips back
874
+ # from JS as if it had never been sent.
875
+ #
876
+ # That is a difference between what Ruby holds and what JS then sees, which
877
+ # is the shape a host-side check can be walked past: a request body with a
878
+ # nested "__proto__" passes inspection in Ruby, where it is an ordinary key
879
+ # with a Hash under it, and arrives in JS as inherited members of the object
880
+ # the host handed over.
881
+ #
882
+ # A computed key is not the special form, so this emits the property that
883
+ # was asked for. Only this one name needs it; nothing else in an object
884
+ # literal means anything other than itself. JS written by hand is untouched:
885
+ # a guest writing `{__proto__: x}` still sets a prototype, as it should.
886
+ literal == '"__proto__"' ? "[#{literal}]" : literal
887
+ end
888
+ end
889
+ private_constant :ValueLiteral
890
+ end