#1635

Leave out built-ins code mode does not need

Openfazxeswants to merge intofrom

Summary

  • Give programs only the built-ins code mode keeps. Proxy, WeakRef, FinalizationRegistry, the typed arrays, ArrayBuffer, SharedArrayBuffer, Atomics, and DataView are left out. None of the 30 programs that models wrote in fx's code-mode evals used them.
  • Reading a left-out name, typeof included, throws TypeError: <name> is not available in code mode, so a model learns why its program failed and can rewrite it.
  • Make eval and the four Function constructors stand-ins with the same names. A program can still use instanceof Function, typeof eval, or fn.constructor.name, but calling one throws, for example TypeError: The Function constructor is not available in code mode.
  • Make import() throw TypeError: import() is not available in code mode.

Why a prelude

Leaving intrinsic groups out of the context can't remove eval or the Function constructors. JS_AddIntrinsicBaseObjects installs Function, GeneratorFunction, and the global eval, and JS_AddIntrinsicPromise installs the async constructors. Every program needs both groups, because it runs as the body of an async arrow function. QuickJS also has no hook like the one V8 gives Node's --disallow-code-generation-from-strings and Cloudflare Workers for refusing code generation from strings.

So a short prelude runs before each program and swaps in the stand-ins. Its properties are neither writable nor configurable, so a program can't undo them. Direct eval(...) compiles code only when the callee is QuickJS's own eval, which no program can reach.

The other left-out names throw when read rather than going missing, so a model sees that a name was left out on purpose instead of ReferenceError: Proxy is not defined. The cost is that typeof Proxy throws too, so feature detection doesn't work for these names.

Architecture

Before, run built a context with every built-in QuickJS offers:

flowchart LR
  run["engine.run"] --> ctx["JS_NewContext: every built-in group"]
  ctx --> program["program"]

After, run refuses every import(), builds a context with only the kept groups, and runs the prelude before the program:

flowchart LR
  run["engine.run"] --> loader["module loader: refuseImport"]
  run --> raw["JS_NewContextRaw"]
  raw --> groups["kept groups: base objects, Date, compiler, RegExp, JSON, Map and Set, Promise, atob and btoa, performance"]
  groups --> prelude["cut_prelude: throwing getters for left-out names, stand-ins for eval and the Function constructors"]
  prelude --> program["program"]

Testing

  • zig build test-js passes 22 tests in Debug and ReleaseSafe. They cover the 30 eval programs run against stand-in tools, every path to code generation, attempts to undo the prelude, and the patterns that must keep working, such as instanceof Function, fn.constructor.name, and typeof eval.
  • The deep-nesting test now nests the program's own text, because its eval call had been throwing before it reached the parser.

Stacked on #1634.