Summary
- Give programs only the built-ins code mode keeps.
Proxy,WeakRef,FinalizationRegistry, the typed arrays,ArrayBuffer,SharedArrayBuffer,Atomics, andDataVieware left out. None of the 30 programs that models wrote in fx's code-mode evals used them. - Reading a left-out name,
typeofincluded, throwsTypeError: <name> is not available in code mode, so a model learns why its program failed and can rewrite it. - Make
evaland the four Function constructors stand-ins with the same names. A program can still useinstanceof Function,typeof eval, orfn.constructor.name, but calling one throws, for exampleTypeError: The Function constructor is not available in code mode. - Make
import()throwTypeError: 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-jspasses 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 asinstanceof Function,fn.constructor.name, andtypeof eval.- The deep-nesting test now nests the program's own text, because its
evalcall had been throwing before it reached the parser.
Stacked on #1634.